mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-15 02:42:45 +08:00
Add benchmark results for fast3 and fast4, implement KeepAliveLspBridge, and add tests for staged strategies
- Added new benchmark result files: compare_2026-02-09_score_fast3.json and compare_2026-02-09_score_fast4.json. - Implemented KeepAliveLspBridge to maintain a persistent LSP connection across multiple queries, improving performance. - Created unit tests for staged clustering strategies in test_staged_stage3_fast_strategies.py, ensuring correct behavior of score and dir_rr strategies.
This commit is contained in:
153
ccw/src/core/routes/cli-sessions-routes.ts
Normal file
153
ccw/src/core/routes/cli-sessions-routes.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* CLI Sessions (PTY) Routes Module
|
||||
* Independent from existing /api/cli/* execution endpoints.
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/cli-sessions
|
||||
* - POST /api/cli-sessions
|
||||
* - GET /api/cli-sessions/:sessionKey/buffer
|
||||
* - POST /api/cli-sessions/:sessionKey/send
|
||||
* - POST /api/cli-sessions/:sessionKey/execute
|
||||
* - POST /api/cli-sessions/:sessionKey/resize
|
||||
* - POST /api/cli-sessions/:sessionKey/close
|
||||
*/
|
||||
|
||||
import type { RouteContext } from './types.js';
|
||||
import { getCliSessionManager } from '../services/cli-session-manager.js';
|
||||
|
||||
export async function handleCliSessionsRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, req, res, handlePostRequest, initialPath } = ctx;
|
||||
const manager = getCliSessionManager(process.cwd());
|
||||
|
||||
// GET /api/cli-sessions
|
||||
if (pathname === '/api/cli-sessions' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ sessions: manager.listSessions() }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/cli-sessions
|
||||
if (pathname === '/api/cli-sessions' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const {
|
||||
workingDir,
|
||||
cols,
|
||||
rows,
|
||||
preferredShell,
|
||||
tool,
|
||||
model,
|
||||
resumeKey
|
||||
} = (body || {}) as any;
|
||||
|
||||
const session = manager.createSession({
|
||||
workingDir: workingDir || initialPath,
|
||||
cols: typeof cols === 'number' ? cols : undefined,
|
||||
rows: typeof rows === 'number' ? rows : undefined,
|
||||
preferredShell: preferredShell === 'pwsh' ? 'pwsh' : 'bash',
|
||||
tool,
|
||||
model,
|
||||
resumeKey
|
||||
});
|
||||
|
||||
return { success: true, session };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/cli-sessions/:sessionKey/buffer
|
||||
const bufferMatch = pathname.match(/^\/api\/cli-sessions\/([^/]+)\/buffer$/);
|
||||
if (bufferMatch && req.method === 'GET') {
|
||||
const sessionKey = decodeURIComponent(bufferMatch[1]);
|
||||
const session = manager.getSession(sessionKey);
|
||||
if (!session) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Session not found' }));
|
||||
return true;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ session, buffer: manager.getBuffer(sessionKey) }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/cli-sessions/:sessionKey/send
|
||||
const sendMatch = pathname.match(/^\/api\/cli-sessions\/([^/]+)\/send$/);
|
||||
if (sendMatch && req.method === 'POST') {
|
||||
const sessionKey = decodeURIComponent(sendMatch[1]);
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const { text, appendNewline } = (body || {}) as any;
|
||||
if (typeof text !== 'string') {
|
||||
return { error: 'text is required', status: 400 };
|
||||
}
|
||||
manager.sendText(sessionKey, text, appendNewline !== false);
|
||||
return { success: true };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/cli-sessions/:sessionKey/execute
|
||||
const executeMatch = pathname.match(/^\/api\/cli-sessions\/([^/]+)\/execute$/);
|
||||
if (executeMatch && req.method === 'POST') {
|
||||
const sessionKey = decodeURIComponent(executeMatch[1]);
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const {
|
||||
tool,
|
||||
prompt,
|
||||
mode,
|
||||
model,
|
||||
workingDir,
|
||||
category,
|
||||
resumeKey,
|
||||
resumeStrategy
|
||||
} = (body || {}) as any;
|
||||
|
||||
if (!tool || typeof tool !== 'string') {
|
||||
return { error: 'tool is required', status: 400 };
|
||||
}
|
||||
if (!prompt || typeof prompt !== 'string') {
|
||||
return { error: 'prompt is required', status: 400 };
|
||||
}
|
||||
|
||||
const result = manager.execute(sessionKey, {
|
||||
tool,
|
||||
prompt,
|
||||
mode,
|
||||
model,
|
||||
workingDir,
|
||||
category,
|
||||
resumeKey,
|
||||
resumeStrategy: resumeStrategy === 'promptConcat' ? 'promptConcat' : 'nativeResume'
|
||||
});
|
||||
|
||||
return { success: true, ...result };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/cli-sessions/:sessionKey/resize
|
||||
const resizeMatch = pathname.match(/^\/api\/cli-sessions\/([^/]+)\/resize$/);
|
||||
if (resizeMatch && req.method === 'POST') {
|
||||
const sessionKey = decodeURIComponent(resizeMatch[1]);
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
const { cols, rows } = (body || {}) as any;
|
||||
if (typeof cols !== 'number' || typeof rows !== 'number') {
|
||||
return { error: 'cols and rows are required', status: 400 };
|
||||
}
|
||||
manager.resize(sessionKey, cols, rows);
|
||||
return { success: true };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/cli-sessions/:sessionKey/close
|
||||
const closeMatch = pathname.match(/^\/api\/cli-sessions\/([^/]+)\/close$/);
|
||||
if (closeMatch && req.method === 'POST') {
|
||||
const sessionKey = decodeURIComponent(closeMatch[1]);
|
||||
manager.close(sessionKey);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
|
||||
// GET /api/queue/:id or /api/issues/queue/:id - Get specific queue by ID
|
||||
const queueDetailMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)$/);
|
||||
const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge', 'activate'];
|
||||
const reservedQueuePaths = ['history', 'reorder', 'move', 'switch', 'deactivate', 'merge', 'activate'];
|
||||
if (queueDetailMatch && req.method === 'GET' && !reservedQueuePaths.includes(queueDetailMatch[1])) {
|
||||
const queueId = queueDetailMatch[1];
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
@@ -592,6 +592,89 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/queue/move - Move an item to a different execution_group (and optionally insert at index)
|
||||
if (normalizedPath === '/api/queue/move' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const { itemId, toGroupId, toIndex } = body;
|
||||
if (!itemId || !toGroupId) {
|
||||
return { error: 'itemId and toGroupId required' };
|
||||
}
|
||||
|
||||
const queue = readQueue(issuesDir);
|
||||
const items = getQueueItems(queue);
|
||||
const isSolutionBased = isSolutionBasedQueue(queue);
|
||||
|
||||
const itemIndex = items.findIndex((i: any) => i.item_id === itemId);
|
||||
if (itemIndex === -1) return { error: `Item ${itemId} not found` };
|
||||
|
||||
const moved = { ...items[itemIndex] };
|
||||
const fromGroupId = moved.execution_group || 'ungrouped';
|
||||
|
||||
// Build per-group ordered lists based on current execution_order
|
||||
const groupToIds = new Map<string, string[]>();
|
||||
const sorted = [...items].sort((a: any, b: any) => (a.execution_order || 0) - (b.execution_order || 0));
|
||||
for (const it of sorted) {
|
||||
const gid = it.execution_group || 'ungrouped';
|
||||
if (!groupToIds.has(gid)) groupToIds.set(gid, []);
|
||||
groupToIds.get(gid)!.push(it.item_id);
|
||||
}
|
||||
|
||||
// Remove from old group
|
||||
const fromList = groupToIds.get(fromGroupId) || [];
|
||||
groupToIds.set(fromGroupId, fromList.filter((id) => id !== itemId));
|
||||
|
||||
// Insert into target group
|
||||
const targetList = groupToIds.get(toGroupId) || [];
|
||||
const insertAt = typeof toIndex === 'number' ? Math.max(0, Math.min(targetList.length, toIndex)) : targetList.length;
|
||||
const nextTarget = [...targetList];
|
||||
nextTarget.splice(insertAt, 0, itemId);
|
||||
groupToIds.set(toGroupId, nextTarget);
|
||||
|
||||
moved.execution_group = toGroupId;
|
||||
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_id, i]));
|
||||
itemMap.set(itemId, moved);
|
||||
|
||||
const groupIds = Array.from(groupToIds.keys());
|
||||
groupIds.sort((a, b) => {
|
||||
const aGroup = parseInt(a.match(/\\d+/)?.[0] || '999');
|
||||
const bGroup = parseInt(b.match(/\\d+/)?.[0] || '999');
|
||||
if (aGroup !== bGroup) return aGroup - bGroup;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
const nextItems: any[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const gid of groupIds) {
|
||||
const ids = groupToIds.get(gid) || [];
|
||||
for (const id of ids) {
|
||||
const it = itemMap.get(id);
|
||||
if (!it) continue;
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
nextItems.push(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: append any missing items
|
||||
for (const it of items) {
|
||||
if (!seen.has(it.item_id)) nextItems.push(it);
|
||||
}
|
||||
|
||||
nextItems.forEach((it, idx) => { it.execution_order = idx + 1; });
|
||||
|
||||
if (isSolutionBased) {
|
||||
queue.solutions = nextItems;
|
||||
} else {
|
||||
queue.tasks = nextItems;
|
||||
}
|
||||
writeQueue(issuesDir, queue);
|
||||
|
||||
return { success: true, itemId, fromGroupId, toGroupId };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// DELETE /api/queue/:queueId/item/:itemId or /api/issues/queue/:queueId/item/:itemId
|
||||
const queueItemDeleteMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/);
|
||||
if (queueItemDeleteMatch && req.method === 'DELETE') {
|
||||
|
||||
@@ -104,6 +104,28 @@ export interface PromptTemplateNodeData {
|
||||
*/
|
||||
mode?: ExecutionMode;
|
||||
|
||||
/**
|
||||
* Delivery target for CLI-mode execution.
|
||||
* - newExecution: spawn a fresh CLI execution (default)
|
||||
* - sendToSession: route to a PTY session (tmux-like send)
|
||||
*/
|
||||
delivery?: 'newExecution' | 'sendToSession';
|
||||
|
||||
/**
|
||||
* When delivery=sendToSession, route execution to this PTY session key.
|
||||
*/
|
||||
targetSessionKey?: string;
|
||||
|
||||
/**
|
||||
* Optional logical resume key for chaining executions.
|
||||
*/
|
||||
resumeKey?: string;
|
||||
|
||||
/**
|
||||
* Optional resume mapping strategy.
|
||||
*/
|
||||
resumeStrategy?: 'nativeResume' | 'promptConcat';
|
||||
|
||||
/**
|
||||
* References to outputs from previous steps
|
||||
* Use the outputName values from earlier nodes
|
||||
|
||||
Reference in New Issue
Block a user