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
2 changes: 2 additions & 0 deletions .github/skills/coc-knowledge/references/dashboard-spa.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ Focus indicator propagates mode-colored ring from contenteditable to parent card

New chats use `AgentSelectorChip` to choose a per-chat provider. The initial selection comes from the workspace's `lastChatProvider` preference when that provider is enabled and available; otherwise it falls back to the configured `defaultProvider` from runtime config, and then to Copilot if the configured default provider cannot be selected. Follow-up inputs show the provider stored on the process metadata so existing chats continue using their original provider.

Modal job-submission dialogs use `shared/ModalJobAiControls.tsx` when they need New Chat-compatible provider/model/reasoning controls. Its `useModalJobAiSelection()` hook centralizes workspace-scoped `lastChatProvider` restore/persist, provider-scoped model catalogs, effort-tier mode, legacy model picker + `EffortPillSelector` fallback, and resolved `{ provider, model?, reasoningEffort? }` payload values for queue/chat submissions. `queue/EnqueueDialog.tsx` uses these compact controls in its Advanced area for Ask AI, ad hoc autopilot tasks, skill/context-file runs, bulk context-file submissions, and floating-chat launches; it sends `payload.provider` plus optional model/reasoning-effort config while preserving legacy template model overrides. `tasks/GenerateTaskDialog.tsx` uses these compact controls in its configuration area and forwards the resolved values to `/api/workspaces/:id/queue/generate`; `shared/UpdateDocumentDialog.tsx` uses them in the existing configuration area and enqueues custom chat tasks with `payload.provider` plus optional model/reasoning-effort config; `features/work-items/WorkItemExecuteDialog.tsx` renders the same controls through `RunSkillPanel` and forwards them to `/api/workspaces/:id/work-items/:wid/execute`; `features/chat/SkillContextDialog.tsx` uses them for git commit, multi-commit, and branch-range skill runs and sends `payload.provider` plus optional model/reasoning-effort config through the queued chat task; `features/chat/RalphStartPanel.tsx` uses them for confirmed grilling-phase Ralph starts and posts the resolved provider plus optional model/reasoning-effort config to `/api/processes/:id/ralph-start`; `shared/RalphLaunchDialog.tsx` uses the same controls for direct goal-file Ralph launches and sends the resolved provider plus optional model/reasoning-effort config to `/api/ralph-launch`.

`EffortPillSelector` drives the per-turn `reasoningEffort` override (Low/Medium/High; `null` = no override, falls back to the persisted per-model effort then the SDK default). The chip is structurally a dropdown menu (`AgentSelectorChip` style): trigger button (bars icon + label + chevron) opens a popover listbox with `Auto`/`Low`/`Medium`/`High` entries. The `Auto` entry explicitly clears the override and is also what the currently-selected level toggles to when re-clicked. New chats persist the selection alongside the draft (`useDraftStore` → `Draft.effortOverride`). Follow-ups thread the choice through `useSendMessage → ProcessMessageRequest.reasoningEffort → POST /api/processes/:id/message` and into either `bridge.enqueue` (queued) or `bridge.executeFollowUp` (direct/buffered). The server mirrors the value into `task.config.reasoningEffort` via `queue-shared.validateAndParseTask`, so executors see it from a single canonical location.

When effort-tier mode is enabled, `EffortTierSelector` tooltips expose the concrete model and reasoning effort mapped to the selected tier and each configured menu option; empty reasoning effort displays as `Auto`, and unconfigured options remain disabled with an Admin configuration tooltip.
Expand Down
18 changes: 18 additions & 0 deletions .github/skills/coc-knowledge/references/ralph.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ See `docs/spec-slices.md` for the full slice template, decision-tagging
convention, and ready-for-Ralph checklist that the bundled `grill-me` skill
produces.

## Direct Goal-File Launch

`POST /api/ralph-launch` (`packages/coc/src/server/routes/ralph-launch-routes.ts`)
starts an execution-phase Ralph session directly from an already-written goal
spec. The SPA `shared/RalphLaunchDialog.tsx` uses `ModalJobAiControls` so
goal-file launches share New Chat's workspace-scoped provider defaults,
effort-tier resolution, and legacy model/reasoning-effort controls. The route
validates optional `provider` and `reasoningEffort` inputs and carries them,
alongside optional `config.model`, onto the first queued Ralph execution task.

`POST /api/processes/:id/ralph-start`
(`packages/coc/src/server/routes/queue-ralph-routes.ts`) starts execution from
a completed grilling-phase session. The SPA `features/chat/RalphStartPanel.tsx`
uses the same `ModalJobAiControls` as direct launch and sends the resolved
provider plus optional `config.model`/`config.reasoningEffort`; the route
validates those overrides and applies them only to the first queued execution
task.

