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;
}
): Promise<MemberLogSummary[]> {
const t0 = performance.now();
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 { projectDir, projectId, config, sessionIds, knownMembers } = discovery;
@ -171,6 +180,12 @@ export class TeamMemberLogsFinder {
// 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) {
const subagentsDir = path.join(projectDir, sessionId, 'subagents');
@ -183,9 +198,15 @@ export class TeamMemberLogsFinder {
for (const file of files) {
if (!file.startsWith('agent-') || !file.endsWith('.jsonl')) continue;
if (file.startsWith('agent-acompact')) continue;
totalFiles++;
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;
}
if (this.fileMentionsCache.size === cacheSizeBefore) cacheHits++;
mentionHits++;
const attribution = await this.attributeSubagent(filePath, knownMembers);
if (!attribution) continue;
const summary = await this.parseSubagentSummary(
@ -199,6 +220,7 @@ export class TeamMemberLogsFinder {
if (summary) results.push(summary);
}
}
const tScan = performance.now();
const normalizedOwner =
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()
);
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;
}
): Promise<{ filePath: string; memberName: string }[]> {
const t0 = performance.now();
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 { projectDir, config, sessionIds, knownMembers } = discovery;
@ -330,6 +376,10 @@ export class TeamMemberLogsFinder {
// file missing or unreadable
}
}
const tLead = performance.now();
let totalFiles = 0;
let mentionHits = 0;
for (const sessionId of sessionIds) {
const subagentsDir = path.join(projectDir, sessionId, 'subagents');
@ -342,11 +392,13 @@ export class TeamMemberLogsFinder {
for (const file of files) {
if (!file.startsWith('agent-') || !file.endsWith('.jsonl')) continue;
if (file.startsWith('agent-acompact')) continue;
totalFiles++;
const filePath = path.join(subagentsDir, file);
if (!(await this.fileMentionsTaskIdCached(filePath, teamName, taskId, false, sinceMs))) {
continue;
}
mentionHits++;
const attribution = await this.attributeSubagent(filePath, knownMembers);
if (!attribution) continue;
@ -357,6 +409,7 @@ export class TeamMemberLogsFinder {
);
}
}
const tScan = performance.now();
const normalizedOwner =
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 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 }));
}
@ -1067,6 +1133,16 @@ export class TeamMemberLogsFinder {
} catch {
// 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;

View file

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

View file

@ -1175,8 +1175,8 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
const effortLabel = launchParams.effort
? launchParams.effort.charAt(0).toUpperCase() + launchParams.effort.slice(1)
: '';
const extLabel = launchParams.extendedContext ? '1M' : '';
const parts = [modelLabel, effortLabel, extLabel].filter(Boolean).join(' ');
const limitLabel = launchParams.limitContext ? '200K' : '';
const parts = [modelLabel, effortLabel, limitLabel].filter(Boolean).join(' ');
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)]">
{parts}

View file

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

View file

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

View file

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