fix(team): refine launch dialog previews
This commit is contained in:
parent
0f9f79da66
commit
472a1501ad
6 changed files with 260 additions and 21 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)' }}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue