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();
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 | ` +

View file

@ -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');
}
}

View file

@ -1168,7 +1168,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
const MODEL_LABELS: Record<string, string> = {
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;

View file

@ -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'}
/>
<SkipPermissionsCheckbox
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('--mcp-config', '<auto>', '--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', '<previous>');
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'}
/>
<SkipPermissionsCheckbox
id="dialog-skip-permissions"

View file

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

View file

@ -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 {

View file

@ -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 && (
<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
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}
>
<h4 className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text)]">
{icon}
{title}
</h4>
<div className="absolute right-2 top-2 z-10 flex items-center gap-2">
{headerAccessory}
<Badge variant="secondary" className="px-2 py-0.5 text-[10px] font-normal">
{count}
</Badge>
</div>
{headerAccessory && <div className="absolute right-2 top-2 z-10">{headerAccessory}</div>}
</header>
<div className={cn('flex max-h-[480px] flex-col gap-1.5 overflow-auto p-2', bodyClassName)}>
{children}

View file

@ -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={
<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>
}
headerDragClassName="kanban-grid-drag-handle cursor-grab active:cursor-grabbing"
>
{column.content}
</KanbanColumn>

View file

@ -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<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (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) => ({

View file

@ -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 <name>. */
@ -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 <name>. */