refactor: update context handling in team provisioning and UI components

- Replaced references to extended context with limit context in TeamProvisioningService and related components, ensuring consistent terminology.
- Updated LaunchTeamDialog and CreateTeamDialog to reflect the new limit context functionality, enhancing user clarity.
- Removed the deprecated ExtendedContextCheckbox component and integrated its functionality into the new LimitContextCheckbox.
- Adjusted teamSlice to accommodate changes in launch parameters, streamlining the context management process.
This commit is contained in:
iliya 2026-03-14 17:54:49 +02:00
parent da4d98ec2b
commit 1073a51039
7 changed files with 126 additions and 40 deletions

View file

@ -145,8 +145,17 @@ export class TeamMemberLogsFinder {
since?: string; since?: string;
} }
): Promise<MemberLogSummary[]> { ): Promise<MemberLogSummary[]> {
const t0 = performance.now();
const discovery = await this.discoverProjectSessions(teamName); const discovery = await this.discoverProjectSessions(teamName);
if (!discovery) return []; const tDiscovery = performance.now();
if (!discovery) {
logger.info(
`[perf] findLogsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms`
);
return [];
}
const sinceMs = this.deriveSinceMs(options); const sinceMs = this.deriveSinceMs(options);
const { projectDir, projectId, config, sessionIds, knownMembers } = discovery; const { projectDir, projectId, config, sessionIds, knownMembers } = discovery;
@ -171,6 +180,12 @@ export class TeamMemberLogsFinder {
// file missing or unreadable // file missing or unreadable
} }
} }
const tLead = performance.now();
let totalFiles = 0;
let mentionHits = 0;
let cacheHits = 0;
const cacheSnapshotBefore = this.fileMentionsCache.size;
for (const sessionId of sessionIds) { for (const sessionId of sessionIds) {
const subagentsDir = path.join(projectDir, sessionId, 'subagents'); const subagentsDir = path.join(projectDir, sessionId, 'subagents');
@ -183,9 +198,15 @@ export class TeamMemberLogsFinder {
for (const file of files) { for (const file of files) {
if (!file.startsWith('agent-') || !file.endsWith('.jsonl')) continue; if (!file.startsWith('agent-') || !file.endsWith('.jsonl')) continue;
if (file.startsWith('agent-acompact')) continue; if (file.startsWith('agent-acompact')) continue;
totalFiles++;
const filePath = path.join(subagentsDir, file); const filePath = path.join(subagentsDir, file);
if (!(await this.fileMentionsTaskIdCached(filePath, teamName, taskId, false, sinceMs))) const cacheSizeBefore = this.fileMentionsCache.size;
if (!(await this.fileMentionsTaskIdCached(filePath, teamName, taskId, false, sinceMs))) {
if (this.fileMentionsCache.size === cacheSizeBefore) cacheHits++;
continue; continue;
}
if (this.fileMentionsCache.size === cacheSizeBefore) cacheHits++;
mentionHits++;
const attribution = await this.attributeSubagent(filePath, knownMembers); const attribution = await this.attributeSubagent(filePath, knownMembers);
if (!attribution) continue; if (!attribution) continue;
const summary = await this.parseSubagentSummary( const summary = await this.parseSubagentSummary(
@ -199,6 +220,7 @@ export class TeamMemberLogsFinder {
if (summary) results.push(summary); if (summary) results.push(summary);
} }
} }
const tScan = performance.now();
const normalizedOwner = const normalizedOwner =
typeof options?.owner === 'string' ? options.owner.trim() : options?.owner; typeof options?.owner === 'string' ? options.owner.trim() : options?.owner;
@ -281,10 +303,25 @@ export class TeamMemberLogsFinder {
} }
} }
} }
const tOwner = performance.now();
return results.sort( const sorted = results.sort(
(a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime() (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
); );
const tTotal = performance.now();
logger.info(
`[perf] findLogsForTask(${taskId}@${teamName}) ` +
`total=${(tTotal - t0).toFixed(0)}ms | ` +
`discovery=${(tDiscovery - t0).toFixed(0)}ms | ` +
`lead=${(tLead - tDiscovery).toFixed(0)}ms | ` +
`scan=${(tScan - tLead).toFixed(0)}ms (${totalFiles} files, ${mentionHits} hits, ${cacheHits} cache) | ` +
`owner=${(tOwner - tScan).toFixed(0)}ms | ` +
`sessions=${sessionIds.length} | cache=${cacheSnapshotBefore}→${this.fileMentionsCache.size} | ` +
`results=${sorted.length}`
);
return sorted;
} }
/** /**
@ -301,8 +338,17 @@ export class TeamMemberLogsFinder {
since?: string; since?: string;
} }
): Promise<{ filePath: string; memberName: string }[]> { ): Promise<{ filePath: string; memberName: string }[]> {
const t0 = performance.now();
const discovery = await this.discoverProjectSessions(teamName); const discovery = await this.discoverProjectSessions(teamName);
if (!discovery) return []; const tDiscovery = performance.now();
if (!discovery) {
logger.info(
`[perf] findLogFileRefsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms`
);
return [];
}
const sinceMs = this.deriveSinceMs(options); const sinceMs = this.deriveSinceMs(options);
const { projectDir, config, sessionIds, knownMembers } = discovery; const { projectDir, config, sessionIds, knownMembers } = discovery;
@ -330,6 +376,10 @@ export class TeamMemberLogsFinder {
// file missing or unreadable // file missing or unreadable
} }
} }
const tLead = performance.now();
let totalFiles = 0;
let mentionHits = 0;
for (const sessionId of sessionIds) { for (const sessionId of sessionIds) {
const subagentsDir = path.join(projectDir, sessionId, 'subagents'); const subagentsDir = path.join(projectDir, sessionId, 'subagents');
@ -342,11 +392,13 @@ export class TeamMemberLogsFinder {
for (const file of files) { for (const file of files) {
if (!file.startsWith('agent-') || !file.endsWith('.jsonl')) continue; if (!file.startsWith('agent-') || !file.endsWith('.jsonl')) continue;
if (file.startsWith('agent-acompact')) continue; if (file.startsWith('agent-acompact')) continue;
totalFiles++;
const filePath = path.join(subagentsDir, file); const filePath = path.join(subagentsDir, file);
if (!(await this.fileMentionsTaskIdCached(filePath, teamName, taskId, false, sinceMs))) { if (!(await this.fileMentionsTaskIdCached(filePath, teamName, taskId, false, sinceMs))) {
continue; continue;
} }
mentionHits++;
const attribution = await this.attributeSubagent(filePath, knownMembers); const attribution = await this.attributeSubagent(filePath, knownMembers);
if (!attribution) continue; if (!attribution) continue;
@ -357,6 +409,7 @@ export class TeamMemberLogsFinder {
); );
} }
} }
const tScan = performance.now();
const normalizedOwner = const normalizedOwner =
typeof options?.owner === 'string' ? options.owner.trim() : options?.owner; typeof options?.owner === 'string' ? options.owner.trim() : options?.owner;
@ -432,8 +485,21 @@ export class TeamMemberLogsFinder {
); );
} }
} }
const tOwner = performance.now();
const sortedRefs = [...refs].sort((a, b) => b.sortTime - a.sortTime); const sortedRefs = [...refs].sort((a, b) => b.sortTime - a.sortTime);
const tTotal = performance.now();
logger.info(
`[perf] findLogFileRefsForTask(${taskId}@${teamName}) ` +
`total=${(tTotal - t0).toFixed(0)}ms | ` +
`discovery=${(tDiscovery - t0).toFixed(0)}ms | ` +
`lead=${(tLead - tDiscovery).toFixed(0)}ms | ` +
`scan=${(tScan - tLead).toFixed(0)}ms (${totalFiles} files, ${mentionHits} hits) | ` +
`owner=${(tOwner - tScan).toFixed(0)}ms | ` +
`sessions=${sessionIds.length} | results=${sortedRefs.length}`
);
return sortedRefs.map(({ filePath, memberName }) => ({ filePath, memberName })); return sortedRefs.map(({ filePath, memberName }) => ({ filePath, memberName }));
} }
@ -1067,6 +1133,16 @@ export class TeamMemberLogsFinder {
} catch { } catch {
// Skip malformed lines // Skip malformed lines
} }
// Early exit: reliable signal found and description extracted — no need to scan further.
// Only process_team and routing_sender trigger this; teammate_id is unreliable (identifies
// the message sender, not the agent) so we keep scanning for better signals.
if (
description &&
signals.some((s) => s.source === 'process_team' || s.source === 'routing_sender')
) {
break;
}
} }
if (signals.length === 0) return null; if (signals.length === 0) return null;

View file

@ -2779,7 +2779,10 @@ export class TeamProvisioningService {
try { try {
child = spawnCli(claudePath, spawnArgs, { child = spawnCli(claudePath, spawnArgs, {
cwd: request.cwd, cwd: request.cwd,
env: { ...shellEnv }, env: {
...shellEnv,
...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}),
},
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
}); });
} catch (error) { } catch (error) {
@ -2797,7 +2800,10 @@ export class TeamProvisioningService {
claudePath, claudePath,
args: spawnArgs, args: spawnArgs,
cwd: request.cwd, cwd: request.cwd,
env: { ...shellEnv }, env: {
...shellEnv,
...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}),
},
prompt, prompt,
}; };
@ -3182,6 +3188,7 @@ export class TeamProvisioningService {
cwd: request.cwd, cwd: request.cwd,
env: { env: {
...shellEnv, ...shellEnv,
...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}),
}, },
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
}); });
@ -3202,7 +3209,10 @@ export class TeamProvisioningService {
claudePath, claudePath,
args: launchArgs, args: launchArgs,
cwd: request.cwd, cwd: request.cwd,
env: { ...shellEnv }, env: {
...shellEnv,
...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}),
},
prompt, prompt,
}; };

View file

@ -1175,8 +1175,8 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
const effortLabel = launchParams.effort const effortLabel = launchParams.effort
? launchParams.effort.charAt(0).toUpperCase() + launchParams.effort.slice(1) ? launchParams.effort.charAt(0).toUpperCase() + launchParams.effort.slice(1)
: ''; : '';
const extLabel = launchParams.extendedContext ? '1M' : ''; const limitLabel = launchParams.limitContext ? '200K' : '';
const parts = [modelLabel, effortLabel, extLabel].filter(Boolean).join(' '); const parts = [modelLabel, effortLabel, limitLabel].filter(Boolean).join(' ');
return ( return (
<span className="inline-flex items-center gap-1 rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-text-secondary)]"> <span className="inline-flex items-center gap-1 rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-text-secondary)]">
{parts} {parts}

View file

@ -36,7 +36,7 @@ import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
import { AdvancedCliSection } from './AdvancedCliSection'; import { AdvancedCliSection } from './AdvancedCliSection';
import { EffortLevelSelector } from './EffortLevelSelector'; import { EffortLevelSelector } from './EffortLevelSelector';
import { LimitContextCheckbox } from './ExtendedContextCheckbox'; import { LimitContextCheckbox } from './LimitContextCheckbox';
import { OptionalSettingsSection } from './OptionalSettingsSection'; import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector'; import { ProjectPathSelector } from './ProjectPathSelector';
import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox'; import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox';

View file

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { api } from '@renderer/api'; import { api } from '@renderer/api';
import { ExtendedContextCheckbox } from '@renderer/components/team/dialogs/ExtendedContextCheckbox'; import { LimitContextCheckbox } from '@renderer/components/team/dialogs/LimitContextCheckbox';
import { SkipPermissionsCheckbox } from '@renderer/components/team/dialogs/SkipPermissionsCheckbox'; import { SkipPermissionsCheckbox } from '@renderer/components/team/dialogs/SkipPermissionsCheckbox';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { Checkbox } from '@renderer/components/ui/checkbox'; import { Checkbox } from '@renderer/components/ui/checkbox';
@ -170,8 +170,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
// Launch-only state // Launch-only state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const [extendedContext, setExtendedContextRaw] = useState( const [limitContext, setLimitContextRaw] = useState(
() => localStorage.getItem('team:lastExtendedContext') === 'true' () => localStorage.getItem('team:lastLimitContext') === 'true'
); );
const [clearContext, setClearContext] = useState(false); const [clearContext, setClearContext] = useState(false);
const [conflictDismissed, setConflictDismissed] = useState(false); const [conflictDismissed, setConflictDismissed] = useState(false);
@ -235,9 +235,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
localStorage.setItem('team:lastSelectedModel', value); localStorage.setItem('team:lastSelectedModel', value);
}; };
const setExtendedContext = (value: boolean): void => { const setLimitContext = (value: boolean): void => {
setExtendedContextRaw(value); setLimitContextRaw(value);
localStorage.setItem('team:lastExtendedContext', String(value)); localStorage.setItem('team:lastLimitContext', String(value));
}; };
const setSkipPermissions = (value: boolean): void => { const setSkipPermissions = (value: boolean): void => {
@ -512,12 +512,12 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
args.push('--verbose', '--setting-sources', 'user,project,local'); args.push('--verbose', '--setting-sources', 'user,project,local');
args.push('--mcp-config', '<auto>', '--disallowedTools', 'TeamDelete,TodoWrite'); args.push('--mcp-config', '<auto>', '--disallowedTools', 'TeamDelete,TodoWrite');
if (skipPermissions) args.push('--dangerously-skip-permissions'); if (skipPermissions) args.push('--dangerously-skip-permissions');
const model = computeEffectiveTeamModel(selectedModel, extendedContext); const model = computeEffectiveTeamModel(selectedModel);
if (model) args.push('--model', model); if (model) args.push('--model', model);
if (selectedEffort) args.push('--effort', selectedEffort); if (selectedEffort) args.push('--effort', selectedEffort);
if (!clearContext) args.push('--resume', '<previous>'); if (!clearContext) args.push('--resume', '<previous>');
return args; return args;
}, [isLaunch, skipPermissions, selectedModel, extendedContext, selectedEffort, clearContext]); }, [isLaunch, skipPermissions, selectedModel, selectedEffort, clearContext]);
const launchOptionalSummary = useMemo(() => { const launchOptionalSummary = useMemo(() => {
if (!isLaunch) return []; if (!isLaunch) return [];
@ -526,7 +526,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
if (promptDraft.value.trim()) summary.push('Lead prompt'); if (promptDraft.value.trim()) summary.push('Lead prompt');
if (selectedModel) summary.push(`Model: ${selectedModel}`); if (selectedModel) summary.push(`Model: ${selectedModel}`);
if (selectedEffort) summary.push(`Effort: ${selectedEffort}`); if (selectedEffort) summary.push(`Effort: ${selectedEffort}`);
if (extendedContext) summary.push('Extended context'); if (limitContext) summary.push('Limited to 200K context');
if (skipPermissions) summary.push('Auto-approve tools'); if (skipPermissions) summary.push('Auto-approve tools');
if (clearContext) summary.push('Fresh session'); if (clearContext) summary.push('Fresh session');
if (worktreeEnabled && worktreeName.trim()) summary.push(`Worktree: ${worktreeName.trim()}`); if (worktreeEnabled && worktreeName.trim()) summary.push(`Worktree: ${worktreeName.trim()}`);
@ -537,7 +537,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
promptDraft.value, promptDraft.value,
selectedModel, selectedModel,
selectedEffort, selectedEffort,
extendedContext, limitContext,
skipPermissions, skipPermissions,
clearContext, clearContext,
worktreeEnabled, worktreeEnabled,
@ -590,9 +590,10 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
teamName: effectiveTeamName, teamName: effectiveTeamName,
cwd: effectiveCwd, cwd: effectiveCwd,
prompt: promptDraft.value.trim() || undefined, prompt: promptDraft.value.trim() || undefined,
model: computeEffectiveTeamModel(selectedModel, extendedContext), model: computeEffectiveTeamModel(selectedModel),
effort: (selectedEffort as EffortLevel) || undefined, effort: (selectedEffort as EffortLevel) || undefined,
clearContext: clearContext || undefined, clearContext: clearContext || undefined,
limitContext: limitContext || undefined,
skipPermissions, skipPermissions,
worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined, worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined,
extraCliArgs: customArgs.trim() || undefined, extraCliArgs: customArgs.trim() || undefined,
@ -953,11 +954,10 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
onValueChange={setSelectedEffort} onValueChange={setSelectedEffort}
id="dialog-effort" id="dialog-effort"
/> />
<ExtendedContextCheckbox <LimitContextCheckbox
id="launch-extended-context" id="launch-limit-context"
checked={extendedContext} checked={limitContext}
onCheckedChange={setExtendedContext} onCheckedChange={setLimitContext}
disabled={selectedModel === 'haiku'}
/> />
<SkipPermissionsCheckbox <SkipPermissionsCheckbox
id="dialog-skip-permissions" id="dialog-skip-permissions"

View file

@ -310,7 +310,7 @@ export interface GlobalTaskDetailState {
export interface TeamLaunchParams { export interface TeamLaunchParams {
model?: string; // 'opus' | 'sonnet' | 'haiku' model?: string; // 'opus' | 'sonnet' | 'haiku'
effort?: EffortLevel; effort?: EffortLevel;
extendedContext?: boolean; limitContext?: boolean;
} }
export interface TeamSlice { export interface TeamSlice {
@ -542,15 +542,15 @@ function saveLaunchParams(teamName: string, params: TeamLaunchParams): void {
} }
/** /**
* Parse raw model string from TeamLaunchRequest back into base model + extended context flag. * Parse raw model string — simply returns the base model name.
* E.g. 'opus[1m]' → { model: 'opus', extendedContext: true } * The [1m] suffix is no longer used; context limiting is handled via env var.
* 'sonnet' → { model: 'sonnet', extendedContext: false }
*/ */
function parseModelString(raw?: string): { model?: string; extendedContext: boolean } { function parseModelString(raw?: string): { model?: string } {
if (!raw) return { extendedContext: false }; if (!raw) return {};
// Strip legacy [1m] suffix if present in saved data
const match = raw.match(/^(\w+)\[1m\]$/); const match = raw.match(/^(\w+)\[1m\]$/);
if (match) return { model: match[1], extendedContext: true }; if (match) return { model: match[1] };
return { model: raw, extendedContext: false }; return { model: raw };
} }
function loadToolApprovalSettings(): ToolApprovalSettings { function loadToolApprovalSettings(): ToolApprovalSettings {
@ -1421,12 +1421,12 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
} }
const response = await unwrapIpc('team:create', () => api.teams.createTeam(request)); const response = await unwrapIpc('team:create', () => api.teams.createTeam(request));
// Persist per-team launch params (model, effort, extended context) // Persist per-team launch params (model, effort, limit context)
const { model: baseModel, extendedContext } = parseModelString(request.model); const { model: baseModel } = parseModelString(request.model);
const params: TeamLaunchParams = { const params: TeamLaunchParams = {
model: baseModel || 'default', model: baseModel || 'default',
effort: request.effort, effort: request.effort,
extendedContext, limitContext: request.limitContext,
}; };
saveLaunchParams(request.teamName, params); saveLaunchParams(request.teamName, params);
set((state) => ({ set((state) => ({
@ -1558,12 +1558,12 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
try { try {
const response = await unwrapIpc('team:launch', () => api.teams.launchTeam(request)); const response = await unwrapIpc('team:launch', () => api.teams.launchTeam(request));
// Persist per-team launch params (model, effort, extended context) // Persist per-team launch params (model, effort, limit context)
const { model: baseModel, extendedContext } = parseModelString(request.model); const { model: baseModel } = parseModelString(request.model);
const params: TeamLaunchParams = { const params: TeamLaunchParams = {
model: baseModel || 'default', model: baseModel || 'default',
effort: request.effort, effort: request.effort,
extendedContext, limitContext: request.limitContext,
}; };
saveLaunchParams(request.teamName, params); saveLaunchParams(request.teamName, params);
set((state) => ({ set((state) => ({