From e6b7a6fe536f84622b622535b5700195092707ed Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 14 Mar 2026 18:24:13 +0200 Subject: [PATCH] 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. --- .../services/team/TeamMemberLogsFinder.ts | 8 ++-- .../services/team/TeamProvisioningService.ts | 38 +++++++------------ .../components/team/TeamDetailView.tsx | 2 +- .../team/dialogs/CreateTeamDialog.tsx | 8 ++-- .../team/dialogs/LaunchTeamDialog.tsx | 8 ++-- .../team/dialogs/LimitContextCheckbox.tsx | 1 + .../team/dialogs/TeamModelSelector.tsx | 18 ++++++--- .../components/team/kanban/KanbanColumn.tsx | 23 +++++++---- .../team/kanban/KanbanGridLayout.tsx | 11 +----- src/renderer/store/slices/teamSlice.ts | 23 +++++------ src/shared/types/team.ts | 4 -- 11 files changed, 70 insertions(+), 74 deletions(-) diff --git a/src/main/services/team/TeamMemberLogsFinder.ts b/src/main/services/team/TeamMemberLogsFinder.ts index 313136b9..b62c7d99 100644 --- a/src/main/services/team/TeamMemberLogsFinder.ts +++ b/src/main/services/team/TeamMemberLogsFinder.ts @@ -151,7 +151,7 @@ export class TeamMemberLogsFinder { const tDiscovery = performance.now(); if (!discovery) { - logger.info( + console.log( `[perf] findLogsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms` ); return []; @@ -310,7 +310,7 @@ export class TeamMemberLogsFinder { ); const tTotal = performance.now(); - logger.info( + console.log( `[perf] findLogsForTask(${taskId}@${teamName}) ` + `total=${(tTotal - t0).toFixed(0)}ms | ` + `discovery=${(tDiscovery - t0).toFixed(0)}ms | ` + @@ -344,7 +344,7 @@ export class TeamMemberLogsFinder { const tDiscovery = performance.now(); if (!discovery) { - logger.info( + console.log( `[perf] findLogFileRefsForTask(${taskId}) discovery=null ${(tDiscovery - t0).toFixed(0)}ms` ); return []; @@ -490,7 +490,7 @@ export class TeamMemberLogsFinder { const sortedRefs = [...refs].sort((a, b) => b.sortTime - a.sortTime); const tTotal = performance.now(); - logger.info( + console.log( `[perf] findLogFileRefsForTask(${taskId}@${teamName}) ` + `total=${(tTotal - t0).toFixed(0)}ms | ` + `discovery=${(tDiscovery - t0).toFixed(0)}ms | ` + diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 0d92932b..c1ba1b6f 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -2779,10 +2779,7 @@ export class TeamProvisioningService { try { child = spawnCli(claudePath, spawnArgs, { cwd: request.cwd, - env: { - ...shellEnv, - ...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}), - }, + env: { ...shellEnv }, stdio: ['pipe', 'pipe', 'pipe'], }); } catch (error) { @@ -2800,10 +2797,7 @@ export class TeamProvisioningService { claudePath, args: spawnArgs, cwd: request.cwd, - env: { - ...shellEnv, - ...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}), - }, + env: { ...shellEnv }, prompt, }; @@ -3186,10 +3180,7 @@ export class TeamProvisioningService { try { child = spawnCli(claudePath, launchArgs, { cwd: request.cwd, - env: { - ...shellEnv, - ...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}), - }, + env: { ...shellEnv }, stdio: ['pipe', 'pipe', 'pipe'], }); } catch (error) { @@ -3209,10 +3200,7 @@ export class TeamProvisioningService { claudePath, args: launchArgs, cwd: request.cwd, - env: { - ...shellEnv, - ...(request.limitContext ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: '1' } : {}), - }, + env: { ...shellEnv }, prompt, }; @@ -4360,7 +4348,7 @@ export class TeamProvisioningService { /** * 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); if (!runId) { return; @@ -4378,25 +4366,25 @@ export class TeamProvisioningService { run.cancelRequested = true; // Note: do NOT call stdin.end() before kill — EOF triggers CLI's graceful // shutdown which deletes team files (config.json, inboxes/, tasks/). - // SIGTERM alone kills the process before cleanup runs, preserving files. - killProcessTree(run.child); + // SIGTERM/SIGKILL kills the process before cleanup runs, preserving files. + killProcessTree(run.child, signal); const progress = updateProgress(run, 'disconnected', 'Team stopped by user'); run.onProgress(progress); 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 - * processes via SIGTERM before the OS closes stdin (which would trigger - * CLI's graceful cleanup and delete team files). + * Stop all running team processes. Called during app shutdown. + * Uses SIGKILL (uncatchable) to guarantee the process dies instantly + * without any cleanup — prevents CLI from deleting team files on exit. */ stopAllTeams(): void { const alive = this.getAliveTeams(); 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) { - this.stopTeam(teamName); + this.stopTeam(teamName, 'SIGKILL'); } } diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 3dca3c4d..2d2d9c9e 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -1168,7 +1168,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele const MODEL_LABELS: Record = { default: 'Default', opus: 'Opus 4.6', - sonnet: 'Sonnet 4.5', + sonnet: 'Sonnet 4.6', haiku: 'Haiku 4.5', }; const modelLabel = MODEL_LABELS[launchParams.model] ?? launchParams.model; diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx index 358c25d5..b76b7cac 100644 --- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx @@ -546,7 +546,10 @@ export const CreateTeamDialog = ({ [memberColorMap, members, soloTeam] ); - const effectiveModel = useMemo(() => computeEffectiveTeamModel(selectedModel), [selectedModel]); + const effectiveModel = useMemo( + () => computeEffectiveTeamModel(selectedModel, limitContext), + [selectedModel, limitContext] + ); const sanitizedTeamName = sanitizeTeamName(teamName.trim()); @@ -561,7 +564,6 @@ export const CreateTeamDialog = ({ model: effectiveModel, effort: (selectedEffort as EffortLevel) || undefined, skipPermissions, - limitContext: limitContext || undefined, worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined, extraCliArgs: customArgs.trim() || undefined, }), @@ -576,7 +578,6 @@ export const CreateTeamDialog = ({ effectiveModel, selectedEffort, skipPermissions, - limitContext, worktreeEnabled, worktreeName, customArgs, @@ -985,6 +986,7 @@ export const CreateTeamDialog = ({ id="create-limit-context" checked={limitContext} onCheckedChange={setLimitContext} + disabled={selectedModel === 'haiku'} /> ', '--disallowedTools', 'TeamDelete,TodoWrite'); if (skipPermissions) args.push('--dangerously-skip-permissions'); - const model = computeEffectiveTeamModel(selectedModel); + const model = computeEffectiveTeamModel(selectedModel, limitContext); if (model) args.push('--model', model); if (selectedEffort) args.push('--effort', selectedEffort); if (!clearContext) args.push('--resume', ''); return args; - }, [isLaunch, skipPermissions, selectedModel, selectedEffort, clearContext]); + }, [isLaunch, skipPermissions, selectedModel, limitContext, selectedEffort, clearContext]); const launchOptionalSummary = useMemo(() => { if (!isLaunch) return []; @@ -590,10 +590,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen teamName: effectiveTeamName, cwd: effectiveCwd, prompt: promptDraft.value.trim() || undefined, - model: computeEffectiveTeamModel(selectedModel), + model: computeEffectiveTeamModel(selectedModel, limitContext), effort: (selectedEffort as EffortLevel) || undefined, clearContext: clearContext || undefined, - limitContext: limitContext || undefined, skipPermissions, worktree: worktreeEnabled && worktreeName.trim() ? worktreeName.trim() : undefined, extraCliArgs: customArgs.trim() || undefined, @@ -958,6 +957,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen id="launch-limit-context" checked={limitContext} onCheckedChange={setLimitContext} + disabled={selectedModel === 'haiku'} /> = ({ }`} > Limit context to 200K tokens + {disabled && (always 200K for this model)} ); diff --git a/src/renderer/components/team/dialogs/TeamModelSelector.tsx b/src/renderer/components/team/dialogs/TeamModelSelector.tsx index 92a5995d..cb2ea94e 100644 --- a/src/renderer/components/team/dialogs/TeamModelSelector.tsx +++ b/src/renderer/components/team/dialogs/TeamModelSelector.tsx @@ -78,16 +78,24 @@ const ACTIVE_PROVIDER = PROVIDERS[0]; const MODEL_OPTIONS = [ { value: '', label: 'Default' }, { value: 'opus', label: 'Opus 4.6' }, - { value: 'sonnet', label: 'Sonnet 4.5' }, + { value: 'sonnet', label: 'Sonnet 4.6' }, { value: 'haiku', label: 'Haiku 4.5' }, ] as const; /** - * Returns the effective model string for team provisioning. - * Simply maps empty selection to undefined. + * Computes the effective model string for team provisioning. + * 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 { - return selectedModel || undefined; +export function computeEffectiveTeamModel( + 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 { diff --git a/src/renderer/components/team/kanban/KanbanColumn.tsx b/src/renderer/components/team/kanban/KanbanColumn.tsx index 27623609..e883e281 100644 --- a/src/renderer/components/team/kanban/KanbanColumn.tsx +++ b/src/renderer/components/team/kanban/KanbanColumn.tsx @@ -10,6 +10,7 @@ interface KanbanColumnProps { className?: string; headerClassName?: string; bodyClassName?: string; + headerDragClassName?: string; headerAccessory?: React.ReactNode; children: React.ReactNode; } @@ -23,6 +24,7 @@ export const KanbanColumn = ({ className, headerClassName, bodyClassName, + headerDragClassName, headerAccessory, children, }: KanbanColumnProps): React.JSX.Element => { @@ -35,20 +37,27 @@ export const KanbanColumn = ({ )} style={bodyBg ? { backgroundColor: bodyBg } : undefined} > + {count > 0 && ( + + {count} + + )}

{icon} {title}

-
- {headerAccessory} - - {count} - -
+ {headerAccessory &&
{headerAccessory}
}
{children} diff --git a/src/renderer/components/team/kanban/KanbanGridLayout.tsx b/src/renderer/components/team/kanban/KanbanGridLayout.tsx index 2b6cf392..7159f93f 100644 --- a/src/renderer/components/team/kanban/KanbanGridLayout.tsx +++ b/src/renderer/components/team/kanban/KanbanGridLayout.tsx @@ -4,7 +4,6 @@ import ReactGridLayout, { WidthProvider } from 'react-grid-layout/legacy'; import { usePersistedGridLayout } from '@renderer/hooks/usePersistedGridLayout'; import { browserGridLayoutRepository } from '@renderer/services/layout-system/BrowserGridLayoutRepository'; -import { GripVertical } from 'lucide-react'; import { KanbanColumn } from './KanbanColumn'; @@ -213,15 +212,7 @@ const LoadedKanbanGridLayout = ({ className="flex h-full min-h-0 flex-col" headerClassName="shrink-0" bodyClassName="kanban-grid-no-drag min-h-0 max-h-none flex-1" - headerAccessory={ - - } + headerDragClassName="kanban-grid-drag-handle cursor-grab active:cursor-grabbing" > {column.content} diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 2c600b90..39e6f410 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -542,15 +542,16 @@ function saveLaunchParams(teamName: string, params: TeamLaunchParams): void { } /** - * Parse raw model string — simply returns the base model name. - * The [1m] suffix is no longer used; context limiting is handled via env var. + * Parse raw model string from TeamCreateRequest/TeamLaunchRequest. + * 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 } { - if (!raw) return {}; - // Strip legacy [1m] suffix if present in saved data +function parseModelString(raw?: string): { model?: string; limitContext: boolean } { + if (!raw) return { limitContext: true }; const match = raw.match(/^(\w+)\[1m\]$/); - if (match) return { model: match[1] }; - return { model: raw }; + if (match) return { model: match[1], limitContext: false }; + return { model: raw, limitContext: true }; } function loadToolApprovalSettings(): ToolApprovalSettings { @@ -1422,11 +1423,11 @@ export const createTeamSlice: StateCreator = (set, const response = await unwrapIpc('team:create', () => api.teams.createTeam(request)); // 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 = { model: baseModel || 'default', effort: request.effort, - limitContext: request.limitContext, + limitContext, }; saveLaunchParams(request.teamName, params); set((state) => ({ @@ -1559,11 +1560,11 @@ export const createTeamSlice: StateCreator = (set, const response = await unwrapIpc('team:launch', () => api.teams.launchTeam(request)); // 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 = { model: baseModel || 'default', effort: request.effort, - limitContext: request.limitContext, + limitContext, }; saveLaunchParams(request.teamName, params); set((state) => ({ diff --git a/src/shared/types/team.ts b/src/shared/types/team.ts index 6cee65c7..bf73edb4 100644 --- a/src/shared/types/team.ts +++ b/src/shared/types/team.ts @@ -401,8 +401,6 @@ export interface TeamLaunchRequest { effort?: EffortLevel; /** When true, skip --resume and start a fresh session (clears context memory). */ 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. */ skipPermissions?: boolean; /** Worktree name — CLI: --worktree . */ @@ -525,8 +523,6 @@ export interface TeamCreateRequest { prompt?: string; model?: string; 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. */ skipPermissions?: boolean; /** Worktree name — CLI: --worktree . */