mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
Merge remote-tracking branch 'origin/master' into worktree-grok-mode
# Conflicts: # src/web/public/app.js
This commit is contained in:
@@ -26,3 +26,4 @@ export { registerAdminRoutes } from './admin-routes.js';
|
||||
export { registerWsRoutes } from './ws-routes.js';
|
||||
export { registerVoiceRoutes } from './voice-routes.js';
|
||||
export { registerWebviewRoutes, tryWebviewRefererFallback } from './webview-routes.js';
|
||||
export { registerTabLayoutRoutes } from './tab-layout-routes.js';
|
||||
|
||||
@@ -411,7 +411,7 @@ export function registerRalphRoutes(
|
||||
writeFileSync(promptPath, fullPrompt, 'utf-8');
|
||||
|
||||
// Register session
|
||||
ctx.addSession(session);
|
||||
await ctx.addSession(session);
|
||||
ctx.store.incrementSessionsCreated();
|
||||
ctx.persistSessionState(session);
|
||||
await ctx.setupSessionListeners(session);
|
||||
|
||||
@@ -48,7 +48,8 @@ import {
|
||||
SessionWaitQuerySchema,
|
||||
SessionWaitOutputQuerySchema,
|
||||
} from '../schemas.js';
|
||||
import { mergeSessionOrder } from '../../session-order.js';
|
||||
import { ownerLayoutKey } from '../../tab-layout-persistence.js';
|
||||
import { TabLayoutValidationError } from '../../tab-layout.js';
|
||||
import {
|
||||
sessionWaits,
|
||||
resolveWaitSignals,
|
||||
@@ -108,7 +109,7 @@ import {
|
||||
setHistoryIndexRefresher,
|
||||
setHistorySessionIndex,
|
||||
} from '../session-history-index.js';
|
||||
import type { SessionPort, EventPort, ConfigPort, InfraPort, AuthPort } from '../ports/index.js';
|
||||
import type { SessionPort, EventPort, ConfigPort, InfraPort, AuthPort, TabLayoutPort } from '../ports/index.js';
|
||||
import { RunSummaryTracker } from '../../run-summary.js';
|
||||
|
||||
import { MAX_INPUT_LENGTH, MAX_SESSION_NAME_LENGTH } from '../../config/terminal-limits.js';
|
||||
@@ -650,7 +651,7 @@ async function injectAgentSkill(casePath: string): Promise<void> {
|
||||
|
||||
export function registerSessionRoutes(
|
||||
app: FastifyInstance,
|
||||
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort
|
||||
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort & TabLayoutPort
|
||||
): void {
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Auth
|
||||
@@ -682,16 +683,23 @@ export function registerSessionRoutes(
|
||||
return (list as Array<{ owner?: string }>).filter((s) => canAccessOwned(user, s.owner));
|
||||
});
|
||||
|
||||
// ========== Session Tab Order (global sync, COD-131) ==========
|
||||
// ========== Legacy Session Tab Order (temporary synchronized compatibility bridge) ==========
|
||||
|
||||
app.put('/api/session-order', async (req): Promise<ApiResponse<{ order: string[] }>> => {
|
||||
const { order } = parseBody(SessionOrderUpdateSchema, req.body, 'Invalid session order');
|
||||
// Server is authoritative but never drops ids it knows about that the
|
||||
// pushing device hadn't loaded yet — those fall to the end (mergeSessionOrder).
|
||||
const merged = mergeSessionOrder(order, ctx.store.getSessionOrder());
|
||||
ctx.store.setSessionOrder(merged);
|
||||
ctx.broadcast(SseEvent.SessionOrderChanged, { order: merged });
|
||||
return { success: true, data: { order: merged } };
|
||||
app.put('/api/session-order', async (req, reply): Promise<ApiResponse<{ order: string[] }>> => {
|
||||
try {
|
||||
const { order } = parseBody(SessionOrderUpdateSchema, req.body, 'Invalid session order');
|
||||
const user = getAuthUser(req);
|
||||
const result = await ctx.tabLayouts.putLegacyOrder(
|
||||
{ owner: ownerLayoutKey(ownerFor(req)), isAdmin: user.role === 'admin' },
|
||||
order
|
||||
);
|
||||
return { success: true, data: { order: result.order } };
|
||||
} catch (error) {
|
||||
if (error instanceof TabLayoutValidationError) {
|
||||
return reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, error.message));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Session Creation ==========
|
||||
@@ -951,7 +959,7 @@ export function registerSessionRoutes(
|
||||
parentSessionId: resolveParentSessionId(ctx, req, body.parentSessionId, owner),
|
||||
});
|
||||
|
||||
ctx.addSession(session);
|
||||
await ctx.addSession(session);
|
||||
ctx.store.incrementSessionsCreated();
|
||||
ctx.persistSessionState(session);
|
||||
await ctx.setupSessionListeners(session);
|
||||
@@ -2665,7 +2673,7 @@ export function registerSessionRoutes(
|
||||
allowedTools: runClaudeModeConfig.allowedTools,
|
||||
owner: runOwner,
|
||||
});
|
||||
ctx.addSession(session);
|
||||
await ctx.addSession(session);
|
||||
ctx.store.incrementSessionsCreated();
|
||||
ctx.persistSessionState(session);
|
||||
await ctx.setupSessionListeners(session);
|
||||
@@ -3102,7 +3110,7 @@ export function registerSessionRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
ctx.addSession(session);
|
||||
await ctx.addSession(session);
|
||||
ctx.store.incrementSessionsCreated();
|
||||
ctx.persistSessionState(session);
|
||||
await ctx.setupSessionListeners(session);
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
import { SseEvent } from '../sse-events.js';
|
||||
import { getInstallInfo, checkForUpdate, startUpdate, getUpdateStatusForApi } from '../self-update.js';
|
||||
import { getRepositoryStatus } from '../repo-status.js';
|
||||
import type { SessionPort, EventPort, ConfigPort, InfraPort, AuthPort } from '../ports/index.js';
|
||||
import type { SessionPort, EventPort, ConfigPort, InfraPort, AuthPort, TabLayoutPort } from '../ports/index.js';
|
||||
import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
|
||||
import { QR_AUTH_FAILURE_MAX } from '../../config/tunnel-config.js';
|
||||
import { AUTH_SESSION_TTL_MS } from '../../config/auth-config.js';
|
||||
@@ -129,7 +129,7 @@ export function resolveSpanUrl(hostHeader: string | undefined, fallbackPort = '3
|
||||
|
||||
export function registerSystemRoutes(
|
||||
app: FastifyInstance,
|
||||
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort
|
||||
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort & TabLayoutPort
|
||||
): void {
|
||||
const windowStatesPath = dataPath('subagent-window-states.json');
|
||||
const parentMapPath = dataPath('subagent-parents.json');
|
||||
@@ -469,7 +469,9 @@ export function registerSystemRoutes(
|
||||
|
||||
app.post('/api/cleanup-state', async () => {
|
||||
const activeSessionIds = new Set(ctx.sessions.keys());
|
||||
const result = ctx.store.cleanupStaleSessions(activeSessionIds);
|
||||
const result = await ctx.tabLayouts.runStaleSessionCleanup(activeSessionIds, (ids) =>
|
||||
ctx.store.cleanupSessionsByIds(ids)
|
||||
);
|
||||
const lifecycleLog = getLifecycleLog();
|
||||
for (const s of result.cleaned) {
|
||||
lifecycleLog.log({ event: 'stale_cleaned', sessionId: s.id, name: s.name });
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/** @fileoverview Authenticated owner-scoped tab-layout read/write API. */
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { ownerLayoutKey } from '../../tab-layout-persistence.js';
|
||||
import { TabLayoutValidationError } from '../../tab-layout.js';
|
||||
import { ApiErrorCode, createErrorResponse } from '../../types.js';
|
||||
import { ownerFor } from '../route-helpers.js';
|
||||
import type { TabLayoutPort } from '../ports/index.js';
|
||||
|
||||
export const TAB_LAYOUT_BODY_LIMIT = 128 * 1024;
|
||||
|
||||
function parseWriteBody(body: unknown): { baseVersion: number; layout: unknown } {
|
||||
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
||||
throw new TabLayoutValidationError('body must be an object');
|
||||
}
|
||||
const keys = Object.keys(body);
|
||||
if (keys.length !== 2 || !Object.hasOwn(body, 'baseVersion') || !Object.hasOwn(body, 'layout')) {
|
||||
throw new TabLayoutValidationError('body must contain exactly baseVersion and layout');
|
||||
}
|
||||
const input = body as { baseVersion?: unknown; layout?: unknown };
|
||||
if (!Number.isSafeInteger(input.baseVersion) || (input.baseVersion as number) < 0 || input.layout === undefined) {
|
||||
throw new TabLayoutValidationError('baseVersion must be a non-negative safe integer and layout is required');
|
||||
}
|
||||
return { baseVersion: input.baseVersion as number, layout: input.layout };
|
||||
}
|
||||
|
||||
export function registerTabLayoutRoutes(app: FastifyInstance, ctx: TabLayoutPort): void {
|
||||
app.get('/api/tab-layout', async (req) => ({
|
||||
success: true,
|
||||
data: { layout: await ctx.tabLayouts.get(ownerLayoutKey(ownerFor(req))) },
|
||||
}));
|
||||
|
||||
app.put('/api/tab-layout', { bodyLimit: TAB_LAYOUT_BODY_LIMIT }, async (req, reply) => {
|
||||
try {
|
||||
const { baseVersion, layout } = parseWriteBody(req.body);
|
||||
const result = await ctx.tabLayouts.put(ownerLayoutKey(ownerFor(req)), layout, baseVersion);
|
||||
if (result.status === 'conflict') {
|
||||
return reply.code(409).send({
|
||||
...createErrorResponse(ApiErrorCode.CONFLICT, 'Tab layout version conflict'),
|
||||
data: { layout: result.layout },
|
||||
});
|
||||
}
|
||||
return { success: true, data: { layout: result.layout } };
|
||||
} catch (error) {
|
||||
if (error instanceof TabLayoutValidationError) {
|
||||
return reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, error.message));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -53,7 +53,8 @@ import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
|
||||
import { canAccessOwned, getAuthUser, ownerFor, parseBody } from '../route-helpers.js';
|
||||
import { WebviewCreateSchema, WebviewProbeSchema, WebviewUpdateSchema } from '../schemas.js';
|
||||
import { SseEvent } from '../sse-events.js';
|
||||
import type { EventPort } from '../ports/index.js';
|
||||
import type { EventPort, TabLayoutPort } from '../ports/index.js';
|
||||
import { ownerLayoutKey } from '../../tab-layout-persistence.js';
|
||||
import {
|
||||
buildDownstreamResponseHeaders,
|
||||
buildProxyCorsHeaders,
|
||||
@@ -98,14 +99,14 @@ function withWebviews<T>(fn: (list: Webview[]) => Promise<T> | T): Promise<T> {
|
||||
return next;
|
||||
}
|
||||
|
||||
export function registerWebviewRoutes(app: FastifyInstance, ctx: EventPort): void {
|
||||
export function registerWebviewRoutes(app: FastifyInstance, ctx: EventPort & TabLayoutPort): void {
|
||||
registerCrudRoutes(app, ctx);
|
||||
registerProxyRoutes(app);
|
||||
}
|
||||
|
||||
// ───────────────────────────── CRUD ─────────────────────────────
|
||||
|
||||
function registerCrudRoutes(app: FastifyInstance, ctx: EventPort): void {
|
||||
function registerCrudRoutes(app: FastifyInstance, ctx: EventPort & TabLayoutPort): void {
|
||||
app.get('/api/webviews', async (req) => {
|
||||
const user = getAuthUser(req);
|
||||
const all = await readWebviews(configDir());
|
||||
@@ -145,6 +146,21 @@ function registerCrudRoutes(app: FastifyInstance, ctx: EventPort): void {
|
||||
.send(createErrorResponse(ApiErrorCode.INVALID_INPUT, `Webview limit reached (max ${MAX_WEBVIEWS})`));
|
||||
}
|
||||
|
||||
try {
|
||||
await ctx.tabLayouts.webviewCreated(ownerLayoutKey(created.owner));
|
||||
} catch (error) {
|
||||
// The saved webview and its layout ref are one logical creation. If the
|
||||
// layout rejects the new ref (for example at MAX_TAB_REFS), roll back the
|
||||
// already-written JSON record and publish neither creation event.
|
||||
await withWebviews(async (list) => {
|
||||
const index = list.findIndex((webview) => webview.id === created.id);
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1);
|
||||
await writeWebviews(configDir(), list);
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
ctx.broadcast(SseEvent.WebviewChanged, { action: 'created', id: created.id });
|
||||
return { success: true, data: created };
|
||||
});
|
||||
@@ -187,9 +203,19 @@ function registerCrudRoutes(app: FastifyInstance, ctx: EventPort): void {
|
||||
const index = list.findIndex((w) => w.id === id);
|
||||
if (index === -1) return 'not-found' as const;
|
||||
if (!canAccessOwned(user, list[index].owner)) return 'forbidden' as const;
|
||||
const removed = list[index];
|
||||
list.splice(index, 1);
|
||||
await writeWebviews(configDir(), list);
|
||||
return 'deleted' as const;
|
||||
try {
|
||||
await ctx.tabLayouts.webviewDeleted(ownerLayoutKey(removed.owner), id);
|
||||
} catch (error) {
|
||||
// Still inside withWebviews' mutex: restore the exact record at its
|
||||
// original position without overwriting any concurrent mutation.
|
||||
list.splice(index, 0, removed);
|
||||
await writeWebviews(configDir(), list);
|
||||
throw error;
|
||||
}
|
||||
return { status: 'deleted' as const, owner: removed.owner };
|
||||
});
|
||||
|
||||
if (result === 'not-found') {
|
||||
|
||||
Reference in New Issue
Block a user