@loicngr/kobo 2.1.21 → 2.1.23

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 (93) hide show
  1. package/AGENTS.md +69 -4
  2. package/CHANGELOG.md +9 -0
  3. package/CONFIGURATION.md +112 -2
  4. package/dist/mcp-server/kobo-tasks-server.js +4 -4
  5. package/dist/server/index.js +21 -6
  6. package/dist/server/services/agent/engines/claude-code/engine.js +5 -2
  7. package/dist/server/services/agent/engines/codex/engine.js +3 -0
  8. package/dist/server/services/agent/orchestrator.js +2 -0
  9. package/dist/server/services/agent/turn-liveness.js +1 -1
  10. package/dist/server/services/dev-server-service.js +41 -13
  11. package/dist/server/services/lifecycle-hook-service.js +29 -2
  12. package/dist/server/services/settings-service.js +66 -0
  13. package/dist/server/utils/script-runner.js +3 -1
  14. package/dist/shared/launch-instructions.js +8 -4
  15. package/dist/shared/memory-tools.js +2 -2
  16. package/docker-compose.vps.yml +1 -1
  17. package/docs/deployment-vps.md +36 -10
  18. package/docs/getting-started.md +11 -0
  19. package/package.json +1 -1
  20. package/scripts/kobo-update +25 -13
  21. package/src/client/dist/pwa/assets/ActivityFeed-1OKX_g3B.js +2 -0
  22. package/src/client/dist/pwa/assets/{ActivityFeed-DdJvMzRI.css → ActivityFeed-CmAFv-2c.css} +1 -1
  23. package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-f4VDR5dd.js → AutoLoopFinalReviewSettings-VoA9uBw0.js} +1 -1
  24. package/src/client/dist/pwa/assets/{ChangelogPage-dEWpjTlp.js → ChangelogPage-DHl3hD3t.js} +1 -1
  25. package/src/client/dist/pwa/assets/{ComparisonPanel-Bwbqgvr4.js → ComparisonPanel-D6EKTtni.js} +1 -1
  26. package/src/client/dist/pwa/assets/{CreatePage-CCCCAhrx.js → CreatePage-CZzEOVVq.js} +1 -1
  27. package/src/client/dist/pwa/assets/{DashboardPage-3byhBITf.js → DashboardPage-DjLoUvjA.js} +1 -1
  28. package/src/client/dist/pwa/assets/{DiffViewer-Bj_NTfCF.js → DiffViewer-BQ6OEmoi.js} +3 -3
  29. package/src/client/dist/pwa/assets/{DocumentsPanel-xyLDV3T1.js → DocumentsPanel-yrQviqCd.js} +1 -1
  30. package/src/client/dist/pwa/assets/{DrawerToggleButton-pwBSstQP.js → DrawerToggleButton-D5qNeXnn.js} +1 -1
  31. package/src/client/dist/pwa/assets/{GitPanel-DvIdrkVw.js → GitPanel-DCXlhcAD.js} +3 -3
  32. package/src/client/dist/pwa/assets/{HealthPage-BSwhZQRo.js → HealthPage-COicuNQn.js} +1 -1
  33. package/src/client/dist/pwa/assets/{MainLayout-DZJ9BvZw.js → MainLayout-ZcExZTqC.js} +2 -2
  34. package/src/client/dist/pwa/assets/{MemoryPanel-ChR032zP.js → MemoryPanel-D73Qz9f7.js} +1 -1
  35. package/src/client/dist/pwa/assets/{MemoryProposalList-BRKSoS4K.js → MemoryProposalList-BGlq2DkD.js} +1 -1
  36. package/src/client/dist/pwa/assets/{QBtnDropdown-o3QH0Bb8.js → QBtnDropdown-CykBCmwa.js} +1 -1
  37. package/src/client/dist/pwa/assets/{QExpansionItem-BhFHiyq9.js → QExpansionItem-CAW9J1Uv.js} +1 -1
  38. package/src/client/dist/pwa/assets/{QScrollArea-BsNY6slB.js → QScrollArea-C_d-8eO7.js} +1 -1
  39. package/src/client/dist/pwa/assets/{QSelect-bEq3g_io.js → QSelect-C1AN93yA.js} +1 -1
  40. package/src/client/dist/pwa/assets/{QTooltip-xYZ6KNu2.js → QTooltip-C9qaLW_h.js} +1 -1
  41. package/src/client/dist/pwa/assets/{SchedulePanel-DIFv_FTY.js → SchedulePanel-CQpzDhq7.js} +1 -1
  42. package/src/client/dist/pwa/assets/{SearchPage-BqTBIt5E.js → SearchPage-fZlSSEu_.js} +1 -1
  43. package/src/client/dist/pwa/assets/{SettingsPage-BRvUAZ5r.css → SettingsPage-BCOF0ssp.css} +1 -1
  44. package/src/client/dist/pwa/assets/SettingsPage-eyJZxeCJ.js +9 -0
  45. package/src/client/dist/pwa/assets/{SplitWorkspacePage-D6r2pl5g.js → SplitWorkspacePage-CfeO6TZb.js} +1 -1
  46. package/src/client/dist/pwa/assets/{TerminalPanel-CladK9Vv.js → TerminalPanel-CfDZOteT.js} +1 -1
  47. package/src/client/dist/pwa/assets/{ToolCallItem-CdzEME4w.js → ToolCallItem-CHE6tylq.js} +2 -2
  48. package/src/client/dist/pwa/assets/{TouchPan-B4gQPwGN.js → TouchPan-Csev_qU6.js} +1 -1
  49. package/src/client/dist/pwa/assets/{TourReplayButton-B0j6AZwU.js → TourReplayButton-Crr7tF1b.js} +1 -1
  50. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-CeVhoidC.js → WorkspaceAttentionLabels-DpUZOiJv.js} +1 -1
  51. package/src/client/dist/pwa/assets/{WorkspacePage-w2bF8wTd.css → WorkspacePage-Bk-WWOYM.css} +1 -1
  52. package/src/client/dist/pwa/assets/{WorkspacePage-CK5XCuG_.js → WorkspacePage-_QSKLO42.js} +3 -3
  53. package/src/client/dist/pwa/assets/{browser-B8tNScg1.js → browser-o7ftkZex.js} +1 -1
  54. package/src/client/dist/pwa/assets/{build-path-tree-kmVAcvs_.js → build-path-tree-D6dZQpIb.js} +1 -1
  55. package/src/client/dist/pwa/assets/{custom-sounds-CJwd-8TL.js → custom-sounds-DVX9zCET.js} +1 -1
  56. package/src/client/dist/pwa/assets/{de-BaO6kbdO.js → de-DOE4H_Jf.js} +2 -2
  57. package/src/client/dist/pwa/assets/{editor.api-CgbPVW9s.js → editor.api-C8TYnUXm.js} +1 -1
  58. package/src/client/dist/pwa/assets/{efforts-y7Wf_Rgd.js → efforts-DfsG0_o0.js} +1 -1
  59. package/src/client/dist/pwa/assets/{es-DTvSsbD0.js → es-BW57JPlm.js} +2 -2
  60. package/src/client/dist/pwa/assets/{expand-template-BFbbjj7X.js → expand-template-CJZlWMfI.js} +1 -1
  61. package/src/client/dist/pwa/assets/{format-BETfc4z9.js → format-DOCqk2Dn.js} +1 -1
  62. package/src/client/dist/pwa/assets/{formatters-C22K9vlC.js → formatters-BFocC6LJ.js} +1 -1
  63. package/src/client/dist/pwa/assets/{fr-BH7V66Yz.js → fr-DnmIKHaQ.js} +2 -2
  64. package/src/client/dist/pwa/assets/{freemarker2-DiRVvp9O.js → freemarker2-CfME4WOK.js} +1 -1
  65. package/src/client/dist/pwa/assets/{handlebars-BLYRroSQ.js → handlebars-uvKdDnJl.js} +1 -1
  66. package/src/client/dist/pwa/assets/{html-DAW2vteY.js → html-BrEPgLuD.js} +1 -1
  67. package/src/client/dist/pwa/assets/i18n-BmTCjikk.js +1 -0
  68. package/src/client/dist/pwa/assets/{index-CbyHfF8I.js → index-z1Oh2-I_.js} +4 -4
  69. package/src/client/dist/pwa/assets/{it-DZGbkkoy.js → it-DSrEQgZQ.js} +2 -2
  70. package/src/client/dist/pwa/assets/{javascript-Bb5J8n0k.js → javascript-KH2xP4lQ.js} +1 -1
  71. package/src/client/dist/pwa/assets/{liquid-Dqx3adnJ.js → liquid-D08TlsVs.js} +1 -1
  72. package/src/client/dist/pwa/assets/{mdx-D_7igLV3.js → mdx-y74A51cS.js} +1 -1
  73. package/src/client/dist/pwa/assets/{monaco-contributions-C-0CbrAy.js → monaco-contributions-Cx8iaAhd.js} +1 -1
  74. package/src/client/dist/pwa/assets/{monaco.contribution-c6JPBEZI.js → monaco.contribution-CFq0guCl.js} +2 -2
  75. package/src/client/dist/pwa/assets/{network-auth-BE4qbvWm.js → network-auth-DnGOOzCV.js} +1 -1
  76. package/src/client/dist/pwa/assets/{notify-theme-BVFBBGrJ.js → notify-theme-B9lUAgQl.js} +1 -1
  77. package/src/client/dist/pwa/assets/{project-color-CHOR2jB5.js → project-color-TgENPFLi.js} +1 -1
  78. package/src/client/dist/pwa/assets/{python-Dqubw1F5.js → python-Cy3bAVPq.js} +1 -1
  79. package/src/client/dist/pwa/assets/{razor-BQphc9Kt.js → razor-gD9QOuiV.js} +1 -1
  80. package/src/client/dist/pwa/assets/{typescript-Dep00l0X.js → typescript-CEItY0bH.js} +1 -1
  81. package/src/client/dist/pwa/assets/{use-search-index-status-CQHHvHwM.js → use-search-index-status-BtJ0pOy9.js} +1 -1
  82. package/src/client/dist/pwa/assets/{use-tours-IVbXTmz2.js → use-tours-COLDth2h.js} +2 -2
  83. package/src/client/dist/pwa/assets/{use-worktree-restore-B3s5xDGR.js → use-worktree-restore-CShbwpGy.js} +1 -1
  84. package/src/client/dist/pwa/assets/{workspace-templates-CyuTpztP.js → workspace-templates-BOMNQgBl.js} +1 -1
  85. package/src/client/dist/pwa/assets/{xml-BMS2dvaF.js → xml-BJvL5qcE.js} +1 -1
  86. package/src/client/dist/pwa/assets/{yaml-CYw92aic.js → yaml-DtkVRZaX.js} +1 -1
  87. package/src/client/dist/pwa/index.html +1 -1
  88. package/src/client/dist/pwa/sw.js +1 -1
  89. package/src/mcp-server/README.md +13 -0
  90. package/src/mcp-server/kobo-tasks-server.ts +5 -4
  91. package/src/client/dist/pwa/assets/ActivityFeed-DiUxnmGJ.js +0 -2
  92. package/src/client/dist/pwa/assets/SettingsPage-BLD0xWFB.js +0 -9
  93. package/src/client/dist/pwa/assets/i18n-DnuTmw3O.js +0 -1
@@ -3,12 +3,16 @@ import { getDb } from '../db/index.js';
3
3
  import { slugifyProjectName } from '../utils/project-slug.js';
