@loicngr/kobo 1.9.5 → 1.9.6

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 (74) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/dist/mcp-server/kobo-tasks-handlers.js +28 -0
  4. package/dist/mcp-server/kobo-tasks-server.js +22 -1
  5. package/dist/server/db/migrations.js +13 -0
  6. package/dist/server/db/schema.js +1 -0
  7. package/dist/server/index.js +11 -3
  8. package/dist/server/routes/workspaces.js +21 -18
  9. package/dist/server/services/agent/engines/claude-code/engine.js +66 -5
  10. package/dist/server/services/agent/engines/claude-code/options-builder.js +1 -0
  11. package/dist/server/services/agent/orchestrator.js +15 -12
  12. package/dist/server/services/auto-loop-service.js +1 -0
  13. package/dist/server/services/templates-service.js +1 -0
  14. package/dist/server/services/workspace-service.js +2 -0
  15. package/package.json +1 -1
  16. package/src/client/dist/spa/assets/{ActivityFeed-DWLJtRrK.js → ActivityFeed-BMgs5z9z.js} +1 -1
  17. package/src/client/dist/spa/assets/{ChangelogPage-n_42Jqdm.js → ChangelogPage-jf_31RZp.js} +1 -1
  18. package/src/client/dist/spa/assets/{ClosePopup-DEM04O_b.js → ClosePopup-C1A60wce.js} +1 -1
  19. package/src/client/dist/spa/assets/CreatePage-BBOwKtLV.js +2 -0
  20. package/src/client/dist/spa/assets/CreatePage-BmMaRpPY.css +1 -0
  21. package/src/client/dist/spa/assets/{DiffViewer-8e6e5RC4.js → DiffViewer-CVYtBqC8.js} +3 -3
  22. package/src/client/dist/spa/assets/{HealthPage-CViMfsRz.js → HealthPage-_2qS3z3n.js} +1 -1
  23. package/src/client/dist/spa/assets/{MainLayout-x_uEqHwb.js → MainLayout-Dk9ilieA.js} +2 -2
  24. package/src/client/dist/spa/assets/{QBtnToggle-CnP9Tf3U.js → QBtnToggle-CtKvYj1o.js} +1 -1
  25. package/src/client/dist/spa/assets/{QChip-DFLtGMWP.js → QChip-CjN_RB2y.js} +1 -1
  26. package/src/client/dist/spa/assets/{QExpansionItem-DiMxYRgY.js → QExpansionItem-Dka3X1IN.js} +1 -1
  27. package/src/client/dist/spa/assets/{QScrollArea-DDktnSkh.js → QScrollArea-Mwe2ihwC.js} +1 -1
  28. package/src/client/dist/spa/assets/{QSelect-BS5KsK_6.js → QSelect-jjUTbbns.js} +1 -1
  29. package/src/client/dist/spa/assets/{QTooltip-BJ3q2ddB.js → QTooltip-B9mxxMEM.js} +1 -1
  30. package/src/client/dist/spa/assets/{SearchPage-C-9dypx7.js → SearchPage-De3lLcr6.js} +1 -1
  31. package/src/client/dist/spa/assets/{SettingsPage-5ox4DLxS.js → SettingsPage-Dge9J6e_.js} +1 -1
  32. package/src/client/dist/spa/assets/{TouchPan-DMRF6FQN.js → TouchPan-BGeQ1z7O.js} +1 -1
  33. package/src/client/dist/spa/assets/WorkspacePage-B8jyN4oW.js +4 -0
  34. package/src/client/dist/spa/assets/{WorkspacePage-axYxoa9m.css → WorkspacePage-CVkUPM-N.css} +1 -1
  35. package/src/client/dist/spa/assets/{build-path-tree-CE8-gPul.js → build-path-tree-CQkUkFK4.js} +1 -1
  36. package/src/client/dist/spa/assets/{cssMode-B4pMjeW3.js → cssMode-CGcsO0OQ.js} +1 -1
  37. package/src/client/dist/spa/assets/{documents-DeTIiQsc.js → documents-5sWEpISn.js} +1 -1
  38. package/src/client/dist/spa/assets/{editor.api2-BU25jjFv.js → editor.api2-DNjj3Vc0.js} +1 -1
  39. package/src/client/dist/spa/assets/{editor.main-189xz8QD.js → editor.main-DpxxCkC7.js} +2 -2
  40. package/src/client/dist/spa/assets/{expand-template-CbLIIqEY.js → expand-template-Bq1mopti.js} +1 -1
  41. package/src/client/dist/spa/assets/{formatters-C_mbKnk8.js → formatters-VhgPXQNa.js} +1 -1
  42. package/src/client/dist/spa/assets/{freemarker2-BPzSVCkB.js → freemarker2-BVvp9ac3.js} +1 -1
  43. package/src/client/dist/spa/assets/{handlebars-DdssNach.js → handlebars-CmulgLgY.js} +1 -1
  44. package/src/client/dist/spa/assets/{html-D1Q4zaVk.js → html-kmOuarw4.js} +1 -1
  45. package/src/client/dist/spa/assets/{htmlMode-CMF_eZU-.js → htmlMode-Drc_ueTa.js} +1 -1
  46. package/src/client/dist/spa/assets/i18n-EIgL_1Id.js +1 -0
  47. package/src/client/dist/spa/assets/{index-D52x_y0L.js → index-BKXtMMl8.js} +7 -7
  48. package/src/client/dist/spa/assets/{javascript-BTBpKpf6.js → javascript-BapMfe_r.js} +1 -1
  49. package/src/client/dist/spa/assets/{jsonMode-CUGtCK30.js → jsonMode-CC4B535-.js} +1 -1
  50. package/src/client/dist/spa/assets/{kobo-commands-B6Dv8R07.js → kobo-commands-Bz-cjyHQ.js} +1 -1
  51. package/src/client/dist/spa/assets/{layout-B_HUD5tM.js → layout-C4xmHWJM.js} +1 -1
  52. package/src/client/dist/spa/assets/{liquid-CMWxqOEm.js → liquid-BDE8v2lj.js} +1 -1
  53. package/src/client/dist/spa/assets/{lspLanguageFeatures-BLf0RVkp.js → lspLanguageFeatures-Cy9S4aKY.js} +1 -1
  54. package/src/client/dist/spa/assets/{mdx-CN8Vsu32.js → mdx-CyNdhKZN.js} +1 -1
  55. package/src/client/dist/spa/assets/{monaco.contribution-xh9ndmsX.js → monaco.contribution-DbqInEyL.js} +2 -2
  56. package/src/client/dist/spa/assets/{network-auth-uiPlm6YP.js → network-auth-0eEgS22h.js} +1 -1
  57. package/src/client/dist/spa/assets/{project-color-DfN85oj1.js → project-color-DdAtoeU2.js} +1 -1
  58. package/src/client/dist/spa/assets/{python-Dffg69Yt.js → python-CKjYYxee.js} +1 -1
  59. package/src/client/dist/spa/assets/{razor-BKmItEtg.js → razor-CvTe87xk.js} +1 -1
  60. package/src/client/dist/spa/assets/{render-chat-markdown-CmFoGD9u.js → render-chat-markdown-BjVGm1Vi.js} +1 -1
  61. package/src/client/dist/spa/assets/{tsMode-Hw0nlE4U.js → tsMode-CeJZQNU_.js} +1 -1
  62. package/src/client/dist/spa/assets/{typescript-C1sdOzev.js → typescript-ReN8Og3R.js} +1 -1
  63. package/src/client/dist/spa/assets/{use-onboarding-66aKJfag.js → use-onboarding-BnFTwLyx.js} +1 -1
  64. package/src/client/dist/spa/assets/{use-quasar-CjzohfOc.js → use-quasar-DzB1x4Fl.js} +1 -1
  65. package/src/client/dist/spa/assets/{workers-j9N8klvU.js → workers-BUy4rUs5.js} +1 -1
  66. package/src/client/dist/spa/assets/{xml-DhjD8NFX.js → xml-DzIIIusY.js} +1 -1
  67. package/src/client/dist/spa/assets/{yaml-Cf8KmnUN.js → yaml-2OnB0-yv.js} +1 -1
  68. package/src/client/dist/spa/index.html +1 -1
  69. package/src/mcp-server/kobo-tasks-handlers.ts +44 -0
  70. package/src/mcp-server/kobo-tasks-server.ts +26 -0
  71. package/src/client/dist/spa/assets/CreatePage-DfMdB1Jr.js +0 -2
  72. package/src/client/dist/spa/assets/CreatePage-Slheqpp6.css +0 -1
  73. package/src/client/dist/spa/assets/WorkspacePage-82_R1AUr.js +0 -4
  74. package/src/client/dist/spa/assets/i18n-HeO_z76f.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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.9.6
