Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions frontend/src/components/TaskBoardSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function TaskBoardSection() {
pauseLoop,
resumeLoop,
stopLoop,
setSpawnEnabled,
approveTask,
rejectTask,
approveSpec,
Expand All @@ -47,7 +48,7 @@ export function TaskBoardSection() {
[deleteTask],
);

const { state, progress, awaitingApproval } = loopStatus;
const { state, progress, awaitingApproval, spawnEnabled } = loopStatus;

// Count items needing attention
const needsAttention = tasks.filter(
Expand All @@ -60,9 +61,18 @@ export function TaskBoardSection() {
badge={needsAttention || undefined}
storageKey="cc-taskboard"
actions={
<button className="cc-section-action-btn" onClick={refresh} title="Refresh">
&#x21bb;
</button>
<>
<button
className={`cc-section-action-btn ${spawnEnabled ? 'cc-spawn-enabled' : 'cc-spawn-disabled'}`}
onClick={() => setSpawnEnabled(!spawnEnabled)}
title={spawnEnabled ? 'Disable session spawning' : 'Enable session spawning'}
>
{spawnEnabled ? '\u26A1' : '\u26D4'}
</button>
<button className="cc-section-action-btn" onClick={refresh} title="Refresh">
&#x21bb;
</button>
</>
}
>
{/* Compact loop bar */}
Expand Down
8 changes: 0 additions & 8 deletions frontend/src/components/TokenBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,6 @@ export function TokenBar({ tokenState }: Props) {
// Show only session total in that case — the agent context bar is meaningless.
const isCompleted = agentContext === 0 && sessionTotal > 0;

if (tokenState.compacting) {
return (
<div className="token-bar token-bar--compacting" aria-label="Compacting context">
<span className="token-bar-compacting-label">COMPACTING</span>
</div>
);
}

return (
<>
<button
Expand Down
11 changes: 0 additions & 11 deletions frontend/src/components/__tests__/TokenBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ function makeState(overrides: Partial<TokenState> = {}): TokenState {
numTurns: 0,
turnIndex: 0,
numCompactions: 0,
compacting: false,
...overrides,
};
}
Expand Down Expand Up @@ -105,16 +104,6 @@ describe('TokenBar', () => {
expect(screen.getByText(/Session tokens/)).toBeTruthy();
});

it('shows compacting indicator when compacting is true', () => {
const { container } = render(
<TokenBar tokenState={makeState({ agentContext: 170000, turnIndex: 3, compacting: true })} />,
);
expect(container.querySelector('.token-bar--compacting')).toBeTruthy();
expect(screen.getByText(/COMPACTING/)).toBeTruthy();
// Normal token display should not be visible
expect(screen.queryByText(/170k/)).toBeNull();
});

it('expands detail panel on tap', () => {
render(
<TokenBar
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/__tests__/useTaskBoard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ describe('useTaskBoard', () => {
progress: null,
specMode: false,
awaitingApproval: false,
spawnEnabled: false,
});

const { result } = renderHook(() => useTaskBoard(), { wrapper });
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/hooks/useTaskBoard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export interface UseTaskBoardResult {
pauseLoop: () => Promise<void>;
resumeLoop: () => Promise<void>;
stopLoop: () => Promise<void>;
setSpawnEnabled: (enabled: boolean) => Promise<void>;
approveTask: (id: string) => Promise<void>;
rejectTask: (id: string, feedback: string) => Promise<void>;
approveSpec: () => Promise<void>;
Expand All @@ -170,6 +171,7 @@ export function useTaskBoard(): UseTaskBoardResult {
const storePauseLoop = useMitzoStore((s) => s.pauseLoop);
const storeResumeLoop = useMitzoStore((s) => s.resumeLoop);
const storeStopLoop = useMitzoStore((s) => s.stopLoop);
const storeSetSpawnEnabled = useMitzoStore((s) => s.setSpawnEnabled);
const storeApproveTask = useMitzoStore((s) => s.approveTask);
const storeRejectTask = useMitzoStore((s) => s.rejectTask);
const storeApproveSpec = useMitzoStore((s) => s.approveSpec);
Expand Down Expand Up @@ -237,6 +239,7 @@ export function useTaskBoard(): UseTaskBoardResult {
pauseLoop: storePauseLoop,
resumeLoop: storeResumeLoop,
stopLoop: storeStopLoop,
setSpawnEnabled: storeSetSpawnEnabled,
approveTask: storeApproveTask,
rejectTask: storeRejectTask,
approveSpec: storeApproveSpec,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/__tests__/DesktopChatView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function createMockStore() {
progress: null,
specMode: false,
awaitingApproval: false,
spawnEnabled: false,
},
},
workload: { items: [], profiles: [] },
Expand All @@ -112,7 +113,6 @@ function createMockStore() {
numTurns: 0,
turnIndex: 0,
numCompactions: 0,
compacting: false,
},
progress: { blocks: {}, toolIndex: {} },
sendError: null,
Expand All @@ -137,6 +137,7 @@ function createMockStore() {
pauseLoop: vi.fn().mockResolvedValue(undefined),
resumeLoop: vi.fn().mockResolvedValue(undefined),
stopLoop: vi.fn().mockResolvedValue(undefined),
setSpawnEnabled: vi.fn().mockResolvedValue(undefined),
approveTask: vi.fn().mockResolvedValue(undefined),
rejectTask: vi.fn().mockResolvedValue(undefined),
approveSpec: vi.fn().mockResolvedValue(undefined),
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/__tests__/TaskBoard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ vi.mock('../../hooks/useTaskBoard', () => ({
progress: null,
specMode: false,
awaitingApproval: false,
spawnEnabled: false,
},
createTask: mockCreateTask,
updateTask: mockUpdateTask,
Expand Down
46 changes: 10 additions & 36 deletions frontend/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ a {
text-decoration: none;
}

.cc-spawn-enabled {
color: var(--task-active);
}

.cc-spawn-disabled {
color: var(--danger);
}

a:hover {
text-decoration: underline;
}
Expand Down Expand Up @@ -1475,9 +1483,6 @@ textarea:focus {
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
position: relative;
z-index: 20;
box-shadow: 0 4px 0 0 var(--surface);
transition:
max-height 0.2s,
padding 0.2s,
Expand Down Expand Up @@ -1669,7 +1674,7 @@ textarea:focus {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 0 var(--space-3) var(--space-4);
padding: var(--space-4) var(--space-3);
display: flex;
flex-direction: column;
gap: var(--space-3);
Expand Down Expand Up @@ -2430,10 +2435,8 @@ textarea:focus {
position: sticky;
top: 0;
z-index: 10;
margin: -1px calc(-1 * var(--space-3)) 0;
padding: 0 var(--space-3);
border-bottom: 1px solid var(--border);
background: var(--surface);
background: var(--bg-primary);
}

.session-banner-header {
Expand Down Expand Up @@ -3467,35 +3470,6 @@ textarea:focus {
animation: token-flash 0.8s ease-in-out infinite alternate;
}

.token-bar--compacting {
color: #fbbf24;
border-color: rgba(251, 191, 36, 0.6);
background: rgba(251, 191, 36, 0.15);
animation: compaction-pulse 1s ease-in-out infinite alternate;
cursor: default;
pointer-events: none;
}

.token-bar-compacting-label {
font-weight: 700;
font-size: var(--text-xxs);
letter-spacing: 0.12em;
text-transform: uppercase;
}

@keyframes compaction-pulse {
from {
opacity: 1;
border-color: rgba(251, 191, 36, 0.6);
box-shadow: 0 0 6px rgba(251, 191, 36, 0.3);
}
to {
opacity: 0.5;
border-color: rgba(251, 191, 36, 0.3);
box-shadow: 0 0 2px rgba(251, 191, 36, 0.1);
}
}

@keyframes token-flash {
from {
opacity: 1;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface LoopStatus {
progress: { done: number; total: number } | null;
specMode: boolean;
awaitingApproval: boolean;
spawnEnabled: boolean;
}

export interface Task {
Expand Down
7 changes: 1 addition & 6 deletions frontend/src/types/ws-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ export type ServerMessage =
| TaskUpdatedMsg
| TaskDeletedMsg
| TokenUpdateMsg
| CompactionStatusMsg
| LoopStatusMsg
| ProgressStartMsg
| ProgressUpdateMsg
Expand Down Expand Up @@ -302,11 +301,6 @@ export interface TokenUpdateMsg {
turnIndex: number;
}

export interface CompactionStatusMsg {
type: 'compaction_status';
active: boolean;
}

export interface LoopStatusMsg {
type: 'loop_status';
state: 'idle' | 'running' | 'paused';
Expand All @@ -315,6 +309,7 @@ export interface LoopStatusMsg {
progress: { done: number; total: number } | null;
specMode: boolean;
awaitingApproval: boolean;
spawnEnabled: boolean;
}

// Subagent lifecycle events
Expand Down
21 changes: 2 additions & 19 deletions packages/client/__tests__/protocol-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,14 +473,15 @@ describe('task system messages', () => {
progress: { done: 2, total: 5 },
specMode: false,
awaitingApproval: false,
spawnEnabled: false,
},
makeState(),
makeCallbacks(),
POOL_KEY,
);
expect(r.tasksUpdate).toMatchObject({
type: 'loop_status',
status: { state: 'running', goalId: 'g1' },
status: { state: 'running', goalId: 'g1', spawnEnabled: false },
});
});
});
Expand Down Expand Up @@ -533,24 +534,6 @@ describe('token_update', () => {
expect('sessionTotal' in r.tokensUpdate!).toBe(false);
expect('numTurns' in r.tokensUpdate!).toBe(false);
});

it('compaction_status sets compacting flag on tokensUpdate', () => {
const active = parseServerMessage(
{ type: 'compaction_status', active: true },
makeState(),
makeCallbacks(),
POOL_KEY,
);
expect(active.tokensUpdate).toEqual({ compacting: true });

const done = parseServerMessage(
{ type: 'compaction_status', active: false },
makeState(),
makeCallbacks(),
POOL_KEY,
);
expect(done.tokensUpdate).toEqual({ compacting: false });
});
});

// ─── Inbox ────────────────────────────────────────────────────────────────────
Expand Down
12 changes: 12 additions & 0 deletions packages/client/src/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export class MitzoApiClient {
progress?: { completed: number; total: number } | null;
specMode?: boolean;
awaitingApproval?: boolean;
spawnEnabled?: boolean;
}> {
const res = await this.assertOk(
await this.fetch('/api/loop/status', { credentials: 'include' }),
Expand Down Expand Up @@ -318,6 +319,17 @@ export class MitzoApiClient {
);
}

async setSpawnEnabled(enabled: boolean): Promise<void> {
await this.assertOk(
await this.fetch('/api/loop/spawn', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ enabled }),
}),
);
}

async approveSpec(): Promise<void> {
await this.assertOk(
await this.fetch('/api/loop/spec/approve', { method: 'POST', credentials: 'include' }),
Expand Down
5 changes: 1 addition & 4 deletions packages/client/src/protocol-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ export function parseServerMessage(
progress: (msg.progress as LoopStatus['progress']) ?? null,
specMode: (msg.specMode as boolean) ?? false,
awaitingApproval: (msg.awaitingApproval as boolean) ?? false,
spawnEnabled: (msg.spawnEnabled as boolean) ?? false,
},
};
break;
Expand Down Expand Up @@ -516,10 +517,6 @@ export function parseServerMessage(
break;
}

case 'compaction_status':
result.tokensUpdate = { compacting: Boolean(msg.active) };
break;

// Progress tracking messages
case 'progress_start':
result.progressUpdate = {
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/slices/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface LoopStatus {
progress: { done: number; total: number } | null;
specMode: boolean;
awaitingApproval: boolean;
spawnEnabled: boolean;
}

export interface TasksState {
Expand All @@ -62,5 +63,6 @@ export const INITIAL_TASKS_STATE: TasksState = {
progress: null,
specMode: false,
awaitingApproval: false,
spawnEnabled: false,
},
};
2 changes: 0 additions & 2 deletions packages/client/src/slices/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export interface TokensState {
numTurns: number;
turnIndex: number;
numCompactions: number;
compacting: boolean;
}

export const DEFAULT_CONTEXT_CEILING = 200_000;
Expand All @@ -17,5 +16,4 @@ export const INITIAL_TOKENS_STATE: TokensState = {
numTurns: 0,
turnIndex: 0,
numCompactions: 0,
compacting: false,
};
Loading
Loading