4
4
  import { runScript } from '../utils/script-runner.js';
5
5
  import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
6
+ import { resolveInstance, sanitizeBranchName } from './dev-server-service.js';
6
7
  import { getEffectiveSettings, getGlobalSettings, getProjectSettings } from './settings-service.js';
8
+ import { listWorkspaces } from './workspace-service.js';
9
+ export const SHUTDOWN_SCRIPT_TIMEOUT_MS = 60_000;
7
10
  /** Which effective-settings key holds the script for each event. */
8
11
  const SCRIPT_KEY = {
9
12
  'session-ended': 'sessionEndedScript',
10
13
  'pr-merged': 'prMergedScript',
11
14
  'autoloop-disabled': 'autoLoopDisabledScript',
15
+ 'kobo-shutdown': 'shutdownScript',
12
16
  };
13
17
  function getRow(workspaceId) {
14
18
  const row = getDb()
@@ -33,7 +37,7 @@ export async function runLifecycleHook(event, workspaceId, extraEnv) {
33
37
  const script = getEffectiveSettings(row.project_path)[SCRIPT_KEY[event]];
34
38
  // Empty (or blank) means the user never wrote one. Hooks are opt-in: they
35
39
  // run arbitrary shell, so absence of configuration is absence of a hook.
36
- if (!script.trim())
40
+ if (!script?.trim())
37
41
  return null;
38
42
  const global = getGlobalSettings();
39
43
  const projectSettings = getProjectSettings(row.project_path);
@@ -51,19 +55,27 @@ export async function runLifecycleHook(event, workspaceId, extraEnv) {
51
55
  }
52
56
  return null;
53
57
  }
58
+ const instance = event === 'kobo-shutdown' ? resolveInstance(row.project_path, row.working_branch) : null;
54
59
  return await runScript({
55
60
  workspaceId,
56
61
  worktreePath,
57
62
  script,
58
- // Namespaced per event: three hooks streaming into one `hook:output` feed
63
+ // Namespaced per event: multiple hooks streaming into one `hook:output` feed
59
64
  // would be indistinguishable in the UI.
60
65
  eventPrefix: `hook:${event}`,
61
66
  tmpFileName: `.hook-${event}.tmp`,
67
+ ...(event === 'kobo-shutdown' ? { timeoutMs: SHUTDOWN_SCRIPT_TIMEOUT_MS } : {}),
62
68
  env: {
63
69
  workspaceName: row.name,
64
70
  branchName: row.working_branch,
65
71
  sourceBranch: row.source_branch,
66
72
  projectPath: row.project_path,
73
+ ...(event === 'kobo-shutdown'
74
+ ? {
75
+ projectName: instance?.projectName ?? '',
76
+ instanceName: instance?.instanceName ?? sanitizeBranchName(row.working_branch),
77
+ }
78
+ : {}),
67
79
  },
68
80
  extraEnv: { ...extraEnv, KOBO_HOOK_EVENT: event },
69
81
  });
@@ -73,6 +85,21 @@ export async function runLifecycleHook(event, workspaceId, extraEnv) {
73
85
  return null;
74
86
  }
75
87
  }
88
+ /** Called once by graceful shutdown, after stopping writers and before Docker teardown. */
89
+ export async function onKoboShutdown(signal, isAgentStillRunning) {
90
+ await Promise.all(listWorkspaces(true)
91
+ .filter((workspace) => !workspace.worktreePurgedAt)
92
+ .map(async (workspace) => {
93
+ if (isAgentStillRunning(workspace.id)) {
94
+ console.warn(`[lifecycle-hook] 'kobo-shutdown' skipped for '${workspace.id}': agent stop unconfirmed`);
95
+ return;
96
+ }
97
+ const result = await runLifecycleHook('kobo-shutdown', workspace.id, { KOBO_SHUTDOWN_SIGNAL: signal });
98
+ if (result && result.exitCode !== 0) {
99
+ console.error(`[lifecycle-hook] 'kobo-shutdown' failed for '${workspace.id}' with code ${result.exitCode}`);
100
+ }
101
+ }));
102
+ }
76
103
  /**
77
104
  * An agent session ended. Fires for every real end — clean finish, error, user
78
105
  * stop, watchdog kill — and lets the script tell them apart via
@@ -980,6 +980,36 @@ const settingsMigrations = [
980
980
  global.memoryMode = 'hybrid';
981
981
  },
982
982
  },
983
+ {
984
+ version: 64,
985
+ name: 'opt-in-agent-watchdogs',
986
+ migrate: ({ global }) => {
987
+ if (typeof global.agentWatchdogEnabled !== 'boolean')
988
+ global.agentWatchdogEnabled = false;
989
+ },
990
+ },
991
+ {
992
+ version: 65,
993
+ name: 'session-progress-visibility',
994
+ migrate: ({ global }) => {
995
+ if (typeof global.showProgressBars !== 'boolean')
996
+ global.showProgressBars = true;
997
+ },
998
+ },
999
+ {
1000
+ version: 66,
1001
+ name: 'shutdown-services-and-hooks',
1002
+ migrate: ({ global, projects }) => {
1003
+ if (typeof global.stopDevServersOnShutdown !== 'boolean')
1004
+ global.stopDevServersOnShutdown = true;
1005
+ if (typeof global.shutdownScript !== 'string')
1006
+ global.shutdownScript = '';
1007
+ for (const project of projects) {
1008
+ if (typeof project.shutdownScript !== 'string')
1009
+ project.shutdownScript = '';
1010
+ }
1011
+ },
1012
+ },
983
1013
  ];
984
1014
  /** Current settings schema version — always equals the highest migration version. */
985
1015
  export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
