Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
934d527
Fix whisper mode hiding rich final answer split by hidden tool
plusplusoneplusplus May 31, 2026
2870cba
Write Ralph grilling goal file under notes/Plans, not the repo
plusplusoneplusplus May 31, 2026
31e62bd
docs(classify-diff): add getter/setter classification heuristic
plusplusoneplusplus May 31, 2026
f67e433
fix(classification): eliminate cross-PR state bleed in useClassificat…
plusplusoneplusplus May 31, 2026
611ce66
feat(coc): AC-02 self-heal stale .pending markers in generic classifi…
plusplusoneplusplus May 31, 2026
da79187
feat(coc): AC-03 per-invocation provider/model selection for classify…
plusplusoneplusplus May 31, 2026
ed164a3
fix(spa): resolve TDZ bug in PullRequestDetail and fix test mocks
plusplusoneplusplus May 31, 2026
05ee357
fix(pr-popout-chat): use pullRequestChat context so backend emits PR …
plusplusoneplusplus May 31, 2026
7f66ba3
feat: give commit review popout structural parity with PR review popout
plusplusoneplusplus May 31, 2026
d433f74
feat(coc): AC-05 give embedded CommitDetail classification/review-pro…
plusplusoneplusplus May 31, 2026
f745659
test(spa): fix 6 failing tests after commit-review parity refactor
plusplusoneplusplus May 31, 2026
3703071
fix(coc): persist token usage for queue-dispatched chats and Ralph it…
plusplusoneplusplus May 31, 2026
6a44bf3
feat(work-items): add workItems.aiAuthoring feature flag and AI draft…
plusplusoneplusplus May 31, 2026
8846925
feat(work-items): implement LLM generator for AI authoring draft endp…
plusplusoneplusplus May 31, 2026
1d8af08
feat(work-items): AI authoring composer UI and coc-client typed methods
plusplusoneplusplus May 31, 2026
9fc6733
feat(ai-authoring): add Create with AI to hierarchy tree and admin to…
plusplusoneplusplus May 31, 2026
2eb6d2b
test(work-items): add hierarchy smoke tests and AC compliance checks …
plusplusoneplusplus May 31, 2026
041ae1d
refactor(admin): move Providers and Servers from Connections to Confi…
plusplusoneplusplus May 31, 2026
899f78d
fix(whisper): detect bash tool commits when whisper group is expanded
plusplusoneplusplus May 31, 2026
4e43a0c
fix: register workItems.aiAuthoring.enabled in ADMIN_CONFIG_FIELDS so…
plusplusoneplusplus May 31, 2026
b69b94d
fix: normalize tool names to lowercase in commit/PR detection
plusplusoneplusplus May 31, 2026
63bc8e3
feat: capture token breakdown (system/tool/conversation) in data pipe…
plusplusoneplusplus May 31, 2026
7fb79a6
feat(coc-spa): segmented context-window bar with breakdown popover
plusplusoneplusplus May 31, 2026
c260ed4
fix(coc): widen ChatDetail load-effect substring to 1200 chars
plusplusoneplusplus May 31, 2026
45eb33e
feat(spa): always show ctx hover popover with total fallback
plusplusoneplusplus May 31, 2026
02ff866
feat(spa): redesign work items hierarchy UI to match mockup
plusplusoneplusplus May 31, 2026
4894792
scripts: add coc.service.template for Linux systemd daemon setup
plusplusoneplusplus May 31, 2026
892dffa
fix(spa): allow CTX popover to escape ComposerMetaStrip overflow clip
plusplusoneplusplus May 31, 2026
150c5cc
test(config): update inline snapshot and all-sources test for workIte…
plusplusoneplusplus May 31, 2026
6edbaec
test(ralph): fix flaky ENOTEMPTY in queue continuity AC-01 teardown
plusplusoneplusplus May 31, 2026
68ab82f
fix(coc): harden AssistantStatsBadge against partial tokenUsage
plusplusoneplusplus May 31, 2026
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
2 changes: 1 addition & 1 deletion .github/skills/coc-knowledge/references/admin-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The admin page is structured as a two-column shell that fills the available vert
- `<div className="ar-shell">` is a CSS grid (`var(--ar-sidebar-w)` + `1fr`) with `height: 100%; min-height: 0; overflow: hidden`.
- `<aside className="ar-sidebar">` fills the grid row (`height: 100%; min-height: 0`) and only scrolls internally if its own brand/nav/stats stack ever exceeds the viewport. It is **not** sticky and must not use `100vh` — both would break inside the `AdminDialog` and any nested pane whose container height differs from the viewport.
- `<main className="ar-main">` is the **single scroll region** of the admin route: `min-height: 0; height: 100%; overflow-y: auto`. The sticky topbar (`.ar-topbar` with the `.ar-breadcrumb`) pins to the top of this scroller, and the page body (`.ar-page` with `.ar-page-header` + cards) flows underneath.
- The tabs live in the sidebar as `.ar-nav-item` buttons grouped by user task: Configure, Connections, Operations, and Developer / Internals. The grouped nav can mix admin sections (`admin-tab-*` data-testids), promoted settings sections (`settings-subtab-*` data-testids), and embedded tool routes (`skills-toggle`, `logs-toggle`, `stats-toggle`, `models-toggle`, `servers-toggle`). A grouped `.ar-mobile-tab-select` appears only under the responsive `@media (max-width: 600px)` rule, which hides the sidebar and falls back to a `<select>` with `<optgroup>` labels — the main pane still scrolls internally on mobile.
- The tabs live in the sidebar as `.ar-nav-item` buttons grouped by user task: Configure, Knowledge, Connections (container-only), Operations, and Developer / Internals. The Configure group contains the settings entry, AI Provider, Providers, and Servers. The Connections group only appears in container mode (for Messaging and container Agents). The grouped nav can mix admin sections (`admin-tab-*` data-testids), promoted settings sections (`settings-subtab-*` data-testids), and embedded tool routes (`skills-toggle`, `logs-toggle`, `stats-toggle`, `models-toggle`, `servers-toggle`). A grouped `.ar-mobile-tab-select` appears only under the responsive `@media (max-width: 600px)` rule, which hides the sidebar and falls back to a `<select>` with `<optgroup>` labels — the main pane still scrolls internally on mobile.

