@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.
Files changed (113) hide show
  1. package/AGENTS.md +29 -6
  2. package/CHANGELOG.md +8 -0
  3. package/CONFIGURATION.md +28 -4
  4. package/README.md +1 -1
  5. package/dist/mcp-server/kobo-tasks-server.js +43 -1
  6. package/dist/server/db/auto-loop-final-review-schema.js +18 -0
  7. package/dist/server/db/migrations.js +10 -1
  8. package/dist/server/db/review-return-schema.js +18 -1
  9. package/dist/server/db/schema.js +2 -0
  10. package/dist/server/index.js +2 -1
  11. package/dist/server/routes/workspaces.js +61 -1
  12. package/dist/server/services/agent/engines/claude-code/engine.js +37 -1
  13. package/dist/server/services/agent/orchestrator.js +158 -38
  14. package/dist/server/services/auto-loop-final-review-service.js +329 -0
  15. package/dist/server/services/auto-loop-service.js +53 -10
  16. package/dist/server/services/review-launch-runtime.js +17 -0
  17. package/dist/server/services/review-return-service.js +58 -10
  18. package/dist/server/services/review-service.js +59 -11
  19. package/dist/server/services/workspace-service.js +11 -0
  20. package/dist/server/services/workspace-template-service.js +10 -0
  21. package/dist/shared/auto-loop-review.js +1 -0
  22. package/dist/shared/review-return.js +1 -0
  23. package/package.json +1 -1
  24. package/src/client/dist/pwa/assets/ActivityFeed-D7jouiuP.js +2 -0
  25. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +1 -0
  26. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-lKjfaC6V.js +1 -0
  27. package/src/client/dist/pwa/assets/{ChangelogPage-DD90Nl6c.js → ChangelogPage-BplM7qEB.js} +1 -1
  28. package/src/client/dist/pwa/assets/{ComparisonPanel-D_BQFWiT.js → ComparisonPanel-DxK5v9w_.js} +1 -1
  29. package/src/client/dist/pwa/assets/CreatePage-D5ilP5kt.js +2 -0
  30. package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +1 -0
  31. package/src/client/dist/pwa/assets/{DashboardPage-DyvTdmBg.js → DashboardPage-BsP-MATS.js} +1 -1
  32. package/src/client/dist/pwa/assets/DiffViewer-Cd15vFWj.js +8 -0
  33. package/src/client/dist/pwa/assets/{DocumentsPanel-bm639sIu.js → DocumentsPanel-CGrf8Q3H.js} +1 -1
  34. package/src/client/dist/pwa/assets/{DrawerToggleButton-KfwSzjeY.js → DrawerToggleButton-BANeLSsR.js} +1 -1
  35. package/src/client/dist/pwa/assets/GitPanel-CEPV5Qw0.js +3 -0
  36. package/src/client/dist/pwa/assets/{HealthPage-37IHOIW-.js → HealthPage-BVTNx-92.js} +1 -1
  37. package/src/client/dist/pwa/assets/MainLayout-C6nvIQjU.css +1 -0
  38. package/src/client/dist/pwa/assets/MainLayout-DBKtwilW.js +2 -0
  39. package/src/client/dist/pwa/assets/{MemoryPanel-CucZMhjy.js → MemoryPanel-C71rTpv9.js} +1 -1
  40. package/src/client/dist/pwa/assets/{MemoryProposalList-CUQybzVR.js → MemoryProposalList-CoqhUGrv.js} +1 -1
  41. package/src/client/dist/pwa/assets/{QBtnDropdown-x6qPyl_7.js → QBtnDropdown-U9Ey5W8h.js} +1 -1
  42. package/src/client/dist/pwa/assets/{QExpansionItem-D33aoRoG.js → QExpansionItem-Be_A1MT1.js} +1 -1
  43. package/src/client/dist/pwa/assets/{QScrollArea-Di-D6t2u.js → QScrollArea-D4lYzAPQ.js} +1 -1
  44. package/src/client/dist/pwa/assets/{QSelect-MdPXvwm4.js → QSelect-fS0DLOd1.js} +1 -1
  45. package/src/client/dist/pwa/assets/{QTooltip-BhwG7lp2.js → QTooltip-kStOgY0X.js} +1 -1
  46. package/src/client/dist/pwa/assets/{SchedulePanel-qFi9Wxzh.js → SchedulePanel-C7nTGdWE.js} +1 -1
  47. package/src/client/dist/pwa/assets/{SearchPage-DA9nRmsA.js → SearchPage-C6J72pkQ.js} +1 -1
  48. package/src/client/dist/pwa/assets/SettingsPage-SuH-aIzv.js +9 -0
  49. package/src/client/dist/pwa/assets/{SplitWorkspacePage-Brw2XsYJ.js → SplitWorkspacePage-Bqn4gdVw.js} +1 -1
  50. package/src/client/dist/pwa/assets/{TerminalPanel-DVP18-nk.js → TerminalPanel-zLeDCReI.js} +9 -9
  51. package/src/client/dist/pwa/assets/{ToolCallItem-cSHh3p_F.js → ToolCallItem-B8b_Sagj.js} +3 -3
  52. package/src/client/dist/pwa/assets/{TouchPan-C6LZZ3rw.js → TouchPan-Bl8VzIDK.js} +1 -1
  53. package/src/client/dist/pwa/assets/{TourReplayButton-Czlqo18H.js → TourReplayButton-pL0HCboU.js} +1 -1
  54. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BDYGq0pC.js → WorkspaceAttentionLabels-9Eomx0rQ.js} +1 -1
  55. package/src/client/dist/pwa/assets/WorkspacePage-B248nKIU.css +1 -0
  56. package/src/client/dist/pwa/assets/WorkspacePage-B6clgURl.js +4 -0
  57. package/src/client/dist/pwa/assets/{browser-ifCwUGpE.js → browser-C-PJ4alw.js} +1 -1
  58. package/src/client/dist/pwa/assets/{build-path-tree-BBkGq3lN.js → build-path-tree-SW9rj7B_.js} +1 -1
  59. package/src/client/dist/pwa/assets/custom-sounds-toz-4drX.js +1 -0
  60. package/src/client/dist/pwa/assets/de-Ddxe8cc6.js +17 -0
  61. package/src/client/dist/pwa/assets/{editor.api-CV_seGJR.js → editor.api-BJTPhnh8.js} +1 -1
  62. package/src/client/dist/pwa/assets/{efforts-ttw0bL63.js → efforts-BO9xGn9q.js} +1 -1
  63. package/src/client/dist/pwa/assets/es-DBJIQeWW.js +17 -0
  64. package/src/client/dist/pwa/assets/{expand-template-C8biMRXo.js → expand-template-BOZMtWau.js} +1 -1
  65. package/src/client/dist/pwa/assets/{format-Cu95dbxt.js → format-hEnpRyit.js} +1 -1
  66. package/src/client/dist/pwa/assets/{formatters-C25D1RSd.js → formatters-DD_eFc4z.js} +1 -1
  67. package/src/client/dist/pwa/assets/fr-yBzIIPaQ.js +17 -0
  68. package/src/client/dist/pwa/assets/{freemarker2-DJEFr74q.js → freemarker2-B6nUofvT.js} +1 -1
  69. package/src/client/dist/pwa/assets/{handlebars-C_lzsMjX.js → handlebars-4P3gtM8p.js} +1 -1
  70. package/src/client/dist/pwa/assets/{html-CWmLFAmY.js → html-C9ZzWo-y.js} +1 -1
  71. package/src/client/dist/pwa/assets/i18n-2-ULabk2.js +1 -0
  72. package/src/client/dist/pwa/assets/index-Vk99jtxD.js +20 -0
  73. package/src/client/dist/pwa/assets/it-D44U-PT1.js +17 -0
  74. package/src/client/dist/pwa/assets/{javascript-tsuiEUe-.js → javascript-BGEC5ggd.js} +1 -1
  75. package/src/client/dist/pwa/assets/{liquid-BBr9BvUk.js → liquid-DNtF630O.js} +1 -1
  76. package/src/client/dist/pwa/assets/{mdx-gjBH1cXG.js → mdx-CICwyOd0.js} +1 -1
  77. package/src/client/dist/pwa/assets/{monaco-contributions-CvGcBIsb.js → monaco-contributions-BkuwvgrT.js} +1 -1
  78. package/src/client/dist/pwa/assets/{monaco.contribution-Uw-Lfb8Z.js → monaco.contribution-GX8XA-PE.js} +2 -2
  79. package/src/client/dist/pwa/assets/network-auth-KnWzjYIc.js +1 -0
  80. package/src/client/dist/pwa/assets/notify-theme-CWuZTz4Z.js +1 -0
  81. package/src/client/dist/pwa/assets/{project-color-BDQa2TgU.js → project-color-B1SMFjSe.js} +1 -1
  82. package/src/client/dist/pwa/assets/{python-DlIgD1n6.js → python-n5uPdTVS.js} +1 -1
  83. package/src/client/dist/pwa/assets/{razor-AraFPlpM.js → razor-AOj0nsUt.js} +1 -1
  84. package/src/client/dist/pwa/assets/{typescript-CB8UpXHM.js → typescript-Uhkrqihh.js} +1 -1
  85. package/src/client/dist/pwa/assets/{use-search-index-status-CSREHQM4.js → use-search-index-status-CW0BdpBZ.js} +1 -1
  86. package/src/client/dist/pwa/assets/{use-tours-6yxNVmjH.js → use-tours-CndFlyL-.js} +2 -2
  87. package/src/client/dist/pwa/assets/{use-worktree-restore-BM3Giig1.js → use-worktree-restore-BZxibZS5.js} +1 -1
  88. package/src/client/dist/pwa/assets/{workspace-templates-BPMFvTnQ.js → workspace-templates-W557e1-m.js} +1 -1
  89. package/src/client/dist/pwa/assets/{xml-uRjL1P43.js → xml-B-hnMgEs.js} +1 -1
  90. package/src/client/dist/pwa/assets/{yaml-C9CFR4qV.js → yaml-DqXkTvgm.js} +1 -1
  91. package/src/client/dist/pwa/index.html +1 -1
  92. package/src/client/dist/pwa/sw.js +1 -1
  93. package/src/mcp-server/README.md +26 -0
  94. package/src/mcp-server/kobo-tasks-server.ts +46 -1
  95. package/src/client/dist/pwa/assets/ActivityFeed-BTRzc60o.js +0 -2
  96. package/src/client/dist/pwa/assets/CreatePage-BZAoCukb.js +0 -2
  97. package/src/client/dist/pwa/assets/CreatePage-CdCs8PRS.css +0 -1
  98. package/src/client/dist/pwa/assets/DiffViewer-D0K2PlZD.js +0 -8
  99. package/src/client/dist/pwa/assets/GitPanel-C-Ejlc9_.js +0 -3
  100. package/src/client/dist/pwa/assets/MainLayout-BQOOup9o.css +0 -1
  101. package/src/client/dist/pwa/assets/MainLayout-DXQ1ouYl.js +0 -2
  102. package/src/client/dist/pwa/assets/SettingsPage-DcV6apqE.js +0 -9
  103. package/src/client/dist/pwa/assets/WorkspacePage-BwUkijpS.js +0 -4
  104. package/src/client/dist/pwa/assets/WorkspacePage-Bwfv559o.css +0 -1
  105. package/src/client/dist/pwa/assets/custom-sounds-CeSgjIY7.js +0 -1
  106. package/src/client/dist/pwa/assets/de-CHY2BAfL.js +0 -17
  107. package/src/client/dist/pwa/assets/es-DbcSKp38.js +0 -17
  108. package/src/client/dist/pwa/assets/fr-CyyJ8S2c.js +0 -17
  109. package/src/client/dist/pwa/assets/i18n-CobVQiTM.js +0 -1
  110. package/src/client/dist/pwa/assets/index-ImUH7Bbt.js +0 -20
  111. package/src/client/dist/pwa/assets/it-XQJW2Xkr.js +0 -17
  112. package/src/client/dist/pwa/assets/network-auth-D0Vm6a09.js +0 -1
  113. 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