## Promote Ask-Mode Chat to Ralph

A completed ask-mode chat can be promoted to a Ralph session in place via
Expand Down
6 changes: 4 additions & 2 deletions .github/skills/coc-knowledge/references/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ CoC server exposes HTTP endpoints organized by domain. All routes are registered
| GET | `/api/queue` | List queue tasks |
| GET | `/api/queue/models` | List model IDs for the configured default provider |
| POST | `/api/queue` | Enqueue a task |
| POST | `/api/workspaces/:id/queue/generate` | Enqueue a Generate Plan chat task. Body accepts optional `provider`, `model`, and `reasoningEffort` overrides, which are validated through the shared chat queue validation path. |
| DELETE | `/api/queue/:id` | Remove from queue |
| POST | `/api/queue/:id/cancel` | Cancel queued task |
| PATCH | `/api/queue/pause` | Pause/resume queue |
Expand All @@ -86,8 +87,8 @@ CoC server exposes HTTP endpoints organized by domain. All routes are registered

| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/processes/:id/ralph-start` | Start Ralph execution after grilling |
| POST | `/api/ralph-launch` | Direct Ralph launch (skip grilling) |
| POST | `/api/processes/:id/ralph-start` | Start Ralph execution after grilling. Body accepts optional `provider`, `config.model`, and `config.reasoningEffort` overrides for the first execution task. |
| POST | `/api/ralph-launch` | Direct Ralph launch (skip grilling). Body accepts optional `provider`, `config.model`, and `config.reasoningEffort` overrides for the first execution task. |
| GET | `/api/workspaces/:wsId/ralph-sessions/:sessionId` | Read session journal (record + progress sections) |
| POST | `/api/workspaces/:wsId/ralph-sessions/:sessionId/continue` | Extend completed session (CAP_REACHED or NO_SIGNAL) by N iterations |
| POST | `/api/workspaces/:wsId/ralph-sessions/:sessionId/new-loop` | New goal loop after RALPH_COMPLETE |
Expand Down Expand Up @@ -245,6 +246,7 @@ See [mcp-settings.md](mcp-settings.md).
| GET | `/api/workspaces/:id/work-items/:itemId` | Read work item |
| PATCH | `/api/workspaces/:id/work-items/:itemId` | Update work item |
| DELETE | `/api/workspaces/:id/work-items/:itemId` | Delete work item |
| POST | `/api/workspaces/:id/work-items/:itemId/execute` | Enqueue a work-item implementation run. Body accepts optional `skillNames`, `provider`, `model`, and `reasoningEffort` overrides. |

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

Expand Down
3 changes: 3 additions & 0 deletions packages/coc-client/src/contracts/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export type JsonObject = Record<string, unknown>;

export type ChatProvider = 'copilot' | 'codex' | 'claude';
export type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh';

export interface PaginatedResponse<T> {
items: T[];
total: number;
Expand Down
4 changes: 3 additions & 1 deletion packages/coc-client/src/contracts/work-items.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { JsonObject } from './common';
import type { ChatProvider, JsonObject, ReasoningEffort } from './common';

export type WorkItemStatus =
| 'created'
Expand Down Expand Up @@ -169,6 +169,8 @@ export interface UpdateWorkItemRequest extends Partial<Pick<WorkItem, 'title' |

export interface ExecuteWorkItemRequest extends JsonObject {
model?: string;
provider?: ChatProvider;
reasoningEffort?: ReasoningEffort;
mode?: string;
skillNames?: string[];
}
Expand Down
1 change: 1 addition & 0 deletions packages/coc/src/server/preferences-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export const PerRepoPreferencesSchema = z.object({
.optional(),
defaultModel: z.string().max(100).optional(),
defaultModels: DefaultModelsByModeSchema.optional(),
lastChatProvider: z.enum(['copilot', 'codex', 'claude']).optional(),
maxRalphIterations: z.number().int().min(1).max(RALPH_MAX_ITERATIONS_LIMIT).optional(),
additionalNotesRoots: z.array(z.unknown())
.transform(arr => {
Expand Down
11 changes: 9 additions & 2 deletions packages/coc/src/server/ralph/enqueue-iteration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { buildRalphIterationPrompt } from './iteration-prompt';
import { RalphSessionStore } from './ralph-session-store';
import type { ChatProvider } from '../tasks/task-types';
import type { ChatProvider, ReasoningEffort } from '../tasks/task-types';

export interface BuildRalphIterationTaskInput {
workspaceId?: string;
Expand All @@ -31,6 +31,10 @@ export interface BuildRalphIterationTaskInput {
priority?: 'normal' | 'low' | 'high';
/** AI provider to use for this Ralph execution task. */
provider?: ChatProvider;
/** Optional model override for this Ralph execution task. */
model?: string;
/** Optional reasoning-effort override for this Ralph execution task. */
reasoningEffort?: ReasoningEffort;
/**
* When set, the enqueued task is tagged as a continuation of this Ralph
* session, allowing the queue manager to admit it ahead of unrelated
Expand Down Expand Up @@ -59,7 +63,10 @@ export function buildRalphIterationTask(input: BuildRalphIterationTaskInput) {
folderPath: input.folderPath,
continuationOfSessionId: input.continuationOfSessionId,
displayName,
config: {},
config: {
...(input.model ? { model: input.model } : {}),
...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
},
payload: {
kind: 'chat' as const,
mode: 'ralph' as const,
Expand Down
68 changes: 37 additions & 31 deletions packages/coc/src/server/routes/queue-ralph-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import type { ProcessStore } from '@plusplusoneplusplus/forge';
import { toQueueProcessId, isQueueProcessId, toTaskId, getLogger, LogCategory } from '@plusplusoneplusplus/forge';
import { getRalphContext } from '../tasks/task-types';
import { RalphSessionStore } from '../ralph/ralph-session-store';
import { buildRalphIterationPrompt } from '../ralph/iteration-prompt';
import { buildRalphIterationTask } from '../ralph/enqueue-iteration';
import { RALPH_DEFAULT_MAX_ITERATIONS, readRepoPreferences } from '../preferences-handler';
import { VALID_CHAT_PROVIDERS, VALID_REASONING_EFFORTS } from '../tasks/task-types';
import type { ChatProvider, ReasoningEffort } from '../tasks/task-types';

export interface QueueRalphRouteContext {
bridge: MultiRepoQueueRouter;
Expand Down Expand Up @@ -55,6 +57,25 @@ export function registerRalphRoutes(routes: Route[], ctx: QueueRalphRouteContext
const workspaceId = typeof body.workspaceId === 'string' && body.workspaceId
? body.workspaceId
: undefined;
const provider = body.provider === undefined
? undefined
: body.provider as ChatProvider;
if (provider !== undefined && !VALID_CHAT_PROVIDERS.has(provider)) {
return sendError(res, 400, `Invalid provider: '${String(body.provider)}'. Valid providers: ${[...VALID_CHAT_PROVIDERS].join(', ')}`);
}
const config = body.config && typeof body.config === 'object'
? body.config as Record<string, unknown>
: {};
const model = typeof config.model === 'string' && config.model.trim()
? config.model.trim()
: undefined;
const rawReasoningEffort = config.reasoningEffort ?? body.reasoningEffort;
const reasoningEffort = rawReasoningEffort === undefined
? undefined
: rawReasoningEffort as ReasoningEffort;
if (reasoningEffort !== undefined && !VALID_REASONING_EFFORTS.has(reasoningEffort)) {
return sendError(res, 400, `Invalid reasoningEffort: '${String(rawReasoningEffort)}'. Valid reasoningEffort values: ${[...VALID_REASONING_EFFORTS].join(', ')}`);
}

// Resolve process (handle queue_ prefix vs bare UUID)
let proc = await store.getProcess(rawId, workspaceId);
Expand All @@ -76,6 +97,9 @@ export function registerRalphRoutes(routes: Route[], ctx: QueueRalphRouteContext
if (!ralphCtx || ralphCtx.phase !== 'grilling') {
return sendError(res, 400, 'Process is not in grilling phase');
}
if (!ralphCtx.sessionId) {
return sendError(res, 400, 'Process is missing Ralph session ID');
}

const wsId: string | undefined = workspaceId
?? procPayload?.workspaceId
Expand Down Expand Up @@ -116,37 +140,19 @@ export function registerRalphRoutes(routes: Route[], ctx: QueueRalphRouteContext
}

// Enqueue the first Ralph execution task
const taskId = await bridge.enqueue({
type: 'chat',
priority: 'normal',
repoId: wsId,
const taskId = await bridge.enqueue(buildRalphIterationTask({
workspaceId: wsId,
workingDirectory,
folderPath,
payload: {
kind: 'chat',
mode: 'ralph',
prompt: buildRalphIterationPrompt({
originalGoal: goalSpec,
progressPath: (dataDir && wsId && ralphCtx.sessionId)
? new RalphSessionStore({ dataDir }).getProgressPath(wsId, ralphCtx.sessionId)
: undefined,
currentIteration: 1,
maxIterations,
}),
workspaceId: wsId,
workingDirectory,
folderPath,
context: {
ralph: {
phase: 'executing',
sessionId: ralphCtx.sessionId,
originalGoal: goalSpec,
currentIteration: 1,
maxIterations,
},
},
},
config: {},
});
sessionId: ralphCtx.sessionId,
originalGoal: goalSpec,
iteration: 1,
maxIterations,
dataDir,
provider,
model,
reasoningEffort,
}));

sendJSON(res, 200, { processId: toQueueProcessId(taskId) });
},
Expand Down
20 changes: 12 additions & 8 deletions packages/coc/src/server/routes/ralph-launch-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { toQueueProcessId, getLogger, LogCategory } from '@plusplusoneplusplus/f
import { RalphSessionStore } from '../ralph/ralph-session-store';
import { buildRalphIterationTask } from '../ralph/enqueue-iteration';
import { RALPH_DEFAULT_MAX_ITERATIONS, readRepoPreferences } from '../preferences-handler';
import { VALID_CHAT_PROVIDERS } from '../tasks/task-types';
import type { ChatProvider } from '../tasks/task-types';
import { VALID_CHAT_PROVIDERS, VALID_REASONING_EFFORTS } from '../tasks/task-types';
import type { ChatProvider, ReasoningEffort } from '../tasks/task-types';

export interface RalphLaunchRouteContext {
bridge: MultiRepoQueueRouter;
Expand Down Expand Up @@ -67,6 +67,13 @@ export function registerRalphLaunchRoutes(routes: Route[], ctx: RalphLaunchRoute
const model = typeof config.model === 'string' && config.model.trim()
? config.model.trim()
: undefined;
const rawReasoningEffort = config.reasoningEffort ?? body.reasoningEffort;
const reasoningEffort = rawReasoningEffort === undefined
? undefined
: rawReasoningEffort as ReasoningEffort;
if (reasoningEffort !== undefined && !VALID_REASONING_EFFORTS.has(reasoningEffort)) {
return sendError(res, 400, `Invalid reasoningEffort: '${String(rawReasoningEffort)}'. Valid reasoningEffort values: ${[...VALID_REASONING_EFFORTS].join(', ')}`);
}

// Resolve max iterations: per-repo preference > hardcoded default.
let prefMax: number | undefined;
Expand Down Expand Up @@ -108,14 +115,11 @@ export function registerRalphLaunchRoutes(routes: Route[], ctx: RalphLaunchRoute
maxIterations,
dataDir,
provider,
model,
reasoningEffort,
});

const taskId = await bridge.enqueue({
...task,
config: {
...(model ? { model } : {}),
},
});
const taskId = await bridge.enqueue(task);

sendJSON(res, 200, { processId: toQueueProcessId(taskId), sessionId });
},
Expand Down
19 changes: 19 additions & 0 deletions packages/coc/src/server/routes/work-item-execution-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { TaskCommentsManager } from '../tasks/comments/task-comments-manager';
import { DiffCommentsManager } from '../tasks/comments/diff-comments-manager';
import { buildBatchResolvePrompt } from '../tasks/comments/task-comments-ai';
import { buildMultiFileBatchResolvePrompt } from '../tasks/comments/diff-comments-ai';
import { VALID_CHAT_PROVIDERS, VALID_REASONING_EFFORTS, type ChatProvider, type ReasoningEffort } from '../tasks/task-types';

export interface WorkItemExecutionRouteContext {
routes: Route[];
Expand Down Expand Up @@ -102,9 +103,27 @@ export function registerWorkItemExecutionRoutes(ctx: WorkItemExecutionRouteConte
const skillNames: string[] | undefined = Array.isArray(body.skillNames)
? body.skillNames.filter((s: unknown): s is string => typeof s === 'string' && s.trim().length > 0)
: undefined;
const provider: ChatProvider | undefined = body.provider === undefined
? undefined
: typeof body.provider === 'string' && VALID_CHAT_PROVIDERS.has(body.provider as ChatProvider)
? body.provider as ChatProvider
: undefined;
if (body.provider !== undefined && !provider) {
return handleAPIError(res, badRequest(`Invalid provider: '${body.provider}'`));
}
const reasoningEffort: ReasoningEffort | undefined = body.reasoningEffort === undefined
? undefined
: typeof body.reasoningEffort === 'string' && VALID_REASONING_EFFORTS.has(body.reasoningEffort as ReasoningEffort)
? body.reasoningEffort as ReasoningEffort
: undefined;
if (body.reasoningEffort !== undefined && !reasoningEffort) {
return handleAPIError(res, badRequest(`Invalid reasoningEffort: '${body.reasoningEffort}'`));
}

const result = await executeWorkItem(workItemId, workItemStore, enqueue, {
model: body.model,
provider,
reasoningEffort,
mode: body.mode,
headBefore,
taskFilePath,
Expand Down
Loading
Loading