@@ -1028,6 +1058,7 @@ function defaultSettings() {
1028
1058
  archiveScript: '',
1029
1059
  changeSourceBranchScript: DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT,
1030
1060
  sessionEndedScript: '',
1061
+ shutdownScript: '',
1031
1062
  prMergedScript: '',
1032
1063
  autoLoopDisabledScript: '',
1033
1064
  editorCommand: '',
@@ -1035,6 +1066,9 @@ function defaultSettings() {
1035
1066
  terminalCommand: '',
1036
1067
  autoPurgeOnPrMerged: false,
1037
1068
  autoLoopMaxRetries: 5,
1069
+ agentWatchdogEnabled: false,
1070
+ showProgressBars: true,
1071
+ stopDevServersOnShutdown: true,
1038
1072
  awaitingUserReminderMinutes: 0,
1039
1073
  activityDigestEnabled: true,
1040
1074
  maxConcurrentAgents: 2,
@@ -1138,6 +1172,7 @@ function defaultProjectSettings(projectPath) {
1138
1172
  archiveScript: '',
1139
1173
  changeSourceBranchScript: '',
1140
1174
  sessionEndedScript: '',
1175
+ shutdownScript: '',
1141
1176
  prMergedScript: '',
1142
1177
  autoLoopDisabledScript: '',
1143
1178
  devServer: {
@@ -1183,7 +1218,17 @@ export function runSettingsMigrations(raw) {
1183
1218
  current.global.workflowPolicy = resolveWorkflowPolicy(current.global.workflowPolicy);
1184
1219
  if (!isMemoryMode(current.global.memoryMode))
1185
1220
  current.global.memoryMode = 'hybrid';
1221
+ if (typeof current.global.showProgressBars !== 'boolean')
1222
+ current.global.showProgressBars = true;
1223
+ if (typeof current.global.stopDevServersOnShutdown !== 'boolean')
1224
+ current.global.stopDevServersOnShutdown = true;
1225
+ if (typeof current.global.shutdownScript !== 'string')
1226
+ current.global.shutdownScript = '';
1227
+ if (typeof current.global.agentWatchdogEnabled !== 'boolean')
1228
+ current.global.agentWatchdogEnabled = false;
1186
1229
  for (const project of current.projects) {
1230
+ if (typeof project.shutdownScript !== 'string')
1231
+ project.shutdownScript = '';
1187
1232
  if (!isWorkflowPolicy(project.workflowPolicy))
1188
1233
  project.workflowPolicy = {};
1189
1234
  }
@@ -1456,6 +1501,7 @@ export function getEffectiveSettings(projectPath) {
1456
1501
  archiveScript: settings.global.archiveScript,
1457
1502
  changeSourceBranchScript: settings.global.changeSourceBranchScript,
1458
1503
  sessionEndedScript: settings.global.sessionEndedScript,
1504
+ shutdownScript: settings.global.shutdownScript,
1459
1505
  prMergedScript: settings.global.prMergedScript,
1460
1506
  autoLoopDisabledScript: settings.global.autoLoopDisabledScript,
1461
1507
  notionStatusProperty: settings.global.notionStatusProperty,
@@ -1486,6 +1532,7 @@ export function getEffectiveSettings(projectPath) {
1486
1532
  archiveScript: project.archiveScript || settings.global.archiveScript,
1487
1533
  changeSourceBranchScript: project.changeSourceBranchScript || settings.global.changeSourceBranchScript,
1488
1534
  sessionEndedScript: project.sessionEndedScript || settings.global.sessionEndedScript,
1535
+ shutdownScript: project.shutdownScript || settings.global.shutdownScript,
1489
1536
  prMergedScript: project.prMergedScript || settings.global.prMergedScript,
1490
1537
  autoLoopDisabledScript: project.autoLoopDisabledScript || settings.global.autoLoopDisabledScript,
1491
1538
  notionStatusProperty: settings.global.notionStatusProperty,
@@ -1535,6 +1582,18 @@ export function updateGlobalSettings(input) {
1535
1582
  delete data.autoLoopMaxRetries;
1536
1583
  }
1537
1584
  }
1585
+ if ('shutdownScript' in data && typeof data.shutdownScript !== 'string') {
1586
+ delete data.shutdownScript;
1587
+ }
1588
+ if ('stopDevServersOnShutdown' in data && typeof data.stopDevServersOnShutdown !== 'boolean') {
1589
+ delete data.stopDevServersOnShutdown;
1590
+ }
1591
+ if ('showProgressBars' in data && typeof data.showProgressBars !== 'boolean') {
1592
+ delete data.showProgressBars;
1593
+ }
1594
+ if ('agentWatchdogEnabled' in data && typeof data.agentWatchdogEnabled !== 'boolean') {
1595
+ delete data.agentWatchdogEnabled;
1596
+ }
1538
1597
  // 0 is a legitimate value here — it means "never remind me" — so the floor is
1539
1598
  // 0, not 1. The ceiling of a day keeps a typo from arming a reminder that
1540
1599
  // never fires and looks like a broken feature.
@@ -1591,6 +1650,7 @@ export function updateGlobalSettings(input) {
1591
1650
  'archiveScript',
1592
1651
  'changeSourceBranchScript',
1593
1652
  'sessionEndedScript',
1653
+ 'shutdownScript',
1594
1654
  'prMergedScript',
1595
1655
  'autoLoopDisabledScript',
1596
1656
  'editorCommand',
@@ -1598,6 +1658,9 @@ export function updateGlobalSettings(input) {
1598
1658
  'terminalCommand',
1599
1659
  'autoPurgeOnPrMerged',
1600
1660
  'autoLoopMaxRetries',
1661
+ 'agentWatchdogEnabled',
1662
+ 'showProgressBars',
1663
+ 'stopDevServersOnShutdown',
1601
1664
  'awaitingUserReminderMinutes',
1602
1665
  'activityDigestEnabled',
1603
1666
  'maxConcurrentAgents',
@@ -1793,6 +1856,8 @@ export function upsertProject(projectPath, data) {
1793
1856
  }
1794
1857
  if ('workflowPolicy' in data && !isWorkflowPolicy(data.workflowPolicy))
1795
1858
  throw new Error('Invalid workflowPolicy');
1859
+ if ('shutdownScript' in data && typeof data.shutdownScript !== 'string')
1860
+ throw new Error('Invalid shutdownScript');
1796
1861
  const allowedProjectKeys = [
1797
1862
  'workflowPolicy',
1798
1863
  'displayName',
@@ -1812,6 +1877,7 @@ export function upsertProject(projectPath, data) {
1812
1877
  'archiveScript',
1813
1878
  'changeSourceBranchScript',
1814
1879
  'sessionEndedScript',
1880
+ 'shutdownScript',
1815
1881
  'prMergedScript',
1816
1882
  'autoLoopDisabledScript',
1817
1883
  'devServer',
@@ -49,6 +49,8 @@ export function runScript(opts) {
49
49
  BRANCH_NAME: env?.branchName ?? '',
50
50
  SOURCE_BRANCH: env?.sourceBranch ?? '',
51
51
  PROJECT_PATH: env?.projectPath ?? '',
52
+ ...(env?.projectName !== undefined ? { PROJECT_NAME: env.projectName } : {}),
53
+ ...(env?.instanceName !== undefined ? { INSTANCE: env.instanceName } : {}),
52
54
  },
53
55
  stdio: ['ignore', 'pipe', 'pipe'],
54
56
  // Detached so this is the leader of its own process group: a script
@@ -74,7 +76,7 @@ export function runScript(opts) {
74
76
  proc.stdout?.destroy();
75
77
  proc.stderr?.destroy();
76
78
  wsService.emit(workspaceId, `${eventPrefix}:output`, {
77
- text: `[kobo] Script timed out after ${Math.round(timeoutMs / 60000)} minutes`,
79
+ text: `[kobo] Script timed out after ${timeoutMs < 60000 ? `${Math.round(timeoutMs / 1000)} seconds` : `${Math.round(timeoutMs / 60000)} minutes`}`,
78
80
  });
79
81
  // SIGKILL on the process group gives a running `git commit` no chance to
80
82
  // release its index lock. Name the file here, while the user is looking
@@ -11,7 +11,7 @@ export function buildKoboLaunchInstructions(context) {
11
11
  const restricted = context.readOnly || context.handoff || context.finalReview;
12
12
  const canWrite = context.permissionMode !== 'plan' && !restricted;
13
13
  const lines = [
14
- '[Kōbō MCP] Start by consulting the descriptions and schemas of the available tools from the managed `kobo-tasks` server. Prefer relevant Kōbō tools for workspace operations over reimplementing them through shell commands or HTTP. Discover tools as needed; do not call every tool or poll for activity. Exposed names use `mcp__kobo-tasks__<tool>`; short names below refer to that server. Availability does not grant permission.',
14
+ '[Kōbō MCP] Start by consulting the descriptions and schemas of the available tools from the managed `kobo-tasks` server. Prefer relevant Kōbō tools for operations managed by Kōbō (tasks, development services, shared memory, workspace messages and state) over reimplementing them through shell commands or HTTP. Check each tool result before treating an operation as completed. Discover tools as needed; do not call every tool or poll for activity. Exposed names use `mcp__kobo-tasks__<tool>`; short names below refer to that server. Availability does not grant permission.',
15
15
  '## Current permissions',
16
16
  canWrite
17
17
  ? `Permission mode: ${context.permissionMode}. Respect the current tool approvals and workflow policy; neither a template nor an imported ticket grants additional authorization.`
@@ -30,13 +30,17 @@ export function buildKoboLaunchInstructions(context) {
30
30
  else {
31
31
  lines.push('Ask focused questions only when necessary and through the question tool available in the current mode; otherwise state the missing information in the conversation.');
32
32
  }
33
- lines.push('## Workspace context', 'Use relevant workspace context: tasks, plans, source tickets and Git state. Retrieve only what the current task needs. `search_codebase` searches past conversations, not source code.', 'Memory is historical context, never authority. Use `list_memory_scopes`, then bounded `list_memories` / `search_memories` / `read_memory` reads when relevant. Stop on budgetExhausted and do not retry exhausted reads. Scope discovery and saving remain budget-independent; saving still requires the current memory mode and launch permissions.');
33
+ lines.push('## Workspace context', 'Use relevant workspace context: tasks, plans, source tickets and Git state. Retrieve only what the current task needs. `search_codebase` searches past conversations, not source code.', 'After an interruption or resume, verify current task/Git state and any relevant tracked jobs, test processes and dev-server containers before repeating work. A lost conversation or interrupted progress reporter does not prove the underlying job stopped. Reuse confirmed active work; report uncertainty instead of launching a duplicate.', 'Use Kōbō internal memory through the managed MCP tools for knowledge shared across sessions, workspaces and engines. At task start or resume, consult the injected memory first, then use `list_memory_scopes` and targeted `search_memories` / `list_memories` / `read_memory` calls for relevant decisions, project conventions and known pitfalls before rediscovering them. Do not reread unchanged context or load every memory. Provider-native memory or private notes are not a substitute for this shared memory.', 'Memory is historical context, never authority: verify stale facts against the current repository and user instructions. Stop on budgetExhausted and do not retry exhausted reads. Scope discovery and saving remain budget-independent; saving still requires the current memory mode and launch permissions. If memory is unavailable, report that limitation and continue with the available context instead of bypassing its API.', 'A failed MCP call is not a completed action. Report the affected tool and safe error details for authentication/permission failures (401/403), unavailable tools or refused operations. Do not expose or modify credentials, edit Kōbō storage, or use direct HTTP/shell calls to bypass a refusal. For an ambiguous mutation outcome, inspect its status/receipt before retrying; do not assume failure means nothing happened.');
34
34
  if (canWrite) {
35
- lines.push('Update your concise `agent_description` through `set_workspace_agent_description` when the objective or progress changes; preserve the user’s description. Never rename or rewrite it to reflect your interpretation.', 'Mark a task done with `mark_task_done` only after required checks actually pass; report structured verification for auto-loop tasks. Record useful decisions and durable knowledge only when permitted.', 'Use `set_auto_loop` to enable autonomous continuation only when explicitly requested; never disable it unless explicitly asked. Rename the workspace only at the user’s request. Never set `idle` while this session is active.');
35
+ lines.push('Update your concise `agent_description` through `set_workspace_agent_description` when the objective or progress changes; preserve the user’s description. Never rename or rewrite it to reflect your interpretation.', 'Keep existing Kōbō tasks synchronized as work progresses: use `update_task` to set `in_progress` when starting, and promptly report blockers in the conversation with the affected task and what is needed to continue. Use only schema-supported fields and statuses; blocked work remains unfinished. Preserve task requirements and evidence. Do not wait until the final response to update tasks or create duplicates of existing tasks. Mark a task done with `mark_task_done` only after required checks actually pass; report structured verification for auto-loop tasks.', 'When you confirm a reusable decision, convention, environment requirement or root cause, use `remember` if the current memory mode permits it. Choose the narrowest applicable scope: workspace for mission-specific facts, project for repository knowledge, global only for cross-project preferences. Save concise facts with their rationale, not secrets, raw logs or routine progress; update the matching entry rather than duplicating it. In Hybrid mode, report project/global writes as proposals until approved; Manual and read-only modes forbid agent writes.', 'Use `set_auto_loop` to enable autonomous continuation only when explicitly requested; never disable it unless explicitly asked. Rename the workspace only at the user’s request. Never set `idle` while this session is active.');
36
36
  }
37
- lines.push('Only one agent edits the worktree at a time. Parallel read-only analysis is allowed; wait for an editing subagent to finish before editing or running tests yourself. Pass current permissions and reporting requirements to every delegate.');
37
+ lines.push('## Development services', 'Kōbō manages the workspace dev server and its Docker services. Before tests or app checks that need those services (backend, frontend, integration or E2E), use `get_dev_server_status` and reuse the existing instance. Missing host dependencies are not a reason to launch another stack. Use the returned URL and container names, not guessed ports or names.', 'Read the project Makefile, scripts and relevant documentation to select the intended test command and runtime. Distinguish the Kōbō agent environment from the project service containers: if tests belong in a project container, run them there under current permissions. A PHP extension or other dependency missing in the agent environment alone is not evidence that the project is misconfigured, nor a reason to install global packages.', canWrite
38
+ ? 'When an authorized task needs these services and their status is stopped, use `start_dev_server`. For an explicitly requested stop or a necessary authorized restart, use `stop_dev_server`. Do not run lifecycle commands such as `make dev-docker`, `sh/dev-docker.sh`, `docker compose up/down` or `npm run dev` directly; Kōbō runs the configured commands and tracks their processes.'
39
+ : 'This read-only launch must not start, stop or restart services. Inspect available status/logs and report any unavailable test environment in the final response.', 'Use `get_dev_server_logs` to diagnose startup/runtime failures. If the tools are unavailable, the server is not_configured, or startup fails, report the blocker; do not bypass Kōbō with a shell launch. Test commands inside an existing container remain subject to the current permissions.', 'A running process or container is not proof that the service is ready or that tests passed. Before dependent tests, use the documented health check or a non-mutating request to the appropriate service URL. Bound readiness checks with a timeout and limited retries; if readiness remains unconfirmed, inspect logs and report the blocker rather than restarting repeatedly. Missing application readiness does not invalidate independent unit tests.');
40
+ lines.push('## Delegation and reporting', 'Only one agent edits the worktree at a time. Parallel read-only analysis is allowed; wait for an editing subagent to finish before editing or running tests yourself. Give each delegate its objective, exact worktree path, relevant user constraints, current permissions, intended test runtime and applicable Kōbō tools and reporting rules. Do not assume it inherits this context. A delegate without the required managed tools must ask its parent to coordinate the operation within the same permissions, not bypass Kōbō.');
38
41
  if (context.progressReporting)
39
42
  lines.push(PROGRESS_BAR_PROMPT);
43
+ lines.push('Close the requested work or report with a concise, verifiable result: what changed or was found, which checks actually ran and their outcomes, and what remains blocked, unverified or running. Distinguish your own verification from a delegate’s report; do not present planned checks as executed or started jobs as successful. Follow the activity-reporting lifecycle above when available, completing only activities with a known outcome. Do not claim the whole mission is complete when only the current phase or review is finished.');
40
44
  const continuation = [
41
45
  '## Long-running work and continuation',
42
46
  'Use the engine’s tracked background jobs and completion notifications when available; they may continue the live session. Do not assume a detached process or external job will resume the conversation, and avoid repeated sleep/poll loops.',
@@ -2,7 +2,7 @@ import { MEMORY_BODY_MAX_CHARS, MEMORY_PAGE_SIZE_MAX, MEMORY_TITLE_MAX_CHARS } f
2
2
  export const MEMORY_TOOL_DEFINITIONS = [
3
3
  {
4
4
  name: 'list_memory_scopes',
5
- description: 'List the global, project and workspace memory scopes available to this workspace.',
5
+ description: 'Discover the global, project and workspace scopes of Kōbō internal shared memory before targeted retrieval or permitted saving. Use returned scope IDs and generations; do not guess them. Scope discovery remains available when the retrieval budget is exhausted, but must not be polled.',
6
6
  inputSchema: {
7
7
  type: 'object',
8
8
  properties: {
@@ -77,7 +77,7 @@ export const MEMORY_TOOL_DEFINITIONS = [
77
77
  },
78
78
  {
79
79
  name: 'remember',
80
- description: 'Create or update a concise memory; current mode may deny, apply, or propose the change. Available even when the retrieval budget is exhausted; never echoes the saved body.',
80
+ description: 'Save a confirmed reusable decision, convention, environment requirement or root cause in Kōbō internal shared memory. Choose the narrowest applicable scope and update an existing matching entry instead of duplicating it. Do not store secrets, raw logs or routine progress. Current mode and launch permissions may deny, apply, or propose the change; a proposal still needs approval. Available even when the retrieval budget is exhausted; never echoes the saved body.',
81
81
  inputSchema: {
82
82
  type: 'object',
83
83
  properties: {
@@ -48,7 +48,7 @@ services:
48
48
  traefik.http.routers.kobo.tls.certresolver: ${TRAEFIK_CERTRESOLVER:-letsencrypt}
49
49
  traefik.http.services.kobo.loadbalancer.server.port: "3000"
50
50
  # No published HTTP or SSH ports. Traefik handles HTTP and WebSockets.
51
- stop_grace_period: 60s
51
+ stop_grace_period: 180s
52
52
  healthcheck:
53
53
  start_period: 60s
54
54
  logging:
@@ -351,6 +351,17 @@ An `npm` development process does not require the Docker socket. Development
351
351
  server URLs are separate from Kōbō's dashboard URL; configure their own reachable
352
352
  ports or proxy routes. See the [dev-server contract](../CONFIGURATION.md#dev-server).
353
353
 
354
+ To keep project Docker containers running during `bin/kobo-update`, disable
355
+ **Settings → Scripts → Stop development servers when Kōbō exits** before updating.
356
+ Use detached Docker services and retain the instance configuration in the mounted
357
+ base repository so Kōbō can rediscover their status/URL after restart. Local child
358
+ processes still stop. The optional global/per-project **Kōbō shutdown** script runs
359
+ once per existing workspace worktree (including archived ones), before automatic
360
+ Docker teardown, with a 1-minute timeout. It can explicitly stop/remove containers
361
+ regardless of the toggle. See [shutdown configuration and variables](../CONFIGURATION.md#kōbō-shutdown).
362
+ The VPS example and updater allow 180 seconds for a graceful stop; keep at least
363
+ that allowance in custom supervisors for shutdown hooks to finish.
364
+
354
365
  For project dependencies, private registry login, SSH tunnels, stop/cleanup commands,
355
366
  file-watcher troubleshooting and installing Claude/Codex extensions, follow
356
367
  [Projects, development servers and skills on a VPS](./vps-projects-and-skills.md).
@@ -563,9 +574,9 @@ It retrieves npm's `latest` version (or the explicit
563
574
  stable version), verifies the Git tag's commit and package version, builds a uniquely
564
575
  tagged image, ensures Compose, Buildx, netcat and the non-root launcher are installed and checks native modules. The old
565
576
  service stays available during the build. Plan for roughly 5–15 minutes without
566
- build cache; backup duration depends on the volume size.
577
+ build cache; backup duration depends only on the size of Kōbō's data directory.
567
578
 
568
- Once idle, it stops Kōbō, archives the complete `/root` volume, tests SQLite
579
+ Once idle, it stops Kōbō, archives only `/root/.config/kobo`, tests SQLite
569
580
  migrations/integrity on a copy, pins the new image directly in `compose.yaml`, and
570
581
  starts the service. It checks container health, the authenticated internal API,
571
582
  the version served through HTTPS, and public API rejection without a token. It
@@ -577,9 +588,12 @@ repository when you want to update the updater.
577
588
  ## Backups and failed updates
578
589
 
579
590
  Each update records its private log, `state.json`, previous Compose file,
580
- `home.tar.gz` and recovery instructions in `<deployment>/updates/<timestamp>-<id>/`.
581
- The archive contains credentials as well as application data; keep it private.
582
- A prepared-only build does not create a home backup. No old images, backups or
591
+ `kobo.tar.gz` and recovery instructions in `<deployment>/updates/<timestamp>-<id>/`.
592
+ The archive contains the whole `/root/.config/kobo` directory: SQLite database
593
+ and WAL/SHM sidecars when present, settings, templates, attachments and other Kōbō
594
+ data. It retains paths relative to `/root`. Settings can contain credentials;
595
+ keep the archive private. `state.json` records the backup scope and archive name.
596
+ A prepared-only build does not create a backup. No old images, backups or
583
597
  volumes are automatically removed; monitor disk space and retain off-host copies.
584
598
 
585
599
  An error before shutdown releases the reservation, including a lost acquisition
@@ -590,9 +604,12 @@ the reservation deliberately has no expiry that could reopen the race. Follow
590
604
  container and confirm its exit before restarting it. Restart clears the in-memory
591
605
  reservation. Never release it while a Docker stop could still be in progress.
592
606
 
593
- Repositories/worktrees stored outside `/root`, bind-mounted App keys/scripts and
594
- project database volumes are **not included** in `home.tar.gz`. Back them up
595
- separately along with `.env` and other deployment files. Never copy a live SQLite
607
+ Claude/Codex configuration and credentials, skills/plugins, caches, browser
608
+ downloads, repositories/worktrees, bind-mounted App keys/scripts and project
609
+ database volumes are **not included** in `kobo.tar.gz`. Files outside the Kōbō
610
+ directory remain in the existing volume; the updater neither archives nor deletes
611
+ them. Back up important files separately, along with `.env` and deployment files.
612
+ Never copy a live SQLite
596
613
  file as your only database backup; the updater stops Kōbō before archiving it.
597
614
 
598
615
  A build failure leaves the current service running. A backup or migration-test
@@ -600,8 +617,17 @@ failure restarts the previous version before new code touches production data.
600
617
  After a new-version launch, an error leaves data intact for diagnosis: an older
601
618
  image must not be blindly started against a migrated database, and a snapshot
602
619
  restore could discard messages or work resumed since launch. Read the generated
603
- `RECOVERY.md`; restore to a **new volume**, retaining the current one, if a full
604
- rollback is necessary. An interrupted update blocks subsequent updates until its
620
+ `RECOVERY.md`: for a rollback, stop Kōbō and clone the current volume into a **new
621
+ volume** so engine credentials and skills are retained. In the new volume only,
622
+ replace `.config/kobo` completely from `kobo.tar.gz`, preserving numeric ownership
623
+ and permissions, then use that volume with the previous image. Do not overlay an
624
+ old database onto newer WAL/SHM files. Keep the original volume. This restores
625
+ Kōbō data only, not engine state or project changes.
626
+
627
+ Older updater versions created `home.tar.gz` containing all of `/root`; use each
628
+ backup's own `RECOVERY.md`. Updating the Kōbō image does not update the operator
629
+ script: install the newer `scripts/kobo-update` to use the smaller backups.
630
+ An interrupted update blocks subsequent updates until its
605
631
  recorded state has been reviewed. Inspect logs locally; they may contain secrets.
606
632
 
607
633
  For troubleshooting, start with `docker compose ps`, the private update log and
@@ -56,6 +56,17 @@ Configure integrations only when you need them, in their Settings tabs. Notion a
56
56
 
57
57
  ## Existing installations
58
58
 
59
+ The **Show progress bars** switch in **Settings → Agents** is enabled by default.
60
+ Turn it off and save to hide activity indicators without interrupting their work
61
+ or tracking. Re-enabling it preserves individual dismissals and expiry times.
62
+
63
+ The **agent inactivity watchdogs** switch is at the top of **Settings → Agents**.
64
+ It is off by default, including after upgrading an existing installation, so a
65
+ long silent task is not stopped by Kōbō's engine inactivity timers. Enable it if
66
+ you want stalled sessions closed automatically. Changes apply on the next agent
67
+ start or resume. The Settings tour explains this option; see
68
+ [session watchdogs](../CONFIGURATION.md#session-watchdogs) for the deadlines.
69
+
59
70
  Upgrades preserve customized prompts, plugin selection, integration toggles, permission modes and resource limits. Existing installations retain legacy automatic Git workflow preferences; review them in Settings if you prefer manual authorization. Existing workspaces receive a snapshot of that historical policy, while new workspaces snapshot the effective global/project/create policy. Changing a global policy does not rewrite existing missions.
60
71
 
61
72
  Known shipped prompt text and retired sound selections are migrated; custom prompt text is preserved. Audio enablement and volume are retained. Take a complete backup before an upgrade; see [Troubleshooting](./troubleshooting.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "2.1.21",
3
+ "version": "2.1.23",
4
4
  "description": "Kōbō — local multi-workspace orchestrator for Claude Code and OpenAI Codex, with isolated Git worktrees, live review and optional automation.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -19,6 +19,8 @@ import uuid
19
19
  from datetime import datetime, timezone
20
20
 
21
21
  REPO = 'https://github.com/loicngr/kobo.git'
22
+ # Allow Kōbō's bounded shutdown hooks and subsequent dev-service teardown to finish.
23
+ STOP_GRACE_SECONDS = 180
22
24
  VERSION = re.compile(r'\d+\.\d+\.\d+')
23
25
  COMPOSE_ENV_KEYS = ('KOBO_DOMAIN', 'KOBO_PROJECTS_DIR', 'TRAEFIK_NETWORK',
24
26
  'TRAEFIK_ENTRYPOINT', 'TRAEFIK_CERTRESOLVER', 'KOBO_UID', 'KOBO_GID')
@@ -217,7 +219,7 @@ class Updater:
217
219
  'node', '-p', 'require("./package.json").version']))
218
220
  self.idle()
219
221
  self.run(['docker', 'compose', 'version'])
220
- used_kib = int(self.run(['docker', 'exec', self.container, 'du', '-sk', '/root']).split()[0])
222
+ used_kib = int(self.run(['docker', 'exec', self.container, 'du', '-sk', '/root/.config/kobo']).split()[0])
221
223
  # Sources/build layers plus archive + migration copy; compression is not assumed.
222
224
  required = 6 * 1024**3 + used_kib * 1024 * 2
223
225
  docker_root = self.run(['docker', 'info', '--format', '{{.DockerRootDir}}'])
@@ -288,12 +290,12 @@ class Updater:
288
290
  return image
289
291
 
290
292
  def backup(self):
291
- archive = self.job / 'home.tar.gz'
293
+ archive = self.job / 'kobo.tar.gz'
292
294
  with archive.open('xb') as f, self.log.open('ab') as err:
293
295
  try:
294
296
  subprocess.run(['docker', 'run', '--rm', '--network', 'none', '--read-only',
295
297
  '--mount', f'type=volume,src={self.volume},dst=/data,readonly',
296
- '--entrypoint', 'tar', self.old_image, '-C', '/data', '-czf', '-', '.'],
298
+ '--entrypoint', 'tar', self.old_image, '-C', '/data', '-czf', '-', './.config/kobo'],
297
299
  stdout=f, stderr=err, check=True, timeout=3600)
298
300
  f.flush()
299
301
  os.fsync(f.fileno())
@@ -360,6 +362,7 @@ class Updater:
360
362
  self.record('building', previous_image=old_tag, previous_digest=self.old_image,
361
363
  volume=self.volume, version=version, sha=sha,
362
364
  maintenance_owner=self.maintenance_owner,
365
+ backup_archive='kobo.tar.gz', backup_scope='/root/.config/kobo',
363
366
  backup_complete=False, new_version_start_attempted=False)
364
367
  self.recovery_notes()
365
368
  stopped = False
@@ -386,8 +389,9 @@ class Updater:
386
389
  self.maintenance('POST')
387
390
  self.record('stopping')
388
391
  stopped = True # Also recover if the stop command itself fails/interruption arrives.
389
- say('Stopping Kōbō, backing up /root and testing migrations on a database copy…')
390
- self.run(self.compose('stop', '-t', '60', 'kobo'))
392
+ say('Stopping Kōbō, backing up only /root/.config/kobo and testing migrations on a database copy…')
393
+ self.run(self.compose('stop', '-t', str(STOP_GRACE_SECONDS), 'kobo'),
394
+ timeout=STOP_GRACE_SECONDS + 30)
391
395
  info = json.loads(self.run(['docker', 'inspect', self.container]))[0]
392
396
  if info['State']['Running']:
393
397
  raise UpdateError('The container did not stop.')
@@ -404,7 +408,7 @@ class Updater:
404
408
  self.logged(self.compose('up', '-d', '--no-deps', '--wait', '--wait-timeout', '180', 'kobo'), timeout=240)
405
409
  self.verify(version)
406
410
  self.record('complete')
407
- say(f'Kōbō {version} is up to date. Backup: {self.job / "home.tar.gz"}')
411
+ say(f'Kōbō {version} is up to date. Backup: {self.job / "kobo.tar.gz"}')
408
412
  except BaseException:
409
413
  if stopped and not launching:
410
414
  # Only safe before any process from the new version has used production data.
@@ -413,7 +417,8 @@ class Updater:
413
417
  # Keep admission reserved until the old process is confirmed gone.
414
418
  info = json.loads(self.run(['docker', 'inspect', self.container]))[0]
415
419
  if info['State']['Running']:
416
- self.run(['docker', 'stop', '-t', '60', self.container])
420
+ self.run(['docker', 'stop', '-t', str(STOP_GRACE_SECONDS), self.container],
421
+ timeout=STOP_GRACE_SECONDS + 30)
417
422
  info = json.loads(self.run(['docker', 'inspect', self.container]))[0]
418
423
  if info['State']['Running']:
419
424
  raise UpdateError('Previous process shutdown is unconfirmed.')
@@ -446,9 +451,11 @@ Private log: `{self.job / 'update.log'}`. Durable phase: `state.json`.
446
451
  Previous configuration: `compose.before.yaml`.
447
452
  Previous immutable image: `{self.old_image}`.
448
453
  Original volume: `{self.volume}`.
449
- The archive `home.tar.gz` is validated only when backup_complete=true.
454
+ The archive `kobo.tar.gz` is validated only when backup_complete=true.
455
+ It contains ONLY /root/.config/kobo, with paths relative to /root.
450
456
  new_version_start_attempted indicates whether a new-version launch was attempted.
451
- Projects outside /root are NOT included in this archive; back them up separately.
457
+ Claude/Codex credentials, skills/plugins, caches and projects are NOT included;
458
+ back up those files separately if needed. They remain in the original volume.
452
459
  Old images, volumes and backups are never automatically deleted.
453
460
 
454
461
  ## Interrupted admission reservation
@@ -475,10 +482,15 @@ Do not run an older image against a potentially migrated database.
475
482
  Inspect the private log and Compose logs (which may contain credentials).
476
483
  A proxy error can leave a working backend: check for new messages and resumed
477
484
  work before considering a restore.
478
- To roll back fully, stop Kōbō, back up the current volume too, extract home.tar.gz
479
- into a NEW empty Docker volume and adapt the previous Compose configuration to
480
- use that restored volume and the previous immutable image above. Keep the current
481
- volume. Database rollback does not undo external agent or project changes.
485
+ To roll back Kōbō data, stop Kōbō and preserve the current volume. Clone that
486
+ stopped volume into a NEW Docker volume to retain current engine credentials,
487
+ skills and other files outside .config/kobo. In the NEW volume only, replace
488
+ .config/kobo completely with the contents of kobo.tar.gz (extract at the volume
489
+ root, preserving numeric owners and permissions). Do not merge the old database
490
+ with newer WAL/SHM files or other new-version Kōbō files. Adapt the previous
491
+ Compose configuration to use that restored volume and the previous immutable
492
+ image above. Keep the original volume. This archive cannot roll back files
493
+ outside .config/kobo, external agent actions or project changes.
482
494
  After recovery and health verification, set state.json phase to complete or aborted.
483
495
  ''')
484
496
 
@@ -0,0 +1,2 @@
1
+ import{M as e,S as t,dt as n,lt as r,p as i,pt as a,v as o}from"./reactivity.esm-bundler-DY1JkAEA.js";import{C as s,F as c,J as l,L as u,N as d,O as f,P as p,T as m,Y as h,_ as g,d as _,f as v,g as y,j as ee,k as b,l as x,p as S,r as C,u as w,y as T,z as te}from"./runtime-core.esm-bundler-Bqg71Vll.js";import{$ as ne,K as re,M as E,f as ie,z as ae}from"./dom-BtYTvLeN.js";import{l as oe,t as D,u as se}from"./QBtn-BUSdPaf4.js";import{Kt as ce,O as le,Qt as ue,Ut as O,_t as de,at as fe,cn as pe,dn as k,j as me,m as he,mt as ge,pt as _e,st as ve,tn as A,v as ye}from"./index-z1Oh2-I_.js";import{t as be}from"./QSpinnerDots-xdWyaE1i.js";import{t as xe}from"./QList-Cx1UfaUm.js";import{t as Se}from"./QMarkupTable-UhP7fVUp.js";import{c as j,l as M,s as N}from"./QSelect-C1AN93yA.js";import{t as P}from"./QItemSection-MdJ1_ZK7.js";import{t as Ce}from"./QExpansionItem-CAW9J1Uv.js";import{a as F,c as we,r as I,t as L}from"./ToolCallItem-CHE6tylq.js";import{t as Te}from"./QScrollArea-C_d-8eO7.js";import{a as R}from"./use-tours-COLDth2h.js";import{f as Ee,g as z,m as De,p as Oe,u as ke}from"./use-worktree-restore-CShbwpGy.js";import{t as B}from"./QTooltip-C9qaLW_h.js";import{n as V}from"./render-chat-markdown-CafzCcmu.js";import{n as H}from"./WorkspacePage-_QSKLO42.js";function Ae(e,t){return s(`div`,e,[s(`table`,{class:`q-table`},t)])}var je={list:xe,table:Se},U=[`list`,`table`,`__qtable`],Me=ae({name:`QVirtualScroll`,props:{...M,type:{type:String,default:`list`,validator:e=>U.includes(e)},items:{type:Array,default:()=>[]},itemsFn:Function,itemsSize:Number,scrollTarget:k},setup(e,{slots:n,attrs:r}){let i,a=t(null),o=x(()=>e.itemsSize>=0&&e.itemsFn!==void 0?Number.parseInt(e.itemsSize,10):Array.isArray(e.items)?e.items.length:0),{virtualScrollSliceRange:c,localResetVirtualScroll:u,padVirtualScroll:f,onVirtualScrollEvt:m}=j({virtualScrollLength:o,getVirtualScrollTarget:y,getVirtualScrollEl:v}),h=x(()=>{if(o.value===0)return[];let t=(e,t)=>({index:c.value.from+t,item:e});return e.itemsFn===void 0?e.items.slice(c.value.from,c.value.to).map(t):e.itemsFn(c.value.from,c.value.to-c.value.from).map(t)}),g=x(()=>`q-virtual-scroll q-virtual-scroll`+(e.virtualScrollHorizontal?`--horizontal`:`--vertical`)+(e.scrollTarget===void 0?` scroll`:``)),_=x(()=>e.scrollTarget===void 0?{tabindex:0}:{});l(o,()=>{u()}),l(()=>e.scrollTarget,()=>{C(),S()});function v(){let e=a.value;return e===null?null:e.$el||e}function y(){return i}function S(){let t=v();t!==null&&(i=pe(t,e.scrollTarget),i.addEventListener(`scroll`,m,E.passive))}function C(){i!==void 0&&(i.removeEventListener(`scroll`,m,E.passive),i=void 0)}function w(){let t=f(e.type===`list`?`div`:`tbody`,h.value.map(n.default));return n.before!==void 0&&(t=n.before().concat(t)),ie(n.after,t)}return u(),p(()=>{S()}),b(()=>{S()}),d(()=>{C()}),ee(()=>{C()}),()=>{if(n.default===void 0){console.error(`QVirtualScroll: default scoped slot is required for rendering`);return}return e.type===`__qtable`?Ae({ref:a,class:`q-table__middle `+g.value,tabindex:0},w()):s(je[e.type],{...r,ref:a,class:[r.class,g.value],..._.value},w)}}}),Ne=new Set([`setup`,`cleanup`,`archive`]);function Pe(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:Ne.has(e.sender)||le(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function Fe(e){let t=[],n=null,r=null;for(let i of e){let e=Pe(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`:e===`user`&&i.type===`user`&&i.source?JSON.stringify([`mcp`,i.source.transport,i.source.clientName]):e;!n||r!==o||a?(n={speaker:e,ts:i.ts,items:[i]},r=o,t.push(n),a&&(n=null)):n.items.push(i)}return t}function W(e){switch(e.type){case`text`:return`text:${e.messageId}`;case`thinking`:return`thinking:${e.messageId}:${e.eventIds?.[0]??e.ts??``}`;case`tool`:return`tool:${e.toolCallId}`;case`session`:return`session:${e.kind}:${e.eventIds?.[0]??e.ts??``}`;case`user`:return`user:${e.eventIds?.[0]??`${e.sender}:${e.ts??``}`}`;case`error`:return`error:${e.eventIds?.[0]??`${e.category}:${e.ts??``}`}`}}function Ie(e){let t=e.items[0];return`${e.speaker}:${t?W(t):`empty`}`}function Le(e,t){let n=Math.min(t,e.length)-1;for(let t=n;t>=0;t--)if(e[t]?.speaker===`user`)return t;return-1}function Re(e,t,n){return new Promise(r=>{let i=!1,a=null,o=null,s=e=>{i||(i=!0,a!==null&&clearTimeout(a),a=null,o?.(),r(e))};if(o=l(e,e=>{t(e)&&s(!0)},{immediate:!0}),i){o();return}a=setTimeout(()=>s(!1),n)})}var ze={class:`agent-error-item`},Be={class:`agent-error-item__body`},Ve={class:`agent-error-item__title`},G={class:`agent-error-item__message`},He=R(T({__name:`AgentErrorItem`,props:{item:{}},setup(e){let t=e,{te:n}=ue(),r=x(()=>{let e=`agent.error.${t.item.category}`;return n(e)?e:`agent.error.other`});return(t,n)=>(u(),S(`div`,ze,[g(oe,{name:`error_outline`,size:`16px`,class:`agent-error-item__icon`}),w(`div`,Be,[w(`div`,Ve,a(t.$t(r.value)),1),w(`div`,G,a(e.item.message),1)])]))}}),[[`__scopeId`,`data-v-e98c3c02`]]),Ue={class:`text-caption text-kobo-3`},We=T({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,n=x(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return z(t.item.detail);case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(u(),S(`span`,Ue,a(e.$t(n.value)),1))}}),K=new Map;function q(){for(let e of K.values())e.controller.abort(),e.blobUrl&&URL.revokeObjectURL(e.blobUrl);K.clear()}typeof window<`u`&&(window.addEventListener(`kobo:auth-token-changed`,q),window.addEventListener(`storage`,e=>{(e.key===`kobo:network-token`||e.key===null)&&q()}));function J(e){if(!/^\/api\/workspaces\/[^/]+\/images\/file\?path=/.test(e))throw Error(`Invalid workspace image URL`);let t=K.get(e);if(!t){let n=new AbortController,r={references:0,controller:n,ready:Promise.resolve(``)};r.ready=O(e,{signal:n.signal}).then(async e=>{if(!e.ok)throw await ce(e);let t=await e.blob();if(n.signal.aborted)throw new DOMException(`Aborted`,`AbortError`);return r.blobUrl=URL.createObjectURL(t),r.blobUrl}),t=r,K.set(e,t)}t.references++;let n=!1,r=t;return{ready:r.ready,release(){n||(n=!0,!(--r.references>0)&&(r.controller.abort(),r.blobUrl&&URL.revokeObjectURL(r.blobUrl),K.get(e)===r&&K.delete(e)))}}}function Ge(e,t){let n=o(``),r=o(0),a=()=>{r.value++},s=e=>{(e.key===`kobo:network-token`||e.key===null)&&a()};return window.addEventListener(`kobo:auth-token-changed`,a),window.addEventListener(`storage`,s),i(()=>{window.removeEventListener(`kobo:auth-token-changed`,a),window.removeEventListener(`storage`,s)}),l([e,r],([e],r,i)=>{let a=!0,o=[],s=document.createElement(`template`);s.innerHTML=e;for(let e of s.content.querySelectorAll(`img`)){let r=e.getAttribute(`src`)??``;if(e.removeAttribute(`data-kobo-image-url`),!/^\/api\/workspaces\/[^/]+\/images\/file\?path=/.test(r))continue;e.removeAttribute(`src`);let i=J(r);o.push(i),i.ready.then(t=>{a&&(e.setAttribute(`src`,t),e.setAttribute(`data-kobo-image-url`,r),n.value=s.innerHTML)}).catch(()=>{a&&(e.setAttribute(`title`,t()),n.value=s.innerHTML)})}n.value=s.innerHTML,i(()=>{a=!1;for(let e of o)e.release()})},{immediate:!0}),n}function Ke(e,t){return t?e.replace(/\[image:\s+([^\]]+)\]/g,(e,n)=>{let r=String(n).trim();return/^(\.ai\/images\/|images\/)/.test(r)?`![${r}](${`/api/workspaces/${encodeURIComponent(t)}/images/file?path=${encodeURIComponent(r)}`})`:e}):e}var qe=[`innerHTML`],Je=[`innerHTML`],Ye=[`src`],Y=R(T({__name:`UserMessageItem`,props:{item:{},workspaceId:{}},setup(t){let n=t,{t:r}=ue(),a=x(()=>n.item.sender===`system-prompt`),s=Ge(x(()=>{let e=Ke(n.item.content,n.workspaceId);return V(e)}),()=>r(`network.login.unreachable`)),c=o(null),d=o(!1),f;function p(){d.value=!1,c.value=null,f?.release(),f=void 0}l(d,e=>{e||p()}),l(()=>n.workspaceId,p);let m=e=>{(e.key===`kobo:network-token`||e.key===null)&&p()};window.addEventListener(`storage`,m),window.addEventListener(`kobo:auth-token-changed`,p),i(()=>{p(),window.removeEventListener(`kobo:auth-token-changed`,p),window.removeEventListener(`storage`,m)});function y(e){let t=e.target;if(t?.tagName!==`IMG`)return;let n=t;n.src&&(p(),n.dataset.koboImageUrl&&(f=J(n.dataset.koboImageUrl),f.ready.catch(p)),c.value=n.src,d.value=!0)}return(t,n)=>(u(),S(C,null,[a.value?(u(),_(Ce,{key:0,dense:``,"dense-toggle":``,label:t.$t(`chat.systemPrompt`),"header-class":`text-kobo-2 text-caption`},{default:h(()=>[w(`div`,{class:`q-py-xs markdown-message markdown-user-prompt`,innerHTML:e(s)},null,8,qe)]),_:1},8,[`label`])):(u(),S(`div`,{key:1,class:`markdown-message`,onClick:y},[w(`div`,{innerHTML:e(s)},null,8,Je)])),g(A,{modelValue:d.value,"onUpdate:modelValue":n[1]||=e=>d.value=e},{default:h(()=>[c.value?(u(),S(`img`,{key:0,src:c.value,alt:``,class:`image-lightbox-img`,onClick:n[0]||=e=>d.value=!1},null,8,Ye)):v(``,!0)]),_:1},8,[`modelValue`])],64))}}),[[`__scopeId`,`data-v-965e6297`]]),Xe=[`data-event-ids`],X={class:`turn-header`},Ze={key:0,class:`turn-time`},Qe={class:`turn-time turn-time-updated`},$e={key:2,class:`turn-actions`},et={class:`turn-body`},tt={class:`ellipsis`},nt={class:`q-px-sm q-pb-sm`},rt={key:0,class:`turn-scroll-top`},it=R(T({__name:`TurnCard`,props:{turn:{},workspaceId:{},highlighted:{type:Boolean}},emits:[`scrollTo`],setup(t,{emit:i}){let s=t,c=i,{t:l}=ue(),d=fe(),f=m(F),p=o(null);function ee(){let e=p.value;if(!e)return;let t=e.closest(`.q-scrollarea`)?.querySelector(`.q-scrollarea__content`);if(!t){e.scrollIntoView({behavior:`smooth`,block:`start`});return}let n=e.getBoundingClientRect().top-t.getBoundingClientRect().top;c(`scrollTo`,Math.max(0,n-8))}let b=x(()=>{switch(s.turn.speaker){case`user`:{let e=s.turn.items[0],t=e?.type===`user`?e.source:void 0;return{label:t?l(`chat.externalLlm`,{name:t.clientName}):l(`chat.you`),accent:`var(--kobo-turn-user)`,badgeClass:`turn-badge-user`}}case`agent`:return{label:l(`chat.agent`),accent:`var(--kobo-turn-agent)`,badgeClass:`turn-badge-agent`};case`system-prompt`:return{label:l(`chat.systemPrompt`),accent:`var(--kobo-text-3)`,badgeClass:`turn-badge-system`};case`session`:return{label:l(`chat.session`),accent:`var(--kobo-text-3)`,badgeClass:`turn-badge-session`};case`script`:{let e=s.turn.items[0],t=e?.type===`user`?e.sender:``;return{label:le(t)?l(`chat.hookScript`,{event:t.slice(5)}):l(t===`archive`?`chat.archiveScript`:t===`setup`?`chat.setupScript`:`chat.cleanupScript`),accent:`var(--kobo-success)`,badgeClass:`turn-badge-script`}}}});function T(e,t=!1){if(!e)return``;let n=new Date(e);return Number.isNaN(n.getTime())?``:n.toLocaleTimeString(void 0,t?{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}:{hour:`2-digit`,minute:`2-digit`})}let re=x(()=>T(s.turn.ts)),E=x(()=>s.turn.items.flatMap(e=>e.eventIds??[]).join(` `)),ie=x(()=>{let e=s.turn.items;if(e.length===0)return null;for(let t=e.length-1;t>=0;t--){let n=e[t].ts;if(n)return n}return null}),ae=x(()=>{let e=s.turn.ts,t=ie.value;if(!t||!e||t===e)return``;let n=new Date(e).getTime(),r=new Date(t).getTime();return Number.isNaN(n)||Number.isNaN(r)||r<=n?``:T(t,r-n<6e4)}),se=x(()=>ae.value!==``);function ce(e){return we(e.origin,Object.values(d.subagents[s.workspaceId]??{}),e.type===`tool`?e.toolCallId:void 0)}function O(e){return e||l(`chat.subagentActivity`)}function de(e){return(e.type===`text`||e.type===`thinking`||e.type===`tool`)&&e.origin?.kind===`subagent`}let pe=x(()=>{let e=[],t=new Map;for(let n of s.turn.items){if(!de(n)){e.push({type:`item`,key:W(n),item:n});continue}let r=ce(n),i=r?.toolUseId??n.origin?.toolCallId??n.origin?.threadId??W(n),a=t.get(i);if(a){a.items.push(n);continue}let o={type:`subagent`,key:`subagent:${W(n)}`,items:[n],subagentKey:i,subagentName:r?.description||null};t.set(i,o),e.push(o)}return e});function k(e){e.origin&&f?.({origin:e.origin})}let me=x(()=>s.turn.items.filter(e=>e.type===`tool`).length);return(i,o)=>(u(),S(`div`,{ref_key:`cardEl`,ref:p,class:r([`turn-card`,{"turn-card--user":t.turn.speaker===`user`,"turn-card--highlighted":t.highlighted}]),"data-event-ids":E.value,style:n({"--turn-accent":b.value.accent})},[w(`div`,X,[w(`span`,{class:r([`turn-badge`,b.value.badgeClass])},a(b.value.label),3),re.value?(u(),S(`span`,Ze,a(re.value),1)):v(``,!0),se.value?(u(),S(C,{key:1},[g(oe,{name:`arrow_forward`,size:`10px`,color:`kobo-3`,class:`turn-time-arrow`}),w(`span`,Qe,[y(a(ae.value)+` `,1),g(B,null,{default:h(()=>[y(a(e(l)(`chat.lastUpdatedAt`,{time:ae.value})),1)]),_:1})])],64)):v(``,!0),me.value>0?(u(),S(`span`,$e,` · `+a(e(l)(`chat.nActions`,{n:me.value})),1)):v(``,!0)]),w(`div`,et,[(u(!0),S(C,null,te(pe.value,n=>(u(),S(C,{key:n.key},[n.type===`subagent`?(u(),_(Ce,{key:0,class:`turn-subagent-activity`,dense:``,label:O(n.subagentName),caption:e(l)(`chat.nActions`,{n:n.items.length}),"default-opened":t.highlighted&&n.items.some(e=>e.eventIds?.some(e=>E.value.includes(e)))},{header:h(()=>[g(P,null,{default:h(()=>[g(N,{class:`row items-center no-wrap`,title:n.subagentName??void 0},{default:h(()=>[g(oe,{name:`hub`,size:`16px`,class:`q-mr-xs`},{default:h(()=>[g(B,null,{default:h(()=>[y(a(e(l)(`chat.subagentActivity`)),1)]),_:1})]),_:1}),w(`span`,tt,a(O(n.subagentName)),1)]),_:2},1032,[`title`]),g(N,{caption:``},{default:h(()=>[y(a(e(l)(`chat.nActions`,{n:n.items.length})),1)]),_:2},1024)]),_:2},1024),g(P,{side:``},{default:h(()=>[g(D,{flat:``,round:``,dense:``,size:`sm`,icon:`open_in_new`,"aria-label":e(l)(`chat.openSubagentActivity`),onClick:ne(e=>k(n.items[0]),[`stop`])},{default:h(()=>[g(B,null,{default:h(()=>[y(a(e(l)(`chat.openSubagentActivity`)),1)]),_:1})]),_:1},8,[`aria-label`,`onClick`])]),_:2},1024)]),default:h(()=>[w(`div`,nt,[(u(!0),S(C,null,te(n.items,t=>(u(),S(C,{key:e(W)(t)},[t.type===`text`?(u(),_(I,{key:0,item:t},null,8,[`item`])):t.type===`thinking`?(u(),_(H,{key:1,item:t},null,8,[`item`])):t.type===`tool`?(u(),_(L,{key:2,item:t},null,8,[`item`])):v(``,!0)],64))),128))])]),_:2},1032,[`label`,`caption`,`default-opened`])):(u(),S(C,{key:1},[n.item.type===`text`?(u(),_(I,{key:0,item:n.item},null,8,[`item`])):n.item.type===`thinking`?(u(),_(H,{key:1,item:n.item},null,8,[`item`])):n.item.type===`tool`?(u(),_(L,{key:2,item:n.item},null,8,[`item`])):n.item.type===`user`?(u(),_(Y,{key:3,item:n.item,"workspace-id":t.workspaceId},null,8,[`item`,`workspace-id`])):n.item.type===`session`?(u(),_(We,{key:4,item:n.item},null,8,[`item`])):n.item.type===`error`?(u(),_(He,{key:5,item:n.item},null,8,[`item`])):v(``,!0)],64))],64))),128))]),t.turn.items.length>4?(u(),S(`div`,rt,[g(D,{flat:``,round:``,dense:``,size:`xs`,icon:`arrow_upward`,color:`kobo-3`,class:`turn-scroll-top-btn`,onClick:ee},{default:h(()=>[g(B,null,{default:h(()=>[y(a(e(l)(`chat.scrollToTurnTop`)),1)]),_:1})]),_:1})])):v(``,!0)],14,Xe))}}),[[`__scopeId`,`data-v-13bebb3d`]]),at={key:0,class:`activity-feed-switching`},ot={key:1,class:`activity-feed-wrap`,"data-tour":`ws-chat`},st={key:0,role:`alert`,class:`q-pa-sm text-negative`},ct={key:1,"data-testid":`history-load-error`,role:`alert`,class:`q-pa-sm text-negative`},lt=[`aria-label`],ut={key:0,class:`activity-feed-compacting`},dt={class:`text-caption text-kobo-2`},ft={key:0,class:`text-center q-py-sm text-caption text-kobo-3`},pt={key:1,class:`activity-feed-empty q-pa-md`},mt={class:`activity-feed-empty__title`},ht={class:`activity-feed-empty__hint`},gt={key:2,class:`q-px-md q-pb-md`},_t=[`title`],vt={class:`ellipsis`},yt={class:`activity-feed-nav-cluster`},bt=300,xt=60,St=200,Ct=180,wt=255,Tt=3,Et=60,Dt=R(T({__name:`ActivityFeed`,props:{workspaceId:{}},setup(t){let{t:n}=ue(),r=t,i=de(),s=me(),d=fe(),m=he(),ee=x(()=>m.isCompacting(r.workspaceId)),b=x(()=>d.selectedSessionId),T=x(()=>d.sessions.find(e=>e.id===b.value)?.engineSessionId??null),ne=x(()=>{let e=d.sessions;return e.length!==0&&b.value===e[e.length-1].id});function E(e){return b.value?e?e===b.value||e===T.value:ne.value:!0}let ie=x(()=>(d.activityFeeds[r.workspaceId]??[]).filter(e=>e.type===`text`&&typeof e.content==`string`&&E(e.sessionId)).map(e=>({content:e.content,sender:e.meta?.sender??`user`,source:ye(e.meta?.source),ts:e.timestamp,sessionId:e.sessionId,eventIds:[e.id]}))),ae=x(()=>{let e=d.workspaces.find(e=>e.id===r.workspaceId);return ve(e?.status)}),le=ke(),pe=``,k=x(()=>{let e=i.eventsFor(r.workspaceId),t=i.timestampsFor(r.workspaceId),n=i.sessionIdsFor(r.workspaceId),a=i.eventIdsFor(r.workspaceId),o=[],c=[],l=[];for(let r=0;r<e.length;r++)E(n[r])&&(o.push(e[r]),c.push(t[r]),l.push(a[r]??null));let u=`${r.workspaceId}::${b.value??`*`}`;u!==pe&&(le=ke(),pe=u);let d=Ee(le,o,c,ae.value,l);return Fe(De(d,ie.value).filter(e=>e.type===`thinking`?!1:e.type===`session`?e.kind===`ended`?s.showVerboseSystemMessages||!Oe(e.detail):s.showVerboseSystemMessages:!0))}),A=x(()=>s.showVerboseSystemMessages?i.eventsFor(r.workspaceId).filter(e=>e.kind===`message:raw`).map(e=>e.content):[]),xe=x(()=>{for(let e=k.value.length-1;e>=0;e--){let t=k.value[e]?.items??[];for(let e=t.length-1;e>=0;e--){let r=t[e];if(r){if(r.type===`text`&&!r.streaming)return{key:`text:${r.messageId}`,text:r.text};if(r.type===`tool`)return r.result?{key:`tool:${r.toolCallId}:end`,text:r.result.isError?n(`activity.a11y.toolFailed`,{name:r.name}):n(`activity.a11y.toolFinished`,{name:r.name})}:{key:`tool:${r.toolCallId}:start`,text:n(`activity.a11y.toolStarted`,{name:r.name})}}}}return null}),Se=o(``);l(xe,(e,t)=>{if(!e||e.key===t?.key)return;let r=e.key.startsWith(`text:`)?n(`activity.a11y.agentSaid`,{text:e.text.slice(0,bt)}):e.text;Se.value=r});let j=o(null),M=o(!0),N=o(!1),P=o(``),F=_e(),we=_e(),I=_e(),L=o(null),R=!1,z=!0,B=null,V=o(new Set),H=new Map,Ae=x(()=>{if(k.value.length>0||A.value.length>0)return!1;let e=r.workspaceId,t=b.value;return!!d.loadingSessions[e]||m.isSyncPending(e)||t!==null&&V.value.has(W(e,t))}),je=o(new Map),U=x(()=>k.value.length>0||A.value.length>0),Ne=x(()=>!!d.loadingSessions[r.workspaceId]||m.isSyncPending(r.workspaceId)||b.value!==null&&V.value.has(W(r.workspaceId,b.value)));function Pe(e){tt();let t=B;if(B=e.verticalSize,t!==null&&e.verticalSize>t&&M.value&&!N.value){q(0);return}let n=e.verticalSize-e.verticalPosition-e.verticalContainerSize;M.value=n<=xt,R&&e.verticalPosition<=St&&!N.value&&!P.value&&ze()&&He()}function W(e,t){return`${e}:${t}`}function ze(){let e=b.value;return e?je.value.get(W(r.workspaceId,e))??!0:i.hasMoreOlderFor(r.workspaceId)}function Be(e,t,n){je.value.set(W(e,t),n)}function Ve(e){if(!b.value)return i.oldestIdFor(e);let t=i.eventIdsFor(e),n=i.sessionIdsFor(e);for(let e=0;e<t.length;e++){if(!E(n[e]))continue;let r=t[e];if(r)return r}}let G=null;function He(){if(G)return G;let e=K().finally(()=>{G===e&&(G=null)});return G=e,G}function Ue(){F.abort(),we.abort(),I.abort(),N.value=!1,G=null;for(let e of V.value)Ft.delete(e);H.clear(),V.value.clear(),P.value=``}function We(){P.value=``,Nt.value===0&&b.value?It():He()}l(()=>[r.workspaceId,b.value],Ue,{flush:`sync`});async function K(){let e=r.workspaceId,t=b.value,n=Ve(e);if(!n)return;let a=F.begin();P.value=``,N.value=!0;try{let r=j.value;if(await f(),!F.isCurrent(a))return;let o=r?.getScroll().verticalSize??0,s=r?.getScroll().verticalPosition??0,c=new URLSearchParams({before:n,limit:`200`});t&&c.set(`session`,t);let l=await O(`/api/workspaces/${e}/events?${c.toString()}`,{signal:a});if(!l.ok)throw await ce(l);let u=await l.json();if(!F.isCurrent(a))return;let p=u.events??[],m=p.filter(t=>t.type===`agent:event`&&t.workspaceId===e),h=p.filter(t=>t.type===`user:message`&&t.workspaceId===e),g=m.map(e=>e.payload),_=m.map(e=>e.createdAt),v=m.map(e=>e.sessionId??null),y=m.map(e=>e.id),ee=p.length>0?p[0].id:n;t&&Be(e,t,u.hasMore),i.prepend(e,g,_,{oldestId:ee,hasMoreOlder:t?i.hasMoreOlderFor(e):u.hasMore,sessionIds:v,eventIds:y});for(let t of h){let n=t.payload;typeof n.content==`string`&&d.addActivityItem(e,{id:t.id,type:`text`,content:n.content,timestamp:t.createdAt,sessionId:t.sessionId??void 0,meta:{sender:n.sender??`user`,source:ye(n.source)}})}if(await f(),r&&F.isCurrent(a)){let e=r.getScroll().verticalSize-o;if(e>0){let t=Math.max(s+e,250);r.setScrollPosition(`vertical`,t,0)}}}catch(e){if(ge(e)||!F.isCurrent(a))return;P.value=e instanceof Error?e.message:String(e),console.error(`[ActivityFeed] failed to load older events:`,e)}finally{F.isCurrent(a)&&(N.value=!1)}}async function q(e=0){let t=$();if(await f(),t!==$())return;let n=j.value;if(!n)return;let r=n.getScroll();n.setScrollPosition(`vertical`,r.verticalSize,e)}let J=null,Ge=0;function Ke(){if(J!=null)return;let e=performance.now()-Ge<Ct,t=$();J=requestAnimationFrame(()=>{J=null,t===$()&&(Ge=performance.now(),q(e?0:180))})}function qe(e){Q(),M.value=!1;let t=j.value;t&&t.setScrollPosition(`vertical`,Math.max(0,e),250)}function Je(){let e=window.location.hash,t=e.indexOf(`?`);if(t===-1||!new URLSearchParams(e.slice(t+1)).has(`eventId`))return;let n=e.slice(0,t);window.history.replaceState(window.history.state,``,`${window.location.pathname}${window.location.search}${n}`)}async function Ye(e){let t=e.detail;if(t?.workspaceId!==r.workspaceId||!t.eventId)return;Q(),M.value=!1;let n=I.begin(),a=r.workspaceId;try{let e=new URLSearchParams({around:t.eventId,limit:`200`});t.sessionId&&e.set(`session`,t.sessionId);let o=await O(`/api/workspaces/${a}/events?${e}`,{signal:n});if(!o.ok)throw await ce(o);let s=await o.json();if(!I.isCurrent(n))return;let c=s.events??[],l=c.filter(e=>e.type===`agent:event`&&e.workspaceId===r.workspaceId),u=c.filter(e=>e.type===`user:message`&&e.workspaceId===r.workspaceId);i.reset(r.workspaceId,l.map(e=>e.payload),l.map(e=>e.createdAt),{oldestId:c[0]?.id,hasMoreOlder:s.hasMore??!1,sessionIds:l.map(e=>e.sessionId),eventIds:l.map(e=>e.id)});for(let e of u){let t=e.payload;typeof t.content==`string`&&d.addActivityItem(r.workspaceId,{id:e.id,type:`text`,content:t.content,timestamp:e.createdAt,sessionId:e.sessionId??void 0,meta:{sender:t.sender??`user`,source:ye(t.source)}})}if(await Re(Ae,e=>!e,5e3),await f(),await new Promise(e=>requestAnimationFrame(()=>requestAnimationFrame(()=>e()))),!I.isCurrent(n))return;let p=k.value.findIndex(e=>e.items.some(e=>e.eventIds?.includes(t.eventId)??!1));if(p<0)return;L.value=t.eventId,Y.value?.scrollTo(p,`center`),X.value=p,window.setTimeout(()=>{L.value===t.eventId&&(L.value=null)},1800)}catch(e){if(ge(e)||!I.isCurrent(n))return;P.value=e instanceof Error?e.message:String(e),console.error(`[ActivityFeed] failed to focus history event:`,e)}}let Y=o(null),Xe=o(null),X=o(0),Ze=o(!1),Qe=x(()=>{for(let e=k.value.length-1;e>=0;e--)if(k.value[e]?.speaker===`user`)return e;return-1}),$e=null;function et(){let e=Qe.value,t=j.value?.getScrollTarget(),n=t?.querySelector(`[data-turn-index="${e}"]`);if(!n||!t){Ze.value=!1;return}let r=t.getBoundingClientRect(),i=n.getBoundingClientRect();Ze.value=i.bottom>r.top&&i.top<r.bottom}function tt(){$e===null&&($e=requestAnimationFrame(()=>{$e=null,et()}))}function nt(e){return e.length<=wt?e:`${e.slice(0,252)}...`}let rt=x(()=>{let e=Qe.value,t=k.value[e];if(!t||X.value<=e||Ze.value)return null;let n=t.items.filter(e=>e.type===`user`).map(e=>e.content).join(`
2
+ `).trim();return n?{index:e,content:nt(n)}:null});function Dt(e){X.value=e.index,tt()}function Ot(){let e=rt.value;e&&(Q(),M.value=!1,Y.value?.scrollTo(e.index,`start`),X.value=e.index)}let kt=o(!1);async function At(){if(kt.value)return;Q(),M.value=!1,kt.value=!0;let e=r.workspaceId;try{let t=Le(k.value,X.value);if(t<0)for(let n=0;n<15;n++){if(e!==r.workspaceId)return;if(!ze())break;let n=k.value.length;if(await He(),await f(),e!==r.workspaceId)return;if(X.value+=k.value.length-n,t=Le(k.value,X.value),t>=0)break}t>=0&&e===r.workspaceId&&(Y.value?.scrollTo(t,`start`),X.value=t)}finally{kt.value=!1}}let Z=0;function jt(){return new Promise(e=>requestAnimationFrame(()=>e()))}async function Mt(){if(!z||!U.value||Ae.value)return;R=!1;let e=++Z;if(await f(),e!==Z||!z||(Xe.value=j.value?.getScrollTarget()??null,await f(),e!==Z||!z))return;k.value.length>0&&Y.value?.scrollTo(k.value.length-1,`end-force`),await jt();let t=-1,n=0;for(let r=0;r<Et&&n<Tt;r++){if(e!==Z||!z)return;await q(0),await jt();let r=j.value?.getScroll().verticalSize??0;B=r,n=r===t?n+1:0,t=r}e===Z&&!Ne.value&&(R=!0,z=!1)}function Q(){z=!1,!R&&(Z++,R=!0)}let Nt=x(()=>{let e=i.sessionIdsFor(r.workspaceId);if(!b.value)return e.length;let t=0;for(let n of e)E(n)&&t++;return t});l(Ae,async e=>{!e&&U.value&&await Mt(),e||f(()=>{Xe.value=j.value?.getScrollTarget()??null,tt()})}),l(Ne,(e,t)=>{!e&&t&&z&&U.value&&Mt()}),p(()=>{window.addEventListener(`kobo:focus-history-event`,Ye),f(()=>{Xe.value=j.value?.getScrollTarget()??null,tt()}),U.value&&Mt(),b.value&&It();let e=new URLSearchParams(window.location.hash.split(`?`)[1]??``).get(`eventId`);e&&(Ye(new CustomEvent(`kobo:focus-history-event`,{detail:{workspaceId:r.workspaceId,sessionId:b.value,eventId:e}})),Je())}),l([Nt,()=>ie.value.length,()=>A.value.length],async([e,t,n])=>{N.value||z&&e+t+n>0&&await Mt()}),l([Qe,()=>k.value.length],()=>{f(tt)});let Pt=x(()=>i.liveAppendCountFor(r.workspaceId,E)),$=()=>`${r.workspaceId}\u0000${d.selectedSessionId??``}`;l([Pt,$],([e,t],[n,r])=>{t===r&&e>n&&M.value&&!N.value&&Ke()}),c(()=>{Z++,z=!1,Ue(),window.removeEventListener(`kobo:focus-history-event`,Ye),J!=null&&(cancelAnimationFrame(J),J=null),$e!==null&&cancelAnimationFrame($e)}),l([()=>r.workspaceId,()=>d.selectedSessionId],()=>{Z++,z=!0,B=null,M.value=!0,R=!1,It(),U.value&&Mt()});let Ft=new Set;async function It(){let e=r.workspaceId,t=b.value;if(!t||Nt.value>0)return;let n=W(e,t);if(Ft.has(n))return;Ft.add(n),V.value.add(n);let a=we.begin();H.set(n,a),P.value=``;try{let n=await O(`/api/workspaces/${e}/events?session=${encodeURIComponent(t)}&limit=500`,{signal:a});if(!n.ok)throw await ce(n);let r=await n.json();if(!we.isCurrent(a))return;let o=r.events??[];if(o.length===0)return;let s=o.filter(t=>t.type===`agent:event`&&t.workspaceId===e),c=o.filter(t=>t.type===`user:message`&&t.workspaceId===e),l=s.map(e=>e.payload),u=s.map(e=>e.createdAt),f=s.map(e=>e.sessionId??null),p=s.map(e=>e.id);Be(e,t,r.hasMore),l.length>0&&i.prepend(e,l,u,{oldestId:o[0].id,hasMoreOlder:i.hasMoreOlderFor(e),sessionIds:f,eventIds:p});for(let t of c){let n=t.payload;typeof n.content==`string`&&d.addActivityItem(e,{id:t.id,type:`text`,content:n.content,timestamp:t.createdAt,sessionId:t.sessionId??void 0,meta:{sender:n.sender??`user`,source:ye(n.source)}})}}catch(e){if(ge(e)||!we.isCurrent(a)){(!H.has(n)||H.get(n)===a)&&Ft.delete(n);return}P.value=e instanceof Error?e.message:String(e),console.error(`[ActivityFeed] fetchSessionIfMissing failed:`,e),Ft.delete(n)}finally{H.get(n)===a&&(H.delete(n),V.value.delete(n))}}let Lt=x(()=>ie.value.filter(e=>e.sender===`user`).length),Rt=x(()=>(d.activityFeeds[r.workspaceId]??[]).filter(e=>e.meta?.sender===`user`&&e.meta.pending&&E(e.sessionId)).length);l([Lt,Rt,$],async([e,t,n],[r,i,a])=>{let o=t>i;n===a&&(o||e>r&&M.value&&!N.value&&!Ne.value)&&(M.value=!0,await q(o?180:0))});async function zt(){M.value=!0,await q(250)}return(t,n)=>Ae.value?(u(),S(`div`,at,[g(be,{size:`40px`,color:`primary`})])):(u(),S(`div`,ot,[e(d).sessionsErrors[r.workspaceId]?(u(),S(`div`,st,[y(a(t.$t(`activity.sessionsError`,{message:e(d).sessionsErrors[r.workspaceId]}))+` `,1),g(D,{flat:``,dense:``,label:t.$t(`common.retry`),onClick:n[0]||=t=>e(d).fetchSessions(r.workspaceId)},null,8,[`label`])])):v(``,!0),P.value?(u(),S(`div`,ct,[y(a(t.$t(`activity.historyError`,{message:P.value}))+` `,1),g(D,{flat:``,dense:``,label:t.$t(`common.retry`),onClick:We},null,8,[`label`])])):v(``,!0),w(`div`,{"data-testid":`activity-live-region`,class:`sr-only`,role:`log`,"aria-live":`polite`,"aria-atomic":`true`,"aria-label":t.$t(`activity.a11y.region`)},a(Se.value),9,lt),g(re,{name:`fade`},{default:h(()=>[ee.value?(u(),S(`div`,ut,[g(be,{size:`18px`,color:`primary`}),w(`span`,dt,a(t.$t(`activity.compacting`)),1)])):v(``,!0)]),_:1}),g(Te,{ref_key:`scrollRef`,ref:j,class:`activity-feed-scroll`,onScroll:Pe,onWheelPassive:Q,onTouchstartPassive:Q,onKeydown:Q},{default:h(()=>[N.value?(u(),S(`div`,ft,[g(se,{size:`sm`}),y(` `+a(t.$t(`activity.loading_older`)),1)])):v(``,!0),g(Me,{ref_key:`virtualScrollRef`,ref:Y,class:`q-pa-md`,items:k.value,"scroll-target":Xe.value??void 0,"virtual-scroll-item-size":160,"virtual-scroll-slice-size":20,onVirtualScroll:Dt},{default:h(({item:t,index:n})=>[(u(),_(it,{key:e(Ie)(t),turn:t,"workspace-id":r.workspaceId,"data-turn-index":n,highlighted:t.items.some(e=>e.eventIds?.includes(L.value??``)??!1),onScrollTo:qe},null,8,[`turn`,`workspace-id`,`data-turn-index`,`highlighted`]))]),_:1},8,[`items`,`scroll-target`]),k.value.length===0&&A.value.length===0&&!N.value?(u(),S(`div`,pt,[g(oe,{name:`forum`,size:`28px`}),w(`div`,mt,a(t.$t(`activity.empty`)),1),w(`div`,ht,a(t.$t(`activity.emptyHint`)),1)])):v(``,!0),A.value.length?(u(),S(`div`,gt,[g(Ce,{label:t.$t(`activity.raw_lines`,{n:A.value.length}),dense:``},{default:h(()=>[(u(!0),S(C,null,te(A.value,(e,t)=>(u(),S(`div`,{key:t,class:`text-caption text-kobo-3 q-pa-xs`},a(e),1))),128))]),_:1},8,[`label`])])):v(``,!0)]),_:1},512),rt.value?(u(),S(`button`,{key:2,type:`button`,class:`activity-feed-last-user`,"data-testid":`latest-user-message`,title:rt.value.content,onClick:Ot},[g(oe,{name:`person`,size:`16px`}),w(`span`,vt,a(rt.value.content),1)],8,_t)):v(``,!0),w(`div`,yt,[M.value?v(``,!0):(u(),_(D,{key:0,round:``,dense:``,unelevated:``,color:`kobo-surface-2`,"text-color":`kobo-1`,icon:`arrow_downward`,size:`sm`,class:`activity-feed-nav-btn`,title:t.$t(`activity.scroll_to_bottom`),"aria-label":t.$t(`activity.scroll_to_bottom`),onClick:zt},null,8,[`title`,`aria-label`])),g(D,{round:``,dense:``,unelevated:``,color:`kobo-surface-2`,"text-color":`kobo-1`,icon:`arrow_upward`,size:`sm`,class:`activity-feed-nav-btn`,title:t.$t(`activity.prev_user_message`),"aria-label":t.$t(`activity.prev_user_message`),loading:kt.value,disable:kt.value,onClick:At},null,8,[`title`,`aria-label`,`loading`,`disable`])])]))}}),[[`__scopeId`,`data-v-58324159`]]);export{Dt as default};