8
+
9
+ - feat(chat): inject a queued message into a running Claude session, with a
10
+ session-scoped queue and explicit server acceptance
11
+ - feat(sessions): persist the model used by each agent session and display it
12
+ when it differs from the workspace default
13
+ - fix(auto-loop): apply the brainstorming model before the initial session
14
+ starts; add a separate brainstorming reasoning-effort selector
15
+ - feat(mcp): let agents read paginated user/agent conversation history across
16
+ workspace sessions as CSV, with optional session filtering
17
+ - fix(chat): start a fresh session automatically when a historical session
18
+ cannot be resumed
19
+ - ui(create): clarify auto-loop, session-mode, brainstorming, model and
20
+ reasoning-effort controls; remember reasoning effort per model locally
21
+ - docs: document the Claude queued-message behavior
22
+
7
23
  ## 1.9.5
8
24
 
9
25
  - fix(tests): make macOS tests portable (#16)
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.
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.
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)
@@ -441,6 +441,34 @@ export function getSessionUsageHandler(db, workspaceId) {
441
441
  currentSession: { sessionId: currentSessionId, ...current },
442
442
  };
443
443
  }
444
+ function csvCell(value) {
445
+ return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
446
+ }
447
+ /** Read the user/agent conversation history as paginated CSV. */
448
+ export function readWorkspaceEventsCsvHandler(db, workspaceId, options = {}) {
449
+ const safeLimit = Math.min(Math.max(Math.floor(options.limit ?? 100), 1), 500);
450
+ const safeOffset = Math.max(Math.floor(options.offset ?? 0), 0);
451
+ const sessionFilter = options.sessionId ? ' AND session_id = ?' : '';
452
+ const params = options.sessionId ? [workspaceId, options.sessionId] : [workspaceId];
453
+ const conversationFilter = " AND (type = 'user:message' OR (type = 'agent:event' AND json_extract(payload, '$.kind') = 'message:text'))";
454
+ const rows = db
455
+ .prepare(`SELECT session_id, type, payload, created_at FROM ws_events WHERE workspace_id = ?${sessionFilter}${conversationFilter} ORDER BY rowid ASC LIMIT ? OFFSET ?`)
456
+ .all(...params, safeLimit, safeOffset);
457
+ const total = db
458
+ .prepare(`SELECT COUNT(*) AS count FROM ws_events WHERE workspace_id = ?${sessionFilter}${conversationFilter}`)
459
+ .get(...params).count;
460
+ const lines = [
461
+ ['created_at', 'session_id', 'type', 'payload'],
462
+ ...rows.map((r) => [r.created_at, r.session_id ?? '', r.type, r.payload]),
463
+ ];
464
+ return {
465
+ csv: lines.map((row) => row.map(csvCell).join(',')).join('\n'),
466
+ eventCount: rows.length,
467
+ totalEvents: total,
468
+ offset: safeOffset,
469
+ nextOffset: safeOffset + rows.length < total ? safeOffset + rows.length : null,
470
+ };
471
+ }
444
472
  // ── Crons ────────────────────────────────────────────────────────────────────