### Settings Sub-Tabs

Expand Down
9 changes: 9 additions & 0 deletions .github/skills/coc-knowledge/references/dashboard-spa.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ Inside `WhisperCollapsedGroup`, tool calls render as compact "whisper-row" varia
- Single flat row: kind pill + truncated summary + duration + chevron
- Color-coded pills: Read/blue, Grep/Glob/green, Edit/Write/amber, Shell/PS/SQL/purple, Skill/grey

In whisper mode (`toolCompactness === 3`), `filterWhisperChunks` keeps a tail of
the final assistant message plus any `task_complete`/visible `ask_user` chunks,
collapsing everything else into one summary group. The final message is the last
`content` chunk plus earlier content chunks separated from it only by
non-breaking trailing tools (`suggest_follow_ups`, `report_intent`,
`task_complete`, `ask_user`); the walk-back stops at the first substantive
tool/tool-group. This keeps a rich answer visible even when a hidden
`suggest_follow_ups` call splits it from a trivial closing line.

Chat commit strips are detected from real shell output on `powershell`, `shell`,
and `bash` tool calls. The detector only treats commit-creating commands
(`git commit`, `git merge`, `git cherry-pick`, `git revert`) with native git
Expand Down
14 changes: 14 additions & 0 deletions .github/skills/coc-knowledge/references/ralph.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ The SPA shows a **"Promote to Ralph"** pill in the follow-up area for eligible
chats and calls this endpoint via `coc-client`'s `processes.promoteToRalph`
helper.

### Grilling-Phase Prompt Injection

During the `grilling` phase, `chat-base-executor` prepends a directive to the
**user message** (never the system message) via `buildRalphGrillSuffix(...)`
(`packages/coc/src/server/executors/chat-base-executor.ts`). It carries the
`ultra-ralph` grill-section pointer, the `## Goal` machine contract, and — when
an `AutoFolderContext` resolves — an explicit goal-file save-location directive
pointing at the repo's `notes/Plans` root (`~/.coc/repos/<workspaceId>/notes/Plans/`)
with a `*.goal.md` filename. This keeps the goal file out of the repository
working tree and lets the Notes/scratchpad UI open and edit it (`isGoalFile`
detects `*.goal.md`). The generic bundled `grill-me` skill stays host-agnostic:
it defers to whatever save location the host supplies and only falls back to a
working-directory-relative `Plans/<area>/<feature>/` when none is given.

## Resume Routes

Session resume endpoints share infrastructure in
Expand Down
11 changes: 11 additions & 0 deletions .github/skills/coc-knowledge/references/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ See [mcp-settings.md](mcp-settings.md).
| PATCH | `/api/workspaces/:id/work-items/:itemId` | Update work item |
| DELETE | `/api/workspaces/:id/work-items/:itemId` | Delete work item |

