fix(team): refine launch dialog previews

This commit is contained in:
777genius 2026-05-07 20:36:48 +03:00
parent 0f9f79da66
commit 472a1501ad
6 changed files with 260 additions and 21 deletions

View file

@ -1599,11 +1599,101 @@ Reply to this comment using MCP tool task_add_comment.
expect(result.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Bash result',
preview: 'Tests passed',
preview: 'Tests passed - pnpm test',
});
expect(result.items).toHaveLength(1);
});
it('keeps successful file tool results compact with input context', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'read-call',
timestamp: '2026-04-01T10:00:00.000Z',
content: [
{
type: 'tool_use',
id: 'tool-read',
name: 'Read',
input: {
file_path: 'src/app.ts',
},
},
],
}),
message({
uuid: 'read-result',
type: 'user',
role: 'user',
timestamp: '2026-04-01T10:01:00.000Z',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-read',
content: 'export function app() { return true; }',
},
],
}),
],
});
expect(result.items).toHaveLength(1);
expect(result.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Read result',
preview: 'src/app.ts - export function app() { return true; }',
});
});
it('keeps empty successful file tool results readable without duplicate input rows', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'edit-call',
timestamp: '2026-04-01T10:00:00.000Z',
content: [
{
type: 'tool_use',
id: 'tool-edit',
name: 'Edit',
input: {
file_path: 'src/app.ts',
old_string: 'a',
new_string: 'b',
},
},
],
}),
message({
uuid: 'edit-result',
type: 'user',
role: 'user',
timestamp: '2026-04-01T10:01:00.000Z',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-edit',
content: '',
},
],
}),
],
});
expect(result.items).toHaveLength(1);
expect(result.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Edit result',
preview: 'src/app.ts',
});
});
it('does not label arbitrary message fields as sent messages', () => {
const result = extractMemberLogPreviewItems({
provider: 'opencode_runtime',

View file

@ -522,6 +522,102 @@ function formatGenericToolResultTitle(
return `${formatToolTitle(toolContext.name)} ${isError ? 'error' : 'result'}`;
}
function formatShellResultContext(toolContext: ToolUseContext | undefined): string | null {
if (
!toolContext ||
(toolContext.canonicalName !== 'bash' && toolContext.canonicalName !== 'shell')
) {
return null;
}
const input = asRecord(toolContext.input);
return stringField(input, 'description') ?? stringField(input, 'command');
}
function addContextToSuccessResultPreview(
preview: ValuePreview,
context: string | null,
limit: number,
order: 'context-first' | 'result-first'
): ValuePreview {
if (!context) {
return preview;
}
const compactContext = compactWhitespace(context);
if (!compactContext) {
return preview;
}
if (!preview.preview) {
const fallback = truncatePreview(compactContext, limit);
return {
...fallback,
truncated: preview.truncated || fallback.truncated,
...(preview.title ? { title: preview.title } : {}),
};
}
const compactPreview = compactWhitespace(preview.preview);
if (!compactContext || compactPreview.toLowerCase().startsWith(compactContext.toLowerCase())) {
return preview;
}
const combinedText =
order === 'context-first'
? `${compactContext} - ${compactPreview}`
: `${compactPreview} - ${compactContext}`;
const combined = truncatePreview(combinedText, limit);
return {
...combined,
truncated: preview.truncated || combined.truncated,
...(preview.title ? { title: preview.title } : {}),
};
}
function formatFileToolResultContext(toolContext: ToolUseContext | undefined): string | null {
if (!toolContext) {
return null;
}
const input = asRecord(toolContext.input);
const path =
stringField(input, 'file_path') ??
stringField(input, 'filePath') ??
stringField(input, 'path') ??
stringField(input, 'cwd');
if (toolContext.canonicalName === 'grep') {
const query = stringField(input, 'query') ?? stringField(input, 'pattern');
if (query && path) return `${query} in ${path}`;
return query ?? path;
}
if (toolContext.canonicalName === 'glob') {
const pattern = stringField(input, 'pattern') ?? stringField(input, 'glob');
if (pattern && path) return `${pattern} in ${path}`;
return pattern ?? path;
}
if (
toolContext.canonicalName === 'read' ||
toolContext.canonicalName === 'write' ||
toolContext.canonicalName === 'edit' ||
toolContext.canonicalName === 'ls'
) {
return path;
}
return null;
}
function addToolContextToSuccessResultPreview(
preview: ValuePreview,
toolContext: ToolUseContext | undefined,
limit: number
): ValuePreview {
const shellContext = formatShellResultContext(toolContext);
if (shellContext) {
return addContextToSuccessResultPreview(preview, shellContext, limit, 'result-first');
}
return addContextToSuccessResultPreview(
preview,
formatFileToolResultContext(toolContext),
limit,
'context-first'
);
}
function buildToolUseKey(input: {
provider: MemberLogStreamProvider;
sourceId: string;
@ -535,6 +631,12 @@ function isToolUseSupersededBySuccessResult(toolName: string): boolean {
return (
canonical === 'bash' ||
canonical === 'shell' ||
canonical === 'read' ||
canonical === 'write' ||
canonical === 'edit' ||
canonical === 'grep' ||
canonical === 'glob' ||
canonical === 'ls' ||
canonical === 'sendmessage' ||
canonical === 'message_send' ||
canonical.startsWith('cross_team_') ||
@ -2057,13 +2159,16 @@ function collectToolResultCandidates(input: {
sourceId: input.sourceId,
toolUseId: id,
});
const preview = previewUnknownValue(
const rawPreview = previewUnknownValue(
result.content,
input.textLimit,
TOOL_RESULT_PRIORITY_KEYS,
toolContext
);
const isError = result.isError === true || preview.title === 'Tool error';
const isError = result.isError === true || rawPreview.title === 'Tool error';
const preview = isError
? rawPreview
: addToolContextToSuccessResultPreview(rawPreview, toolContext, input.textLimit);
const title =
preview.title === 'Tool error'
? formatGenericToolResultTitle(toolContext, true)

View file

@ -412,6 +412,7 @@ export const CreateTeamDialog = ({
const [prepareWarnings, setPrepareWarnings] = useState<string[]>([]);
const [prepareChecks, setPrepareChecks] = useState<ProvisioningProviderCheck[]>([]);
const prepareRequestSeqRef = useRef(0);
const appliedDefaultProjectPathRef = useRef<string | null>(null);
const lastAutoDescriptionRef = useRef<string | null>(null);
const [fieldErrors, setFieldErrors] = useState<{
teamName?: string;
@ -1092,17 +1093,35 @@ export const CreateTeamDialog = ({
// Pre-select defaultProjectPath when projects loaded (only while dialog is open)
useEffect(() => {
if (!open) return;
if (cwdMode !== 'project') {
if (!open) {
appliedDefaultProjectPathRef.current = null;
return;
}
if (selectedProjectPath) {
if (cwdMode !== 'project') {
return;
}
const selectableProjects = projects.filter((project) => !isEphemeralProjectPath(project.path));
if (selectableProjects.length === 0) {
return;
}
if (defaultProjectPath && !isEphemeralProjectPath(defaultProjectPath)) {
const normalizedDefaultProjectPath = normalizePath(defaultProjectPath);
const defaultAlreadyApplied =
appliedDefaultProjectPathRef.current === normalizedDefaultProjectPath;
const match = selectableProjects.find(
(p) => normalizePath(p.path) === normalizedDefaultProjectPath
);
if (match && !defaultAlreadyApplied) {
appliedDefaultProjectPathRef.current = normalizedDefaultProjectPath;
if (normalizePath(selectedProjectPath) !== normalizedDefaultProjectPath) {
setSelectedProjectPath(match.path);
}
return;
}
}
if (selectedProjectPath) {
return;
}
if (defaultProjectPath && !isEphemeralProjectPath(defaultProjectPath)) {
const normalizedDefaultProjectPath = normalizePath(defaultProjectPath);
const match = selectableProjects.find(
@ -1678,7 +1697,7 @@ export const CreateTeamDialog = ({
<DialogDescription className="text-xs">
{initialData
? 'Create a new team based on an existing one.'
: 'Team provisioning via local Claude CLI.'}
: 'Set up your team and choose how it starts.'}
</DialogDescription>
</DialogHeader>
@ -2188,7 +2207,8 @@ export const CreateTeamDialog = ({
</Button>
) : null}
<Button
size="sm"
size="lg"
className="min-w-32 text-sm"
disabled={!canCreate || !draftLoaded || isSubmitting || hasCreateFormErrors}
onClick={handleSubmit}
>

View file

@ -444,6 +444,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
const [prepareWarnings, setPrepareWarnings] = useState<string[]>([]);
const [prepareChecks, setPrepareChecks] = useState<ProvisioningProviderCheck[]>([]);
const prepareRequestSeqRef = useRef(0);
const appliedDefaultProjectPathRef = useRef<string | null>(null);
const storeMembers = useStore((s) => selectResolvedMembersForTeamName(s, s.selectedTeamName));
const previousLaunchParams = useStore((s) =>
effectiveTeamName ? s.launchParamsByTeam[effectiveTeamName] : undefined
@ -1628,9 +1629,29 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
// Pre-select defaultProjectPath (launch mode) or first project
useEffect(() => {
if (!open || cwdMode !== 'project' || selectedProjectPath) return;
if (!open) {
appliedDefaultProjectPathRef.current = null;
return;
}
if (cwdMode !== 'project') return;
const selectableProjects = projects.filter((project) => !isEphemeralProjectPath(project.path));
if (selectableProjects.length === 0) return;
if (defaultProjectPath && !isEphemeralProjectPath(defaultProjectPath)) {
const normalizedDefaultProjectPath = normalizePath(defaultProjectPath);
const defaultAlreadyApplied =
appliedDefaultProjectPathRef.current === normalizedDefaultProjectPath;
const match = selectableProjects.find(
(p) => normalizePath(p.path) === normalizedDefaultProjectPath
);
if (match && !defaultAlreadyApplied) {
appliedDefaultProjectPathRef.current = normalizedDefaultProjectPath;
if (normalizePath(selectedProjectPath) !== normalizedDefaultProjectPath) {
setSelectedProjectPath(match.path);
}
return;
}
}
if (selectedProjectPath) return;
if (defaultProjectPath && !isEphemeralProjectPath(defaultProjectPath)) {
const normalizedDefaultProjectPath = normalizePath(defaultProjectPath);
const match = selectableProjects.find(

View file

@ -38,13 +38,16 @@ function renderHighlightedText(text: string, query: string): React.JSX.Element {
return <span key={`${part}-${index}`}>{part}</span>;
}
return (
<mark
<span
key={`${part}-${index}`}
// eslint-disable-next-line tailwindcss/no-custom-classname -- Tailwind arbitrary value with CSS variable
className="bg-[var(--color-accent)]/25 rounded px-0.5 text-[var(--color-text)]"
className="rounded px-0.5 font-semibold text-[var(--color-text)]"
style={{
backgroundColor: 'color-mix(in srgb, var(--color-accent) 35%, transparent)',
boxShadow: 'inset 0 0 0 1px color-mix(in srgb, var(--color-accent) 45%, transparent)',
}}
>
{part}
</mark>
</span>
);
})}
</span>

View file

@ -380,6 +380,14 @@ export const MembersEditorSection = ({
{disableAddMember && addMemberLockReason ? (
<p className="text-[11px] text-[var(--color-text-muted)]">{addMemberLockReason}</p>
) : null}
{jsonEditorOpen && showJsonEditor ? (
<MembersJsonEditor
value={jsonText}
onChange={handleJsonChange}
error={jsonError}
onClose={toggleJsonEditor}
/>
) : null}
<div className="space-y-2">
{activeMembers.map((member, index) => (
<MemberDraftRow
@ -470,14 +478,6 @@ export const MembersEditorSection = ({
</div>
</div>
) : null}
{jsonEditorOpen && showJsonEditor ? (
<MembersJsonEditor
value={jsonText}
onChange={handleJsonChange}
error={jsonError}
onClose={toggleJsonEditor}
/>
) : null}
</div>
{hasDuplicates ? (
<p className="text-[11px]" style={{ color: 'var(--field-error-text)' }}>