@loicngr/kobo 1.9.6 → 1.10.1

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 (70) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -2
  3. package/dist/mcp-server/kobo-tasks-server.js +1 -1
  4. package/dist/server/index.js +25 -13
  5. package/dist/server/routes/workspaces.js +4 -0
  6. package/dist/server/services/agent/engines/claude-code/engine.js +7 -23
  7. package/dist/server/services/agent/engines/claude-code/options-builder.js +5 -1
  8. package/dist/server/services/agent/engines/codex/client.js +3 -0
  9. package/dist/server/services/agent/engines/codex/engine.js +21 -4
  10. package/dist/server/services/agent/engines/codex/options-builder.js +1 -1
  11. package/dist/server/services/agent/engines/codex/server-requests.js +12 -1
  12. package/dist/server/services/agent/orchestrator.js +35 -8
  13. package/dist/server/services/auto-loop-service.js +15 -7
  14. package/package.json +1 -1
  15. package/src/client/dist/spa/assets/{ActivityFeed-BMgs5z9z.js → ActivityFeed-CcUu5qpC.js} +1 -1
  16. package/src/client/dist/spa/assets/{ChangelogPage-jf_31RZp.js → ChangelogPage-BdxtrOnp.js} +1 -1
  17. package/src/client/dist/spa/assets/{ClosePopup-C1A60wce.js → ClosePopup-D0wmrKYA.js} +1 -1
  18. package/src/client/dist/spa/assets/{CreatePage-BBOwKtLV.js → CreatePage-PGgqwwa4.js} +1 -1
  19. package/src/client/dist/spa/assets/{DiffViewer-CVYtBqC8.js → DiffViewer-PD-4yZTi.js} +3 -3
  20. package/src/client/dist/spa/assets/{HealthPage-_2qS3z3n.js → HealthPage-DRqe033h.js} +1 -1
  21. package/src/client/dist/spa/assets/{MainLayout-Dk9ilieA.js → MainLayout-C-THtL4A.js} +3 -3
  22. package/src/client/dist/spa/assets/{QBtnToggle-CtKvYj1o.js → QBtnToggle-C0OuVppD.js} +1 -1
  23. package/src/client/dist/spa/assets/{QChip-CjN_RB2y.js → QChip-Bq-3SzSI.js} +1 -1
  24. package/src/client/dist/spa/assets/{QExpansionItem-Dka3X1IN.js → QExpansionItem-Sh9QNOLB.js} +1 -1
  25. package/src/client/dist/spa/assets/{QScrollArea-Mwe2ihwC.js → QScrollArea-CbA9gH0b.js} +1 -1
  26. package/src/client/dist/spa/assets/{QSelect-jjUTbbns.js → QSelect-Bi7mKFGZ.js} +1 -1
  27. package/src/client/dist/spa/assets/{QTooltip-B9mxxMEM.js → QTooltip-BYnhp9Wu.js} +1 -1
  28. package/src/client/dist/spa/assets/{SearchPage-De3lLcr6.js → SearchPage-BpJsyMPU.js} +1 -1
  29. package/src/client/dist/spa/assets/{SettingsPage-Dge9J6e_.js → SettingsPage-CzRvaRwA.js} +1 -1
  30. package/src/client/dist/spa/assets/{TouchPan-BGeQ1z7O.js → TouchPan-BL3ZAMdn.js} +1 -1
  31. package/src/client/dist/spa/assets/WorkspacePage-3jV0jpk0.js +4 -0
  32. package/src/client/dist/spa/assets/WorkspacePage-C71K0BRq.css +1 -0
  33. package/src/client/dist/spa/assets/{build-path-tree-CQkUkFK4.js → build-path-tree-BYsWR1Ae.js} +1 -1
  34. package/src/client/dist/spa/assets/{cssMode-CGcsO0OQ.js → cssMode-CB-UguH5.js} +1 -1
  35. package/src/client/dist/spa/assets/{documents-5sWEpISn.js → documents-D4iGKwK5.js} +1 -1
  36. package/src/client/dist/spa/assets/{editor.api2-DNjj3Vc0.js → editor.api2-CQZx6qEF.js} +1 -1
  37. package/src/client/dist/spa/assets/{editor.main-DpxxCkC7.js → editor.main-CgIzM5Af.js} +2 -2
  38. package/src/client/dist/spa/assets/{expand-template-Bq1mopti.js → expand-template-lWTPop0I.js} +1 -1
  39. package/src/client/dist/spa/assets/{formatters-VhgPXQNa.js → formatters-DczUaXQz.js} +1 -1
  40. package/src/client/dist/spa/assets/{freemarker2-BVvp9ac3.js → freemarker2-OeOcbnhX.js} +1 -1
  41. package/src/client/dist/spa/assets/{handlebars-CmulgLgY.js → handlebars-D9b92bp0.js} +1 -1
  42. package/src/client/dist/spa/assets/{html-kmOuarw4.js → html-DF7Tipz6.js} +1 -1
  43. package/src/client/dist/spa/assets/{htmlMode-Drc_ueTa.js → htmlMode-BXnuNcvL.js} +1 -1
  44. package/src/client/dist/spa/assets/i18n-BNoynAbv.js +1 -0
  45. package/src/client/dist/spa/assets/{index-BKXtMMl8.js → index-Deinl-Hq.js} +7 -7
  46. package/src/client/dist/spa/assets/{javascript-BapMfe_r.js → javascript-ClzpphE4.js} +1 -1
  47. package/src/client/dist/spa/assets/{jsonMode-CC4B535-.js → jsonMode-Bzqsvs2S.js} +1 -1
  48. package/src/client/dist/spa/assets/{kobo-commands-Bz-cjyHQ.js → kobo-commands-DX9Bimii.js} +1 -1
  49. package/src/client/dist/spa/assets/{layout-C4xmHWJM.js → layout-CXDh7cOk.js} +1 -1
  50. package/src/client/dist/spa/assets/{liquid-BDE8v2lj.js → liquid-CPVa7-G3.js} +1 -1
  51. package/src/client/dist/spa/assets/{lspLanguageFeatures-Cy9S4aKY.js → lspLanguageFeatures-D8wj-yjN.js} +1 -1
  52. package/src/client/dist/spa/assets/{mdx-CyNdhKZN.js → mdx-wcb1Z0A_.js} +1 -1
  53. package/src/client/dist/spa/assets/{monaco.contribution-DbqInEyL.js → monaco.contribution-BozjOUwK.js} +2 -2
  54. package/src/client/dist/spa/assets/{network-auth-0eEgS22h.js → network-auth-BkwyxSIr.js} +1 -1
  55. package/src/client/dist/spa/assets/{project-color-DdAtoeU2.js → project-color-oMfwIMSg.js} +1 -1
  56. package/src/client/dist/spa/assets/{python-CKjYYxee.js → python-jVYLZZL1.js} +1 -1
  57. package/src/client/dist/spa/assets/{razor-CvTe87xk.js → razor-BHjeVPxE.js} +1 -1
  58. package/src/client/dist/spa/assets/{render-chat-markdown-BjVGm1Vi.js → render-chat-markdown-CaUfpvEV.js} +1 -1
  59. package/src/client/dist/spa/assets/{tsMode-CeJZQNU_.js → tsMode-BlnXnkR-.js} +1 -1
  60. package/src/client/dist/spa/assets/{typescript-ReN8Og3R.js → typescript-XYkMCAOm.js} +1 -1
  61. package/src/client/dist/spa/assets/{use-onboarding-BnFTwLyx.js → use-onboarding-DkEHbMS-.js} +1 -1
  62. package/src/client/dist/spa/assets/{use-quasar-DzB1x4Fl.js → use-quasar-CGpMLh-Q.js} +1 -1
  63. package/src/client/dist/spa/assets/{workers-BUy4rUs5.js → workers-qP1P6pzp.js} +1 -1
  64. package/src/client/dist/spa/assets/{xml-DzIIIusY.js → xml-DzTCWoOI.js} +1 -1
  65. package/src/client/dist/spa/assets/{yaml-2OnB0-yv.js → yaml-Osiizqat.js} +1 -1
  66. package/src/client/dist/spa/index.html +1 -1
  67. package/src/mcp-server/kobo-tasks-server.ts +1 -1
  68. package/src/client/dist/spa/assets/WorkspacePage-B8jyN4oW.js +0 -4
  69. package/src/client/dist/spa/assets/WorkspacePage-CVkUPM-N.css +0 -1
  70. package/src/client/dist/spa/assets/i18n-EIgL_1Id.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 1.10.1