### AI Authoring (gated by `workItems.aiAuthoring` flag, default `false`)

Draft generation is ephemeral — no data is persisted until the caller explicitly applies it via the standard create/update/plan endpoints.

Response shape: `{ kind: 'clarification', questions: string[], clarificationCount: number }` or `{ kind: 'draft', workItem: {...}, goal?: string, childTasks?: [...] }`.

| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/workspaces/:id/work-items/ai-draft` | Generate a draft for a **new** work item from a prompt. Body: `{ prompt, type?, parentId?, clarificationAnswers?, clarificationCount? }`. Returns clarification (up to 3 rounds) or a draft. |
| POST | `/api/workspaces/:id/work-items/:itemId/ai-draft` | Generate an **improvement** draft for an existing work item. Body: `{ prompt, targets?: ['fields','goal','childTasks'], clarificationAnswers?, clarificationCount? }`. Returns clarification or a draft. |

## Seen State

| Method | Path | Description |
Expand Down
12 changes: 12 additions & 0 deletions packages/coc-agent-sdk/src/session-telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class SessionTelemetry {
private usageTurnCount = 0;
private usageTokenLimit: number | undefined;
private usageCurrentTokens: number | undefined;
private usageSystemTokens: number | undefined;
private usageToolDefinitionsTokens: number | undefined;
private usageConversationTokens: number | undefined;

// ── Tool call tracking ──────────────────────────────────────────────────
readonly activeToolCalls = new Map<string, ActiveToolCall>();
Expand Down Expand Up @@ -69,9 +72,15 @@ export class SessionTelemetry {
recordUsageInfo(data: {
tokenLimit?: number;
currentTokens?: number;
systemTokens?: number;
toolDefinitionsTokens?: number;
conversationTokens?: number;
}): void {
if (data.tokenLimit != null) { this.usageTokenLimit = data.tokenLimit; }
if (data.currentTokens != null) { this.usageCurrentTokens = data.currentTokens; }
if (data.systemTokens != null) { this.usageSystemTokens = data.systemTokens; }
if (data.toolDefinitionsTokens != null) { this.usageToolDefinitionsTokens = data.toolDefinitionsTokens; }
if (data.conversationTokens != null) { this.usageConversationTokens = data.conversationTokens; }
}

buildTokenUsage(): TokenUsage | undefined {
Expand All @@ -87,6 +96,9 @@ export class SessionTelemetry {
turnCount: this.usageTurnCount,
tokenLimit: this.usageTokenLimit,
currentTokens: this.usageCurrentTokens,
...(this.usageSystemTokens != null ? { systemTokens: this.usageSystemTokens } : {}),
...(this.usageToolDefinitionsTokens != null ? { toolDefinitionsTokens: this.usageToolDefinitionsTokens } : {}),
...(this.usageConversationTokens != null ? { conversationTokens: this.usageConversationTokens } : {}),
};
}

Expand Down
10 changes: 8 additions & 2 deletions packages/coc-agent-sdk/src/streaming-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export interface ISessionEvent {
// Session quota (session.usage_info)
tokenLimit?: number;
currentTokens?: number;
systemTokens?: number;
toolDefinitionsTokens?: number;
conversationTokens?: number;
// Tool execution (tool.execution_start / tool.execution_complete)
toolCallId?: string;
toolName?: string;
Expand Down Expand Up @@ -471,8 +474,11 @@ export class StreamingSession {

private handleUsageInfo(event: ISessionEvent): void {
this.telemetry.recordUsageInfo({
tokenLimit: event.data?.tokenLimit,
currentTokens: event.data?.currentTokens,
tokenLimit: event.data?.tokenLimit,
currentTokens: event.data?.currentTokens,
systemTokens: event.data?.systemTokens,
toolDefinitionsTokens: event.data?.toolDefinitionsTokens,
conversationTokens: event.data?.conversationTokens,
});
const usage = this.telemetry.buildTokenUsage();
this.sessionLog.debug(
Expand Down
6 changes: 6 additions & 0 deletions packages/coc-agent-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,12 @@ export interface TokenUsage {
tokenLimit?: number;
/** Session-level current token count (last seen from session.usage_info) */
currentTokens?: number;
/** Tokens consumed by the system prompt (from session.usage_info breakdown) */
systemTokens?: number;
/** Tokens consumed by tool definitions (from session.usage_info breakdown) */
toolDefinitionsTokens?: number;
/** Tokens consumed by conversation history (from session.usage_info breakdown) */
conversationTokens?: number;
}

// ============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ describe('SessionTelemetry — token usage', () => {
expect(usage!.tokenLimit).toBe(10000);
expect(usage!.currentTokens).toBe(500);
});

it('records usage info breakdown (system, tool, conversation tokens)', () => {
const t = new SessionTelemetry();
t.recordUsage({ inputTokens: 1, outputTokens: 1 });
t.recordUsageInfo({
tokenLimit: 200000,
currentTokens: 70000,
systemTokens: 12400,
toolDefinitionsTokens: 8100,
conversationTokens: 47200,
});

const usage = t.buildTokenUsage();
expect(usage!.tokenLimit).toBe(200000);
expect(usage!.currentTokens).toBe(70000);
expect(usage!.systemTokens).toBe(12400);
expect(usage!.toolDefinitionsTokens).toBe(8100);
expect(usage!.conversationTokens).toBe(47200);
});

it('omits breakdown fields when not provided', () => {
const t = new SessionTelemetry();
t.recordUsage({ inputTokens: 1, outputTokens: 1 });
t.recordUsageInfo({ tokenLimit: 100000, currentTokens: 5000 });

const usage = t.buildTokenUsage();
expect(usage!.systemTokens).toBeUndefined();
expect(usage!.toolDefinitionsTokens).toBeUndefined();
expect(usage!.conversationTokens).toBeUndefined();
});

it('keeps last seen breakdown values across multiple recordUsageInfo calls', () => {
const t = new SessionTelemetry();
t.recordUsage({ inputTokens: 1, outputTokens: 1 });
t.recordUsageInfo({ tokenLimit: 100000, systemTokens: 5000 });
t.recordUsageInfo({ currentTokens: 20000, toolDefinitionsTokens: 3000 });

const usage = t.buildTokenUsage();
expect(usage!.systemTokens).toBe(5000);
expect(usage!.toolDefinitionsTokens).toBe(3000);
expect(usage!.conversationTokens).toBeUndefined();
});
});

describe('SessionTelemetry — tool call tracking', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,50 @@ describe('StreamingSession — token usage', () => {
const result = await promise;
expect(result.tokenUsage).toBeUndefined();
});

it('captures session.usage_info breakdown (system, tool, conversation tokens)', async () => {
const { session, emit } = makeMockSession();
const ss = new StreamingSession();
const promise = ss.run(session, baseOptions());

emit({ type: 'assistant.usage', data: { inputTokens: 1, outputTokens: 1 } });
emit({
type: 'session.usage_info',
data: {
tokenLimit: 200000,
currentTokens: 70000,
systemTokens: 12400,
toolDefinitionsTokens: 8100,
conversationTokens: 47200,
},
});
emit({ type: 'session.idle' });

const result = await promise;
expect(result.tokenUsage).toBeDefined();
expect(result.tokenUsage!.tokenLimit).toBe(200000);
expect(result.tokenUsage!.currentTokens).toBe(70000);
expect(result.tokenUsage!.systemTokens).toBe(12400);
expect(result.tokenUsage!.toolDefinitionsTokens).toBe(8100);
expect(result.tokenUsage!.conversationTokens).toBe(47200);
});

it('handles session.usage_info without breakdown fields', async () => {
const { session, emit } = makeMockSession();
const ss = new StreamingSession();
const promise = ss.run(session, baseOptions());

emit({ type: 'assistant.usage', data: { inputTokens: 1, outputTokens: 1 } });
emit({ type: 'session.usage_info', data: { tokenLimit: 100000, currentTokens: 5000 } });
emit({ type: 'session.idle' });

const result = await promise;
expect(result.tokenUsage!.tokenLimit).toBe(100000);
expect(result.tokenUsage!.currentTokens).toBe(5000);
expect(result.tokenUsage!.systemTokens).toBeUndefined();
expect(result.tokenUsage!.toolDefinitionsTokens).toBeUndefined();
expect(result.tokenUsage!.conversationTokens).toBeUndefined();
});
});

// ─────────────────────────────────────────────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions packages/coc-client/src/contracts/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export interface RuntimeDashboardConfig {
claudeEnabled: boolean;
defaultProvider: 'copilot' | 'codex' | 'claude';
workItemsHierarchyEnabled: boolean;
workItemsAiAuthoringEnabled: boolean;
gitCommitLookupEnabled: boolean;
effortLevelsEnabled: boolean;
};
Expand Down
75 changes: 75 additions & 0 deletions packages/coc-client/src/contracts/work-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,78 @@ export interface WorkItemTreeFilter {
/** When true, items with status "done" are included. Defaults to false. */
includeDone?: boolean;
}

// ============================================================================
// AI Authoring types
// ============================================================================

/** Fields that an AI draft can carry for a work item (all optional). */
export interface WorkItemAiDraftFields {
title?: string;
description?: string;
priority?: WorkItemPriority;
tags?: string[];
/** Markdown plan / goal content (maps to plan.content). */
plan?: string;
/** For goal type items: success criteria. */
successCriteria?: string;
/** Work item type suggested by the AI. */
type?: WorkItemType;
}

/** A drafted child task (leaf work item) for a hierarchy breakdown. */
export interface WorkItemChildTaskDraft {
title: string;
description?: string;
/** 'work-item' or 'bug' — child leaf types. */
type?: 'work-item' | 'bug';
}

/** Response when the AI needs more information before generating a draft. */
export interface WorkItemAiClarificationResponse {
kind: 'clarification';
/** Up to 3 concise clarification questions. */
questions: string[];
/** Total number of clarification rounds completed so far (0-based). */
clarificationCount: number;
}

/** Response when the AI has produced a complete draft. */
export interface WorkItemAiDraftResult {
kind: 'draft';
/** The generated work item fields. */
workItem: WorkItemAiDraftFields;
/** Optional goal/plan markdown stored as plan.content. */
goal?: string;
/** Optional child task breakdown (only when hierarchy is applicable). */
childTasks?: WorkItemChildTaskDraft[];
}

/** Union of all possible AI draft API responses. */
export type WorkItemAiGenerationResponse = WorkItemAiClarificationResponse | WorkItemAiDraftResult;

/** Request body for POST /api/workspaces/:id/work-items/ai-draft */
export interface NewWorkItemAiDraftRequest extends JsonObject {
/** Free-text user prompt describing the feature / problem. Required. */
prompt: string;
/** Hint for the type to generate (defaults to 'work-item'). */
type?: WorkItemType;
/** Parent work item ID for hierarchy context. */
parentId?: string;
/** Answers to previous clarification questions. */
clarificationAnswers?: string[];
/** Number of clarification rounds already completed (0 = first request). */
clarificationCount?: number;
}

/** Request body for POST /api/workspaces/:id/work-items/:workItemId/ai-draft */
export interface ImproveWorkItemAiDraftRequest extends JsonObject {
/** Instruction for what to improve. Required. */
prompt: string;
/** Which aspects to draft ('fields', 'goal', 'childTasks'). Defaults to ['fields', 'goal']. */
targets?: Array<'fields' | 'goal' | 'childTasks'>;
/** Answers to previous clarification questions. */
clarificationAnswers?: string[];
/** Number of clarification rounds already completed. */
clarificationCount?: number;
}
17 changes: 17 additions & 0 deletions packages/coc-client/src/domains/work-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import type {
CreateWorkItemRequest,
ExecuteWorkItemRequest,
ExecuteWorkItemResponse,
ImproveWorkItemAiDraftRequest,
NewWorkItemAiDraftRequest,
RequestWorkItemChangesRequest,
RequestWorkItemChangesResponse,
ResolveWorkItemCommentsRequest,
UpdateWorkItemRequest,
WorkItem,
WorkItemAiGenerationResponse,
WorkItemFilter,
WorkItemGroupedResponse,
WorkItemListResponse,
Expand Down Expand Up @@ -143,4 +146,18 @@ export class WorkItemsClient {
if (filter?.includeDone !== undefined) query.includeDone = filter.includeDone;
return this.transport.request<WorkItemTreeResponse>(path(workspaceId, '/tree'), { query });
}

aiDraft(workspaceId: string, request: NewWorkItemAiDraftRequest): Promise<WorkItemAiGenerationResponse> {
return this.transport.request<WorkItemAiGenerationResponse>(path(workspaceId, '/ai-draft'), {
method: 'POST',
body: { ...request },
});
}

aiImprove(workspaceId: string, workItemId: string, request: ImproveWorkItemAiDraftRequest): Promise<WorkItemAiGenerationResponse> {
return this.transport.request<WorkItemAiGenerationResponse>(
path(workspaceId, `/${encodePathSegment(workItemId)}/ai-draft`),
{ method: 'POST', body: { ...request } },
);
}
}
Loading
Loading