@a-t-h-i/bot-lobby 0.6.48 → 0.6.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/package.json +9 -7
- package/prompts/designer.md +15 -1
- package/prompts/master.md +4 -1
- package/src/ask/project-styles.ts +64 -0
- package/src/ask/tool.ts +23 -4
- package/src/ask/types.ts +1 -1
- package/src/ask/web.ts +27 -6
- package/src/classifier/classifier.ts +14 -2
- package/src/classifier/hosts.ts +12 -18
- package/src/classifier/instance.ts +7 -4
- package/src/execution/delegation.ts +2 -1
- package/src/execution/pi-runner.ts +11 -2
- package/src/lobby/host.ts +6 -0
- package/src/lobby/keys.ts +2 -1
- package/src/lobby/service.ts +75 -7
- package/src/lobby/session-questions.ts +49 -0
- package/src/lobby/sessions.ts +66 -0
- package/src/pi/auto-compaction.ts +46 -19
- package/src/pi/events.ts +2 -0
- package/src/pi/fresh-context.ts +10 -8
- package/src/pi/plan-checklist.ts +47 -15
- package/src/pi/tools.ts +2 -2
- package/src/schemas/task.ts +14 -0
- package/src/state/persistence.ts +4 -1
- package/src/webui/api/index.ts +36 -2
- package/src/webui/api/lobby.ts +1 -1
- package/src/webui/api/plan-facts.ts +9 -4
- package/src/webui/api/sessions.ts +21 -0
- package/src/webui/api/tasks.ts +12 -1
- package/src/webui/api/terminal.ts +42 -0
- package/src/webui/dev/fake-service.ts +42 -1
- package/src/webui/palette-core.ts +228 -228
- package/src/webui/protocol.ts +29 -2
- package/src/webui/server.ts +16 -1
- package/src/webui/terminal.ts +292 -0
- package/src/workflow/workflow.ts +3 -2
- package/webui/dist/assets/TerminalView-BrP-ENHg.css +1 -0
- package/webui/dist/assets/TerminalView-D_j85u4O.js +16 -0
- package/webui/dist/assets/dm-sans-latin-wght-normal-Xz1IZZA0.woff2 +0 -0
- package/webui/dist/assets/figtree-latin-wght-normal-D_ZTVpCC.woff2 +0 -0
- package/webui/dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/webui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/webui/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/webui/dist/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/webui/dist/assets/index-BkM--P3x.js +115 -0
- package/webui/dist/assets/index-DrHXotS7.css +2 -0
- package/webui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/webui/dist/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
- package/webui/dist/assets/nunito-latin-wght-normal-BzFMHfZw.woff2 +0 -0
- package/webui/dist/assets/outfit-latin-wght-normal-Bc-8i84L.woff2 +0 -0
- package/webui/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/webui/dist/assets/roboto-mono-latin-wght-normal-CZtBPCCa.woff2 +0 -0
- package/webui/dist/assets/source-code-pro-latin-wght-normal-BP8Zz55n.woff2 +0 -0
- package/webui/dist/assets/source-sans-3-latin-wght-normal-Bo_j9asI.woff2 +0 -0
- package/webui/dist/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
- package/webui/dist/build.json +110 -94
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-B5AnxwRU.css +0 -2
- package/webui/dist/assets/index-CznnCkjK.js +0 -114
package/src/state/persistence.ts
CHANGED
|
@@ -104,7 +104,7 @@ export function saveTask(root: string, configDir: string, task: Task): void {
|
|
|
104
104
|
rememberTiming(task);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/** Explicit user waivers and resolved approvals must survive an in-flight worker's snapshot. */
|
|
107
|
+
/** Explicit user waivers, skipped steps and resolved approvals must survive an in-flight worker's snapshot. */
|
|
108
108
|
function preserveUserInterventions(task: Task, latest: Task): void {
|
|
109
109
|
const newlyWaived = !task.qaWaiver && latest.qaWaiver;
|
|
110
110
|
task.qaWaiver ??= latest.qaWaiver;
|
|
@@ -117,6 +117,9 @@ function preserveUserInterventions(task: Task, latest: Task): void {
|
|
|
117
117
|
const resolved = latest.approvals.find((entry) => entry.id === approval.id && entry.status !== "pending");
|
|
118
118
|
if (approval.status === "pending" && resolved) Object.assign(approval, resolved);
|
|
119
119
|
}
|
|
120
|
+
const skipped = new Set((task.skippedSteps ?? []).map((entry) => entry.text));
|
|
121
|
+
const newSkips = (latest.skippedSteps ?? []).filter((entry) => !skipped.has(entry.text));
|
|
122
|
+
if (newSkips.length > 0) task.skippedSteps = [...(task.skippedSteps ?? []), ...newSkips];
|
|
120
123
|
const keys = new Set(task.decisions.map((entry) => JSON.stringify(entry)));
|
|
121
124
|
task.decisions.push(...latest.decisions.filter((entry) => !keys.has(JSON.stringify(entry))));
|
|
122
125
|
}
|
package/src/webui/api/index.ts
CHANGED
|
@@ -15,13 +15,15 @@ import { lobbyAbort, lobbyHistory, lobbySend, lobbySnapshot } from "./lobby.ts";
|
|
|
15
15
|
import { promptsAnswer, promptsDismiss, promptsList } from "./prompts.ts";
|
|
16
16
|
import { plannerAnswer, plannerCommentLine, plannerGet, plannerNew, plannerRetry, plannerSend, plannerEditMessage, plannerToggleSeat, plannerSave, plannerPrevious, plannerPreviousArchive, plannerPreviousDelete, plannerPreviousGet, plannerPreviousOpen } from "./planner.ts";
|
|
17
17
|
import { quickfixCancel, quickfixList, quickfixMovedToTask, quickfixRunAnyway, quickfixSubmit } from "./quickfix.ts";
|
|
18
|
-
import { tasksArchive, tasksArchived, tasksAuto, tasksComment, tasksComments, tasksDelete, tasksEditComment, tasksGet, tasksList, tasksMessage, tasksRestore, tasksOpen, tasksResume, tasksDeliveryReview, tasksDeliveryDefer, tasksDeliver, tasksAccept, tasksForceClose, tasksResolveApproval } from "./tasks.ts";
|
|
18
|
+
import { tasksArchive, tasksArchived, tasksAuto, tasksComment, tasksComments, tasksDelete, tasksEditComment, tasksGet, tasksList, tasksMessage, tasksRestore, tasksOpen, tasksResume, tasksDeliveryReview, tasksDeliveryDefer, tasksDeliver, tasksAccept, tasksSkipStep, tasksForceClose, tasksResolveApproval } from "./tasks.ts";
|
|
19
19
|
import { knowledgeAdd, knowledgeAsk, knowledgeComment, knowledgeEdit, knowledgeFiles, knowledgeOpen, knowledgeRemove, knowledgeReplaceFile, knowledgeUnnote } from "./knowledge.ts";
|
|
20
20
|
import { plansDiscard, plansGet, plansStart } from "./plans.ts";
|
|
21
21
|
import { gitCancelReview, gitJev, gitPull, gitPulls, gitReview } from "./git.ts";
|
|
22
22
|
import { issuesCreate, issuesGet, issuesList } from "./issues.ts";
|
|
23
|
-
import { sessionsAnswer, sessionsChat, sessionsList, sessionsMessage, sessionsStart, sessionsStop, sessionsSwitch } from "./sessions.ts";
|
|
23
|
+
import { sessionsAnswer, sessionsArchive, sessionsChat, sessionsList, sessionsMessage, sessionsRename, sessionsStart, sessionsStop, sessionsSwitch } from "./sessions.ts";
|
|
24
24
|
import { settingsGet, settingsLinters, settingsSet } from "./settings.ts";
|
|
25
|
+
import { terminalClose, terminalInput, terminalList, terminalOpen, terminalResize } from "./terminal.ts";
|
|
26
|
+
import { MAX_INPUT_CHARS } from "../terminal.ts";
|
|
25
27
|
import { themesChoose, themesGet, themesRemove, themesRename, themesSave } from "./themes.ts";
|
|
26
28
|
import { noticeText, pushNotice } from "../notices.ts";
|
|
27
29
|
|
|
@@ -134,6 +136,10 @@ function buildRoutes(): Record<string, Route> {
|
|
|
134
136
|
schema: Type.Object({ taskId: TaskId }, { additionalProperties: false }),
|
|
135
137
|
run: (body, ctx) => tasksAccept(body as { taskId: string }, ctx),
|
|
136
138
|
};
|
|
139
|
+
routes["tasks.skipStep"] = {
|
|
140
|
+
schema: Type.Object({ taskId: TaskId, step: Type.Integer({ minimum: 0, maximum: 49 }), text: Type.String({ minLength: 1, maxLength: 20_000 }), reason: Type.String({ minLength: 1, maxLength: 1000 }) }, { additionalProperties: false }),
|
|
141
|
+
run: (body, ctx) => tasksSkipStep(body as Parameters<typeof tasksSkipStep>[0], ctx),
|
|
142
|
+
};
|
|
137
143
|
routes["tasks.forceClose"] = {
|
|
138
144
|
schema: Type.Object({ taskId: TaskId, title: Type.String({ maxLength: 20_000 }) }, { additionalProperties: false }),
|
|
139
145
|
run: (body, ctx) => tasksForceClose(body as { taskId: string; title: string }, ctx),
|
|
@@ -219,6 +225,34 @@ function buildRoutes(): Record<string, Route> {
|
|
|
219
225
|
schema: Type.Object({ key: Type.Optional(TaskId), sessionId: Type.Optional(TaskId), claimTaskId: Type.Optional(TaskId) }, { additionalProperties: false }),
|
|
220
226
|
run: (body, ctx) => sessionsSwitch(body as { key?: string; sessionId?: string; claimTaskId?: string }, ctx),
|
|
221
227
|
};
|
|
228
|
+
const TerminalId = Type.String({ minLength: 1, maxLength: 40 });
|
|
229
|
+
const Cols = Type.Integer({ minimum: 2, maximum: 500 });
|
|
230
|
+
const Rows = Type.Integer({ minimum: 2, maximum: 300 });
|
|
231
|
+
routes["terminal.list"] = { schema: Empty, run: () => terminalList() };
|
|
232
|
+
routes["terminal.open"] = {
|
|
233
|
+
schema: Type.Object({ cols: Cols, rows: Rows }, { additionalProperties: false }),
|
|
234
|
+
run: (body, ctx) => terminalOpen(body as { cols: number; rows: number }, ctx),
|
|
235
|
+
};
|
|
236
|
+
routes["terminal.input"] = {
|
|
237
|
+
schema: Type.Object({ id: TerminalId, data: Type.String({ minLength: 1, maxLength: MAX_INPUT_CHARS }) }, { additionalProperties: false }),
|
|
238
|
+
run: (body) => terminalInput(body as { id: string; data: string }),
|
|
239
|
+
};
|
|
240
|
+
routes["terminal.resize"] = {
|
|
241
|
+
schema: Type.Object({ id: TerminalId, cols: Cols, rows: Rows }, { additionalProperties: false }),
|
|
242
|
+
run: (body) => terminalResize(body as { id: string; cols: number; rows: number }),
|
|
243
|
+
};
|
|
244
|
+
routes["terminal.close"] = {
|
|
245
|
+
schema: Type.Object({ id: TerminalId }, { additionalProperties: false }),
|
|
246
|
+
run: (body) => terminalClose(body as { id: string }),
|
|
247
|
+
};
|
|
248
|
+
routes["sessions.rename"] = {
|
|
249
|
+
schema: Type.Object({ key: Type.Optional(TaskId), name: Type.String({ minLength: 1, maxLength: 500 }) }, { additionalProperties: false }),
|
|
250
|
+
run: (body, ctx) => sessionsRename(body as { key?: string; name: string }, ctx),
|
|
251
|
+
};
|
|
252
|
+
routes["sessions.archive"] = {
|
|
253
|
+
schema: Type.Object({ key: TaskId, archived: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
254
|
+
run: (body, ctx) => sessionsArchive(body as { key: string; archived?: boolean }, ctx),
|
|
255
|
+
};
|
|
222
256
|
routes["sessions.answer"] = {
|
|
223
257
|
schema: Type.Object({ key: TaskId, dialogId: TaskId, answer: DialogAnswer }, { additionalProperties: false }),
|
|
224
258
|
run: (body, ctx) => sessionsAnswer(body as { key: string; dialogId: string; answer: { value: string } | { confirmed: boolean } | { cancelled: true } }, ctx),
|
package/src/webui/api/lobby.ts
CHANGED
|
@@ -22,7 +22,7 @@ function planFacts(task: Task, work: WorkProjection | undefined): Pick<SnapshotT
|
|
|
22
22
|
const active = steps.filter((step) => step.active);
|
|
23
23
|
const current = steps.find((step) => step.status === "current");
|
|
24
24
|
return {
|
|
25
|
-
progress: { done: steps.filter((step) => step.status === "done").length, total: steps.length },
|
|
25
|
+
progress: { done: steps.filter((step) => step.status === "done" || step.status === "skipped").length, total: steps.length },
|
|
26
26
|
...(current ? { currentStep: current.text } : {}),
|
|
27
27
|
...(active.length > 0 ? { activeSteps: active.map((step) => ({ text: step.text, workedMs: step.workedMs, activeAgentCount: step.activeAgentCount, runIds: step.runIds })) } : {}),
|
|
28
28
|
...clock,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* right now and the work time. Kept here (not via `persistedRuns`) so the
|
|
5
5
|
* server never loads the TUI.
|
|
6
6
|
*/
|
|
7
|
-
import { planChecklist, planSteps, stepWork, type PlanStep } from "../../pi/plan-checklist.ts";
|
|
7
|
+
import { planChecklist, planSteps, skippedIndexes, stepWork, type PlanStep } from "../../pi/plan-checklist.ts";
|
|
8
8
|
import type { AgentRun } from "../../schemas/findings.ts";
|
|
9
9
|
import type { Task } from "../../schemas/task.ts";
|
|
10
10
|
import type { WorkProjection } from "../../state/work-time.ts";
|
|
@@ -44,6 +44,8 @@ export interface StepView {
|
|
|
44
44
|
agents: StepAgent[];
|
|
45
45
|
/** Worker time on the step, in ms. */
|
|
46
46
|
workedMs: number;
|
|
47
|
+
/** The user skipped it: why, and when. */
|
|
48
|
+
skip?: { reason: string; at: string };
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface StepAgent {
|
|
@@ -62,8 +64,10 @@ export interface StepAgent {
|
|
|
62
64
|
export function stepViews(task: Task, work: WorkProjection | undefined, now = Date.now()): StepView[] {
|
|
63
65
|
if (!task.plan) return [];
|
|
64
66
|
const runs = [...persistedTaskRuns(task), ...activeRuns(task, work)];
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
+
const texts = planSteps(task.plan);
|
|
68
|
+
const checklist = planChecklist(task.plan, runs, task.skippedSteps);
|
|
69
|
+
const times = stepWork(texts, runs, now, skippedIndexes(texts, task.skippedSteps));
|
|
70
|
+
const skipOf = (text: string) => task.skippedSteps?.find((entry) => skippedIndexes([text], [entry]).size > 0);
|
|
67
71
|
const byId = new Map(runs.map((run) => [run.runId, run]));
|
|
68
72
|
return checklist.map((step, index) => {
|
|
69
73
|
const time = times[index] ?? { ms: 0, active: false, activeAgentCount: 0, runIds: [] };
|
|
@@ -71,6 +75,7 @@ export function stepViews(task: Task, work: WorkProjection | undefined, now = Da
|
|
|
71
75
|
const run = byId.get(runId);
|
|
72
76
|
return run ? [{ runId, ...(run.parentRunId ? { parentRunId: run.parentRunId } : {}), domain: run.domain, instruction: (run.instruction ?? "").split("\n")[0]!.trim(), startedAt: run.startedAt }] : [];
|
|
73
77
|
});
|
|
74
|
-
|
|
78
|
+
const skip = step.status === "skipped" ? skipOf(step.text) : undefined;
|
|
79
|
+
return { text: step.text, status: time.active && !skip ? "current" : step.status, active: time.active && !skip, activeAgentCount: time.activeAgentCount, runIds: time.runIds, agents, workedMs: Math.round(time.ms), ...(skip ? { skip: { reason: skip.reason, at: skip.at } } : {}) };
|
|
75
80
|
});
|
|
76
81
|
}
|
|
@@ -114,3 +114,24 @@ export function sessionsAnswer(body: { key: string; dialogId: string; answer: Di
|
|
|
114
114
|
session!.answer(body.dialogId, body.answer);
|
|
115
115
|
return { notice: "answer sent" };
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
/** Rename this window's session (no key) or a background one. */
|
|
119
|
+
export function sessionsRename(body: { key?: string; name: string }, ctx: ApiContext): { notice: string } {
|
|
120
|
+
if (!ctx.service.renameSession) fail(501, "unsupported", "this lobby cannot rename sessions");
|
|
121
|
+
try {
|
|
122
|
+
return { notice: ctx.service.renameSession!(body.key, body.name) };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
fail(/^no session/.test((error as Error).message) ? 404 : 400, /^no session/.test((error as Error).message) ? "not_found" : "bad_request", (error as Error).message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Put an ended background session away from the list (`archived` true, the default), or bring it back. */
|
|
129
|
+
export function sessionsArchive(body: { key: string; archived?: boolean }, ctx: ApiContext): { notice: string } {
|
|
130
|
+
if (!ctx.service.archiveSession) fail(501, "unsupported", "this lobby cannot archive sessions");
|
|
131
|
+
try {
|
|
132
|
+
return { notice: ctx.service.archiveSession!(body.key, body.archived ?? true) };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const message = (error as Error).message;
|
|
135
|
+
fail(/^no /.test(message) ? 404 : 409, /^no /.test(message) ? "not_found" : "conflict", message);
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/webui/api/tasks.ts
CHANGED
|
@@ -70,7 +70,7 @@ function checkOf(task: Task): TaskRow["check"] {
|
|
|
70
70
|
function progressOf(task: Task, work: WorkProjection | undefined, now: number): TaskRow["progress"] {
|
|
71
71
|
const steps = stepViews(task, work, now);
|
|
72
72
|
if (steps.length === 0) return undefined;
|
|
73
|
-
return { done: steps.filter((step) => step.status === "done").length, total: steps.length };
|
|
73
|
+
return { done: steps.filter((step) => step.status === "done" || step.status === "skipped").length, total: steps.length };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** The work clock as the page shows it; a finished task's no longer counts. */
|
|
@@ -302,6 +302,15 @@ export async function tasksAccept(body: { taskId: string }, ctx: ApiContext): Pr
|
|
|
302
302
|
return ctx.service.acceptTask(body.taskId);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/** Skip a plan step with the user's reason; refused once the plan changed under the page. */
|
|
306
|
+
export async function tasksSkipStep(body: { taskId: string; step: number; text: string; reason: string }, ctx: ApiContext): Promise<{ notice: string }> {
|
|
307
|
+
const task = interventionTask(body.taskId, ctx);
|
|
308
|
+
if (!task.plan) fail(400, "bad_request", "this task has no plan yet");
|
|
309
|
+
if (!body.reason.trim()) fail(400, "bad_request", "say why you are skipping this step");
|
|
310
|
+
if (!ctx.service.skipTaskStep) fail(400, "bad_request", "skipping steps is unavailable");
|
|
311
|
+
return ctx.service.skipTaskStep(body.taskId, body.step, body.text, body.reason);
|
|
312
|
+
}
|
|
313
|
+
|
|
305
314
|
export async function tasksForceClose(body: { taskId: string; title: string }, ctx: ApiContext): Promise<{ notice: string }> {
|
|
306
315
|
const task = interventionTask(body.taskId, ctx);
|
|
307
316
|
if (body.title !== task.title) fail(400, "bad_request", "the title must match exactly");
|
|
@@ -337,6 +346,7 @@ export function tasksGet(body: { taskId: string }, ctx: ApiContext): TaskDetail
|
|
|
337
346
|
const details = found.plan ? planDetails(found.plan) : undefined;
|
|
338
347
|
return {
|
|
339
348
|
canAccept: Boolean(active && ctx.service.acceptTask) && acceptable(found),
|
|
349
|
+
canSkip: Boolean(active && ctx.service.skipTaskStep && found.plan) && !TERMINAL_STATES.includes(found.state),
|
|
340
350
|
canForceClose: Boolean(active && ctx.service.forceCloseTask) && !TERMINAL_STATES.includes(found.state) && (ctx.service.canForceCloseTask?.(found.id) ?? true),
|
|
341
351
|
timing: projectPhaseTiming(found, new Date(now).toISOString()),
|
|
342
352
|
...(clock ? { work: clock } : {}),
|
|
@@ -348,6 +358,7 @@ export function tasksGet(body: { taskId: string }, ctx: ApiContext): TaskDetail
|
|
|
348
358
|
steps: stepViews(found, work, now).map((step) => ({
|
|
349
359
|
text: step.text,
|
|
350
360
|
status: step.status === "pending" ? "open" : step.status,
|
|
361
|
+
...(step.skip ? { skip: step.skip } : {}),
|
|
351
362
|
...(step.active ? { active: true, workedMs: step.workedMs, activeAgentCount: step.activeAgentCount, agents: step.agents } : {}),
|
|
352
363
|
})),
|
|
353
364
|
amendments: [...found.amendments],
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** `terminal.*`: shells in the project's folder for the page's terminal drawer (see `../terminal.ts`). */
|
|
2
|
+
import { fail, type ApiContext } from "./index.ts";
|
|
3
|
+
import { terminalBackend, terminals, TerminalError } from "../terminal.ts";
|
|
4
|
+
import type { TerminalInfo } from "../protocol.ts";
|
|
5
|
+
|
|
6
|
+
function guard<T>(run: () => T): T {
|
|
7
|
+
try {
|
|
8
|
+
return run();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (error instanceof TerminalError) fail(error.missing ? 404 : 409, error.missing ? "not_found" : "conflict", error.message);
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function terminalList(): Promise<{ available: boolean; reason?: string; terminals: TerminalInfo[] }> {
|
|
16
|
+
const backend = await terminalBackend();
|
|
17
|
+
return { available: backend.kind !== "none", ...(backend.kind === "none" ? { reason: backend.reason } : {}), terminals: terminals.list() };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function terminalOpen(body: { cols: number; rows: number }, ctx: ApiContext): Promise<TerminalInfo> {
|
|
21
|
+
try {
|
|
22
|
+
return await terminals.open(ctx.service.projectRoot?.() ?? process.cwd(), body.cols, body.rows);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error instanceof TerminalError) fail(409, "conflict", error.message);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function terminalInput(body: { id: string; data: string }): Record<string, never> {
|
|
30
|
+
guard(() => terminals.input(body.id, body.data));
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function terminalResize(body: { id: string; cols: number; rows: number }): Record<string, never> {
|
|
35
|
+
guard(() => terminals.resize(body.id, body.cols, body.rows));
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function terminalClose(body: { id: string }): Record<string, never> {
|
|
40
|
+
guard(() => terminals.close(body.id));
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
@@ -9,6 +9,7 @@ import { LobbyFeed } from "../../lobby/feed.ts";
|
|
|
9
9
|
import { promptHub } from "../../lobby/prompt-hub.ts";
|
|
10
10
|
import { lobbyTopics } from "../../lobby/topics.ts";
|
|
11
11
|
import type { BackgroundSession, DialogAnswer, SessionDialog } from "../../lobby/sessions.ts";
|
|
12
|
+
import { cleanSessionName } from "../../lobby/sessions.ts";
|
|
12
13
|
import type { PlanComment } from "../../state/comments.ts";
|
|
13
14
|
import type { LobbyService } from "../../lobby/host.ts";
|
|
14
15
|
import { loadScenario, type ScenarioFixture } from "./fixtures.ts";
|
|
@@ -280,11 +281,16 @@ function fakeBackground(entry: NonNullable<ScenarioFixture["backgroundSessions"]
|
|
|
280
281
|
stop() {
|
|
281
282
|
status = "exited";
|
|
282
283
|
dialogs.length = 0;
|
|
284
|
+
lobbyTopics.bump("sessions");
|
|
283
285
|
},
|
|
284
286
|
answer(id: string, _answer: DialogAnswer) {
|
|
285
287
|
const index = dialogs.findIndex((dialog) => dialog.id === id);
|
|
286
288
|
if (index >= 0) dialogs.splice(index, 1);
|
|
287
289
|
},
|
|
290
|
+
rename(name: string) {
|
|
291
|
+
session.name = name;
|
|
292
|
+
lobbyTopics.bump("sessions");
|
|
293
|
+
},
|
|
288
294
|
};
|
|
289
295
|
return session as unknown as BackgroundSession;
|
|
290
296
|
}
|
|
@@ -335,6 +341,8 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
|
|
|
335
341
|
const auto = new Set(fixture.autoTasks ?? []);
|
|
336
342
|
const comments = new Map<string, Array<Record<string, unknown>>>(Object.entries(fixture.taskComments ?? {}));
|
|
337
343
|
const backgrounds = (fixture.backgroundSessions ?? []).map(fakeBackground);
|
|
344
|
+
const archivedBackgrounds: BackgroundSession[] = [];
|
|
345
|
+
let sessionName = fixture.status.sessionName;
|
|
338
346
|
let planning = fakePlanner(fixture.mockPlanner ?? defaultPlanner());
|
|
339
347
|
let config: BotLobbyConfig = resolveConfig(fixture.settings?.config ?? {});
|
|
340
348
|
const models = fixture.settings?.models ?? DEFAULT_MODELS;
|
|
@@ -391,7 +399,7 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
|
|
|
391
399
|
let commentSeq = 0;
|
|
392
400
|
const service = {
|
|
393
401
|
sessionId: () => fixture.status.sessionId,
|
|
394
|
-
sessionName: () =>
|
|
402
|
+
sessionName: () => sessionName,
|
|
395
403
|
masterBusy: () => fixture.status.busy,
|
|
396
404
|
config: () => config,
|
|
397
405
|
linters: () => fixture.settings?.linters ?? [{ tool: "ESLint", folder: "", installed: true }, { tool: "Ruff", folder: "services/api", installed: false }],
|
|
@@ -481,6 +489,15 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
|
|
|
481
489
|
},
|
|
482
490
|
sendToTask: (taskId: string, _text: string) => (tasks.some((task) => (task as { id: string }).id === taskId) ? `sent — the session driving ${taskId} passes it to its oracle` : `no task ${taskId}`),
|
|
483
491
|
sendToSession: (_sessionId: string, _text: string) => "sent — that session passes it to its oracle within a few seconds",
|
|
492
|
+
skipTaskStep: async (taskId: string, step: number, text: string, reason: string) => {
|
|
493
|
+
const task = tasks.find((entry) => (entry as { id: string }).id === taskId) as { id: string; skippedSteps?: Array<{ text: string; reason: string; at: string }>; decisions?: Array<{ domain: string; text: string; createdAt: string }> } | undefined;
|
|
494
|
+
if (!task) throw new Error(`no task ${taskId}`);
|
|
495
|
+
const at = new Date().toISOString();
|
|
496
|
+
task.skippedSteps = [...(task.skippedSteps ?? []), { text, reason: reason.trim(), at }];
|
|
497
|
+
task.decisions = [...(task.decisions ?? []), { domain: "master", text: `The user skipped step ${step + 1} ("${text}") from the Tasks screen. Reason: ${reason.trim()}`, createdAt: at }];
|
|
498
|
+
lobbyTopics.bump("tasks");
|
|
499
|
+
return { notice: `step ${step + 1} skipped; the oracle has been told` };
|
|
500
|
+
},
|
|
484
501
|
resumeTask: async (taskId: string) => {
|
|
485
502
|
const task = tasks.find((entry) => (entry as { id: string }).id === taskId) as { id: string; title: string; paused?: boolean; ownerSessionId?: string } | undefined;
|
|
486
503
|
if (!task) return { notice: `no task ${taskId}` };
|
|
@@ -500,6 +517,30 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
|
|
|
500
517
|
return { notice: `resumed ${taskId} in a background session`, key: session.key };
|
|
501
518
|
},
|
|
502
519
|
sessions: () => [...backgrounds],
|
|
520
|
+
renameSession: (key: string | undefined, name: string) => {
|
|
521
|
+
const clean = cleanSessionName(name);
|
|
522
|
+
if (!key) {
|
|
523
|
+
sessionName = clean;
|
|
524
|
+
lobbyTopics.bump("status");
|
|
525
|
+
lobbyTopics.bump("sessions");
|
|
526
|
+
return `renamed this window to ${clean}`;
|
|
527
|
+
}
|
|
528
|
+
const session = backgrounds.find((entry) => entry.key === key);
|
|
529
|
+
if (!session) throw new Error(`no session ${key}`);
|
|
530
|
+
session.rename(clean);
|
|
531
|
+
return `renamed ${key} to ${clean}`;
|
|
532
|
+
},
|
|
533
|
+
archiveSession: (key: string, archived: boolean) => {
|
|
534
|
+
const from = archived ? backgrounds : archivedBackgrounds;
|
|
535
|
+
const index = from.findIndex((entry) => entry.key === key);
|
|
536
|
+
if (index < 0) throw new Error(archived ? `no session ${key} to archive` : `no archived session ${key}`);
|
|
537
|
+
const session = from[index]!;
|
|
538
|
+
if (session.alive) throw new Error(`${session.name} is still running: stop it before archiving it`);
|
|
539
|
+
from.splice(index, 1);
|
|
540
|
+
(archived ? archivedBackgrounds : backgrounds).push(session);
|
|
541
|
+
lobbyTopics.bump("sessions");
|
|
542
|
+
return archived ? `archived ${session.name}` : `brought back ${session.name}`;
|
|
543
|
+
},
|
|
503
544
|
startSession: (start: { request?: string; plan?: { id: string; title: string }; auto?: boolean }) => {
|
|
504
545
|
const key = `S${backgrounds.length + 1}`;
|
|
505
546
|
const session = fakeBackground({ key, name: start.plan?.title ?? start.request?.trim().slice(0, 40) ?? "mock session", status: "starting", ...(start.plan ? { planId: start.plan.id } : {}) });
|