445
473
  /**
446
474
  * List every cron currently armed for a workspace.
@@ -4,7 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { getDb } from '../server/db/index.js';
6
6
  import { runMigrations } from '../server/db/migrations.js';
7
- import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
7
+ import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, readWorkspaceEventsCsvHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
8
8
  const workspaceId = process.env.KOBO_WORKSPACE_ID;
9
9
  const dbPath = process.env.KOBO_DB_PATH;
10
10
  const settingsPath = process.env.KOBO_SETTINGS_PATH;
@@ -414,6 +414,20 @@ const WORKSPACE_SCOPED_TOOLS = [
414
414
  inputSchema: { type: 'object', properties: {}, required: [] },
415
415
  annotations: { readOnlyHint: true, openWorldHint: false },
416
416
  },
417
+ {
418
+ name: 'read_workspace_events_csv',
419
+ description: 'Read the user/agent conversation history for THIS workspace as paginated CSV. Use it to recover context from prior sessions without loading the whole history at once. Optionally filter to one session_id. Read-only; cannot access another workspace.',
420
+ inputSchema: {
421
+ type: 'object',
422
+ properties: {
423
+ session_id: { type: 'string', description: 'Optional Kōbō session id to restrict the history.' },
424
+ offset: { type: 'number', description: 'Zero-based offset for pagination, default 0.' },
425
+ limit: { type: 'number', description: 'Messages to return, default 100, max 500.' },
426
+ },
427
+ required: [],
428
+ },
429
+ annotations: { readOnlyHint: true, openWorldHint: false },
430
+ },
417
431
  {
418
432
  name: 'schedule_wakeup',
419
433
  description: 'WHEN: use this whenever you would otherwise WAIT for or POLL a long-running task (test suite, CI, build, deploy, or external state) — never block your turn with sleep/poll loops, and never end a turn merely "waiting" on a background job, because the session goes idle and will not resume on its own. CALL to schedule a follow-up turn on THIS workspace after a delay. End the current turn normally; once it finishes and the workspace is idle, Kōbō waits `delaySeconds`, then resumes the same conversation by injecting `prompt` as the next user message. The wakeup is scoped to the current workspace and resumes its latest session — you cannot target another workspace or another session. If a turn is still active when the timer fires, the wakeup is skipped (status: `session-active`). Replaces any previously pending wakeup on this workspace. Delay is clamped to [60, 21600] seconds (1min to 6h). Prefer this over the built-in `ScheduleWakeup` tool — it is the SDK-supported entry point.',
@@ -761,6 +775,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
761
775
  if (name === 'get_session_usage') {
762
776
  return ok(getSessionUsageHandler(db, workspaceId));
763
777
  }
778
+ if (name === 'read_workspace_events_csv') {
779
+ return ok(readWorkspaceEventsCsvHandler(db, workspaceId, {
780
+ sessionId: typeof a.session_id === 'string' ? a.session_id : undefined,
781
+ offset: typeof a.offset === 'number' ? a.offset : undefined,
782
+ limit: typeof a.limit === 'number' ? a.limit : undefined,
783
+ }));
784
+ }
764
785
  if (name === 'schedule_wakeup') {
765
786
  const delaySeconds = a.delaySeconds;
766
787
  const prompt = a.prompt;
@@ -407,6 +407,19 @@ export const migrations = [
407
407
  }
408
408
  },
409
409
  },
410
+ {
411
+ version: 31,
412
+ name: 'add-agent-session-model',
413
+ migrate: (db) => {
414
+ const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='agent_sessions'").get();
415
+ if (!table)
416
+ return;
417
+ const cols = db.prepare('PRAGMA table_info(agent_sessions)').all();
418
+ if (!cols.some((c) => c.name === 'model')) {
419
+ db.prepare('ALTER TABLE agent_sessions ADD COLUMN model TEXT').run();
420
+ }
421
+ },
422
+ },
410
423
  ];
411
424
  /** Current schema version — always equals the highest migration version. */
