@loicngr/kobo 2.1.13 → 2.1.15
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/AGENTS.md +29 -6
- package/CHANGELOG.md +8 -0
- package/CONFIGURATION.md +28 -4
- package/README.md +1 -1
- package/dist/mcp-server/kobo-tasks-server.js +43 -1
- package/dist/server/db/auto-loop-final-review-schema.js +18 -0
- package/dist/server/db/migrations.js +10 -1
- package/dist/server/db/review-return-schema.js +18 -1
- package/dist/server/db/schema.js +2 -0
- package/dist/server/index.js +2 -1
- package/dist/server/routes/workspaces.js +61 -1
- package/dist/server/services/agent/engines/claude-code/engine.js +37 -1
- package/dist/server/services/agent/orchestrator.js +158 -38
- package/dist/server/services/auto-loop-final-review-service.js +329 -0
- package/dist/server/services/auto-loop-service.js +53 -10
- package/dist/server/services/review-launch-runtime.js +17 -0
- package/dist/server/services/review-return-service.js +58 -10
- package/dist/server/services/review-service.js +59 -11
- package/dist/server/services/workspace-service.js +11 -0
- package/dist/server/services/workspace-template-service.js +10 -0
- package/dist/shared/auto-loop-review.js +1 -0
- package/dist/shared/review-return.js +1 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-D7jouiuP.js +2 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +1 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-lKjfaC6V.js +1 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DD90Nl6c.js → ChangelogPage-BplM7qEB.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-D_BQFWiT.js → ComparisonPanel-DxK5v9w_.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-D5ilP5kt.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +1 -0
- package/src/client/dist/pwa/assets/{DashboardPage-DyvTdmBg.js → DashboardPage-BsP-MATS.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-Cd15vFWj.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-bm639sIu.js → DocumentsPanel-CGrf8Q3H.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-KfwSzjeY.js → DrawerToggleButton-BANeLSsR.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-CEPV5Qw0.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-37IHOIW-.js → HealthPage-BVTNx-92.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-C6nvIQjU.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-DBKtwilW.js +2 -0
- package/src/client/dist/pwa/assets/{MemoryPanel-CucZMhjy.js → MemoryPanel-C71rTpv9.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-CUQybzVR.js → MemoryProposalList-CoqhUGrv.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-x6qPyl_7.js → QBtnDropdown-U9Ey5W8h.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-D33aoRoG.js → QExpansionItem-Be_A1MT1.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-Di-D6t2u.js → QScrollArea-D4lYzAPQ.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-MdPXvwm4.js → QSelect-fS0DLOd1.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-BhwG7lp2.js → QTooltip-kStOgY0X.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-qFi9Wxzh.js → SchedulePanel-C7nTGdWE.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-DA9nRmsA.js → SearchPage-C6J72pkQ.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-SuH-aIzv.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-Brw2XsYJ.js → SplitWorkspacePage-Bqn4gdVw.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DVP18-nk.js → TerminalPanel-zLeDCReI.js} +9 -9
- package/src/client/dist/pwa/assets/{ToolCallItem-cSHh3p_F.js → ToolCallItem-B8b_Sagj.js} +3 -3
- package/src/client/dist/pwa/assets/{TouchPan-C6LZZ3rw.js → TouchPan-Bl8VzIDK.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Czlqo18H.js → TourReplayButton-pL0HCboU.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BDYGq0pC.js → WorkspaceAttentionLabels-9Eomx0rQ.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-B248nKIU.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-B6clgURl.js +4 -0
- package/src/client/dist/pwa/assets/{browser-ifCwUGpE.js → browser-C-PJ4alw.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BBkGq3lN.js → build-path-tree-SW9rj7B_.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-toz-4drX.js +1 -0
- package/src/client/dist/pwa/assets/de-Ddxe8cc6.js +17 -0
- package/src/client/dist/pwa/assets/{editor.api-CV_seGJR.js → editor.api-BJTPhnh8.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-ttw0bL63.js → efforts-BO9xGn9q.js} +1 -1
- package/src/client/dist/pwa/assets/es-DBJIQeWW.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-C8biMRXo.js → expand-template-BOZMtWau.js} +1 -1
- package/src/client/dist/pwa/assets/{format-Cu95dbxt.js → format-hEnpRyit.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-C25D1RSd.js → formatters-DD_eFc4z.js} +1 -1
- package/src/client/dist/pwa/assets/fr-yBzIIPaQ.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DJEFr74q.js → freemarker2-B6nUofvT.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-C_lzsMjX.js → handlebars-4P3gtM8p.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CWmLFAmY.js → html-C9ZzWo-y.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-2-ULabk2.js +1 -0
- package/src/client/dist/pwa/assets/index-Vk99jtxD.js +20 -0
- package/src/client/dist/pwa/assets/it-D44U-PT1.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-tsuiEUe-.js → javascript-BGEC5ggd.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-BBr9BvUk.js → liquid-DNtF630O.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-gjBH1cXG.js → mdx-CICwyOd0.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CvGcBIsb.js → monaco-contributions-BkuwvgrT.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-Uw-Lfb8Z.js → monaco.contribution-GX8XA-PE.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-KnWzjYIc.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-CWuZTz4Z.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-BDQa2TgU.js → project-color-B1SMFjSe.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DlIgD1n6.js → python-n5uPdTVS.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-AraFPlpM.js → razor-AOj0nsUt.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-CB8UpXHM.js → typescript-Uhkrqihh.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-CSREHQM4.js → use-search-index-status-CW0BdpBZ.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-6yxNVmjH.js → use-tours-CndFlyL-.js} +2 -2
- package/src/client/dist/pwa/assets/{use-worktree-restore-BM3Giig1.js → use-worktree-restore-BZxibZS5.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-BPMFvTnQ.js → workspace-templates-W557e1-m.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-uRjL1P43.js → xml-B-hnMgEs.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-C9CFR4qV.js → yaml-DqXkTvgm.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +26 -0
- package/src/mcp-server/kobo-tasks-server.ts +46 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BTRzc60o.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-BZAoCukb.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-CdCs8PRS.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-D0K2PlZD.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-C-Ejlc9_.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BQOOup9o.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-DXQ1ouYl.js +0 -2
- package/src/client/dist/pwa/assets/SettingsPage-DcV6apqE.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-BwUkijpS.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-Bwfv559o.css +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-CeSgjIY7.js +0 -1
- package/src/client/dist/pwa/assets/de-CHY2BAfL.js +0 -17
- package/src/client/dist/pwa/assets/es-DbcSKp38.js +0 -17
- package/src/client/dist/pwa/assets/fr-CyyJ8S2c.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CobVQiTM.js +0 -1
- package/src/client/dist/pwa/assets/index-ImUH7Bbt.js +0 -20
- package/src/client/dist/pwa/assets/it-XQJW2Xkr.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-D0Vm6a09.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-PMSxqP6t.js +0 -1
|
@@ -5,11 +5,13 @@ import { slugifyProjectName } from '../utils/project-slug.js';
|
|
|
5
5
|
import { deferUntilWorkspaceAvailable, isWorkspaceLifecycleBusy } from '../utils/workspace-lifecycle-guard.js';
|
|
6
6
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
7
7
|
import * as orchestrator from './agent/orchestrator.js';
|
|
8
|
+
import * as finalReview from './auto-loop-final-review-service.js';
|
|
8
9
|
import { bindLoopMessages, claimLoopMessages, enqueueLoopMessage, hasLoopMessage, listLoopMessages, recoverLoopMessages, settleLoopMessages, } from './auto-loop-message-service.js';
|
|
9
10
|
import { getRuntime, setRuntime } from './auto-loop-state-service.js';
|
|
10
11
|
import * as cleanupScriptService from './cleanup-script-service.js';
|
|
11
12
|
import * as lifecycleHookService from './lifecycle-hook-service.js';
|
|
12
13
|
import * as quotaBackoffService from './quota-backoff-service.js';
|
|
14
|
+
import { getReviewReturn } from './review-return-service.js';
|
|
13
15
|
import * as settingsService from './settings-service.js';
|
|
14
16
|
import { getSuitePrompts } from './skill-suite-prompts.js';
|
|
15
17
|
import { invalidateTaskFinalization } from './task-mutations.js';
|
|
@@ -45,6 +47,7 @@ export function getStatus(workspaceId) {
|
|
|
45
47
|
const pending = quotaBackoffService.getPending(workspaceId);
|
|
46
48
|
return {
|
|
47
49
|
...runtime,
|
|
50
|
+
finalReview: finalReview.getFinalReviewStatus(workspaceId),
|
|
48
51
|
state: !row?.auto_loop
|
|
49
52
|
? runtime.state === 'completed'
|
|
50
53
|
? 'completed'
|
|
@@ -153,12 +156,15 @@ export function enable(workspaceId) {
|
|
|
153
156
|
const db = getDb();
|
|
154
157
|
db.prepare('UPDATE workspaces SET auto_loop = 1, no_progress_streak = 0 WHERE id = ?').run(workspaceId);
|
|
155
158
|
orchestrator.resetAutoLoopRetries(workspaceId);
|
|
159
|
+
finalReview.resumeFinalReview(workspaceId);
|
|
156
160
|
db.prepare('DELETE FROM auto_loop_progress WHERE workspace_id=?').run(workspaceId);
|
|
161
|
+
const activeSessionId = orchestrator.getActiveSessionId(workspaceId);
|
|
157
162
|
setRuntime(workspaceId, {
|
|
158
163
|
state: 'waiting',
|
|
159
164
|
reason: null,
|
|
160
165
|
diagnostic_attempts: 0,
|
|
161
166
|
phase: row.auto_loop_ready ? 'execution' : 'grooming',
|
|
167
|
+
...(activeSessionId ? { current_session_id: activeSessionId } : {}),
|
|
162
168
|
});
|
|
163
169
|
emitEphemeral(workspaceId, 'autoloop:enabled', {});
|
|
164
170
|
if (orchestrator.hasController(workspaceId))
|
|
@@ -178,6 +184,12 @@ export function disable(workspaceId, reason) {
|
|
|
178
184
|
block(workspaceId, reason);
|
|
179
185
|
return;
|
|
180
186
|
}
|
|
187
|
+
if (reason !== 'completed') {
|
|
188
|
+
const pendingReview = getReviewReturn(workspaceId);
|
|
189
|
+
if (pendingReview && finalReview.isAutoLoopReview(workspaceId, pendingReview.reviewSessionId))
|
|
190
|
+
orchestrator.cancelReviewReturn(workspaceId);
|
|
191
|
+
finalReview.cancelAutoLoopFinalReview(workspaceId, reason);
|
|
192
|
+
}
|
|
181
193
|
const db = getDb();
|
|
182
194
|
quotaBackoffService.cancel(workspaceId, 'completed');
|
|
183
195
|
setRuntime(workspaceId, { state: reason === 'completed' ? 'completed' : 'stopped', reason });
|
|
@@ -216,12 +228,16 @@ export function disable(workspaceId, reason) {
|
|
|
216
228
|
* already scheduled a backoff timer and will call `onQuotaBackoffExpired` once
|
|
217
229
|
* the window closes — that function owns the next spawn in that case.
|
|
218
230
|
*/
|
|
219
|
-
export function onSessionEnded(workspaceId, reason, taskProgressDelta, instructionIntake = false) {
|
|
231
|
+
export function onSessionEnded(workspaceId, reason, taskProgressDelta, instructionIntake = false, sessionId) {
|
|
220
232
|
const row = getRow(workspaceId);
|
|
221
233
|
if (!row)
|
|
222
234
|
return;
|
|
223
235
|
if (row.auto_loop !== 1)
|
|
224
236
|
return;
|
|
237
|
+
// The initial writer can have been launched outside spawnNextIteration.
|
|
238
|
+
// Capture the actual completed writer before the final-review gate runs.
|
|
239
|
+
if (sessionId)
|
|
240
|
+
setRuntime(workspaceId, { current_session_id: sessionId });
|
|
225
241
|
const settledInstructions = settleLoopMessages(workspaceId, getRuntime(workspaceId).current_session_id, reason === 'completed');
|
|
226
242
|
// The orchestrator may already have settled this exact turn's deliveries.
|
|
227
243
|
instructionIntake ||= settledInstructions > 0;
|
|
@@ -249,6 +265,7 @@ export function onSessionEnded(workspaceId, reason, taskProgressDelta, instructi
|
|
|
249
265
|
}
|
|
250
266
|
if (getRuntime(workspaceId).state === 'blocked')
|
|
251
267
|
return;
|
|
268
|
+
finalReview.onFinalReviewCorrectionEnded(workspaceId, reason, sessionId);
|
|
252
269
|
if (row.auto_loop_ready === 1 && countPendingTasks(workspaceId) === 0 && listLoopMessages(workspaceId).length === 0) {
|
|
253
270
|
spawnNextIteration(workspaceId);
|
|
254
271
|
return;
|
|
@@ -466,6 +483,10 @@ function spawnNextIteration(workspaceId, opts = {}) {
|
|
|
466
483
|
block(workspaceId, 'message-delivery-unknown');
|
|
467
484
|
return;
|
|
468
485
|
}
|
|
486
|
+
if (getReviewReturn(workspaceId)) {
|
|
487
|
+
orchestrator.resumePendingReviewReturns(workspaceId);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
469
490
|
const grooming = row.auto_loop_ready !== 1;
|
|
470
491
|
if (listLoopMessages(workspaceId).some((m) => m.state === 'pending'))
|
|
471
492
|
invalidateTaskFinalization(getDb(), workspaceId);
|
|
@@ -473,10 +494,20 @@ function spawnNextIteration(workspaceId, opts = {}) {
|
|
|
473
494
|
ensureFinalVerification(workspaceId);
|
|
474
495
|
const task = grooming ? null : pickNextTask(workspaceId);
|
|
475
496
|
const pendingInstructions = listLoopMessages(workspaceId).filter((m) => m.state === 'pending');
|
|
476
|
-
|
|
497
|
+
const summaryContinuation = finalReview.getFinalReviewSummaryContinuation(workspaceId);
|
|
498
|
+
if (!grooming && !task && pendingInstructions.length === 0 && !summaryContinuation) {
|
|
499
|
+
if (finalReview.advanceFinalReview(workspaceId))
|
|
500
|
+
return;
|
|
477
501
|
disable(workspaceId, 'completed');
|
|
478
502
|
return;
|
|
479
503
|
}
|
|
504
|
+
finalReview.invalidateCompletedFinalReview(workspaceId);
|
|
505
|
+
const correctionSession = finalReview.getFinalReviewCorrectionSession(workspaceId);
|
|
506
|
+
const correctionModel = correctionSession
|
|
507
|
+
? getDb()
|
|
508
|
+
.prepare('SELECT model FROM agent_sessions WHERE id=? AND workspace_id=?')
|
|
509
|
+
.get(correctionSession, workspaceId)?.model
|
|
510
|
+
: undefined;
|
|
480
511
|
const runtime = getRuntime(workspaceId);
|
|
481
512
|
const diagnostic = row.no_progress_streak >= NO_PROGRESS_STALL_THRESHOLD && runtime.diagnostic_attempts === 0;
|
|
482
513
|
const iterationNumber = runtime.iteration + 1;
|
|
@@ -514,6 +545,8 @@ ${AUTO_LOOP_HARD_RULES}`;
|
|
|
514
545
|
Three iterations produced no task progress. Read recent workspace events, explain the blocker, and try a different approach or decompose the task. Never mark unfinished or unverified work done. If intervention is required, state the precise action needed.
|
|
515
546
|
${prompt}`;
|
|
516
547
|
prompt += `\n${AUTO_LOOP_ITERATION_RULES}`;
|
|
548
|
+
if (summaryContinuation && !task)
|
|
549
|
+
prompt = summaryContinuation;
|
|
517
550
|
const globalSettings = settingsService.getGlobalSettings();
|
|
518
551
|
const projectSlug = globalSettings.worktreesPrefixByProject
|
|
519
552
|
? slugifyProjectName(projectSettings?.displayName ?? '', row.project_path)
|
|
@@ -522,7 +555,7 @@ ${prompt}`;
|
|
|
522
555
|
resolveWorkspaceWorktreePath(row.project_path, row.working_branch, globalSettings.worktreesPath, projectSlug);
|
|
523
556
|
const storedMode = row.agent_permission_mode;
|
|
524
557
|
const agentPermissionMode = storedMode === 'bypass' || storedMode === 'strict' || storedMode === 'interactive' ? storedMode : 'plan';
|
|
525
|
-
if (!grooming && agentPermissionMode === 'plan') {
|
|
558
|
+
if (!grooming && !summaryContinuation && agentPermissionMode === 'plan') {
|
|
526
559
|
const message = 'Execution requires an explicit permission choice. Change this workspace from Plan to an execution mode (strict, interactive where supported, or bypass), then resume auto-loop. Permissions were not changed.';
|
|
527
560
|
block(workspaceId, message);
|
|
528
561
|
if (opts.throwOnStartAgentError)
|
|
@@ -544,14 +577,14 @@ ${prompt}`;
|
|
|
544
577
|
if (instructions.length)
|
|
545
578
|
prompt = `[Kōbō auto-loop — integrate user instructions]\nRead kobo__list_tasks and relevant workspace history. Integrate the following instructions into the todos and acceptance criteria, preserving the user's constraints. Update existing tasks and create missing ones before ending this turn. Do not implement or finalize work during this intake turn: the next iteration will select the updated highest-priority task. Leave finalization open.\n${instructions.map((m) => `- ${m.content}`).join('\n')}\n${grooming ? prompt : AUTO_LOOP_ITERATION_RULES}`;
|
|
546
579
|
try {
|
|
547
|
-
const agent = orchestrator.startAgent(workspaceId, worktreePath, prompt, row.model,
|
|
580
|
+
const agent = orchestrator.startAgent(workspaceId, worktreePath, prompt, correctionModel ?? row.model,
|
|
548
581
|
// resume=false spawns a fresh session per iteration (default); resume=true
|
|
549
582
|
// (auto_loop_session_mode='continuous') resumes the workspace's last
|
|
550
583
|
// session so the agent keeps full context across tasks. When there is no
|
|
551
584
|
// prior session to resume (e.g. the very first iteration), orchestrator's
|
|
552
585
|
// resolveSessionForResume gracefully falls back to a fresh session — no
|
|
553
586
|
// special-casing needed here.
|
|
554
|
-
grooming || continuousSession, agentPermissionMode,
|
|
587
|
+
grooming || continuousSession || !!correctionSession, agentPermissionMode, correctionSession, row.reasoning_effort);
|
|
555
588
|
agentSessionId = agent.agentSessionId;
|
|
556
589
|
if (agentSessionId)
|
|
557
590
|
bindLoopMessages(workspaceId, agentSessionId);
|
|
@@ -628,7 +661,8 @@ export function onQuotaBackoffExpired(workspaceId, pending) {
|
|
|
628
661
|
return;
|
|
629
662
|
if (row.archived_at !== null)
|
|
630
663
|
return;
|
|
631
|
-
|
|
664
|
+
const pendingReview = getReviewReturn(workspaceId);
|
|
665
|
+
if (row.auto_loop !== 1 && !pendingReview)
|
|
632
666
|
return;
|
|
633
667
|
if (row.status !== 'quota')
|
|
634
668
|
return;
|
|
@@ -638,7 +672,7 @@ export function onQuotaBackoffExpired(workspaceId, pending) {
|
|
|
638
672
|
}
|
|
639
673
|
if (orchestrator.hasController(workspaceId)) {
|
|
640
674
|
pendingQuotaStops.add(workspaceId);
|
|
641
|
-
const stopped = orchestrator.stopAgentAndWait(workspaceId, undefined, 'replacement');
|
|
675
|
+
const stopped = orchestrator.stopAgentAndWait(workspaceId, undefined, pendingReview ? 'review-recovery' : 'replacement');
|
|
642
676
|
// The timer already consumed its row. Persist recovery immediately, after the
|
|
643
677
|
// technical stop's synchronous cancellation, so shutdown/crash during the await loses no intent.
|
|
644
678
|
deferQuotaRetry(workspaceId, pending);
|
|
@@ -646,7 +680,10 @@ export function onQuotaBackoffExpired(workspaceId, pending) {
|
|
|
646
680
|
.then((outcome) => {
|
|
647
681
|
// Stop, archive, or a manual replacement may have superseded this retry while we awaited the engine.
|
|
648
682
|
const current = getRow(workspaceId);
|
|
649
|
-
if (!current ||
|
|
683
|
+
if (!current ||
|
|
684
|
+
current.archived_at ||
|
|
685
|
+
(!current.auto_loop && !getReviewReturn(workspaceId)) ||
|
|
686
|
+
current.status !== 'quota')
|
|
650
687
|
return;
|
|
651
688
|
if (outcome === 'timeout' || outcome === 'failed' || orchestrator.isShuttingDown()) {
|
|
652
689
|
deferQuotaRetry(workspaceId, pending);
|
|
@@ -654,7 +691,10 @@ export function onQuotaBackoffExpired(workspaceId, pending) {
|
|
|
654
691
|
}
|
|
655
692
|
updateWorkspaceStatus(workspaceId, 'idle');
|
|
656
693
|
quotaBackoffService.cancel(workspaceId, 'completed');
|
|
657
|
-
|
|
694
|
+
if (getReviewReturn(workspaceId))
|
|
695
|
+
orchestrator.resumePendingReviewReturns(workspaceId);
|
|
696
|
+
else
|
|
697
|
+
spawnNextIteration(workspaceId);
|
|
658
698
|
})
|
|
659
699
|
.catch((err) => {
|
|
660
700
|
console.error(`[auto-loop] quota controller stop failed for '${workspaceId}':`, err);
|
|
@@ -667,7 +707,10 @@ export function onQuotaBackoffExpired(workspaceId, pending) {
|
|
|
667
707
|
// when a slot frees, even if no agent can start at this instant.
|
|
668
708
|
updateWorkspaceStatus(workspaceId, 'idle');
|
|
669
709
|
emitEphemeral(workspaceId, 'agent:quota-backoff-cancelled', { reason: 'completed' });
|
|
670
|
-
|
|
710
|
+
if (getReviewReturn(workspaceId))
|
|
711
|
+
orchestrator.resumePendingReviewReturns(workspaceId);
|
|
712
|
+
else
|
|
713
|
+
spawnNextIteration(workspaceId);
|
|
671
714
|
}
|
|
672
715
|
/**
|
|
673
716
|
* Called when a workspace transitions to auto_loop_ready=true (grooming done).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Covers explicit Stop during asynchronous git preparation, before a durable return exists. */
|
|
2
|
+
const launches = new Map();
|
|
3
|
+
export function beginReviewLaunch(workspaceId) {
|
|
4
|
+
if (launches.has(workspaceId))
|
|
5
|
+
return null;
|
|
6
|
+
const launch = { cancelled: false };
|
|
7
|
+
launches.set(workspaceId, launch);
|
|
8
|
+
return launch;
|
|
9
|
+
}
|
|
10
|
+
export function cancelReviewLaunch(workspaceId) {
|
|
11
|
+
const launch = launches.get(workspaceId);
|
|
12
|
+
if (launch)
|
|
13
|
+
launch.cancelled = true;
|
|
14
|
+
}
|
|
15
|
+
export function finishReviewLaunch(workspaceId) {
|
|
16
|
+
launches.delete(workspaceId);
|
|
17
|
+
}
|
|
@@ -4,9 +4,10 @@ import { updateWorkspaceEngineConfiguration } from './workspace-service.js';
|
|
|
4
4
|
export function registerReviewReturn(pending) {
|
|
5
5
|
getDb()
|
|
6
6
|
.prepare(`INSERT INTO pending_review_returns
|
|
7
|
-
(workspace_id, review_session_id, original_session_id, original_configuration, review_configuration, created_at)
|
|
8
|
-
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
9
|
-
.run(pending.workspaceId, pending.reviewSessionId, pending.originalSessionId, JSON.stringify(pending.original), JSON.stringify(pending.review), new Date().toISOString());
|
|
7
|
+
(workspace_id, review_session_id, original_session_id, original_configuration, review_configuration, created_at, review_prompt)
|
|
8
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
9
|
+
.run(pending.workspaceId, pending.reviewSessionId, pending.originalSessionId, JSON.stringify(pending.original), JSON.stringify(pending.review), new Date().toISOString(), pending.reviewPrompt ?? null);
|
|
10
|
+
emitEphemeral(pending.workspaceId, 'review:return-status', getReviewReturnStatus(pending.workspaceId));
|
|
10
11
|
}
|
|
11
12
|
export function getReviewReturn(workspaceId, reviewSessionId) {
|
|
12
13
|
const row = getDb().prepare('SELECT * FROM pending_review_returns WHERE workspace_id = ?').get(workspaceId);
|
|
@@ -18,26 +19,73 @@ export function getReviewReturn(workspaceId, reviewSessionId) {
|
|
|
18
19
|
originalSessionId: row.original_session_id,
|
|
19
20
|
original: JSON.parse(row.original_configuration),
|
|
20
21
|
review: JSON.parse(row.review_configuration),
|
|
22
|
+
phase: row.phase,
|
|
23
|
+
reviewPrompt: row.review_prompt,
|
|
24
|
+
returnPrompt: row.return_prompt,
|
|
25
|
+
error: row.last_error,
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
|
-
/**
|
|
24
|
-
export function restoreReviewConfiguration(workspaceId, reviewSessionId) {
|
|
28
|
+
/** Restore initial settings; cancellation removes intent, suspension preserves it. */
|
|
29
|
+
export function restoreReviewConfiguration(workspaceId, reviewSessionId, preserve = false) {
|
|
25
30
|
const pending = getReviewReturn(workspaceId, reviewSessionId);
|
|
26
31
|
if (!pending)
|
|
27
32
|
return null;
|
|
28
33
|
getDb().transaction(() => {
|
|
29
|
-
|
|
34
|
+
if (!preserve)
|
|
35
|
+
getDb().prepare('DELETE FROM pending_review_returns WHERE workspace_id = ?').run(workspaceId);
|
|
30
36
|
const original = pending.original;
|
|
31
37
|
updateWorkspaceEngineConfiguration(workspaceId, original.engine, original.model, original.reasoningEffort, original.agentPermissionMode);
|
|
32
38
|
})();
|
|
33
39
|
emitEphemeral(workspaceId, 'workspace:configuration', pending.original);
|
|
40
|
+
if (!preserve)
|
|
41
|
+
emitEphemeral(workspaceId, 'review:return-status', null);
|
|
34
42
|
return pending;
|
|
35
43
|
}
|
|
36
|
-
/**
|
|
44
|
+
/** Restore user settings without discarding an unfinished review/return. */
|
|
37
45
|
export function reconcileReviewReturns() {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
for (const pending of listReviewReturns()) {
|
|
47
|
+
if (pending.phase === 'dispatching')
|
|
48
|
+
setReviewReturnPhase(pending.workspaceId, 'unknown', 'The server stopped while sending the review report. Check the original session before retrying to avoid duplicate delivery.');
|
|
49
|
+
restoreReviewConfiguration(pending.workspaceId, undefined, true);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function getReviewReturnStatus(workspaceId) {
|
|
53
|
+
const pending = getReviewReturn(workspaceId);
|
|
54
|
+
return pending
|
|
55
|
+
? {
|
|
56
|
+
reviewSessionId: pending.reviewSessionId,
|
|
57
|
+
originalSessionId: pending.originalSessionId,
|
|
58
|
+
phase: pending.phase ?? 'reviewing',
|
|
59
|
+
error: pending.error ?? null,
|
|
60
|
+
}
|
|
61
|
+
: null;
|
|
62
|
+
}
|
|
63
|
+
export function listReviewReturns() {
|
|
64
|
+
return getDb().prepare('SELECT workspace_id FROM pending_review_returns').all()
|
|
65
|
+
.map((row) => getReviewReturn(row.workspace_id))
|
|
66
|
+
.filter((row) => row !== null);
|
|
67
|
+
}
|
|
68
|
+
export function setReviewReturnPhase(workspaceId, phase, error = null, prompt) {
|
|
69
|
+
getDb()
|
|
70
|
+
.prepare('UPDATE pending_review_returns SET phase = ?, last_error = ?, return_prompt = COALESCE(?, return_prompt) WHERE workspace_id = ?')
|
|
71
|
+
.run(phase, error, prompt ?? null, workspaceId);
|
|
72
|
+
emitEphemeral(workspaceId, 'review:return-status', getReviewReturnStatus(workspaceId));
|
|
73
|
+
}
|
|
74
|
+
/** Claim the known-unsent report before any asynchronous engine dispatch. */
|
|
75
|
+
export function claimReviewReturn(workspaceId) {
|
|
76
|
+
const claimed = getDb()
|
|
77
|
+
.prepare("UPDATE pending_review_returns SET phase = 'dispatching', last_error = NULL WHERE workspace_id = ? AND phase = 'ready'")
|
|
78
|
+
.run(workspaceId).changes === 1;
|
|
79
|
+
if (claimed)
|
|
80
|
+
emitEphemeral(workspaceId, 'review:return-status', getReviewReturnStatus(workspaceId));
|
|
81
|
+
return claimed;
|
|
82
|
+
}
|
|
83
|
+
export function completeReviewReturn(workspaceId) {
|
|
84
|
+
const removed = getDb()
|
|
85
|
+
.prepare("DELETE FROM pending_review_returns WHERE workspace_id = ? AND phase = 'dispatching'")
|
|
86
|
+
.run(workspaceId).changes;
|
|
87
|
+
if (removed)
|
|
88
|
+
emitEphemeral(workspaceId, 'review:return-status', null);
|
|
41
89
|
}
|
|
42
90
|
/** Reassemble the last assistant message, handling deltas and final snapshots. */
|
|
43
91
|
export function buildReviewReturnPrompt(workspaceId, reviewSessionId) {
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { reviewConfigurationChanged } from '../../shared/review.js';
|
|
4
|
+
import { getDb } from '../db/index.js';
|
|
4
5
|
import { assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
5
6
|
import * as gitOps from '../utils/git-ops.js';
|
|
6
7
|
import { assertWorkspaceLifecycleAvailable } from '../utils/workspace-lifecycle-guard.js';
|
|
7
8
|
import { listEngines } from './agent/engines/registry.js';
|
|
8
9
|
import * as agentManager from './agent/orchestrator.js';
|
|
10
|
+
import { bindReviewSession, getFinalReviewSourceSession } from './auto-loop-final-review-service.js';
|
|
11
|
+
import { canStartAutomatically } from './auto-loop-service.js';
|
|
12
|
+
import { beginReviewLaunch, finishReviewLaunch } from './review-launch-runtime.js';
|
|
9
13
|
import { getReviewReturn, registerReviewReturn, restoreReviewConfiguration } from './review-return-service.js';
|
|
10
14
|
import { getActiveReviewTemplate, renderReviewTemplate } from './review-template-service.js';
|
|
11
15
|
import * as wsService from './websocket-service.js';
|
|
12
16
|
import * as workspaceService from './workspace-service.js';
|
|
13
17
|
const execFileAsync = promisify(execFile);
|
|
14
|
-
const launching = new Set();
|
|
15
18
|
export class ReviewRequestError extends Error {
|
|
16
19
|
status;
|
|
17
20
|
constructor(message, status) {
|
|
@@ -19,6 +22,11 @@ export class ReviewRequestError extends Error {
|
|
|
19
22
|
this.status = status;
|
|
20
23
|
}
|
|
21
24
|
}
|
|
25
|
+
export class ReviewAdmissionDeferred extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super('Automatic review is waiting for agent capacity');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
22
30
|
export function parseReviewRequest(input) {
|
|
23
31
|
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
24
32
|
throw new ReviewRequestError('Request body must be a JSON object', 400);
|
|
@@ -34,7 +42,7 @@ export function parseReviewRequest(input) {
|
|
|
34
42
|
}
|
|
35
43
|
return body;
|
|
36
44
|
}
|
|
37
|
-
function resolveConfiguration(workspace, body) {
|
|
45
|
+
function resolveConfiguration(workspace, body, allowCustomModel = false) {
|
|
38
46
|
const engineId = body.engine ?? workspace.engine;
|
|
39
47
|
const engine = listEngines().find((e) => e.id === engineId);
|
|
40
48
|
if (!engine)
|
|
@@ -48,7 +56,8 @@ function resolveConfiguration(workspace, body) {
|
|
|
48
56
|
agentPermissionMode: body.returnToSession ? 'plan' : (body.agentPermissionMode ?? workspace.agentPermissionMode),
|
|
49
57
|
};
|
|
50
58
|
// Preserve an existing custom model when unchanged; reject incompatible selections.
|
|
51
|
-
if (
|
|
59
|
+
if (!allowCustomModel &&
|
|
60
|
+
(changedEngine || config.model !== workspace.model) &&
|
|
52
61
|
!engine.capabilities.models.some((m) => m.id === config.model))
|
|
53
62
|
throw new ReviewRequestError(`Model '${config.model}' is not supported by '${engineId}'`, 400);
|
|
54
63
|
if ((changedEngine || config.reasoningEffort !== workspace.reasoningEffort) &&
|
|
@@ -62,22 +71,25 @@ function configure(id, config) {
|
|
|
62
71
|
workspaceService.updateWorkspaceEngineConfiguration(id, config.engine, config.model, config.reasoningEffort, config.agentPermissionMode);
|
|
63
72
|
wsService.emitEphemeral(id, 'workspace:configuration', config);
|
|
64
73
|
}
|
|
65
|
-
export async function startWorkspaceReview(id, input) {
|
|
66
|
-
|
|
74
|
+
export async function startWorkspaceReview(id, input, internal) {
|
|
75
|
+
const launch = beginReviewLaunch(id);
|
|
76
|
+
if (!launch)
|
|
67
77
|
throw new ReviewRequestError('A review is already being started for this workspace', 409);
|
|
68
|
-
launching.add(id);
|
|
69
78
|
try {
|
|
70
79
|
const workspace = workspaceService.getWorkspace(id);
|
|
71
80
|
if (!workspace)
|
|
72
81
|
throw new ReviewRequestError(`Workspace '${id}' not found`, 404);
|
|
73
82
|
const body = parseReviewRequest(input);
|
|
74
|
-
const configuration = resolveConfiguration(workspace, body);
|
|
83
|
+
const configuration = resolveConfiguration(workspace, body, internal?.autoLoopFinalReview);
|
|
75
84
|
assertWorkspaceLifecycleAvailable(id);
|
|
76
85
|
if (workspace.archivedAt || workspace.worktreePurgedAt)
|
|
77
86
|
throw new ReviewRequestError('Restore the workspace before starting a review', 409);
|
|
78
87
|
if (getReviewReturn(id))
|
|
79
88
|
throw new ReviewRequestError('A review with an automatic return is already in progress', 409);
|
|
80
|
-
const
|
|
89
|
+
const sourceSessionId = internal?.autoLoopFinalReview ? getFinalReviewSourceSession(id) : null;
|
|
90
|
+
const originalSession = internal?.autoLoopFinalReview
|
|
91
|
+
? workspaceService.listSessions(id).find((session) => session.id === sourceSessionId)
|
|
92
|
+
: workspaceService.getActiveSession(id);
|
|
81
93
|
const changed = reviewConfigurationChanged(workspace, configuration);
|
|
82
94
|
const differsFromSession = !!originalSession &&
|
|
83
95
|
((!!originalSession.engine && originalSession.engine !== configuration.engine) ||
|
|
@@ -127,20 +139,34 @@ export async function startWorkspaceReview(id, input) {
|
|
|
127
139
|
'\n\nThis is a read-only review session; these rules override any template instruction or skill that suggests fixing issues. Do not modify, create or delete files, and do not commit or push: the original agent session applies the fixes. Running read-only checks such as tests or linters is fine. Do not ask the user questions or wait for input: note any decision that needs the user in your summary instead.' +
|
|
128
140
|
'\n\nFinish with a standalone summary of your review findings, including severity, file/line references, recommended fixes and any remaining uncertainty. This final message will be handed back to the original agent session. Do not rely on earlier messages to explain your findings.';
|
|
129
141
|
assertWorkspaceLifecycleAvailable(id);
|
|
142
|
+
if (launch.cancelled)
|
|
143
|
+
throw new ReviewRequestError('Review launch was cancelled', 409);
|
|
130
144
|
const current = workspaceService.getWorkspace(id);
|
|
131
145
|
if (!current || current.archivedAt || current.worktreePurgedAt || current.status === 'compacting')
|
|
132
146
|
throw new ReviewRequestError('The workspace is no longer available for a review', 409);
|
|
133
147
|
if (reviewConfigurationChanged(current, workspace))
|
|
134
148
|
throw new ReviewRequestError('The workspace configuration changed; reopen the review dialog', 409);
|
|
135
|
-
if (body.returnToSession &&
|
|
149
|
+
if (body.returnToSession &&
|
|
150
|
+
(internal?.autoLoopFinalReview ? getFinalReviewSourceSession(id) : workspaceService.getActiveSession(id)?.id) !==
|
|
151
|
+
originalSession?.id)
|
|
136
152
|
throw new ReviewRequestError('The current session changed; reopen the review dialog', 409);
|
|
153
|
+
if (internal?.autoLoopFinalReview && (!current.autoLoop || internal.shouldLaunch?.() === false))
|
|
154
|
+
throw new ReviewRequestError('Final review was cancelled before launch', 409);
|
|
137
155
|
let emitSessionId;
|
|
138
156
|
if (newSession) {
|
|
139
157
|
assertAgentStopped(await agentManager.stopAgentAndWait(id, undefined, 'replacement'));
|
|
140
158
|
assertWorkspaceLifecycleAvailable(id);
|
|
159
|
+
if (launch.cancelled)
|
|
160
|
+
throw new ReviewRequestError('Review launch was cancelled', 409);
|
|
161
|
+
if (internal?.autoLoopFinalReview &&
|
|
162
|
+
(!workspaceService.getWorkspace(id)?.autoLoop || internal.shouldLaunch?.() === false))
|
|
163
|
+
throw new ReviewRequestError('Final review was cancelled before launch', 409);
|
|
141
164
|
if (agentManager.getAgentStatus(id))
|
|
142
165
|
throw new ReviewRequestError('Another agent has started in this workspace', 409);
|
|
166
|
+
if (internal?.autoLoopFinalReview && !canStartAutomatically(id))
|
|
167
|
+
throw new ReviewAdmissionDeferred();
|
|
143
168
|
let pendingSessionId;
|
|
169
|
+
let reviewLaunch = null;
|
|
144
170
|
try {
|
|
145
171
|
if (body.returnToSession && originalSession) {
|
|
146
172
|
pendingSessionId = workspaceService.createIdleSession(id).id;
|
|
@@ -156,11 +182,33 @@ export async function startWorkspaceReview(id, input) {
|
|
|
156
182
|
agentPermissionMode: workspace.agentPermissionMode,
|
|
157
183
|
},
|
|
158
184
|
review: configuration,
|
|
185
|
+
reviewPrompt: rendered,
|
|
159
186
|
});
|
|
160
187
|
}
|
|
188
|
+
if (internal?.autoLoopFinalReview && pendingSessionId && originalSession) {
|
|
189
|
+
reviewLaunch = bindReviewSession(id, pendingSessionId, originalSession.id);
|
|
190
|
+
rendered += reviewLaunch.promptSuffix;
|
|
191
|
+
getDb()
|
|
192
|
+
.prepare('UPDATE pending_review_returns SET review_prompt = ? WHERE workspace_id = ?')
|
|
193
|
+
.run(rendered, id);
|
|
194
|
+
}
|
|
161
195
|
if (changed)
|
|
162
196
|
configure(id, configuration);
|
|
163
|
-
const
|
|
197
|
+
const startArgs = [
|
|
198
|
+
id,
|
|
199
|
+
worktreePath,
|
|
200
|
+
rendered,
|
|
201
|
+
configuration.model,
|
|
202
|
+
false,
|
|
203
|
+
configuration.agentPermissionMode,
|
|
204
|
+
pendingSessionId,
|
|
205
|
+
configuration.reasoningEffort,
|
|
206
|
+
];
|
|
207
|
+
if (reviewLaunch) {
|
|
208
|
+
startArgs[8] = undefined;
|
|
209
|
+
startArgs[9] = { mcpEnv: reviewLaunch.mcpEnv };
|
|
210
|
+
}
|
|
211
|
+
const agent = agentManager.startAgent(...startArgs);
|
|
164
212
|
workspaceService.updateWorkspaceStatus(id, 'executing');
|
|
165
213
|
emitSessionId = agent.agentSessionId;
|
|
166
214
|
}
|
|
@@ -190,6 +238,6 @@ export async function startWorkspaceReview(id, input) {
|
|
|
190
238
|
return { ok: true, messageSent: true, newSession };
|
|
191
239
|
}
|
|
192
240
|
finally {
|
|
193
|
-
|
|
241
|
+
finishReviewLaunch(id);
|
|
194
242
|
}
|
|
195
243
|
}
|
|
@@ -873,6 +873,8 @@ export function renameSession(sessionId, workspaceId, name) {
|
|
|
873
873
|
const row = db.prepare('SELECT * FROM agent_sessions WHERE id = ?').get(sessionId);
|
|
874
874
|
return row ? mapSession(row) : null;
|
|
875
875
|
}
|
|
876
|
+
export class ReviewSessionInUseError extends Error {
|
|
877
|
+
}
|
|
876
878
|
/** Delete a stopped session and only the persisted conversation attached to it. */
|
|
877
879
|
export function deleteSession(sessionId, workspaceId) {
|
|
878
880
|
const db = getDb();
|
|
@@ -881,6 +883,15 @@ export function deleteSession(sessionId, workspaceId) {
|
|
|
881
883
|
.get(sessionId, workspaceId);
|
|
882
884
|
if (!session)
|
|
883
885
|
return false;
|
|
886
|
+
const pendingReview = db
|
|
887
|
+
.prepare(`SELECT 1 FROM pending_review_returns WHERE workspace_id=? AND (original_session_id=? OR review_session_id=?)`)
|
|
888
|
+
.get(workspaceId, sessionId, sessionId);
|
|
889
|
+
const pendingFinalReview = db
|
|
890
|
+
.prepare(`SELECT 1 FROM auto_loop_final_reviews r JOIN workspaces w ON w.id=r.workspace_id
|
|
891
|
+
WHERE r.workspace_id=? AND w.auto_loop=1 AND r.state IN ('reviewing','fixing','blocked') AND (r.original_session_id=? OR r.review_session_id=?)`)
|
|
892
|
+
.get(workspaceId, sessionId, sessionId);
|
|
893
|
+
if (pendingReview || pendingFinalReview)
|
|
894
|
+
throw new ReviewSessionInUseError('This session is required by a pending review. Cancel the review return or stop auto-loop before deleting it.');
|
|
884
895
|
if (session.status === 'running') {
|
|
885
896
|
throw new Error(`Cannot delete active session '${sessionId}'`);
|
|
886
897
|
}
|
|
@@ -3,6 +3,7 @@ import { nanoid } from 'nanoid';
|
|
|
3
3
|
import { getDb } from '../db/index.js';
|
|
4
4
|
import { writeJsonFileAtomically } from '../utils/atomic-json-file.js';
|
|
5
5
|
import { getWorkspaceTemplatesPath } from '../utils/paths.js';
|
|
6
|
+
import { getFinalReviewStatus, parseFinalReviewConfiguration } from './auto-loop-final-review-service.js';
|
|
6
7
|
import { getGlobalSettings } from './settings-service.js';
|
|
7
8
|
const FILE_VERSION = 1;
|
|
8
9
|
export const MAX_WORKSPACE_TEMPLATES = 100;
|
|
@@ -33,6 +34,14 @@ export function sanitizePreset(input) {
|
|
|
33
34
|
return {};
|
|
34
35
|
const raw = input;
|
|
35
36
|
const preset = {};
|
|
37
|
+
if (raw.autoLoopFinalReview !== undefined) {
|
|
38
|
+
try {
|
|
39
|
+
preset.autoLoopFinalReview = parseFinalReviewConfiguration(raw.autoLoopFinalReview);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* Unsupported saved configuration is omitted. */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
36
45
|
for (const key of STRING_KEYS) {
|
|
37
46
|
if (typeof raw[key] === 'string')
|
|
38
47
|
preset[key] = raw[key];
|
|
@@ -216,6 +225,7 @@ export function presetFromWorkspace(workspaceId) {
|
|
|
216
225
|
model: row.model,
|
|
217
226
|
reasoningEffort: row.reasoning_effort,
|
|
218
227
|
autoLoop: row.auto_loop === 1,
|
|
228
|
+
autoLoopFinalReview: getFinalReviewStatus(workspaceId).configuration,
|
|
219
229
|
...(row.brainstorm_model ? { brainstormModel: row.brainstorm_model } : {}),
|
|
220
230
|
...(row.description ? { description: row.description } : {}),
|
|
221
231
|
tasks: tasks.filter((t) => t.is_acceptance_criterion !== 1).map((t) => t.title),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.15",
|
|
4
4
|
"description": "Kōbō — local multi-workspace orchestrator for Claude Code and OpenAI Codex, with isolated Git worktrees, live review and optional automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{M as e,S as t,dt as n,lt as r,p as i,pt as a,v as o}from"./reactivity.esm-bundler-DY1JkAEA.js";import{C as s,F as c,J as l,L as u,N as d,O as f,P as p,T as m,Y as h,_ as g,d as _,f as v,g as y,j as ee,k as b,l as x,p as S,r as C,u as w,y as T,z as te}from"./runtime-core.esm-bundler-Bqg71Vll.js";import{$ as ne,K as re,M as E,f as ie,z as ae}from"./dom-BtYTvLeN.js";import{l as oe,t as D,u as se}from"./QBtn-BUSdPaf4.js";import{$t as ce,O as le,Vt as O,Wt as ue,Xt as de,at as fe,ft as pe,ht as me,j as he,ln as k,m as ge,on as A,ot as _e,pt as ve,v as ye}from"./index-Vk99jtxD.js";import{t as be}from"./QSpinnerDots-xdWyaE1i.js";import{t as xe}from"./QList-Cx1UfaUm.js";import{t as Se}from"./QMarkupTable-UhP7fVUp.js";import{c as j,l as M,s as N}from"./QSelect-fS0DLOd1.js";import{t as P}from"./QItemSection-MdJ1_ZK7.js";import{t as Ce}from"./QExpansionItem-Be_A1MT1.js";import{a as F,c as we,r as I,t as L}from"./ToolCallItem-B8b_Sagj.js";import{t as Te}from"./QScrollArea-D4lYzAPQ.js";import{a as R}from"./use-tours-CndFlyL-.js";import{f as Ee,g as z,m as De,p as Oe,u as ke}from"./use-worktree-restore-BZxibZS5.js";import{t as B}from"./QTooltip-kStOgY0X.js";import{n as V}from"./render-chat-markdown-CafzCcmu.js";import{n as H}from"./WorkspacePage-B6clgURl.js";function Ae(e,t){return s(`div`,e,[s(`table`,{class:`q-table`},t)])}var je={list:xe,table:Se},U=[`list`,`table`,`__qtable`],Me=ae({name:`QVirtualScroll`,props:{...M,type:{type:String,default:`list`,validator:e=>U.includes(e)},items:{type:Array,default:()=>[]},itemsFn:Function,itemsSize:Number,scrollTarget:k},setup(e,{slots:n,attrs:r}){let i,a=t(null),o=x(()=>e.itemsSize>=0&&e.itemsFn!==void 0?Number.parseInt(e.itemsSize,10):Array.isArray(e.items)?e.items.length:0),{virtualScrollSliceRange:c,localResetVirtualScroll:u,padVirtualScroll:f,onVirtualScrollEvt:m}=j({virtualScrollLength:o,getVirtualScrollTarget:y,getVirtualScrollEl:v}),h=x(()=>{if(o.value===0)return[];let t=(e,t)=>({index:c.value.from+t,item:e});return e.itemsFn===void 0?e.items.slice(c.value.from,c.value.to).map(t):e.itemsFn(c.value.from,c.value.to-c.value.from).map(t)}),g=x(()=>`q-virtual-scroll q-virtual-scroll`+(e.virtualScrollHorizontal?`--horizontal`:`--vertical`)+(e.scrollTarget===void 0?` scroll`:``)),_=x(()=>e.scrollTarget===void 0?{tabindex:0}:{});l(o,()=>{u()}),l(()=>e.scrollTarget,()=>{C(),S()});function v(){let e=a.value;return e===null?null:e.$el||e}function y(){return i}function S(){let t=v();t!==null&&(i=A(t,e.scrollTarget),i.addEventListener(`scroll`,m,E.passive))}function C(){i!==void 0&&(i.removeEventListener(`scroll`,m,E.passive),i=void 0)}function w(){let t=f(e.type===`list`?`div`:`tbody`,h.value.map(n.default));return n.before!==void 0&&(t=n.before().concat(t)),ie(n.after,t)}return u(),p(()=>{S()}),b(()=>{S()}),d(()=>{C()}),ee(()=>{C()}),()=>{if(n.default===void 0){console.error(`QVirtualScroll: default scoped slot is required for rendering`);return}return e.type===`__qtable`?Ae({ref:a,class:`q-table__middle `+g.value,tabindex:0},w()):s(je[e.type],{...r,ref:a,class:[r.class,g.value],..._.value},w)}}}),Ne=new Set([`setup`,`cleanup`,`archive`]);function Pe(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:Ne.has(e.sender)||le(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function Fe(e){let t=[],n=null,r=null;for(let i of e){let e=Pe(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`:e===`user`&&i.type===`user`&&i.source?JSON.stringify([`mcp`,i.source.transport,i.source.clientName]):e;!n||r!==o||a?(n={speaker:e,ts:i.ts,items:[i]},r=o,t.push(n),a&&(n=null)):n.items.push(i)}return t}function W(e){switch(e.type){case`text`:return`text:${e.messageId}`;case`thinking`:return`thinking:${e.messageId}:${e.eventIds?.[0]??e.ts??``}`;case`tool`:return`tool:${e.toolCallId}`;case`session`:return`session:${e.kind}:${e.eventIds?.[0]??e.ts??``}`;case`user`:return`user:${e.eventIds?.[0]??`${e.sender}:${e.ts??``}`}`;case`error`:return`error:${e.eventIds?.[0]??`${e.category}:${e.ts??``}`}`}}function Ie(e){let t=e.items[0];return`${e.speaker}:${t?W(t):`empty`}`}function Le(e,t){let n=Math.min(t,e.length)-1;for(let t=n;t>=0;t--)if(e[t]?.speaker===`user`)return t;return-1}function Re(e,t,n){return new Promise(r=>{let i=!1,a=null,o=null,s=e=>{i||(i=!0,a!==null&&clearTimeout(a),a=null,o?.(),r(e))};if(o=l(e,e=>{t(e)&&s(!0)},{immediate:!0}),i){o();return}a=setTimeout(()=>s(!1),n)})}var ze={class:`agent-error-item`},Be={class:`agent-error-item__body`},Ve={class:`agent-error-item__title`},G={class:`agent-error-item__message`},He=R(T({__name:`AgentErrorItem`,props:{item:{}},setup(e){let t=e,{te:n}=de(),r=x(()=>{let e=`agent.error.${t.item.category}`;return n(e)?e:`agent.error.other`});return(t,n)=>(u(),S(`div`,ze,[g(oe,{name:`error_outline`,size:`16px`,class:`agent-error-item__icon`}),w(`div`,Be,[w(`div`,Ve,a(t.$t(r.value)),1),w(`div`,G,a(e.item.message),1)])]))}}),[[`__scopeId`,`data-v-e98c3c02`]]),Ue={class:`text-caption text-kobo-3`},We=T({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=x(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return z(t.item.detail);case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(u(),S(`span`,Ue,a(e.$t(n.value)),1))}}),K=new Map;function q(){for(let e of K.values())e.controller.abort(),e.blobUrl&&URL.revokeObjectURL(e.blobUrl);K.clear()}typeof window<`u`&&(window.addEventListener(`kobo:auth-token-changed`,q),window.addEventListener(`storage`,e=>{(e.key===`kobo:network-token`||e.key===null)&&q()}));function J(e){if(!/^\/api\/workspaces\/[^/]+\/images\/file\?path=/.test(e))throw Error(`Invalid workspace image URL`);let t=K.get(e);if(!t){let n=new AbortController,r={references:0,controller:n,ready:Promise.resolve(``)};r.ready=O(e,{signal:n.signal}).then(async e=>{if(!e.ok)throw await ue(e);let t=await e.blob();if(n.signal.aborted)throw new DOMException(`Aborted`,`AbortError`);return r.blobUrl=URL.createObjectURL(t),r.blobUrl}),t=r,K.set(e,t)}t.references++;let n=!1,r=t;return{ready:r.ready,release(){n||(n=!0,!(--r.references>0)&&(r.controller.abort(),r.blobUrl&&URL.revokeObjectURL(r.blobUrl),K.get(e)===r&&K.delete(e)))}}}function Ge(e,t){let n=o(``),r=o(0),a=()=>{r.value++},s=e=>{(e.key===`kobo:network-token`||e.key===null)&&a()};return window.addEventListener(`kobo:auth-token-changed`,a),window.addEventListener(`storage`,s),i(()=>{window.removeEventListener(`kobo:auth-token-changed`,a),window.removeEventListener(`storage`,s)}),l([e,r],([e],r,i)=>{let a=!0,o=[],s=document.createElement(`template`);s.innerHTML=e;for(let e of s.content.querySelectorAll(`img`)){let r=e.getAttribute(`src`)??``;if(e.removeAttribute(`data-kobo-image-url`),!/^\/api\/workspaces\/[^/]+\/images\/file\?path=/.test(r))continue;e.removeAttribute(`src`);let i=J(r);o.push(i),i.ready.then(t=>{a&&(e.setAttribute(`src`,t),e.setAttribute(`data-kobo-image-url`,r),n.value=s.innerHTML)}).catch(()=>{a&&(e.setAttribute(`title`,t()),n.value=s.innerHTML)})}n.value=s.innerHTML,i(()=>{a=!1;for(let e of o)e.release()})},{immediate:!0}),n}function Ke(e,t){return t?e.replace(/\[image:\s+([^\]]+)\]/g,(e,n)=>{let r=String(n).trim();return/^(\.ai\/images\/|images\/)/.test(r)?`}/images/file?path=${encodeURIComponent(r)}`})`:e}):e}var qe=[`innerHTML`],Je=[`innerHTML`],Ye=[`src`],Y=R(T({__name:`UserMessageItem`,props:{item:{},workspaceId:{}},setup(t){let n=t,{t:r}=de(),a=x(()=>n.item.sender===`system-prompt`),s=Ge(x(()=>{let e=Ke(n.item.content,n.workspaceId);return V(e)}),()=>r(`network.login.unreachable`)),c=o(null),d=o(!1),f;function p(){d.value=!1,c.value=null,f?.release(),f=void 0}l(d,e=>{e||p()}),l(()=>n.workspaceId,p);let m=e=>{(e.key===`kobo:network-token`||e.key===null)&&p()};window.addEventListener(`storage`,m),window.addEventListener(`kobo:auth-token-changed`,p),i(()=>{p(),window.removeEventListener(`kobo:auth-token-changed`,p),window.removeEventListener(`storage`,m)});function y(e){let t=e.target;if(t?.tagName!==`IMG`)return;let n=t;n.src&&(p(),n.dataset.koboImageUrl&&(f=J(n.dataset.koboImageUrl),f.ready.catch(p)),c.value=n.src,d.value=!0)}return(t,n)=>(u(),S(C,null,[a.value?(u(),_(Ce,{key:0,dense:``,"dense-toggle":``,label:t.$t(`chat.systemPrompt`),"header-class":`text-kobo-2 text-caption`},{default:h(()=>[w(`div`,{class:`q-py-xs markdown-user-prompt`,innerHTML:e(s)},null,8,qe)]),_:1},8,[`label`])):(u(),S(`div`,{key:1,class:`markdown-message`,onClick:y},[w(`div`,{innerHTML:e(s)},null,8,Je)])),g(ce,{modelValue:d.value,"onUpdate:modelValue":n[1]||=e=>d.value=e},{default:h(()=>[c.value?(u(),S(`img`,{key:0,src:c.value,alt:``,class:`image-lightbox-img`,onClick:n[0]||=e=>d.value=!1},null,8,Ye)):v(``,!0)]),_:1},8,[`modelValue`])],64))}}),[[`__scopeId`,`data-v-db2fc622`]]),Xe=[`data-event-ids`],X={class:`turn-header`},Ze={key:0,class:`turn-time`},Qe={class:`turn-time turn-time-updated`},$e={key:2,class:`turn-actions`},et={class:`turn-body`},tt={class:`ellipsis`},nt={class:`q-px-sm q-pb-sm`},rt={key:0,class:`turn-scroll-top`},it=R(T({__name:`TurnCard`,props:{turn:{},workspaceId:{},highlighted:{type:Boolean}},emits:[`scrollTo`],setup(t,{emit:i}){let s=t,c=i,{t:l}=de(),d=fe(),f=m(F),p=o(null);function ee(){let e=p.value;if(!e)return;let t=e.closest(`.q-scrollarea`)?.querySelector(`.q-scrollarea__content`);if(!t){e.scrollIntoView({behavior:`smooth`,block:`start`});return}let n=e.getBoundingClientRect().top-t.getBoundingClientRect().top;c(`scrollTo`,Math.max(0,n-8))}let b=x(()=>{switch(s.turn.speaker){case`user`:{let e=s.turn.items[0],t=e?.type===`user`?e.source:void 0;return{label:t?l(`chat.externalLlm`,{name:t.clientName}):l(`chat.you`),accent:`var(--kobo-turn-user)`,badgeClass:`turn-badge-user`}}case`agent`:return{label:l(`chat.agent`),accent:`var(--kobo-turn-agent)`,badgeClass:`turn-badge-agent`};case`system-prompt`:return{label:l(`chat.systemPrompt`),accent:`var(--kobo-text-3)`,badgeClass:`turn-badge-system`};case`session`:return{label:l(`chat.session`),accent:`var(--kobo-text-3)`,badgeClass:`turn-badge-session`};case`script`:{let e=s.turn.items[0],t=e?.type===`user`?e.sender:``;return{label:le(t)?l(`chat.hookScript`,{event:t.slice(5)}):l(t===`archive`?`chat.archiveScript`:t===`setup`?`chat.setupScript`:`chat.cleanupScript`),accent:`var(--kobo-success)`,badgeClass:`turn-badge-script`}}}});function T(e,t=!1){if(!e)return``;let n=new Date(e);return Number.isNaN(n.getTime())?``:n.toLocaleTimeString(void 0,t?{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}:{hour:`2-digit`,minute:`2-digit`})}let re=x(()=>T(s.turn.ts)),E=x(()=>s.turn.items.flatMap(e=>e.eventIds??[]).join(` `)),ie=x(()=>{let e=s.turn.items;if(e.length===0)return null;for(let t=e.length-1;t>=0;t--){let n=e[t].ts;if(n)return n}return null}),ae=x(()=>{let e=s.turn.ts,t=ie.value;if(!t||!e||t===e)return``;let n=new Date(e).getTime(),r=new Date(t).getTime();return Number.isNaN(n)||Number.isNaN(r)||r<=n?``:T(t,r-n<6e4)}),se=x(()=>ae.value!==``);function ce(e){return we(e.origin,Object.values(d.subagents[s.workspaceId]??{}),e.type===`tool`?e.toolCallId:void 0)}function O(e){return e||l(`chat.subagentActivity`)}function ue(e){return(e.type===`text`||e.type===`thinking`||e.type===`tool`)&&e.origin?.kind===`subagent`}let pe=x(()=>{let e=[],t=new Map;for(let n of s.turn.items){if(!ue(n)){e.push({type:`item`,key:W(n),item:n});continue}let r=ce(n),i=r?.toolUseId??n.origin?.toolCallId??n.origin?.threadId??W(n),a=t.get(i);if(a){a.items.push(n);continue}let o={type:`subagent`,key:`subagent:${W(n)}`,items:[n],subagentKey:i,subagentName:r?.description||null};t.set(i,o),e.push(o)}return e});function me(e){e.origin&&f?.({origin:e.origin})}let he=x(()=>s.turn.items.filter(e=>e.type===`tool`).length);return(i,o)=>(u(),S(`div`,{ref_key:`cardEl`,ref:p,class:r([`turn-card`,{"turn-card--user":t.turn.speaker===`user`,"turn-card--highlighted":t.highlighted}]),"data-event-ids":E.value,style:n({"--turn-accent":b.value.accent})},[w(`div`,X,[w(`span`,{class:r([`turn-badge`,b.value.badgeClass])},a(b.value.label),3),re.value?(u(),S(`span`,Ze,a(re.value),1)):v(``,!0),se.value?(u(),S(C,{key:1},[g(oe,{name:`arrow_forward`,size:`10px`,color:`kobo-3`,class:`turn-time-arrow`}),w(`span`,Qe,[y(a(ae.value)+` `,1),g(B,null,{default:h(()=>[y(a(e(l)(`chat.lastUpdatedAt`,{time:ae.value})),1)]),_:1})])],64)):v(``,!0),he.value>0?(u(),S(`span`,$e,` · `+a(e(l)(`chat.nActions`,{n:he.value})),1)):v(``,!0)]),w(`div`,et,[(u(!0),S(C,null,te(pe.value,n=>(u(),S(C,{key:n.key},[n.type===`subagent`?(u(),_(Ce,{key:0,class:`turn-subagent-activity`,dense:``,label:O(n.subagentName),caption:e(l)(`chat.nActions`,{n:n.items.length}),"default-opened":t.highlighted&&n.items.some(e=>e.eventIds?.some(e=>E.value.includes(e)))},{header:h(()=>[g(P,null,{default:h(()=>[g(N,{class:`row items-center no-wrap`,title:n.subagentName??void 0},{default:h(()=>[g(oe,{name:`hub`,size:`16px`,class:`q-mr-xs`},{default:h(()=>[g(B,null,{default:h(()=>[y(a(e(l)(`chat.subagentActivity`)),1)]),_:1})]),_:1}),w(`span`,tt,a(O(n.subagentName)),1)]),_:2},1032,[`title`]),g(N,{caption:``},{default:h(()=>[y(a(e(l)(`chat.nActions`,{n:n.items.length})),1)]),_:2},1024)]),_:2},1024),g(P,{side:``},{default:h(()=>[g(D,{flat:``,round:``,dense:``,size:`sm`,icon:`open_in_new`,"aria-label":e(l)(`chat.openSubagentActivity`),onClick:ne(e=>me(n.items[0]),[`stop`])},{default:h(()=>[g(B,null,{default:h(()=>[y(a(e(l)(`chat.openSubagentActivity`)),1)]),_:1})]),_:1},8,[`aria-label`,`onClick`])]),_:2},1024)]),default:h(()=>[w(`div`,nt,[(u(!0),S(C,null,te(n.items,t=>(u(),S(C,{key:e(W)(t)},[t.type===`text`?(u(),_(I,{key:0,item:t},null,8,[`item`])):t.type===`thinking`?(u(),_(H,{key:1,item:t},null,8,[`item`])):t.type===`tool`?(u(),_(L,{key:2,item:t},null,8,[`item`])):v(``,!0)],64))),128))])]),_:2},1032,[`label`,`caption`,`default-opened`])):(u(),S(C,{key:1},[n.item.type===`text`?(u(),_(I,{key:0,item:n.item},null,8,[`item`])):n.item.type===`thinking`?(u(),_(H,{key:1,item:n.item},null,8,[`item`])):n.item.type===`tool`?(u(),_(L,{key:2,item:n.item},null,8,[`item`])):n.item.type===`user`?(u(),_(Y,{key:3,item:n.item,"workspace-id":t.workspaceId},null,8,[`item`,`workspace-id`])):n.item.type===`session`?(u(),_(We,{key:4,item:n.item},null,8,[`item`])):n.item.type===`error`?(u(),_(He,{key:5,item:n.item},null,8,[`item`])):v(``,!0)],64))],64))),128))]),t.turn.items.length>4?(u(),S(`div`,rt,[g(D,{flat:``,round:``,dense:``,size:`xs`,icon:`arrow_upward`,color:`kobo-3`,class:`turn-scroll-top-btn`,onClick:ee},{default:h(()=>[g(B,null,{default:h(()=>[y(a(e(l)(`chat.scrollToTurnTop`)),1)]),_:1})]),_:1})])):v(``,!0)],14,Xe))}}),[[`__scopeId`,`data-v-13bebb3d`]]),at={key:0,class:`activity-feed-switching`},ot={key:1,class:`activity-feed-wrap`,"data-tour":`ws-chat`},st={key:0,role:`alert`,class:`q-pa-sm text-negative`},ct={key:1,"data-testid":`history-load-error`,role:`alert`,class:`q-pa-sm text-negative`},lt=[`aria-label`],ut={key:0,class:`activity-feed-compacting`},dt={class:`text-caption text-kobo-2`},ft={key:0,class:`text-center q-py-sm text-caption text-kobo-3`},pt={key:1,class:`activity-feed-empty q-pa-md`},mt={class:`activity-feed-empty__title`},ht={class:`activity-feed-empty__hint`},gt={key:2,class:`q-px-md q-pb-md`},_t=[`title`],vt={class:`ellipsis`},yt={class:`activity-feed-nav-cluster`},bt=300,xt=60,St=200,Ct=180,wt=255,Tt=3,Et=60,Dt=R(T({__name:`ActivityFeed`,props:{workspaceId:{}},setup(t){let{t:n}=de(),r=t,i=me(),s=he(),d=fe(),m=ge(),ee=x(()=>m.isCompacting(r.workspaceId)),b=x(()=>d.selectedSessionId),T=x(()=>d.sessions.find(e=>e.id===b.value)?.engineSessionId??null),ne=x(()=>{let e=d.sessions;return e.length!==0&&b.value===e[e.length-1].id});function E(e){return b.value?e?e===b.value||e===T.value:ne.value:!0}let ie=x(()=>(d.activityFeeds[r.workspaceId]??[]).filter(e=>e.type===`text`&&typeof e.content==`string`&&E(e.sessionId)).map(e=>({content:e.content,sender:e.meta?.sender??`user`,source:ye(e.meta?.source),ts:e.timestamp,sessionId:e.sessionId,eventIds:[e.id]}))),ae=x(()=>{let e=d.workspaces.find(e=>e.id===r.workspaceId);return _e(e?.status)}),ce=ke(),le=``,k=x(()=>{let e=i.eventsFor(r.workspaceId),t=i.timestampsFor(r.workspaceId),n=i.sessionIdsFor(r.workspaceId),a=i.eventIdsFor(r.workspaceId),o=[],c=[],l=[];for(let r=0;r<e.length;r++)E(n[r])&&(o.push(e[r]),c.push(t[r]),l.push(a[r]??null));let u=`${r.workspaceId}::${b.value??`*`}`;u!==le&&(ce=ke(),le=u);let d=Ee(ce,o,c,ae.value,l);return Fe(De(d,ie.value).filter(e=>e.type===`thinking`?!1:e.type===`session`?e.kind===`ended`?s.showVerboseSystemMessages||!Oe(e.detail):s.showVerboseSystemMessages:!0))}),A=x(()=>s.showVerboseSystemMessages?i.eventsFor(r.workspaceId).filter(e=>e.kind===`message:raw`).map(e=>e.content):[]),xe=x(()=>{for(let e=k.value.length-1;e>=0;e--){let t=k.value[e]?.items??[];for(let e=t.length-1;e>=0;e--){let r=t[e];if(r){if(r.type===`text`&&!r.streaming)return{key:`text:${r.messageId}`,text:r.text};if(r.type===`tool`)return r.result?{key:`tool:${r.toolCallId}:end`,text:r.result.isError?n(`activity.a11y.toolFailed`,{name:r.name}):n(`activity.a11y.toolFinished`,{name:r.name})}:{key:`tool:${r.toolCallId}:start`,text:n(`activity.a11y.toolStarted`,{name:r.name})}}}}return null}),Se=o(``);l(xe,(e,t)=>{if(!e||e.key===t?.key)return;let r=e.key.startsWith(`text:`)?n(`activity.a11y.agentSaid`,{text:e.text.slice(0,bt)}):e.text;Se.value=r});let j=o(null),M=o(!0),N=o(!1),P=o(``),F=pe(),we=pe(),I=pe(),L=o(null),R=!1,z=!0,B=null,V=o(new Set),H=new Map,Ae=x(()=>{if(k.value.length>0||A.value.length>0)return!1;let e=r.workspaceId,t=b.value;return!!d.loadingSessions[e]||m.isSyncPending(e)||t!==null&&V.value.has(W(e,t))}),je=o(new Map),U=x(()=>k.value.length>0||A.value.length>0),Ne=x(()=>!!d.loadingSessions[r.workspaceId]||m.isSyncPending(r.workspaceId)||b.value!==null&&V.value.has(W(r.workspaceId,b.value)));function Pe(e){tt();let t=B;if(B=e.verticalSize,t!==null&&e.verticalSize>t&&M.value&&!N.value){q(0);return}let n=e.verticalSize-e.verticalPosition-e.verticalContainerSize;M.value=n<=xt,R&&e.verticalPosition<=St&&!N.value&&!P.value&&ze()&&He()}function W(e,t){return`${e}:${t}`}function ze(){let e=b.value;return e?je.value.get(W(r.workspaceId,e))??!0:i.hasMoreOlderFor(r.workspaceId)}function Be(e,t,n){je.value.set(W(e,t),n)}function Ve(e){if(!b.value)return i.oldestIdFor(e);let t=i.eventIdsFor(e),n=i.sessionIdsFor(e);for(let e=0;e<t.length;e++){if(!E(n[e]))continue;let r=t[e];if(r)return r}}let G=null;function He(){if(G)return G;let e=K().finally(()=>{G===e&&(G=null)});return G=e,G}function Ue(){F.abort(),we.abort(),I.abort(),N.value=!1,G=null;for(let e of V.value)Ft.delete(e);H.clear(),V.value.clear(),P.value=``}function We(){P.value=``,Nt.value===0&&b.value?It():He()}l(()=>[r.workspaceId,b.value],Ue,{flush:`sync`});async function K(){let e=r.workspaceId,t=b.value,n=Ve(e);if(!n)return;let a=F.begin();P.value=``,N.value=!0;try{let r=j.value;if(await f(),!F.isCurrent(a))return;let o=r?.getScroll().verticalSize??0,s=r?.getScroll().verticalPosition??0,c=new URLSearchParams({before:n,limit:`200`});t&&c.set(`session`,t);let l=await O(`/api/workspaces/${e}/events?${c.toString()}`,{signal:a});if(!l.ok)throw await ue(l);let u=await l.json();if(!F.isCurrent(a))return;let p=u.events??[],m=p.filter(t=>t.type===`agent:event`&&t.workspaceId===e),h=p.filter(t=>t.type===`user:message`&&t.workspaceId===e),g=m.map(e=>e.payload),_=m.map(e=>e.createdAt),v=m.map(e=>e.sessionId??null),y=m.map(e=>e.id),ee=p.length>0?p[0].id:n;t&&Be(e,t,u.hasMore),i.prepend(e,g,_,{oldestId:ee,hasMoreOlder:t?i.hasMoreOlderFor(e):u.hasMore,sessionIds:v,eventIds:y});for(let t of h){let n=t.payload;typeof n.content==`string`&&d.addActivityItem(e,{id:t.id,type:`text`,content:n.content,timestamp:t.createdAt,sessionId:t.sessionId??void 0,meta:{sender:n.sender??`user`,source:ye(n.source)}})}if(await f(),r&&F.isCurrent(a)){let e=r.getScroll().verticalSize-o;if(e>0){let t=Math.max(s+e,250);r.setScrollPosition(`vertical`,t,0)}}}catch(e){if(ve(e)||!F.isCurrent(a))return;P.value=e instanceof Error?e.message:String(e),console.error(`[ActivityFeed] failed to load older events:`,e)}finally{F.isCurrent(a)&&(N.value=!1)}}async function q(e=0){let t=$();if(await f(),t!==$())return;let n=j.value;if(!n)return;let r=n.getScroll();n.setScrollPosition(`vertical`,r.verticalSize,e)}let J=null,Ge=0;function Ke(){if(J!=null)return;let e=performance.now()-Ge<Ct,t=$();J=requestAnimationFrame(()=>{J=null,t===$()&&(Ge=performance.now(),q(e?0:180))})}function qe(e){Q(),M.value=!1;let t=j.value;t&&t.setScrollPosition(`vertical`,Math.max(0,e),250)}function Je(){let e=window.location.hash,t=e.indexOf(`?`);if(t===-1||!new URLSearchParams(e.slice(t+1)).has(`eventId`))return;let n=e.slice(0,t);window.history.replaceState(window.history.state,``,`${window.location.pathname}${window.location.search}${n}`)}async function Ye(e){let t=e.detail;if(t?.workspaceId!==r.workspaceId||!t.eventId)return;Q(),M.value=!1;let n=I.begin(),a=r.workspaceId;try{let e=new URLSearchParams({around:t.eventId,limit:`200`});t.sessionId&&e.set(`session`,t.sessionId);let o=await O(`/api/workspaces/${a}/events?${e}`,{signal:n});if(!o.ok)throw await ue(o);let s=await o.json();if(!I.isCurrent(n))return;let c=s.events??[],l=c.filter(e=>e.type===`agent:event`&&e.workspaceId===r.workspaceId),u=c.filter(e=>e.type===`user:message`&&e.workspaceId===r.workspaceId);i.reset(r.workspaceId,l.map(e=>e.payload),l.map(e=>e.createdAt),{oldestId:c[0]?.id,hasMoreOlder:s.hasMore??!1,sessionIds:l.map(e=>e.sessionId),eventIds:l.map(e=>e.id)});for(let e of u){let t=e.payload;typeof t.content==`string`&&d.addActivityItem(r.workspaceId,{id:e.id,type:`text`,content:t.content,timestamp:e.createdAt,sessionId:e.sessionId??void 0,meta:{sender:t.sender??`user`,source:ye(t.source)}})}if(await Re(Ae,e=>!e,5e3),await f(),await new Promise(e=>requestAnimationFrame(()=>requestAnimationFrame(()=>e()))),!I.isCurrent(n))return;let p=k.value.findIndex(e=>e.items.some(e=>e.eventIds?.includes(t.eventId)??!1));if(p<0)return;L.value=t.eventId,Y.value?.scrollTo(p,`center`),X.value=p,window.setTimeout(()=>{L.value===t.eventId&&(L.value=null)},1800)}catch(e){if(ve(e)||!I.isCurrent(n))return;P.value=e instanceof Error?e.message:String(e),console.error(`[ActivityFeed] failed to focus history event:`,e)}}let Y=o(null),Xe=o(null),X=o(0),Ze=o(!1),Qe=x(()=>{for(let e=k.value.length-1;e>=0;e--)if(k.value[e]?.speaker===`user`)return e;return-1}),$e=null;function et(){let e=Qe.value,t=j.value?.getScrollTarget(),n=t?.querySelector(`[data-turn-index="${e}"]`);if(!n||!t){Ze.value=!1;return}let r=t.getBoundingClientRect(),i=n.getBoundingClientRect();Ze.value=i.bottom>r.top&&i.top<r.bottom}function tt(){$e===null&&($e=requestAnimationFrame(()=>{$e=null,et()}))}function nt(e){return e.length<=wt?e:`${e.slice(0,252)}...`}let rt=x(()=>{let e=Qe.value,t=k.value[e];if(!t||X.value<=e||Ze.value)return null;let n=t.items.filter(e=>e.type===`user`).map(e=>e.content).join(`
|
|
2
|
+
`).trim();return n?{index:e,content:nt(n)}:null});function Dt(e){X.value=e.index,tt()}function Ot(){let e=rt.value;e&&(Q(),M.value=!1,Y.value?.scrollTo(e.index,`start`),X.value=e.index)}let kt=o(!1);async function At(){if(kt.value)return;Q(),M.value=!1,kt.value=!0;let e=r.workspaceId;try{let t=Le(k.value,X.value);if(t<0)for(let n=0;n<15;n++){if(e!==r.workspaceId)return;if(!ze())break;let n=k.value.length;if(await He(),await f(),e!==r.workspaceId)return;if(X.value+=k.value.length-n,t=Le(k.value,X.value),t>=0)break}t>=0&&e===r.workspaceId&&(Y.value?.scrollTo(t,`start`),X.value=t)}finally{kt.value=!1}}let Z=0;function jt(){return new Promise(e=>requestAnimationFrame(()=>e()))}async function Mt(){if(!z||!U.value||Ae.value)return;R=!1;let e=++Z;if(await f(),e!==Z||!z||(Xe.value=j.value?.getScrollTarget()??null,await f(),e!==Z||!z))return;k.value.length>0&&Y.value?.scrollTo(k.value.length-1,`end-force`),await jt();let t=-1,n=0;for(let r=0;r<Et&&n<Tt;r++){if(e!==Z||!z)return;await q(0),await jt();let r=j.value?.getScroll().verticalSize??0;B=r,n=r===t?n+1:0,t=r}e===Z&&!Ne.value&&(R=!0,z=!1)}function Q(){z=!1,!R&&(Z++,R=!0)}let Nt=x(()=>{let e=i.sessionIdsFor(r.workspaceId);if(!b.value)return e.length;let t=0;for(let n of e)E(n)&&t++;return t});l(Ae,async e=>{!e&&U.value&&await Mt(),e||f(()=>{Xe.value=j.value?.getScrollTarget()??null,tt()})}),l(Ne,(e,t)=>{!e&&t&&z&&U.value&&Mt()}),p(()=>{window.addEventListener(`kobo:focus-history-event`,Ye),f(()=>{Xe.value=j.value?.getScrollTarget()??null,tt()}),U.value&&Mt(),b.value&&It();let e=new URLSearchParams(window.location.hash.split(`?`)[1]??``).get(`eventId`);e&&(Ye(new CustomEvent(`kobo:focus-history-event`,{detail:{workspaceId:r.workspaceId,sessionId:b.value,eventId:e}})),Je())}),l([Nt,()=>ie.value.length,()=>A.value.length],async([e,t,n])=>{N.value||z&&e+t+n>0&&await Mt()}),l([Qe,()=>k.value.length],()=>{f(tt)});let Pt=x(()=>i.liveAppendCountFor(r.workspaceId,E)),$=()=>`${r.workspaceId}\u0000${d.selectedSessionId??``}`;l([Pt,$],([e,t],[n,r])=>{t===r&&e>n&&M.value&&!N.value&&Ke()}),c(()=>{Z++,z=!1,Ue(),window.removeEventListener(`kobo:focus-history-event`,Ye),J!=null&&(cancelAnimationFrame(J),J=null),$e!==null&&cancelAnimationFrame($e)}),l([()=>r.workspaceId,()=>d.selectedSessionId],()=>{Z++,z=!0,B=null,M.value=!0,R=!1,It(),U.value&&Mt()});let Ft=new Set;async function It(){let e=r.workspaceId,t=b.value;if(!t||Nt.value>0)return;let n=W(e,t);if(Ft.has(n))return;Ft.add(n),V.value.add(n);let a=we.begin();H.set(n,a),P.value=``;try{let n=await O(`/api/workspaces/${e}/events?session=${encodeURIComponent(t)}&limit=500`,{signal:a});if(!n.ok)throw await ue(n);let r=await n.json();if(!we.isCurrent(a))return;let o=r.events??[];if(o.length===0)return;let s=o.filter(t=>t.type===`agent:event`&&t.workspaceId===e),c=o.filter(t=>t.type===`user:message`&&t.workspaceId===e),l=s.map(e=>e.payload),u=s.map(e=>e.createdAt),f=s.map(e=>e.sessionId??null),p=s.map(e=>e.id);Be(e,t,r.hasMore),l.length>0&&i.prepend(e,l,u,{oldestId:o[0].id,hasMoreOlder:i.hasMoreOlderFor(e),sessionIds:f,eventIds:p});for(let t of c){let n=t.payload;typeof n.content==`string`&&d.addActivityItem(e,{id:t.id,type:`text`,content:n.content,timestamp:t.createdAt,sessionId:t.sessionId??void 0,meta:{sender:n.sender??`user`,source:ye(n.source)}})}}catch(e){if(ve(e)||!we.isCurrent(a)){(!H.has(n)||H.get(n)===a)&&Ft.delete(n);return}P.value=e instanceof Error?e.message:String(e),console.error(`[ActivityFeed] fetchSessionIfMissing failed:`,e),Ft.delete(n)}finally{H.get(n)===a&&(H.delete(n),V.value.delete(n))}}let Lt=x(()=>ie.value.filter(e=>e.sender===`user`).length),Rt=x(()=>(d.activityFeeds[r.workspaceId]??[]).filter(e=>e.meta?.sender===`user`&&e.meta.pending&&E(e.sessionId)).length);l([Lt,Rt,$],async([e,t,n],[r,i,a])=>{let o=t>i;n===a&&(o||e>r&&M.value&&!N.value&&!Ne.value)&&(M.value=!0,await q(o?180:0))});async function zt(){M.value=!0,await q(250)}return(t,n)=>Ae.value?(u(),S(`div`,at,[g(be,{size:`40px`,color:`primary`})])):(u(),S(`div`,ot,[e(d).sessionsErrors[r.workspaceId]?(u(),S(`div`,st,[y(a(t.$t(`activity.sessionsError`,{message:e(d).sessionsErrors[r.workspaceId]}))+` `,1),g(D,{flat:``,dense:``,label:t.$t(`common.retry`),onClick:n[0]||=t=>e(d).fetchSessions(r.workspaceId)},null,8,[`label`])])):v(``,!0),P.value?(u(),S(`div`,ct,[y(a(t.$t(`activity.historyError`,{message:P.value}))+` `,1),g(D,{flat:``,dense:``,label:t.$t(`common.retry`),onClick:We},null,8,[`label`])])):v(``,!0),w(`div`,{"data-testid":`activity-live-region`,class:`sr-only`,role:`log`,"aria-live":`polite`,"aria-atomic":`true`,"aria-label":t.$t(`activity.a11y.region`)},a(Se.value),9,lt),g(re,{name:`fade`},{default:h(()=>[ee.value?(u(),S(`div`,ut,[g(be,{size:`18px`,color:`primary`}),w(`span`,dt,a(t.$t(`activity.compacting`)),1)])):v(``,!0)]),_:1}),g(Te,{ref_key:`scrollRef`,ref:j,class:`activity-feed-scroll`,onScroll:Pe,onWheelPassive:Q,onTouchstartPassive:Q,onKeydown:Q},{default:h(()=>[N.value?(u(),S(`div`,ft,[g(se,{size:`sm`}),y(` `+a(t.$t(`activity.loading_older`)),1)])):v(``,!0),g(Me,{ref_key:`virtualScrollRef`,ref:Y,class:`q-pa-md`,items:k.value,"scroll-target":Xe.value??void 0,"virtual-scroll-item-size":160,"virtual-scroll-slice-size":20,onVirtualScroll:Dt},{default:h(({item:t,index:n})=>[(u(),_(it,{key:e(Ie)(t),turn:t,"workspace-id":r.workspaceId,"data-turn-index":n,highlighted:t.items.some(e=>e.eventIds?.includes(L.value??``)??!1),onScrollTo:qe},null,8,[`turn`,`workspace-id`,`data-turn-index`,`highlighted`]))]),_:1},8,[`items`,`scroll-target`]),k.value.length===0&&A.value.length===0&&!N.value?(u(),S(`div`,pt,[g(oe,{name:`forum`,size:`28px`}),w(`div`,mt,a(t.$t(`activity.empty`)),1),w(`div`,ht,a(t.$t(`activity.emptyHint`)),1)])):v(``,!0),A.value.length?(u(),S(`div`,gt,[g(Ce,{label:t.$t(`activity.raw_lines`,{n:A.value.length}),dense:``},{default:h(()=>[(u(!0),S(C,null,te(A.value,(e,t)=>(u(),S(`div`,{key:t,class:`text-caption text-kobo-3 q-pa-xs`},a(e),1))),128))]),_:1},8,[`label`])])):v(``,!0)]),_:1},512),rt.value?(u(),S(`button`,{key:2,type:`button`,class:`activity-feed-last-user`,"data-testid":`latest-user-message`,title:rt.value.content,onClick:Ot},[g(oe,{name:`person`,size:`16px`}),w(`span`,vt,a(rt.value.content),1)],8,_t)):v(``,!0),w(`div`,yt,[M.value?v(``,!0):(u(),_(D,{key:0,round:``,dense:``,unelevated:``,color:`kobo-surface-2`,"text-color":`kobo-1`,icon:`arrow_downward`,size:`sm`,class:`activity-feed-nav-btn`,title:t.$t(`activity.scroll_to_bottom`),"aria-label":t.$t(`activity.scroll_to_bottom`),onClick:zt},null,8,[`title`,`aria-label`])),g(D,{round:``,dense:``,unelevated:``,color:`kobo-surface-2`,"text-color":`kobo-1`,icon:`arrow_upward`,size:`sm`,class:`activity-feed-nav-btn`,title:t.$t(`activity.prev_user_message`),"aria-label":t.$t(`activity.prev_user_message`),loading:kt.value,disable:kt.value,onClick:At},null,8,[`title`,`aria-label`,`loading`,`disable`])])]))}}),[[`__scopeId`,`data-v-58324159`]]);export{Dt as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.review-card[data-v-a4419350]{width:720px;max-width:calc(100vw - 2 * var(--kobo-space-lg));background:var(--kobo-surface)}.final-review-settings[data-v-b7f36f83]{padding-top:var(--kobo-space-sm)}.final-review-settings p[data-v-b7f36f83]{margin:var(--kobo-space-xs) 0}
|