refactor: streamline logging and context handling in team services

- Replaced logger.info calls with console.log in TeamMemberLogsFinder for performance logging.
- Simplified environment variable handling in TeamProvisioningService by removing unnecessary context settings.
- Updated stopTeam method to accept a signal parameter for improved process termination control.
- Enhanced model handling in team dialogs to support new context management logic, ensuring consistent behavior across components.
- Adjusted teamSlice to reflect changes in model parsing and context handling, improving overall state management.
This commit is contained in:
iliya 2026-03-14 18:24:13 +02:00
parent 1073a51039
commit e6b7a6fe53
11 changed files with 70 additions and 74 deletions

View file

@ -151,7 +151,7 @@ export class TeamMemberLogsFinder {
const tDiscovery = performance.now(); const tDiscovery = performance.now();
if (!discovery) { if (!discovery) {
logger.info( console.log(
`[perf] findLogsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms` `[perf] findLogsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms`
); );
return []; return [];
@ -310,7 +310,7 @@ export class TeamMemberLogsFinder {
); );
const tTotal = performance.now(); const tTotal = performance.now();
logger.info( console.log(
`[perf] findLogsForTask(${taskId}@${teamName}) ` + `[perf] findLogsForTask(${taskId}@${teamName}) ` +
`total=${(tTotal - t0).toFixed(0)}ms | ` + `total=${(tTotal - t0).toFixed(0)}ms | ` +
`discovery=${(tDiscovery - t0).toFixed(0)}ms | ` + `discovery=${(tDiscovery - t0).toFixed(0)}ms | ` +
@ -344,7 +344,7 @@ export class TeamMemberLogsFinder {
const tDiscovery = performance.now(); const tDiscovery = performance.now();
if (!discovery) { if (!discovery) {
logger.info( console.log(
`[perf] findLogFileRefsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms` `[perf] findLogFileRefsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms`
); );
return []; return [];
@ -490,7 +490,7 @@ export class TeamMemberLogsFinder {
const sortedRefs = [...refs].sort((a, b) => b.sortTime - a.sortTime); const sortedRefs = [...refs].sort((a, b) => b.sortTime - a.sortTime);
const tTotal = performance.now(); const tTotal = performance.now();
logger.info( console.log(
`[perf] findLogFileRefsForTask(${taskId}@${teamName}) ` + `[perf] findLogFileRefsForTask(${taskId}@${teamName}) ` +
`total=${(tTotal - t0).toFixed(0)}ms | ` + `total=${(tTotal - t0).toFixed(0)}ms | ` +
`discovery=${(tDiscovery - t0).toFixed(0)}ms | ` + `discovery=${(tDiscovery - t0).toFixed(0)}ms | ` +

View file

@ -2779,10 +2779,7 @@ export class TeamProvisioningService {
try { try {
child = spawnCli(claudePath, spawnArgs, { child = spawnCli(claudePath, spawnArgs, {
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'],
}); });
} catch (error) { } catch (error) {
@ -2800,10 +2797,7 @@ export class TeamProvisioningService {
claudePath, claudePath,
args: spawnArgs, args: spawnArgs,
cwd: request.cwd, cwd: request.cwd,
env: { env: { ...shellEnv },
...shellEnv,
...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}),
},
prompt, prompt,
}; };
@ -3186,10 +3180,7 @@ export class TeamProvisioningService {
try { try {
child = spawnCli(claudePath, launchArgs, { child = spawnCli(claudePath, launchArgs, {
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'],
}); });
} catch (error) { } catch (error) {
@ -3209,10 +3200,7 @@ export class TeamProvisioningService {
claudePath, claudePath,
args: launchArgs, args: launchArgs,
cwd: request.cwd, cwd: request.cwd,
env: { env: { ...shellEnv },
...shellEnv,
...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}),
},
prompt, prompt,
}; };
@ -4360,7 +4348,7 @@ export class TeamProvisioningService {
/** /**
* Stop the running process for a team. No-op if team is not running. * Stop the running process for a team. No-op if team is not running.
*/ */
stopTeam(teamName: string): void { stopTeam(teamName: string, signal?: NodeJS.Signals): void {
const runId = this.getTrackedRunId(teamName); const runId = this.getTrackedRunId(teamName);
if (!runId) { if (!runId) {
return; return;
@ -4378,25 +4366,25 @@ export class TeamProvisioningService {
run.cancelRequested = true; run.cancelRequested = true;
// Note: do NOT call stdin.end() before kill — EOF triggers CLI's graceful // Note: do NOT call stdin.end() before kill — EOF triggers CLI's graceful
// shutdown which deletes team files (config.json, inboxes/, tasks/). // shutdown which deletes team files (config.json, inboxes/, tasks/).
// SIGTERM alone kills the process before cleanup runs, preserving files. // SIGTERM/SIGKILL kills the process before cleanup runs, preserving files.
killProcessTree(run.child); killProcessTree(run.child, signal);
const progress = updateProgress(run, 'disconnected', 'Team stopped by user'); const progress = updateProgress(run, 'disconnected', 'Team stopped by user');
run.onProgress(progress); run.onProgress(progress);
this.cleanupRun(run); this.cleanupRun(run);
logger.info(`[${teamName}] Process stopped by user`); logger.info(`[${teamName}] Process stopped (signal=${signal ?? 'SIGTERM'})`);
} }
/** /**
* Stop all running team processes. Called during app shutdown to kill * Stop all running team processes. Called during app shutdown.
* processes via SIGTERM before the OS closes stdin (which would trigger * Uses SIGKILL (uncatchable) to guarantee the process dies instantly
* CLI's graceful cleanup and delete team files). * without any cleanup prevents CLI from deleting team files on exit.
*/ */
stopAllTeams(): void { stopAllTeams(): void {
const alive = this.getAliveTeams(); const alive = this.getAliveTeams();
if (alive.length === 0) return; if (alive.length === 0) return;
logger.info(`Stopping all team processes on shutdown: ${alive.join(', ')}`); logger.info(`Killing all team processes on shutdown (SIGKILL): ${alive.join(', ')}`);
for (const teamName of alive) { for (const teamName of alive) {
this.stopTeam(teamName); this.stopTeam(teamName, 'SIGKILL');
} }
} }

View file

@ -1168,7 +1168,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
const MODEL_LABELS: Record<string, string> = { const MODEL_LABELS: Record<string, string> = {
default: 'Default', default: 'Default',
opus: 'Opus 4.6', opus: 'Opus 4.6',
sonnet: 'Sonnet 4.5', sonnet: 'Sonnet 4.6',
haiku: 'Haiku 4.5', haiku: 'Haiku 4.5',
}; };
const modelLabel = MODEL_LABELS[launchParams.model] ?? launchParams.model; const modelLabel = MODEL_LABELS[launchParams.model] ?? launchParams.model;

View file

@ -546,7 +546,10 @@ export const CreateTeamDialog = ({
[memberColorMap, members, soloTeam] [memberColorMap, members, soloTeam]
); );
const effectiveModel = useMemo(() => computeEffectiveTeamModel(selectedModel), [selectedModel]); const effectiveModel = useMemo(
() => computeEffectiveTeamModel(selectedModel, limitContext),
[selectedModel, limitContext]
);
const sanitizedTeamName = sanitizeTeamName(teamName.trim()); const sanitizedTeamName = sanitizeTeamName(teamName.trim());
@ -561,7 +564,6 @@ export const CreateTeamDialog = ({
model: effectiveModel, model: effectiveModel,
effort: (selectedEffort as EffortLevel) || undefined, effort: (selectedEffort as EffortLevel) || undefined,
skipPermissions, skipPermissions,
limitContext: limitContext || undefined,
worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined, worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined,
extraCliArgs: customArgs.trim() || undefined, extraCliArgs: customArgs.trim() || undefined,
}), }),
@ -576,7 +578,6 @@ export const CreateTeamDialog = ({
effectiveModel, effectiveModel,
selectedEffort, selectedEffort,
skipPermissions, skipPermissions,
limitContext,
worktreeEnabled, worktreeEnabled,
worktreeName, worktreeName,
customArgs, customArgs,
@ -985,6 +986,7 @@ export const CreateTeamDialog = ({
id="create-limit-context" id="create-limit-context"
checked={limitContext} checked={limitContext}
onCheckedChange={setLimitContext} onCheckedChange={setLimitContext}
disabled={selectedModel === 'haiku'}
/> />
<SkipPermissionsCheckbox <SkipPermissionsCheckbox
id="create-skip-permissions" id="create-skip-permissions"

View file

@ -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); const model = computeEffectiveTeamModel(selectedModel, limitContext);
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, selectedEffort, clearContext]); }, [isLaunch, skipPermissions, selectedModel, limitContext, selectedEffort, clearContext]);
const launchOptionalSummary = useMemo(() => { const launchOptionalSummary = useMemo(() => {
if (!isLaunch) return []; if (!isLaunch) return [];
@ -590,10 +590,9 @@ 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), model: computeEffectiveTeamModel(selectedModel, limitContext),
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,
@ -958,6 +957,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
id="launch-limit-context" id="launch-limit-context"
checked={limitContext} checked={limitContext}
onCheckedChange={setLimitContext} onCheckedChange={setLimitContext}
disabled={selectedModel === 'haiku'}
/> />
<SkipPermissionsCheckbox <SkipPermissionsCheckbox
id="dialog-skip-permissions" id="dialog-skip-permissions"

View file

@ -30,6 +30,7 @@ export const LimitContextCheckbox: React.FC<LimitContextCheckboxProps> = ({
}`} }`}
> >
Limit context to 200K tokens Limit context to 200K tokens
{disabled && <span className="text-[10px] italic">(always 200K for this model)</span>}
</Label> </Label>
</div> </div>
); );

View file

@ -78,16 +78,24 @@ const ACTIVE_PROVIDER = PROVIDERS[0];
const MODEL_OPTIONS = [ const MODEL_OPTIONS = [
{ value: '', label: 'Default' }, { value: '', label: 'Default' },
{ value: 'opus', label: 'Opus 4.6' }, { value: 'opus', label: 'Opus 4.6' },
{ value: 'sonnet', label: 'Sonnet 4.5' }, { value: 'sonnet', label: 'Sonnet 4.6' },
{ value: 'haiku', label: 'Haiku 4.5' }, { value: 'haiku', label: 'Haiku 4.5' },
] as const; ] as const;
/** /**
* Returns the effective model string for team provisioning. * Computes the effective model string for team provisioning.
* Simply maps empty selection to undefined. * By default adds [1m] suffix for 1M context (Opus/Sonnet).
* When limitContext=true, returns base model without [1m] (200K context).
* Haiku does not support 1M always returned as-is.
*/ */
export function computeEffectiveTeamModel(selectedModel: string): string | undefined { export function computeEffectiveTeamModel(
return selectedModel || undefined; selectedModel: string,
limitContext: boolean
): string | undefined {
const base = selectedModel || undefined;
if (limitContext) return base;
if (base === 'haiku') return base;
return base ? `${base}[1m]` : 'sonnet[1m]';
} }
export interface TeamModelSelectorProps { export interface TeamModelSelectorProps {

View file

@ -10,6 +10,7 @@ interface KanbanColumnProps {
className?: string; className?: string;
headerClassName?: string; headerClassName?: string;
bodyClassName?: string; bodyClassName?: string;
headerDragClassName?: string;
headerAccessory?: React.ReactNode; headerAccessory?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
} }
@ -23,6 +24,7 @@ export const KanbanColumn = ({
className, className,
headerClassName, headerClassName,
bodyClassName, bodyClassName,
headerDragClassName,
headerAccessory, headerAccessory,
children, children,
}: KanbanColumnProps): React.JSX.Element => { }: KanbanColumnProps): React.JSX.Element => {
@ -35,20 +37,27 @@ export const KanbanColumn = ({
)} )}
style={bodyBg ? { backgroundColor: bodyBg } : undefined} style={bodyBg ? { backgroundColor: bodyBg } : undefined}
> >
{count > 0 && (
<Badge
variant="secondary"
className="absolute -right-2 -top-2 z-10 min-w-5 px-1.5 py-0 text-[10px] font-medium leading-5"
>
{count}
</Badge>
)}
<header <header
className={cn('border-b border-[var(--color-border)] px-3 py-2 pr-14', headerClassName)} className={cn(
'border-b border-[var(--color-border)] px-3 py-2',
headerClassName,
headerDragClassName
)}
style={headerBg ? { backgroundColor: headerBg } : undefined} style={headerBg ? { backgroundColor: headerBg } : undefined}
> >
<h4 className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text)]"> <h4 className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text)]">
{icon} {icon}
{title} {title}
</h4> </h4>
<div className="absolute right-2 top-2 z-10 flex items-center gap-2"> {headerAccessory && <div className="absolute right-2 top-2 z-10">{headerAccessory}</div>}
{headerAccessory}
<Badge variant="secondary" className="px-2 py-0.5 text-[10px] font-normal">
{count}
</Badge>
</div>
</header> </header>
<div className={cn('flex max-h-[480px] flex-col gap-1.5 overflow-auto p-2', bodyClassName)}> <div className={cn('flex max-h-[480px] flex-col gap-1.5 overflow-auto p-2', bodyClassName)}>
{children} {children}

View file

@ -4,7 +4,6 @@ import ReactGridLayout, { WidthProvider } from 'react-grid-layout/legacy';
import { usePersistedGridLayout } from '@renderer/hooks/usePersistedGridLayout'; import { usePersistedGridLayout } from '@renderer/hooks/usePersistedGridLayout';
import { browserGridLayoutRepository } from '@renderer/services/layout-system/BrowserGridLayoutRepository'; import { browserGridLayoutRepository } from '@renderer/services/layout-system/BrowserGridLayoutRepository';
import { GripVertical } from 'lucide-react';
import { KanbanColumn } from './KanbanColumn'; import { KanbanColumn } from './KanbanColumn';
@ -213,15 +212,7 @@ const LoadedKanbanGridLayout = ({
className="flex h-full min-h-0 flex-col" className="flex h-full min-h-0 flex-col"
headerClassName="shrink-0" headerClassName="shrink-0"
bodyClassName="kanban-grid-no-drag min-h-0 max-h-none flex-1" bodyClassName="kanban-grid-no-drag min-h-0 max-h-none flex-1"
headerAccessory={ headerDragClassName="kanban-grid-drag-handle cursor-grab active:cursor-grabbing"
<button
type="button"
className="kanban-grid-drag-handle inline-flex cursor-grab items-center justify-center rounded-sm p-1 text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text)] active:cursor-grabbing"
aria-label={`Drag ${column.title} column`}
>
<GripVertical size={14} />
</button>
}
> >
{column.content} {column.content}
</KanbanColumn> </KanbanColumn>

View file

@ -542,15 +542,16 @@ function saveLaunchParams(teamName: string, params: TeamLaunchParams): void {
} }
/** /**
* Parse raw model string simply returns the base model name. * Parse raw model string from TeamCreateRequest/TeamLaunchRequest.
* The [1m] suffix is no longer used; context limiting is handled via env var. * E.g. 'opus[1m]' { model: 'opus', limitContext: false }
* 'sonnet' { model: 'sonnet', limitContext: true }
* Models without [1m] suffix mean context is limited to 200K.
*/ */
function parseModelString(raw?: string): { model?: string } { function parseModelString(raw?: string): { model?: string; limitContext: boolean } {
if (!raw) return {}; if (!raw) return { limitContext: true };
// 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] }; if (match) return { model: match[1], limitContext: false };
return { model: raw }; return { model: raw, limitContext: true };
} }
function loadToolApprovalSettings(): ToolApprovalSettings { function loadToolApprovalSettings(): ToolApprovalSettings {
@ -1422,11 +1423,11 @@ 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, limit context) // Persist per-team launch params (model, effort, limit context)
const { model: baseModel } = parseModelString(request.model); const { model: baseModel, limitContext } = parseModelString(request.model);
const params: TeamLaunchParams = { const params: TeamLaunchParams = {
model: baseModel || 'default', model: baseModel || 'default',
effort: request.effort, effort: request.effort,
limitContext: request.limitContext, limitContext,
}; };
saveLaunchParams(request.teamName, params); saveLaunchParams(request.teamName, params);
set((state) => ({ set((state) => ({
@ -1559,11 +1560,11 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
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, limit context) // Persist per-team launch params (model, effort, limit context)
const { model: baseModel } = parseModelString(request.model); const { model: baseModel, limitContext } = parseModelString(request.model);
const params: TeamLaunchParams = { const params: TeamLaunchParams = {
model: baseModel || 'default', model: baseModel || 'default',
effort: request.effort, effort: request.effort,
limitContext: request.limitContext, limitContext,
}; };
saveLaunchParams(request.teamName, params); saveLaunchParams(request.teamName, params);
set((state) => ({ set((state) => ({

View file

@ -401,8 +401,6 @@ export interface TeamLaunchRequest {
effort?: EffortLevel; effort?: EffortLevel;
/** When true, skip --resume and start a fresh session (clears context memory). */ /** When true, skip --resume and start a fresh session (clears context memory). */
clearContext?: boolean; clearContext?: boolean;
/** When true, set CLAUDE_CODE_DISABLE_1M_CONTEXT=1 to limit context to 200K tokens. */
limitContext?: boolean;
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */ /** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
skipPermissions?: boolean; skipPermissions?: boolean;
/** Worktree name — CLI: --worktree <name>. */ /** Worktree name — CLI: --worktree <name>. */
@ -525,8 +523,6 @@ export interface TeamCreateRequest {
prompt?: string; prompt?: string;
model?: string; model?: string;
effort?: EffortLevel; effort?: EffortLevel;
/** When true, set CLAUDE_CODE_DISABLE_1M_CONTEXT=1 to limit context to 200K tokens. */
limitContext?: boolean;
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */ /** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
skipPermissions?: boolean; skipPermissions?: boolean;
/** Worktree name — CLI: --worktree <name>. */ /** Worktree name — CLI: --worktree <name>. */