412
425
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -58,6 +58,7 @@ export function initSchema(db) {
58
58
  pid INTEGER,
59
59
  engine_session_id TEXT,
60
60
  status TEXT NOT NULL DEFAULT 'running',
61
+ model TEXT,
61
62
  started_at TEXT NOT NULL,
62
63
  ended_at TEXT,
63
64
  name TEXT
@@ -321,10 +321,16 @@ setMessageHandler((type, payload) => {
321
321
  // never steal the tagging.
322
322
  const activeSession = getActiveSession(p.workspaceId);
323
323
  const sessionTag = p.sessionId ?? activeSession?.id ?? undefined;
324
- // Persist user message so it survives page refresh
325
- emit(p.workspaceId, 'user:message', { content: p.content, sender: 'user' }, sessionTag);
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);
326
328
  try {
327
- sendMessage(p.workspaceId, p.content);
329
+ sendMessage(p.workspaceId, p.content, p.sessionId);
330
+ if (p.force) {
331
+ emit(p.workspaceId, 'user:message', { content: p.content, sender: 'user' }, sessionTag);
332
+ emitEphemeral(p.workspaceId, 'chat:accepted', { sessionId: p.sessionId });
333
+ }
328
334
  }
329
335
  catch (err) {
330
336
  const msg = err instanceof Error ? err.message : String(err);
@@ -332,6 +338,8 @@ setMessageHandler((type, payload) => {
332
338
  // (stdin closed, process dead mid-write, etc.) should surface to the
333
339
  // logs instead of silently respawning a fresh agent.
334
340
  if (!msg.includes('No agent running')) {
341
+ if (p.force)
342
+ emitEphemeral(p.workspaceId, 'chat:rejected', { sessionId: p.sessionId, message: msg });
335
343
  console.error(`[ws] chat:message failed for workspace ${p.workspaceId}:`, err);
336
344
  return;
337
345
  }
@@ -311,6 +311,23 @@ app.post('/', migrationGuard, async (c) => {
311
311
  engine: body.engine,
312
312
  ...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
313
313
  });
314
+ // Enable auto-loop before starting the initial brainstorming session so
315
+ // its model override is available when selecting that session's model.
316
+ if (body.autoLoop === true) {
317
+ const notionProducedTasks = body.notionUrl !== undefined &&
318
+ notionContent != null &&
319
+ notionContent.todos.length > 0 &&
320
+ notionContent.gherkinFeatures.length > 0;
321
+ const sessionMode = body.autoLoopSessionMode === 'continuous' ? 'continuous' : 'per_task';
322
+ const db = getDb();
323
+ db.prepare('UPDATE workspaces SET auto_loop = 1, auto_loop_ready = ?, auto_loop_session_mode = ? WHERE id = ?').run(notionProducedTasks ? 1 : 0, sessionMode, workspace.id);
324
+ workspace = workspaceService.getWorkspace(workspace.id) ?? workspace;
325
+ // Emit events so the frontend refreshes autoLoopStates without F5.
326
+ wsService.emitEphemeral(workspace.id, 'autoloop:enabled', {});
327
+ if (notionProducedTasks) {
328
+ wsService.emitEphemeral(workspace.id, 'autoloop:ready-flipped', {});
329
+ }
330
+ }
314
331
  // Auto-tag the workspace based on its creation source — `notion` when
315
332
  // imported from a Notion page, `sentry` when bootstrapped from a Sentry
316
333
  // issue URL. Pre-seeded in the global tag catalogue via migration v9.
@@ -784,7 +801,10 @@ Once the brainstorming + planning steps above are complete and you have a saved
784
801
  // (see auto-loop-service.ts's spawnNextIteration) — brainstormModel
785
802
  // is consumed exactly once, here.
786
803
  const initialSessionModel = workspace.autoLoop && workspace.brainstormModel ? workspace.brainstormModel : workspace.model;
787
- const agent = agentManager.startAgent(workspace.id, worktreePath, brainstormPrompt, initialSessionModel, false, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
804
+ const initialSessionEffort = body.autoLoop === true && body.brainstormReasoningEffort
805
+ ? body.brainstormReasoningEffort
806
+ : workspace.reasoningEffort;
807
+ const agent = agentManager.startAgent(workspace.id, worktreePath, brainstormPrompt, initialSessionModel, false, workspace.agentPermissionMode, undefined, initialSessionEffort);
788
808
  // Persist the initial prompt in the feed so it's visible in the chat,
789
809
  // tagged with the freshly created session id so the strict session filter shows it.
790
810
  wsService.emit(workspace.id, 'user:message', { content: brainstormPrompt, sender: 'system-prompt' }, agent.agentSessionId);
@@ -809,23 +829,6 @@ Once the brainstorming + planning steps above are complete and you have a saved
809
829
  }
810
830
  }
811
831
  }
812
- // Apply the auto-loop checkbox from CreatePage. Notion-imported workspaces
813
- // with both todos AND gherkin features auto-unlock `auto_loop_ready=1` —
814
- // they're considered good enough to drive the loop without grooming.
815
- if (body.autoLoop === true) {
816
- const notionProducedTasks = body.notionUrl !== undefined &&
817
- notionContent != null &&
818
- notionContent.todos.length > 0 &&
819
- notionContent.gherkinFeatures.length > 0;
820
- const sessionMode = body.autoLoopSessionMode === 'continuous' ? 'continuous' : 'per_task';
821
- const db = getDb();
822
- db.prepare('UPDATE workspaces SET auto_loop = 1, auto_loop_ready = ?, auto_loop_session_mode = ? WHERE id = ?').run(notionProducedTasks ? 1 : 0, sessionMode, workspace.id);
823
- // Emit events so the frontend refreshes autoLoopStates without F5.
824
- wsService.emitEphemeral(workspace.id, 'autoloop:enabled', {});
825
- if (notionProducedTasks) {
826
- wsService.emitEphemeral(workspace.id, 'autoloop:ready-flipped', {});
827
- }
828
- }
829
832
  // Return created workspace with tasks
830
833
  const workspaceWithTasks = workspaceService.getWorkspaceWithTasks(workspace.id);
831
834
  if (workingBranchAdjusted) {
@@ -14,6 +14,7 @@ import { buildStopHookOutput } from './stop-hook.js';
14
14
  * and auto-loop are not frozen forever.
15
15
  */
16
16
  const RESULT_DRAIN_TIMEOUT_MS = 15_000;
17
+ const MAX_PENDING_USER_MESSAGES = 20;
17
18
  function toMcpServersMap(specs) {
18
19
  if (!specs || specs.length === 0)
19
20
  return undefined;
@@ -27,6 +28,55 @@ function toMcpServersMap(specs) {
27
28
  }
28
29
  return map;
29
30
  }
31
+ class ClaudeInputStream {
32
+ messages;
33
+ waiting;
34
+ closed = false;
35
+ queuedForcedMessages = 0;
36
+ yieldedMessages = 0;
37
+ constructor(initialPrompt) {
38
+ this.messages = [{ message: this.toUserMessage(initialPrompt), forced: false }];
39
+ }
40
+ send(text) {
41
+ if (this.closed)
42
+ throw new Error('Claude input stream is closed');
43
+ if (this.queuedForcedMessages >= MAX_PENDING_USER_MESSAGES) {
44
+ throw new Error(`Claude input queue is full (max ${MAX_PENDING_USER_MESSAGES} messages)`);
45
+ }
46
+ this.messages.push({ message: this.toUserMessage(text), forced: true });
47
+ this.queuedForcedMessages++;
48
+ const wake = this.waiting;
49
+ this.waiting = undefined;
50
+ wake?.();
51
+ }
52
+ close() {
53
+ this.closed = true;
54
+ const wake = this.waiting;
55
+ this.waiting = undefined;
56
+ wake?.();
57
+ }
58
+ hasUnansweredInput(completedResponses) {
59
+ return this.queuedForcedMessages > 0 || this.yieldedMessages > completedResponses;
60
+ }
61
+ async *[Symbol.asyncIterator]() {
62
+ while (!this.closed || this.messages.length > 0) {
63
+ const next = this.messages.shift();
64
+ if (next) {
65
+ if (next.forced)
66
+ this.queuedForcedMessages--;
67
+ this.yieldedMessages++;
68
+ yield next.message;
69
+ continue;
70
+ }
71
+ await new Promise((resolve) => {
72
+ this.waiting = resolve;
73
+ });
74
+ }
75
+ }
76
+ toUserMessage(text) {
77
+ return { type: 'user', message: { role: 'user', content: text }, parent_tool_use_id: null };
78
+ }
79
+ }
30
80
  export function createClaudeCodeEngine() {
31
81
  return {
32
82
  id: 'claude-code',
@@ -143,7 +193,8 @@ export function createClaudeCodeEngine() {
143
193
  const explicitBinary = resolveClaudeBinaryPath();
144
194
  if (explicitBinary)
145
195
  sdkOptions.pathToClaudeCodeExecutable = explicitBinary;
146
- const q = query({ prompt: effectivePrompt, options: sdkOptions });
196
+ const inputStream = new ClaudeInputStream(effectivePrompt);
197
+ const q = query({ prompt: inputStream, options: sdkOptions });
147
198
  let discoveredSessionId;
148
199
  // A throwing onEvent handler (e.g. DB query against a closed connection
149
200
  // during async test teardown) must not escape as an unhandled rejection.
@@ -157,6 +208,7 @@ export function createClaudeCodeEngine() {
157
208
  };
158
209
  let iteratorRunning = false;
159
210
  let userInterrupted = false;
211
+ let completedResponses = 0;
160
212
  // Guard so the post-result drain watchdog and the natural loop exit (or
161
213
  // catch block) never both emit `session:ended` for the same run.
162
214
  let sessionEndedEmitted = false;
@@ -217,8 +269,14 @@ export function createClaudeCodeEngine() {
217
269
  discoveredSessionId = ev.engineSessionId;
218
270
  safeEmit(ev);
219
271
  }
220
- if (msg.type === 'result')
221
- armResultDrainWatchdog();
272
+ if (msg.type === 'result') {
273
+ completedResponses++;
274
+ // A queued forced message starts the next response on this same SDK stream.
275
+ if (!inputStream.hasUnansweredInput(completedResponses)) {
276
+ inputStream.close();
277
+ armResultDrainWatchdog();
278
+ }
279
+ }
222
280
  }
223
281
  // If the SDK ended with a `result.subtype === 'error_*'`, the
224
282
  // event-mapper already surfaced an `error` event but the iterator
@@ -272,6 +330,7 @@ export function createClaudeCodeEngine() {
272
330
  }
273
331
  pendingResolvers.clear();
274
332
  iteratorRunning = false;
333
+ inputStream.close();
275
334
  }
276
335
  })();
277
336
  const engineProcess = {
@@ -284,8 +343,10 @@ export function createClaudeCodeEngine() {
284
343
  isAlive() {
285
344
  return iteratorRunning;
286
345
  },
287
- sendMessage() {
288
- throw new Error('sendMessage not supported in single-shot SDK mode');
346
+ sendMessage(text) {
347
+ if (!iteratorRunning)
348
+ throw new Error('Claude agent is no longer running');
349
+ inputStream.send(text);
289
350
  },
290
351
  interrupt() {
291
352
  userInterrupted = true;
@@ -8,6 +8,7 @@ const KOBO_MCP_BRIEF = [
8
8
  '• `kobo__list_documents` / `kobo__read_document` to discover existing plans and specs under docs/ and .ai/thoughts/ before writing new ones.',
9
9
  '• `kobo__log_thought` to persist notable decisions to `.ai/thoughts/logs/<date>-<slug>.md`.',
10
10
  '• `kobo__search_codebase` to recall prior chat history (conversations, not source — use Grep for source).',
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.',
11
12
  '• `kobo__get_workspace_info` / `kobo__get_git_info` / `kobo__get_ticket` for context.',
12
13
  '• `kobo__set_workspace_status` when the mission is done / blocked / idle.',
13
14
  '• `kobo__schedule_wakeup` / `kobo__cancel_wakeup` to schedule (or cancel) a follow-up session — prefer these over the built-in `ScheduleWakeup` tool.',
@@ -341,7 +341,7 @@ function buildMcpServers(workspaceId) {
341
341
  },
342
342
  ];
343
343
  }
344
- function resolveSessionForResume(workspaceId, existingSessionId) {
344
+ function resolveSessionForResume(workspaceId, existingSessionId, model) {
345
345
  const db = getDb();
346
346
  let lastSession;
347
347
  if (existingSessionId) {
@@ -366,31 +366,31 @@ function resolveSessionForResume(workspaceId, existingSessionId) {
366
366
  .get(engineSessionId)?.id;
367
367
  const agentSessionId = existingId ?? nanoid();
368
368
  if (existingId) {
369
- db.prepare('UPDATE agent_sessions SET status = ?, ended_at = NULL WHERE id = ?').run('running', agentSessionId);
369
+ db.prepare('UPDATE agent_sessions SET status = ?, ended_at = NULL, model = ? WHERE id = ?').run('running', model ?? null, agentSessionId);
370
370
  }
371
371
  else {
372
- db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, status, engine_session_id, started_at) VALUES (?, ?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, 'running', engineSessionId, new Date().toISOString());
372
+ db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, status, engine_session_id, model, started_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, 'running', engineSessionId, model ?? null, new Date().toISOString());
373
373
  }
374
374
  return { agentSessionId, engineSessionId, existed: Boolean(existingId) };
375
375
  }
376
376
  // No engine session to resume — fall through to fresh session creation
377
377
  const agentSessionId = nanoid();
378
- db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, status, started_at) VALUES (?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, 'running', new Date().toISOString());
378
+ db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, status, model, started_at) VALUES (?, ?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, 'running', model ?? null, new Date().toISOString());
379
379
  return { agentSessionId, engineSessionId: undefined, existed: false };
380
380
  }
381
- function reuseOrCreateFreshSession(workspaceId, existingSessionId) {
381
+ function reuseOrCreateFreshSession(workspaceId, existingSessionId, model) {
382
382
  const db = getDb();
383
383
  if (existingSessionId) {
384
384
  const result = db
385
- .prepare('UPDATE agent_sessions SET status = ?, started_at = ?, ended_at = NULL WHERE id = ? AND workspace_id = ?')
386
- .run('running', new Date().toISOString(), existingSessionId, workspaceId);
385
+ .prepare('UPDATE agent_sessions SET status = ?, started_at = ?, ended_at = NULL, model = ? WHERE id = ? AND workspace_id = ?')
386
+ .run('running', new Date().toISOString(), model ?? null, existingSessionId, workspaceId);
387
387
  if (result.changes === 0) {
388
388
  throw new Error(`Agent session '${existingSessionId}' not found for workspace '${workspaceId}'`);
389
389
  }
390
390
  return existingSessionId;
391
391
  }
392
392
  const agentSessionId = nanoid();
393
- db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, status, started_at) VALUES (?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, 'running', new Date().toISOString());
393
+ db.prepare('INSERT INTO agent_sessions (id, workspace_id, pid, status, model, started_at) VALUES (?, ?, ?, ?, ?, ?)').run(agentSessionId, workspaceId, null, 'running', model ?? null, new Date().toISOString());
394
394
  return agentSessionId;
395
395
  }
396
396
  // ── Event handler ─────────────────────────────────────────────────────────────
@@ -580,7 +580,7 @@ function handleEvent(workspaceId, agentSessionId, ev) {
580
580
  sessionIds.set(workspaceId, ev.engineSessionId);
581
581
  try {
582
582
  const db = getDb();
583
- db.prepare('UPDATE agent_sessions SET engine_session_id = ? WHERE id = ?').run(ev.engineSessionId, agentSessionId);
583
+ db.prepare('UPDATE agent_sessions SET engine_session_id = ?, model = COALESCE(?, model) WHERE id = ?').run(ev.engineSessionId, ev.model ?? null, agentSessionId);
584
584
  }
585
585
  catch (err) {
586
586
  console.error('[orchestrator] Failed to persist engine session id:', err);
@@ -717,12 +717,12 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
717
717
  let agentSessionId;
718
718
  let resumeFromEngineSessionId;
719
719
  if (resume) {
720
- const r = resolveSessionForResume(workspaceId, existingSessionId);
720
+ const r = resolveSessionForResume(workspaceId, existingSessionId, model);
721
721
  agentSessionId = r.agentSessionId;
722
722
  resumeFromEngineSessionId = r.engineSessionId;
723
723
  }
724
724
  else {
725
- agentSessionId = reuseOrCreateFreshSession(workspaceId, existingSessionId);
725
+ agentSessionId = reuseOrCreateFreshSession(workspaceId, existingSessionId, model);
726
726
  }
727
727
  const settings = ws ? readEffectiveSettingsSafe(ws.projectPath) : readEffectiveSettingsSafe(workingDir);
728
728
  const options = {
@@ -836,11 +836,14 @@ export function stopAgent(workspaceId) {
836
836
  });
837
837
  }
838
838
  /** Write a user message to the running agent. */
839
- export function sendMessage(workspaceId, content) {
839
+ export function sendMessage(workspaceId, content, expectedSessionId) {
840
840
  const ctrl = controllers.get(workspaceId);
841
841
  if (!ctrl) {
842
842
  throw new Error(`No agent running for workspace '${workspaceId}'`);
843
843
  }
844
+ if (expectedSessionId && ctrl.agentSessionId !== expectedSessionId) {
845
+ throw new Error(`Session '${expectedSessionId}' is not active for workspace '${workspaceId}'`);
846
+ }
844
847
  wakeupService.cancel(workspaceId, 'user-message');
845
848
  ctrl.sendMessage(content);
846
849
  }
@@ -195,6 +195,7 @@ Throughout the steps below, keep the workspace description current via \`kobo__s
195
195
 
196
196
  Your job this iteration:
197
197
  1. Read \`kobo__list_tasks\` to see all tasks and the big picture.
198
+ When prior-session context is useful, call \`kobo__read_workspace_events_csv\` to read the paginated user/agent history for this workspace. Use a small \`limit\` and follow \`nextOffset\` only as needed.
198
199
  2. Implement the SINGLE task above and nothing else. Do not pick a different task.
199
200
  3. Run the project's quality checks (lint, typecheck, tests). Check \`.ai/.git-conventions.md\` for the exact commands if unclear.
200
201
  4. If checks fail, fix until they pass. If blocked, leave the task unchanged and explain in chat.
@@ -207,6 +207,7 @@ export const DEFAULT_TEMPLATES = [
207
207
  `- \`kobo__set_workspace_agent_description\` — short one-line summary shown in the sidebar; keep it current\n` +
208
208
  `- \`kobo__set_workspace_name\` — rename this workspace (the sidebar title); ONLY when the user explicitly asks for a rename, never on your own\n` +
209
209
  `- \`kobo__get_workspace_info\` / \`kobo__get_git_info\` — read workspace metadata + git state\n` +
210
+ `- \`kobo__read_workspace_events_csv\` — read paginated user/agent history from this workspace across sessions when prior context is useful; use \`limit\` + \`offset\` rather than loading everything\n` +
210
211
  `- \`kobo__cron_create\` / \`cron_delete\` / \`cron_list\` — schedule recurring or one-shot triggers on THIS workspace\n` +
211
212
  `- \`kobo__schedule_wakeup\` / \`cancel_wakeup\` — pause now and resume this same session after a one-off delay\n` +
212
213
  `- \`kobo__mark_auto_loop_ready\` — flip the loop into auto-execution after grooming\n\n` +
@@ -650,6 +650,7 @@ function mapSession(row) {
650
650
  pid: row.pid,
651
651
  engineSessionId: row.engine_session_id,
652
652
  status: row.status,
653
+ model: row.model,
653
654
  startedAt: row.started_at,
654
655
  endedAt: row.ended_at,
655
656
  name: row.name,
@@ -703,6 +704,7 @@ export function createIdleSession(workspaceId) {
703
704
  pid: null,
704
705
  engineSessionId: null,
705
706
  status: 'idle',
707
+ model: null,
706
708
  startedAt: now,
707
709
  endedAt: null,
708
710
  name: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.9.5",
3
+ "version": "1.9.6",
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-D52x_y0L.js";import{t as te}from"./QSpinnerDots-DIV68-cV.js";import{t as M}from"./QTooltip-BJ3q2ddB.js";import{t as N}from"./QExpansionItem-DiMxYRgY.js";import{t as ne}from"./QScrollArea-DDktnSkh.js";import{t as re}from"./documents-DeTIiQsc.js";import{i as ie,n as ae,t as P}from"./render-chat-markdown-CmFoGD9u.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-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(`
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-D52x_y0L.js";import{t as v}from"./render-chat-markdown-CmFoGD9u.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-DFLtGMWP.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-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 +1 @@
1
- import{P as e,b as t}from"./QIcon-C6E-oEjI.js";import{nt as n,rt as r}from"./index-D52x_y0L.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-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};