From 472a1501add09ff2201e755f34c4c72615008c8f Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 7 May 2026 20:36:48 +0300 Subject: [PATCH] fix(team): refine launch dialog previews --- .../memberLogPreviewExtractor.test.ts | 92 ++++++++++++++- .../policies/memberLogPreviewExtractor.ts | 109 +++++++++++++++++- .../team/dialogs/CreateTeamDialog.tsx | 30 ++++- .../team/dialogs/LaunchTeamDialog.tsx | 23 +++- .../team/dialogs/ProjectPathSelector.tsx | 11 +- .../team/members/MembersEditorSection.tsx | 16 +-- 6 files changed, 260 insertions(+), 21 deletions(-) diff --git a/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts b/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts index beba5d6a..e5c0ae5d 100644 --- a/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts +++ b/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts @@ -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', diff --git a/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts b/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts index df6a3a31..92c62611 100644 --- a/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts +++ b/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts @@ -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) diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx index 180d05a3..373e1640 100644 --- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx @@ -412,6 +412,7 @@ export const CreateTeamDialog = ({ const [prepareWarnings, setPrepareWarnings] = useState([]); const [prepareChecks, setPrepareChecks] = useState([]); const prepareRequestSeqRef = useRef(0); + const appliedDefaultProjectPathRef = useRef(null); const lastAutoDescriptionRef = useRef(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 = ({ {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.'} @@ -2188,7 +2207,8 @@ export const CreateTeamDialog = ({ ) : null}