- if (!grooming && !task && pendingInstructions.length === 0) {
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, undefined, row.reasoning_effort);
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
- if (row.auto_loop !== 1)
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 || current.archived_at || !current.auto_loop || current.status !== 'quota')
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
- spawnNextIteration(workspaceId);
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
- spawnNextIteration(workspaceId);
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
- /** Consume before dispatch: a repeated end or a restart must never send twice. */
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
- getDb().prepare('DELETE FROM pending_review_returns WHERE workspace_id = ?').run(workspaceId);
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
- /** On restart, restore settings but do not replay a possibly delivered handoff. */
44
+ /** Restore user settings without discarding an unfinished review/return. */
37
45
  export function reconcileReviewReturns() {
38
- const rows = getDb().prepare('SELECT workspace_id FROM pending_review_returns').all();
39
- for (const row of rows)
40
- restoreReviewConfiguration(row.workspace_id);
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 ((changedEngine || config.model !== workspace.model) &&
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
- if (launching.has(id))
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 originalSession = workspaceService.getActiveSession(id);
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 && workspaceService.getActiveSession(id)?.id !== originalSession?.id)
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 agent = agentManager.startAgent(id, worktreePath, rendered, configuration.model, false, configuration.agentPermissionMode, pendingSessionId, configuration.reasoningEffort);
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
- launching.delete(id);
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.13",
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)?`![${r}](${`/api/workspaces/${encodeURIComponent(t)}/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}