8
+
9
+ - fix(auto-loop): recover from interrupted sessions (#18)
10
+
11
+ ## 1.10.0
12
+
13
+ - fix(agent): make Claude sessions recover cleanly from a stuck post-result
14
+ stream, prevent native wakeups from leaving orphaned questions, and keep
15
+ live sessions out of the `idle` state
16
+ - feat(chat): keep queued messages scoped to the selected workspace session,
17
+ restore them after a rejected send, and confirm delivery only after the
18
+ engine accepts it
19
+ - feat(chat): send a queued message immediately to an active Claude session
20
+ or steer an active Codex turn; serialise concurrent Codex steer requests
21
+ against the current turn id
22
+ - feat(questions): support inline free-form answers for **Other**, option
23
+ previews, structured Codex question ids, missing option arrays, secret
24
+ answers, and stale-question-panel recovery
25
+ - fix(codex): map `request_user_input` and its auto-resolution metadata to the
26
+ shared question UI, and document that structured Codex questions require
27
+ `plan` mode
28
+ - docs: document live steering, interactive-question limitations, and the
29
+ updated Q&A experience in README and CONFIGURATION
30
+
7
31
  ## 1.9.6
8
32
 
9
33
  - feat(chat): inject a queued message into a running Claude session, with a
package/README.md CHANGED
@@ -17,7 +17,7 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
17
17
 
18
18
  - **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
19
19
  - **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
20
- - **Live chat**: streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and you can export any workspace's session events to CSV. Messages queued while an agent is busy stay scoped to their workspace and session; for an active Claude Code session, **Send now** injects the queued message into the running SDK stream and keeps it queued until the server confirms acceptance. Codex retains the next-turn queue behaviour.
20
+ - **Live chat**: streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and you can export any workspace's session events to CSV. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance.
21
21
  - **Full MCP toolset (`kobo-tasks`)**: a per-workspace MCP server the agent uses for far more than tasks — task/acceptance-criteria CRUD, starting/stopping the dev server and reading its logs, a unified `get_ticket` (Notion or Sentry), searching past conversations across every workspace, per-session token/cost usage, and a `.ai/thoughts` decision log. Native Claude Code Task tools complement it for lightweight sub-agent coordination. See [`AGENTS.md`](./AGENTS.md) for the full tool list.
22
22
 
23
23
  ![Sub-agents panel showing parallel tool calls](docs/assets/images/sub-agents-panel.png)
@@ -26,7 +26,7 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
26
26
  ![Diff viewer with side-by-side changes](docs/assets/images/diff-viewer.png)
27
27
  - **Dev server panel**: start, stop, and tail logs for a workspace's dev server (Docker or npm) straight from the Tools panel — no need to leave the UI.
28
28
  - **Attention indicators**: workspace cards surface CI failures, review-requested changes, and a conflict-aware **ready-to-merge** badge, plus a one-click **Fix CI** button that spawns a fix workspace automatically.
29
- - **Interactive Q&A**: an agent can pause mid-session to ask a clarifying question through the UI; the workspace surfaces under "Needs Attention" until you answer.
29
+ - **Interactive Q&A**: an agent can pause mid-session to ask a clarifying question through the UI; the workspace surfaces under "Needs Attention" until you answer. Choosing **Other** opens an inline free-form field, and option previews are available on hover. Codex native interactive questions require its `plan` permission mode.
30
30
 
31
31
  ![Agent asking a clarifying question, awaiting the user's answer](docs/assets/images/agent-question.png)
32
32
  - **Auto-loop**: an opt-in mode that walks the task list, spawning a fresh session per task and stopping once there's nothing left to do, progress stalls, an error occurs, or the agent needs input from you. Optionally run the initial brainstorming session on a stronger model and every task after that on a cheaper one, without leaving the engine you picked.
@@ -276,7 +276,7 @@ const WORKSPACE_SCOPED_TOOLS = [
276
276
  },
277
277
  {
278
278
  name: 'set_workspace_status',
279
- description: 'CALL WHEN you believe the mission is done (`completed`), blocked beyond recovery (`error`), or explicitly idle awaiting user input (`idle`). Transitions are validated by the backend — invalid ones are rejected.',
279
+ description: 'CALL WHEN you believe the mission is done (`completed`) or blocked beyond recovery (`error`). Do NOT set `idle` while this agent session is active: ask the user with AskUserQuestion or end the turn instead. Transitions are validated by the backend — invalid or unsafe ones are rejected.',
280
280
  inputSchema: {
281
281
  type: 'object',
282
282
  properties: {
@@ -291,7 +291,7 @@ terminalWss.on('connection', (ws, workspaceId) => {
291
291
  });
292
292
  });
293
293
  // Wire websocket-service message handler to the agent orchestrator
294
- setMessageHandler((type, payload) => {
294
+ setMessageHandler(async (type, payload) => {
295
295
  const p = payload;
296
296
  if (type === 'chat:message' && p?.workspaceId && p?.content) {
297
297
  // Auto-loop owns the agent's turns. A user message means the user wants
@@ -312,6 +312,8 @@ setMessageHandler((type, payload) => {
312
312
  if (wsRow?.status === 'awaiting-user') {
313
313
  emitEphemeral(p.workspaceId, 'chat:rejected', {
314
314
  reason: 'awaiting-user',
315
+ sessionId: p.sessionId,
316
+ content: p.content,
315
317
  message: 'Answer via the question panel — typing in chat would orphan the pending callback',
316
318
  });
317
319
  return;
@@ -321,16 +323,11 @@ setMessageHandler((type, payload) => {
321
323
  // never steal the tagging.
322
324
  const activeSession = getActiveSession(p.workspaceId);
323
325
  const sessionTag = p.sessionId ?? activeSession?.id ?? undefined;
324
- // Forced sends are persisted only once the active engine has accepted the
325
- // input, so a stale session cannot make the client lose its queued text.
326
- if (!p.force)
327
- emit(p.workspaceId, 'user:message', { content: p.content, sender: 'user' }, sessionTag);
328
326
  try {
329
- sendMessage(p.workspaceId, p.content, p.sessionId);
330
- if (p.force) {
331
- emit(p.workspaceId, 'user:message', { content: p.content, sender: 'user' }, sessionTag);
327
+ await sendMessage(p.workspaceId, p.content, p.sessionId);
328
+ emit(p.workspaceId, 'user:message', { content: p.content, sender: 'user' }, sessionTag);
329
+ if (p.force)
332
330
  emitEphemeral(p.workspaceId, 'chat:accepted', { sessionId: p.sessionId });
333
- }
334
331
  }
335
332
  catch (err) {
336
333
  const msg = err instanceof Error ? err.message : String(err);
@@ -338,8 +335,7 @@ setMessageHandler((type, payload) => {
338
335
  // (stdin closed, process dead mid-write, etc.) should surface to the
339
336
  // logs instead of silently respawning a fresh agent.
340
337
  if (!msg.includes('No agent running')) {
341
- if (p.force)
342
- emitEphemeral(p.workspaceId, 'chat:rejected', { sessionId: p.sessionId, message: msg });
338
+ emitEphemeral(p.workspaceId, 'chat:rejected', { sessionId: p.sessionId, content: p.content, message: msg });
343
339
  console.error(`[ws] chat:message failed for workspace ${p.workspaceId}:`, err);
344
340
  return;
345
341
  }
@@ -353,12 +349,25 @@ setMessageHandler((type, payload) => {
353
349
  // requires them (e.g. grooming), it sets the override to bypass the
354
350
  // workspace default for this spawn only.
355
351
  const effectiveMode = p.agentPermissionModeOverride ?? workspace.agentPermissionMode;
356
- startAgent(p.workspaceId, worktreePath, p.content, workspace.model, true, effectiveMode, p.sessionId, workspace.reasoningEffort);
352
+ const started = startAgent(p.workspaceId, worktreePath, p.content, workspace.model, true, effectiveMode, p.sessionId, workspace.reasoningEffort);
357
353
  updateWorkspaceStatus(p.workspaceId, 'executing');
354
+ const resumedSessionId = p.sessionId ?? started.agentSessionId;
355
+ emit(p.workspaceId, 'user:message', { content: p.content, sender: 'user' }, resumedSessionId);
356
+ if (p.force)
357
+ emitEphemeral(p.workspaceId, 'chat:accepted', { sessionId: resumedSessionId });
358
+ }
359
+ else {
360
+ emitEphemeral(p.workspaceId, 'chat:rejected', {
361
+ sessionId: p.sessionId,
362
+ content: p.content,
363
+ message: `Workspace '${p.workspaceId}' not found`,
364
+ });
358
365
  }
359
366
  }
360
367
  catch (restartErr) {
361
- console.error('[ws] Failed to resume agent:', restartErr instanceof Error ? restartErr.message : restartErr);
368
+ const message = restartErr instanceof Error ? restartErr.message : String(restartErr);
369
+ emitEphemeral(p.workspaceId, 'chat:rejected', { sessionId: p.sessionId, content: p.content, message });
370
+ console.error('[ws] Failed to resume agent:', message);
362
371
  }
363
372
  }
364
373
  }
@@ -379,6 +388,9 @@ setMessageHandler((type, payload) => {
379
388
  }
380
389
  if (type === 'workspace:stop' && p?.workspaceId) {
381
390
  try {
391
+ // A deliberate stop must not be treated like a recoverable engine
392
+ // interruption: disable before the engine emits session:ended(killed).
393
+ autoLoopService.disable(p.workspaceId, 'user-action');
382
394
  stopAgent(p.workspaceId);
383
395
  }
384
396
  catch (err) {
@@ -1230,6 +1230,7 @@ app.post('/:id/deferred-tool-use/answer', async (c) => {
1230
1230
  }
1231
1231
  await agentManager.answerPendingQuestion(id, body.answers, body.toolCallId, {
1232
1232
  awaitingFreeForm: body.awaitingFreeForm === true,
1233
+ ...(typeof body.response === 'string' ? { response: body.response } : {}),
1233
1234
  });
1234
1235
  return c.json({ ok: true });
1235
1236
  }
@@ -1795,6 +1796,9 @@ app.patch('/:id', migrationGuard, async (c) => {
1795
1796
  updated = workspaceService.updateAgentPermissionMode(id, body.agentPermissionMode);
1796
1797
  }
1797
1798
  if (body.status) {
1799
+ if (body.status === 'idle' && agentManager.hasController(id)) {
1800
+ return c.json({ error: 'Cannot mark an active agent workspace idle; end or stop the agent session first' }, 409);
1801
+ }
1798
1802
  updated = workspaceService.updateWorkspaceStatus(id, body.status);
1799
1803
  }
1800
1804
  if (body.name !== undefined) {
@@ -233,29 +233,12 @@ export function createClaudeCodeEngine() {
233
233
  console.warn(`[claude-engine] SDK generator still open ${RESULT_DRAIN_TIMEOUT_MS}ms after 'result' — forcing session:ended`);
234
234
  const reason = userInterrupted ? 'killed' : mapperState.sawErrorResult ? 'error' : 'completed';
235
235
  emitSessionEnded(reason, reason === 'completed' ? 0 : null);
236
- // Normally we abort here to unstick the SDK so its subprocesses / MCP
237
- // children tear down. BUT if a wakeup is scheduled for this workspace,
238
- // the agent intentionally left background work running for the wakeup
239
- // to resume and check aborting would kill that work. Skip the abort
240
- // in that case so the background tasks survive. `session:ended` was
241
- // already emitted (so the orchestrator/auto-loop proceed and the
242
- // controller is removed, letting the wakeup fire later); the parked
243
- // generator drains on its own once the background work finishes.
244
- //
245
- // The wakeup check is a dynamic import: a static `import` of
246
- // wakeup-service here would form an engine → wakeup-service →
247
- // orchestrator → engine cycle that breaks module init under the test
248
- // SSR transform. Resolving it at call-time (once, 15s after result)
249
- // sidesteps the cycle with no hot-path cost.
250
- void (async () => {
251
- const { isWakeupScheduled } = await import('../../../wakeup-service.js');
252
- if (isWakeupScheduled(options.workspaceId)) {
253
- console.warn(`[claude-engine] generator still open ${RESULT_DRAIN_TIMEOUT_MS}ms after 'result' but a wakeup is scheduled for ${options.workspaceId} — leaving background work alive (no abort)`);
254
- }
255
- else {
256
- abortController.abort();
257
- }
258
- })();
236
+ // A wakeup starts a new turn later; it cannot safely keep this SDK
237
+ // stream alive after Kōbō has emitted session:ended and detached its
238
+ // controller. Leaving it alive lets late hooks issue AskUserQuestion
239
+ // calls against a closed input stream, producing an unanswerable UI
240
+ // card followed by "Stream closed". Abort deterministically instead.
241
+ abortController.abort();
259
242
  }, RESULT_DRAIN_TIMEOUT_MS);
260
243
  resultDrainTimer.unref?.();
261
244
  };
@@ -397,6 +380,7 @@ export function createClaudeCodeEngine() {
397
380
  updatedInput: {
398
381
  ...(typeof questions !== 'undefined' ? { questions } : {}),
399
382
  answers: response.answers,
383
+ ...(response.response !== undefined ? { response: response.response } : {}),
400
384
  },
401
385
  });
402
386
  return true;
@@ -10,7 +10,7 @@ const KOBO_MCP_BRIEF = [
10
10
  '• `kobo__search_codebase` to recall prior chat history (conversations, not source — use Grep for source).',
11
11
  '• `kobo__read_workspace_events_csv` to read paginated user/agent history from this workspace across sessions when prior context is useful. Use `limit` + `offset` (and optional `session_id`) instead of loading everything.',
12
12
  '• `kobo__get_workspace_info` / `kobo__get_git_info` / `kobo__get_ticket` for context.',
13
- '• `kobo__set_workspace_status` when the mission is done / blocked / idle.',
13
+ '• `kobo__set_workspace_status` when the mission is done or irrecoverably blocked. Never set `idle` while this session is active; use AskUserQuestion for clarification, or end the turn.',
14
14
  '• `kobo__schedule_wakeup` / `kobo__cancel_wakeup` to schedule (or cancel) a follow-up session — prefer these over the built-in `ScheduleWakeup` tool.',
15
15
  'Each tool carries its own "WHEN to use" guidance in its description — follow it.',
16
16
  ].join('\n');
@@ -28,6 +28,10 @@ export function buildClaudeOptions(input) {
28
28
  const options = {
29
29
  cwd: input.workingDir,
30
30
  systemPrompt: { type: 'preset', preset: 'claude_code', append: KOBO_SYSTEM_APPEND },
31
+ // Kōbō persists wakeups independently of the SDK and creates a fresh
32
+ // controller when they fire. The native wakeup is bound to one SDK stream,
33
+ // so it can outlive Kōbō's controller and produce unanswerable questions.
34
+ disallowedTools: ['ScheduleWakeup'],
31
35
  // Leave `tools` undefined: the `claude_code` preset EXCLUDES MCP tools, so
32
36
  // under bypassPermissions MCP tools surface as "haven't granted it yet"
33
37
  // even when the user chose bypass. Undefined → full toolbox (built-ins +
@@ -26,6 +26,9 @@ export function createAppServerClient(opts) {
26
26
  startTurn(params) {
27
27
  return peer.request('turn/start', params);
28
28
  },
29
+ steerTurn(params) {
30
+ return peer.request('turn/steer', params);
31
+ },
29
32
  async interruptTurn(params) {
30
33
  await peer.request('turn/interrupt', params);
31
34
  },
@@ -32,6 +32,8 @@ export function createCodexEngine() {
32
32
  let iteratorRunning = false;
33
33
  let userInterrupted = false;
34
34
  let discoveredSessionId = options.resumeFromEngineSessionId;
35
+ let activeTurnId;
36
+ let steerChain = Promise.resolve();
35
37
  const safeEmit = (ev) => {
36
38
  try {
37
39
  onEvent(ev);
@@ -99,7 +101,8 @@ export function createCodexEngine() {
99
101
  const n = params;
100
102
  for (const ev of handleTurnCompleted(n, mapperState))
101
103
  safeEmit(ev);
102
- resolveTurnDone();
104
+ if (!activeTurnId || !n.turn?.id || n.turn.id === activeTurnId)
105
+ resolveTurnDone();
103
106
  return;
104
107
  }
105
108
  if (method === 'thread/tokenUsage/updated') {
@@ -176,11 +179,12 @@ export function createCodexEngine() {
176
179
  safeEmit(ev);
177
180
  // collaborationMode is sticky server-side — always send it explicitly,
178
181
  // never omit (would leave a Bypass turn stuck in a previous Plan mode).
179
- await client.startTurn({
182
+ const initialTurn = await client.startTurn({
180
183
  threadId: discoveredSessionId,
181
184
  input,
182
185
  collaborationMode,
183
186
  });
187
+ activeTurnId = initialTurn.turnId;
184
188
  await turnDonePromise;
185
189
  const reason = mapperState.sawErrorResult
186
190
  ? 'error'
@@ -235,8 +239,21 @@ export function createCodexEngine() {
235
239
  isAlive() {
236
240
  return iteratorRunning;
237
241
  },
238
- sendMessage() {
239
- throw new Error('sendMessage not supported in Codex app-server single-shot mode');
242
+ sendMessage(text) {
243
+ const steer = async () => {
244
+ if (!discoveredSessionId || !activeTurnId) {
245
+ throw new Error('Codex session is not ready to receive a message');
246
+ }
247
+ const response = await client.steerTurn({
248
+ threadId: discoveredSessionId,
249
+ expectedTurnId: activeTurnId,
250
+ input: [{ type: 'text', text, text_elements: [] }],
251
+ });
252
+ activeTurnId = response.turnId;
253
+ };
254
+ const queued = steerChain.then(steer);
255
+ steerChain = queued.catch(() => { });
256
+ return queued;
240
257
  },
241
258
  interrupt() {
242
259
  userInterrupted = true;
@@ -7,7 +7,7 @@ const CODEX_KOBO_MCP_BRIEF = [
7
7
  '• `mcp__kobo-tasks__log_thought` to persist notable decisions to `.ai/thoughts/logs/<date>-<slug>.md`.',
8
8
  '• `mcp__kobo-tasks__search_codebase` to recall prior chat history (conversations, not source — use shell tools for source).',
9
9
  '• `mcp__kobo-tasks__get_workspace_info` / `mcp__kobo-tasks__get_git_info` / `mcp__kobo-tasks__get_ticket` for context.',
10
- '• `mcp__kobo-tasks__set_workspace_status` when the mission is done / blocked / idle.',
10
+ '• `mcp__kobo-tasks__set_workspace_status` when the mission is done or irrecoverably blocked. Never set `idle` while this session is active; ask the user or end the turn instead.',
11
11
  '• `mcp__kobo-tasks__schedule_wakeup` / `mcp__kobo-tasks__cancel_wakeup` to schedule (or cancel) a follow-up session.',
12
12
  'Each tool carries its own "WHEN to use" guidance in its description — follow it.',
13
13
  ].join('\n');
@@ -36,12 +36,23 @@ export function handleServerRequest(args) {
36
36
  }
37
37
  if (method === 'item/tool/requestUserInput') {
38
38
  register(callId, { requestId, kind: 'user_input', payload: p });
39
+ // Codex allows a free-form question to omit `options` (or send null),
40
+ // while the shared question panel always renders an array. Keep Codex's
41
+ // `id` so the UI can return the protocol key instead of the display text.
42
+ const questions = Array.isArray(p.questions)
43
+ ? p.questions.map((question) => {
44
+ if (!question || typeof question !== 'object')
45
+ return question;
46
+ const raw = question;
47
+ return { ...raw, options: Array.isArray(raw.options) ? raw.options : [] };
48
+ })
49
+ : [];
39
50
  emit({
40
51
  kind: 'session:user-input-requested',
41
52
  requestKind: 'question',
42
53
  toolCallId: callId,
43
54
  toolName: 'AskUserQuestion',
44
- payload: { questions: p.questions },
55
+ payload: { questions, autoResolutionMs: p.autoResolutionMs },
45
56
  });
46
57
  return true;
47
58
  }
@@ -395,11 +395,12 @@ function reuseOrCreateFreshSession(workspaceId, existingSessionId, model) {
395
395
  }
396
396
  // ── Event handler ─────────────────────────────────────────────────────────────
397
397
  /**
398
- * Snapshot of the `tasks` done-count at `session:started`, read back and
399
- * cleared at `session:ended` to compute the per-session delta. Used by
400
- * `auto-loop-service.onSessionEnded` for stall detection.
398
+ * Snapshots of task completion and task state at `session:started`, read back
399
+ * at `session:ended` for auto-loop stall detection. A task moved to
400
+ * `in_progress` is meaningful progress even if it is not done yet.
401
401
  */
402
402
  const tasksDoneSnapshot = new Map();
403
+ const taskStateSnapshot = new Map();
403
404
  function getDoneTaskCount(workspaceId) {
404
405
  try {
405
406
  const db = getDb();
@@ -415,9 +416,23 @@ function getDoneTaskCount(workspaceId) {
415
416
  return 0;
416
417
  }
417
418
  }
419
+ function getTaskStateSignature(workspaceId) {
420
+ try {
421
+ const db = getDb();
422
+ const rows = db
423
+ .prepare('SELECT id, status, updated_at FROM tasks WHERE workspace_id = ? ORDER BY id')
424
+ .all(workspaceId);
425
+ return JSON.stringify(rows);
426
+ }
427
+ catch (err) {
428
+ console.warn('[orchestrator] getTaskStateSignature failed, returning empty state:', err);
429
+ return '';
430
+ }
431
+ }
418
432
  /** Clear the in-memory done-count snapshot for a workspace (called on delete). */
419
433
  export function forgetTasksDoneSnapshot(workspaceId) {
420
434
  tasksDoneSnapshot.delete(workspaceId);
435
+ taskStateSnapshot.delete(workspaceId);
421
436
  }
422
437
  /** Drop the resume-failed flag for a workspace (called on delete). */
423
438
  export function forgetResumeFailed(workspaceId) {
@@ -444,6 +459,7 @@ function handleEvent(workspaceId, agentSessionId, ev) {
444
459
  // can compute a delta for auto-loop stall detection.
445
460
  if (ev.kind === 'session:started') {
446
461
  tasksDoneSnapshot.set(workspaceId, getDoneTaskCount(workspaceId));
462
+ taskStateSnapshot.set(workspaceId, getTaskStateSignature(workspaceId));
447
463
  }
448
464
  // Legacy fallback: the built-in `ScheduleWakeup` tool (CLI tradition) isn't
449
465
  // declared by the SDK, so we intercept the tool:call event and apply the
@@ -531,8 +547,12 @@ function handleEvent(workspaceId, agentSessionId, ev) {
531
547
  const isResumeFailed = resumeFailedSet.delete(workspaceId);
532
548
  const before = tasksDoneSnapshot.get(workspaceId) ?? getDoneTaskCount(workspaceId);
533
549
  const after = getDoneTaskCount(workspaceId);
534
- const delta = Math.max(0, after - before);
550
+ const completedDelta = Math.max(0, after - before);
551
+ const taskStateBefore = taskStateSnapshot.get(workspaceId) ?? getTaskStateSignature(workspaceId);
552
+ const taskStateAfter = getTaskStateSignature(workspaceId);
553
+ const progressDelta = completedDelta > 0 || taskStateBefore !== taskStateAfter ? 1 : 0;
535
554
  tasksDoneSnapshot.delete(workspaceId);
555
+ taskStateSnapshot.delete(workspaceId);
536
556
  clearPendingForSession(workspaceId, agentSessionId);
537
557
  // Must run BEFORE autoLoopService.onSessionEnded → spawnNextIteration →
538
558
  // startAgent, otherwise startAgent throws "Agent already running" because
@@ -546,7 +566,7 @@ function handleEvent(workspaceId, agentSessionId, ev) {
546
566
  // disable() clears it, and the cleanup hook needs to know whether this was
547
567
  // a mid-loop session (never cleans) or a standalone one.
548
568
  const wasAutoLoop = autoLoopService.getStatus(workspaceId).auto_loop;
549
- autoLoopService.onSessionEnded(workspaceId, effectiveReason, delta);
569
+ autoLoopService.onSessionEnded(workspaceId, effectiveReason, progressDelta);
550
570
  cleanupScriptService.onSessionEnded(workspaceId, effectiveReason, { wasAutoLoop });
551
571
  }
552
572
  if (ev.kind === 'session:user-input-requested') {
@@ -845,7 +865,7 @@ export function sendMessage(workspaceId, content, expectedSessionId) {
845
865
  throw new Error(`Session '${expectedSessionId}' is not active for workspace '${workspaceId}'`);
846
866
  }
847
867
  wakeupService.cancel(workspaceId, 'user-message');
848
- ctrl.sendMessage(content);
868
+ return ctrl.sendMessage(content);
849
869
  }
850
870
  /**
851
871
  * Render the user's answer to an AskUserQuestion as a markdown chat
@@ -859,10 +879,13 @@ function formatDeferredAnswerForChat(questions, answers) {
859
879
  for (const q of questions) {
860
880
  if (!q || typeof q !== 'object')
861
881
  continue;
882
+ if (q.isSecret === true)
883
+ continue;
862
884
  const questionText = typeof q.question === 'string' ? q.question : null;
863
885
  if (!questionText)
864
886
  continue;
865
- const answer = answers[questionText];
887
+ const questionId = typeof q.id === 'string' ? q.id : questionText;
888
+ const answer = answers[questionId] ?? answers[questionText];
866
889
  if (!answer)
867
890
  continue;
868
891
  lines.push(`- **${questionText}** → ${answer}`);
@@ -914,7 +937,11 @@ export async function answerPendingQuestion(workspaceId, answers, expectedToolCa
914
937
  if (!engineProcess) {
915
938
  throw new Error(`Agent for workspace '${workspaceId}' has no active engine process`);
916
939
  }
917
- const resolved = engineProcess.resolvePendingUserInput(head.toolCallId, { kind: 'question', answers });
940
+ const resolved = engineProcess.resolvePendingUserInput(head.toolCallId, {
941
+ kind: 'question',
942
+ answers,
943
+ ...(opts?.response !== undefined ? { response: opts.response } : {}),
944
+ });
918
945
  if (!resolved) {
919
946
  throw new Error(`No pending callback for toolCallId '${head.toolCallId}'`);
920
947
  }
@@ -82,7 +82,15 @@ export function disable(workspaceId, reason) {
82
82
  return;
83
83
  const db = getDb();
84
84
  db.prepare('UPDATE workspaces SET auto_loop = 0 WHERE id = ?').run(workspaceId);
85
- emitEphemeral(workspaceId, 'autoloop:disabled', { reason });
85
+ const diagnostic = {
86
+ reason,
87
+ tasksPending: countPendingTasks(workspaceId),
88
+ noProgressStreak: row.no_progress_streak,
89
+ status: row.status,
90
+ };
91
+ console.info(`[auto-loop] disabled workspace '${workspaceId}' reason=${reason}` +
92
+ ` pending=${diagnostic.tasksPending} streak=${diagnostic.noProgressStreak} status=${diagnostic.status}`);
93
+ emitEphemeral(workspaceId, 'autoloop:disabled', diagnostic);
86
94
  // The loop finished every task — run the project's cleanup script. Other
87
95
  // disable reasons (stall / error / user-action) leave tasks unfinished, so
88
96
  // they intentionally skip the cleanup.
@@ -93,15 +101,15 @@ export function disable(workspaceId, reason) {
93
101
  /**
94
102
  * Route a `session:ended` event into the auto-loop state machine.
95
103
  *
96
- * Called by orchestrator.handleEvent. The delta is the number of tasks that
97
- * transitioned to `done` during this session (computed from a snapshot taken
98
- * at `session:started`).
104
+ * Called by orchestrator.handleEvent. The delta records whether task state
105
+ * progressed during this session, including transitions such as pending to
106
+ * in_progress, not only completion.
99
107
  *
100
108
  * When status is `quota` we skip spawning: the orchestrator's handleQuota
101
109
  * already scheduled a backoff timer and will call `onQuotaBackoffExpired` once
102
110
  * the window closes — that function owns the next spawn in that case.
103
111
  */
104
- export function onSessionEnded(workspaceId, reason, tasksDoneDelta) {
112
+ export function onSessionEnded(workspaceId, reason, taskProgressDelta) {
105
113
  const row = getRow(workspaceId);
106
114
  if (!row)
107
115
  return;
@@ -115,7 +123,7 @@ export function onSessionEnded(workspaceId, reason, tasksDoneDelta) {
115
123
  // will resume the deferred turn explicitly.
116
124
  if (row.status === 'awaiting-user')
117
125
  return;
118
- if (reason === 'error' || reason === 'killed') {
126
+ if (reason === 'error') {
119
127
  disable(workspaceId, 'error');
120
128
  return;
121
129
  }
@@ -126,7 +134,7 @@ export function onSessionEnded(workspaceId, reason, tasksDoneDelta) {
126
134
  return;
127
135
  const db = getDb();
128
136
  let streak;
129
- if (tasksDoneDelta > 0) {
137
+ if (taskProgressDelta > 0) {
130
138
  db.prepare('UPDATE workspaces SET no_progress_streak = 0 WHERE id = ?').run(workspaceId);
131
139
  streak = 0;
132
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.9.6",
3
+ "version": "1.10.1",
4
4
  "description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -1,4 +1,4 @@
1
- import{$ as e,B as t,F as n,I as r,R as i,S as a,U as o,W as s,_ as c,at as l,b as u,c as d,ct as f,g as p,h as m,it as h,k as g,m as _,n as v,p as y,s as b,st as x,y as S}from"./vue.runtime.esm-bundler-JZnIeD9D.js";import{l as C,t as w}from"./QIcon-C6E-oEjI.js";import{B as T,C as E,N as D,S as O,d as k,p as A,x as j,z as ee}from"./index-BKXtMMl8.js";import{t as te}from"./QSpinnerDots-DIV68-cV.js";import{t as M}from"./QTooltip-B9mxxMEM.js";import{t as N}from"./QExpansionItem-Dka3X1IN.js";import{t as ne}from"./QScrollArea-Mwe2ihwC.js";import{t as re}from"./documents-5sWEpISn.js";import{i as ie,n as ae,t as P}from"./render-chat-markdown-BjVGm1Vi.js";import{t as F}from"./_plugin-vue_export-helper-BDNMzG2s.js";function oe(e,t,n=!0){let r=[],i=new Map,a=new Map;for(let n=0;n<e.length;n++){let o=e[n],s=t?.[n];switch(o.kind){case`message:text`:{let e=i.get(o.messageId);if(e)e.text+=o.text,e.streaming=o.streaming;else{let e={type:`text`,messageId:o.messageId,text:o.text,streaming:o.streaming,ts:s};i.set(o.messageId,e),r.push(e)}break}case`message:end`:{let e=i.get(o.messageId);e&&(e.streaming=!1);break}case`message:thinking`:r.push({type:`thinking`,messageId:o.messageId,text:o.text,ts:s});break;case`tool:call`:{let e={type:`tool`,toolCallId:o.toolCallId,name:o.name,input:o.input,ts:s};a.set(o.toolCallId,e),r.push(e);break}case`tool:result`:{let e=a.get(o.toolCallId);e&&(e.result={output:o.output,isError:o.isError});break}case`session:started`:r.push({type:`session`,kind:`started`,detail:{engineSessionId:o.engineSessionId,model:o.model},ts:s});break;case`session:ended`:r.push({type:`session`,kind:`ended`,detail:{reason:o.reason,exitCode:o.exitCode},ts:s});break;case`session:compacted`:r.push({type:`session`,kind:`compacted`,ts:s});break;case`session:compacting`:case`session:brainstorm-complete`:case`session:user-input-requested`:case`message:raw`:case`skills:discovered`:case`usage`:case`rate_limit`:case`subagent:progress`:case`error`:break;default:}}let o=null;for(let e of r)e.type===`text`&&e.streaming&&(o&&(o.streaming=!1),o=e);return o&&!n&&(o.streaming=!1),r}function se(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts})),r=[...e,...n];r.sort((e,t)=>{let n=e.ts??``,r=t.ts??``;return n===r?0:n?r?n<r?-1:1:-1:1});let i;for(let e of r)e.type===`user`&&e.sender!==`system-prompt`&&e.ts&&(!i||e.ts>i)&&(i=e.ts);if(i)for(let e of r)e.type===`text`&&e.streaming&&(!e.ts||e.ts<i)&&(e.streaming=!1);return r}var I=new Set([`setup`,`cleanup`,`archive`]);function L(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:I.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function ce(e){let t=[],n=null,r=null;for(let i of e){let e=L(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`: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}var R={class:`text-caption text-grey-6`},z=a({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=y(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return`session.ended`;case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(i(),c(`span`,R,f(e.$t(n.value)),1))}});function le(e,t){if(t.length===0||e.length===0)return e;let n=[...t].sort((e,t)=>t.length-e.length),r=new DOMParser().parseFromString(`<div>${e}</div>`,`text/html`),i=r.body.firstChild;if(!i)return e;function a(e){if(e.nodeType===Node.TEXT_NODE){ue(e,n,r);return}if(e.nodeName===`A`)return;let t=Array.from(e.childNodes);for(let e of t)a(e)}return a(i),i.innerHTML}function ue(e,t,n){let r=e.textContent??``;if(!t.some(e=>r.includes(e)))return;let i=n.createDocumentFragment(),a=0;for(;a<r.length;){let e=B(r,a,t);if(!e){i.appendChild(n.createTextNode(r.slice(a)));break}e.index>a&&i.appendChild(n.createTextNode(r.slice(a,e.index)));let o=n.createElement(`a`);o.className=`document-link`,o.setAttribute(`data-document-path`,e.path),o.setAttribute(`href`,`#`),o.textContent=e.path,i.appendChild(o),a=e.index+e.path.length}e.parentNode?.replaceChild(i,e)}function B(e,t,n){let r=null;for(let i of n){let n=e.indexOf(i,t);n<0||(!r||n<r.index||n===r.index&&i.length>r.path.length)&&(r={index:n,path:i})}return r}var V=[`innerHTML`],H=F(a({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,n=re(),r=k(),a=y(()=>{let e=r.selectedWorkspaceId;return e?n.documentsFor(e).map(e=>e.path):[]}),o=y(()=>ae(le(ie.parse(t.item.text,{async:!1,breaks:!0,gfm:!0}),a.value),{addAttr:[`data-document-path`]}));function s(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let i=t.getAttribute(`data-document-path`),a=r.selectedWorkspaceId;!i||!a||n.openDocumentByPath(a,i)}return(t,n)=>(i(),c(`div`,{class:`markdown-message`,onClick:s},[_(`div`,{innerHTML:o.value},null,8,V),e.item.streaming?(i(),m(C,{key:0,size:`xs`,class:`q-ml-xs`})):p(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]),de={key:0,class:`text-caption text-grey-5`,style:{"font-style":`italic`}},U=[`innerHTML`],W={key:1,style:{"white-space":`pre-wrap`}},G=F(a({__name:`ThinkingItem`,props:{item:{}},setup(e){let t=e,n=y(()=>t.item.text.trim().slice(0,100)),r=y(()=>t.item.text.trim().length>0),a=y(()=>t.item.text.trim().length>100),o=y(()=>P(t.item.text));return(t,l)=>r.value?(i(),c(`div`,de,[a.value?(i(),m(N,{key:0,dense:``,"dense-toggle":``,label:n.value,"header-class":`text-grey-5 text-caption`,style:{"font-style":`italic`}},{default:s(()=>[_(`div`,{class:`q-py-xs markdown-thinking`,innerHTML:o.value},null,8,U)]),_:1},8,[`label`])):(i(),c(`span`,W,f(e.item.text),1))])):p(``,!0)}}),[[`__scopeId`,`data-v-7f45ed94`]]);function K(e,t){let n=e.split(`
1
+ import{$ as e,B as t,F as n,I as r,R as i,S as a,U as o,W as s,_ as c,at as l,b as u,c as d,ct as f,g as p,h as m,it as h,k as g,m as _,n as v,p as y,s as b,st as x,y as S}from"./vue.runtime.esm-bundler-JZnIeD9D.js";import{l as C,t as w}from"./QIcon-C6E-oEjI.js";import{B as T,C as E,N as D,S as O,d as k,p as A,x as j,z as ee}from"./index-Deinl-Hq.js";import{t as te}from"./QSpinnerDots-DIV68-cV.js";import{t as M}from"./QTooltip-BYnhp9Wu.js";import{t as N}from"./QExpansionItem-Sh9QNOLB.js";import{t as ne}from"./QScrollArea-CbA9gH0b.js";import{t as re}from"./documents-D4iGKwK5.js";import{i as ie,n as ae,t as P}from"./render-chat-markdown-CaUfpvEV.js";import{t as F}from"./_plugin-vue_export-helper-BDNMzG2s.js";function oe(e,t,n=!0){let r=[],i=new Map,a=new Map;for(let n=0;n<e.length;n++){let o=e[n],s=t?.[n];switch(o.kind){case`message:text`:{let e=i.get(o.messageId);if(e)e.text+=o.text,e.streaming=o.streaming;else{let e={type:`text`,messageId:o.messageId,text:o.text,streaming:o.streaming,ts:s};i.set(o.messageId,e),r.push(e)}break}case`message:end`:{let e=i.get(o.messageId);e&&(e.streaming=!1);break}case`message:thinking`:r.push({type:`thinking`,messageId:o.messageId,text:o.text,ts:s});break;case`tool:call`:{let e={type:`tool`,toolCallId:o.toolCallId,name:o.name,input:o.input,ts:s};a.set(o.toolCallId,e),r.push(e);break}case`tool:result`:{let e=a.get(o.toolCallId);e&&(e.result={output:o.output,isError:o.isError});break}case`session:started`:r.push({type:`session`,kind:`started`,detail:{engineSessionId:o.engineSessionId,model:o.model},ts:s});break;case`session:ended`:r.push({type:`session`,kind:`ended`,detail:{reason:o.reason,exitCode:o.exitCode},ts:s});break;case`session:compacted`:r.push({type:`session`,kind:`compacted`,ts:s});break;case`session:compacting`:case`session:brainstorm-complete`:case`session:user-input-requested`:case`message:raw`:case`skills:discovered`:case`usage`:case`rate_limit`:case`subagent:progress`:case`error`:break;default:}}let o=null;for(let e of r)e.type===`text`&&e.streaming&&(o&&(o.streaming=!1),o=e);return o&&!n&&(o.streaming=!1),r}function se(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts})),r=[...e,...n];r.sort((e,t)=>{let n=e.ts??``,r=t.ts??``;return n===r?0:n?r?n<r?-1:1:-1:1});let i;for(let e of r)e.type===`user`&&e.sender!==`system-prompt`&&e.ts&&(!i||e.ts>i)&&(i=e.ts);if(i)for(let e of r)e.type===`text`&&e.streaming&&(!e.ts||e.ts<i)&&(e.streaming=!1);return r}var I=new Set([`setup`,`cleanup`,`archive`]);function L(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:I.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function ce(e){let t=[],n=null,r=null;for(let i of e){let e=L(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`: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}var R={class:`text-caption text-grey-6`},z=a({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=y(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return`session.ended`;case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(i(),c(`span`,R,f(e.$t(n.value)),1))}});function le(e,t){if(t.length===0||e.length===0)return e;let n=[...t].sort((e,t)=>t.length-e.length),r=new DOMParser().parseFromString(`<div>${e}</div>`,`text/html`),i=r.body.firstChild;if(!i)return e;function a(e){if(e.nodeType===Node.TEXT_NODE){ue(e,n,r);return}if(e.nodeName===`A`)return;let t=Array.from(e.childNodes);for(let e of t)a(e)}return a(i),i.innerHTML}function ue(e,t,n){let r=e.textContent??``;if(!t.some(e=>r.includes(e)))return;let i=n.createDocumentFragment(),a=0;for(;a<r.length;){let e=B(r,a,t);if(!e){i.appendChild(n.createTextNode(r.slice(a)));break}e.index>a&&i.appendChild(n.createTextNode(r.slice(a,e.index)));let o=n.createElement(`a`);o.className=`document-link`,o.setAttribute(`data-document-path`,e.path),o.setAttribute(`href`,`#`),o.textContent=e.path,i.appendChild(o),a=e.index+e.path.length}e.parentNode?.replaceChild(i,e)}function B(e,t,n){let r=null;for(let i of n){let n=e.indexOf(i,t);n<0||(!r||n<r.index||n===r.index&&i.length>r.path.length)&&(r={index:n,path:i})}return r}var V=[`innerHTML`],H=F(a({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,n=re(),r=k(),a=y(()=>{let e=r.selectedWorkspaceId;return e?n.documentsFor(e).map(e=>e.path):[]}),o=y(()=>ae(le(ie.parse(t.item.text,{async:!1,breaks:!0,gfm:!0}),a.value),{addAttr:[`data-document-path`]}));function s(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let i=t.getAttribute(`data-document-path`),a=r.selectedWorkspaceId;!i||!a||n.openDocumentByPath(a,i)}return(t,n)=>(i(),c(`div`,{class:`markdown-message`,onClick:s},[_(`div`,{innerHTML:o.value},null,8,V),e.item.streaming?(i(),m(C,{key:0,size:`xs`,class:`q-ml-xs`})):p(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]),de={key:0,class:`text-caption text-grey-5`,style:{"font-style":`italic`}},U=[`innerHTML`],W={key:1,style:{"white-space":`pre-wrap`}},G=F(a({__name:`ThinkingItem`,props:{item:{}},setup(e){let t=e,n=y(()=>t.item.text.trim().slice(0,100)),r=y(()=>t.item.text.trim().length>0),a=y(()=>t.item.text.trim().length>100),o=y(()=>P(t.item.text));return(t,l)=>r.value?(i(),c(`div`,de,[a.value?(i(),m(N,{key:0,dense:``,"dense-toggle":``,label:n.value,"header-class":`text-grey-5 text-caption`,style:{"font-style":`italic`}},{default:s(()=>[_(`div`,{class:`q-py-xs markdown-thinking`,innerHTML:o.value},null,8,U)]),_:1},8,[`label`])):(i(),c(`span`,W,f(e.item.text),1))])):p(``,!0)}}),[[`__scopeId`,`data-v-7f45ed94`]]);function K(e,t){let n=e.split(`
2
2
  `),r=t.split(`
3
3
  `),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=i-1;e>=0;e--)for(let t=a-1;t>=0;t--)n[e]===r[t]?o[e][t]=o[e+1][t+1]+1:o[e][t]=Math.max(o[e+1][t],o[e][t+1]);let s=[],c=0,l=0;for(;c<i&&l<a;)n[c]===r[l]?(s.push({type:`context`,content:n[c]}),c++,l++):o[c+1][l]>=o[c][l+1]?(s.push({type:`del`,content:n[c]}),c++):(s.push({type:`add`,content:r[l]}),l++);for(;c<i;)s.push({type:`del`,content:n[c++]});for(;l<a;)s.push({type:`add`,content:r[l++]});return s}function fe(e){let t=e.split(`
4
4
  `),n=[];for(let e of t)e.startsWith(`@@`)||e.startsWith(`+++`)||e.startsWith(`---`)||(e.startsWith(`+`)?n.push({type:`add`,content:e.slice(1)}):e.startsWith(`-`)?n.push({type:`del`,content:e.slice(1)}):e.startsWith(` `)?n.push({type:`context`,content:e.slice(1)}):e.length>0&&n.push({type:`context`,content:e}));return n}function pe(e,t){if(!t||typeof t!=`object`)return null;let n=t;if(e===`Edit`){let e=n.file_path;if(!e)return null;let t=n.old_string??``,r=n.new_string??``,i=typeof n.diff==`string`?n.diff:``;if(!t&&!r&&i.length>0){let t=fe(i);return{toolName:`Edit`,filePath:e,additions:t.filter(e=>e.type===`add`).length,deletions:t.filter(e=>e.type===`del`).length,diffLines:t}}return{toolName:`Edit`,filePath:e,oldString:t,newString:r,replaceAll:n.replace_all??!1,additions:r?r.split(`
@@ -1 +1 @@
1
- import{$ as e,B as t,F as n,R as r,S as i,W as a,_ as o,b as s,c,ct as l,g as u,h as d,it as f,m as p}from"./vue.runtime.esm-bundler-JZnIeD9D.js";import{N as m,gt as h,mt as g,s as _}from"./index-BKXtMMl8.js";import{t as v}from"./render-chat-markdown-BjVGm1Vi.js";import{t as y}from"./_plugin-vue_export-helper-BDNMzG2s.js";import{t as b}from"./QSpace-ezNrZkuj.js";import{t as x}from"./QChip-CjN_RB2y.js";import{t as S}from"./QPage-D3rjwA1l.js";var C={class:`row items-center q-mb-md`},w={class:`text-h6 q-ml-sm`},T={key:0,class:`text-grey-6 text-center q-pa-lg`},E={key:1,class:`text-negative text-center q-pa-lg`},D={key:2,class:`text-grey-6 text-center q-pa-lg`},O={key:3,class:`column q-gutter-md`},k={key:0,class:`text-caption text-grey-6`},A={class:`row items-center q-mb-sm`},j={class:`text-subtitle1 text-indigo-3`,style:{"font-family":`var(--kobo-font-mono, monospace)`}},M=[`innerHTML`],N=y(i({__name:`ChangelogPage`,setup(i){let y=_(),N=e([]),P=e(``),F=e(!1),I=e(null);function L(e){return v(e)}async function R(){F.value=!0,I.value=null;try{let e=await fetch(`/api/changelog`);if(!e.ok)throw Error(`HTTP ${e.status}`);let t=await e.json();P.value=t.currentVersion??``,N.value=t.versions??[]}catch(e){I.value=e instanceof Error?e.message:String(e)}finally{F.value=!1}}return n(R),(e,n)=>(r(),d(S,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:a(()=>[p(`div`,C,[s(m,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:n[0]||=e=>f(y).back()}),p(`div`,w,l(e.$t(`changelog.title`)),1),s(b),s(m,{flat:``,dense:``,icon:`refresh`,loading:F.value,label:e.$t(`common.refresh`),onClick:R},null,8,[`loading`,`label`])]),F.value&&N.value.length===0?(r(),o(`div`,T,l(e.$t(`common.loading`)),1)):I.value?(r(),o(`div`,E,l(I.value),1)):N.value.length===0?(r(),o(`div`,D,l(e.$t(`changelog.empty`)),1)):(r(),o(`div`,O,[P.value?(r(),o(`div`,k,l(e.$t(`changelog.currentVersion`,{version:P.value})),1)):u(``,!0),(r(!0),o(c,null,t(N.value,t=>(r(),d(g,{key:t.version,dark:``,flat:``,bordered:``},{default:a(()=>[s(h,null,{default:a(()=>[p(`div`,A,[p(`div`,j,` v`+l(t.version),1),t.version===P.value?(r(),d(x,{key:0,dense:``,size:`sm`,color:`indigo-7`,"text-color":`grey-2`,label:e.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):u(``,!0)]),p(`div`,{class:`changelog-notes`,innerHTML:L(t.notes)},null,8,M)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-ed73d661`]]);export{N as default};
1
+ import{$ as e,B as t,F as n,R as r,S as i,W as a,_ as o,b as s,c,ct as l,g as u,h as d,it as f,m as p}from"./vue.runtime.esm-bundler-JZnIeD9D.js";import{N as m,gt as h,mt as g,s as _}from"./index-Deinl-Hq.js";import{t as v}from"./render-chat-markdown-CaUfpvEV.js";import{t as y}from"./_plugin-vue_export-helper-BDNMzG2s.js";import{t as b}from"./QSpace-ezNrZkuj.js";import{t as x}from"./QChip-Bq-3SzSI.js";import{t as S}from"./QPage-D3rjwA1l.js";var C={class:`row items-center q-mb-md`},w={class:`text-h6 q-ml-sm`},T={key:0,class:`text-grey-6 text-center q-pa-lg`},E={key:1,class:`text-negative text-center q-pa-lg`},D={key:2,class:`text-grey-6 text-center q-pa-lg`},O={key:3,class:`column q-gutter-md`},k={key:0,class:`text-caption text-grey-6`},A={class:`row items-center q-mb-sm`},j={class:`text-subtitle1 text-indigo-3`,style:{"font-family":`var(--kobo-font-mono, monospace)`}},M=[`innerHTML`],N=y(i({__name:`ChangelogPage`,setup(i){let y=_(),N=e([]),P=e(``),F=e(!1),I=e(null);function L(e){return v(e)}async function R(){F.value=!0,I.value=null;try{let e=await fetch(`/api/changelog`);if(!e.ok)throw Error(`HTTP ${e.status}`);let t=await e.json();P.value=t.currentVersion??``,N.value=t.versions??[]}catch(e){I.value=e instanceof Error?e.message:String(e)}finally{F.value=!1}}return n(R),(e,n)=>(r(),d(S,{class:`q-pa-md`,style:{"max-width":`900px`,margin:`0 auto`}},{default:a(()=>[p(`div`,C,[s(m,{flat:``,dense:``,round:``,icon:`arrow_back`,onClick:n[0]||=e=>f(y).back()}),p(`div`,w,l(e.$t(`changelog.title`)),1),s(b),s(m,{flat:``,dense:``,icon:`refresh`,loading:F.value,label:e.$t(`common.refresh`),onClick:R},null,8,[`loading`,`label`])]),F.value&&N.value.length===0?(r(),o(`div`,T,l(e.$t(`common.loading`)),1)):I.value?(r(),o(`div`,E,l(I.value),1)):N.value.length===0?(r(),o(`div`,D,l(e.$t(`changelog.empty`)),1)):(r(),o(`div`,O,[P.value?(r(),o(`div`,k,l(e.$t(`changelog.currentVersion`,{version:P.value})),1)):u(``,!0),(r(!0),o(c,null,t(N.value,t=>(r(),d(g,{key:t.version,dark:``,flat:``,bordered:``},{default:a(()=>[s(h,null,{default:a(()=>[p(`div`,A,[p(`div`,j,` v`+l(t.version),1),t.version===P.value?(r(),d(x,{key:0,dense:``,size:`sm`,color:`indigo-7`,"text-color":`grey-2`,label:e.$t(`changelog.current`),class:`q-ml-sm`},null,8,[`label`])):u(``,!0)]),p(`div`,{class:`changelog-notes`,innerHTML:L(t.notes)},null,8,M)]),_:2},1024)]),_:2},1024))),128))]))]),_:1}))}}),[[`__scopeId`,`data-v-ed73d661`]]);export{N as default};
@@ -1 +1 @@
1
- import{P as e,b as t}from"./QIcon-C6E-oEjI.js";import{nt as n,rt as r}from"./index-BKXtMMl8.js";var i={left:!0,right:!0,up:!0,down:!0,horizontal:!0,vertical:!0},a=Object.keys(i);i.all=!0;function o(e){let t={};for(let n of a)e[n]&&(t[n]=!0);return Object.keys(t).length===0?i:(t.horizontal?t.left=t.right=!0:t.left&&t.right&&(t.horizontal=!0),t.vertical?t.up=t.down=!0:t.up&&t.down&&(t.vertical=!0),t.horizontal&&t.vertical&&(t.all=!0),t)}var s=[`INPUT`,`TEXTAREA`];function c(e,t){return t.event===void 0&&e.target!==void 0&&!e.target.draggable&&typeof t.handler==`function`&&!s.includes(e.target.nodeName.toUpperCase())&&(e.qClonedBy===void 0||!e.qClonedBy.includes(t.uid))}function l(e){return e===!1?0:e===!0||e===void 0?1:Number.parseInt(e,10)||0}var u=e({name:`close-popup`,beforeMount(e,{value:i}){let a={depth:l(i),handler(t){a.depth!==0&&setTimeout(()=>{let i=r(e);i!==void 0&&n(i,t,a.depth)},0)},handlerKey(e){t(e,13)&&a.handler(e)}};e.__qclosepopup=a,e.addEventListener(`click`,a.handler),e.addEventListener(`keyup`,a.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=l(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{o as n,c as r,u as t};
1
+ import{P as e,b as t}from"./QIcon-C6E-oEjI.js";import{nt as n,rt as r}from"./index-Deinl-Hq.js";var i={left:!0,right:!0,up:!0,down:!0,horizontal:!0,vertical:!0},a=Object.keys(i);i.all=!0;function o(e){let t={};for(let n of a)e[n]&&(t[n]=!0);return Object.keys(t).length===0?i:(t.horizontal?t.left=t.right=!0:t.left&&t.right&&(t.horizontal=!0),t.vertical?t.up=t.down=!0:t.up&&t.down&&(t.vertical=!0),t.horizontal&&t.vertical&&(t.all=!0),t)}var s=[`INPUT`,`TEXTAREA`];function c(e,t){return t.event===void 0&&e.target!==void 0&&!e.target.draggable&&typeof t.handler==`function`&&!s.includes(e.target.nodeName.toUpperCase())&&(e.qClonedBy===void 0||!e.qClonedBy.includes(t.uid))}function l(e){return e===!1?0:e===!0||e===void 0?1:Number.parseInt(e,10)||0}var u=e({name:`close-popup`,beforeMount(e,{value:i}){let a={depth:l(i),handler(t){a.depth!==0&&setTimeout(()=>{let i=r(e);i!==void 0&&n(i,t,a.depth)},0)},handlerKey(e){t(e,13)&&a.handler(e)}};e.__qclosepopup=a,e.addEventListener(`click`,a.handler),e.addEventListener(`keyup`,a.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=l(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{o as n,c as r,u as t};