@loicngr/kobo 2.1.25 → 2.1.28

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 (133) hide show
  1. package/AGENTS.md +30 -3
  2. package/CHANGELOG.md +15 -0
  3. package/CONFIGURATION.md +156 -49
  4. package/README.md +2 -1
  5. package/SECURITY.md +33 -6
  6. package/dist/mcp-server/kobo-tasks-server.js +36 -1021
  7. package/dist/mcp-server/managed-mcp-dispatcher.js +960 -0
  8. package/dist/mcp-server/workspace-dialogue-client.js +26 -1
  9. package/dist/server/index.js +52 -19
  10. package/dist/server/middleware/host-check-middleware.js +8 -5
  11. package/dist/server/middleware/network-auth-middleware.js +51 -29
  12. package/dist/server/middleware/security-headers-middleware.js +19 -0
  13. package/dist/server/middleware/traffic-limits-middleware.js +74 -0
  14. package/dist/server/routes/auth.js +73 -0
  15. package/dist/server/routes/internal-mcp.js +137 -0
  16. package/dist/server/routes/mcp-credentials.js +30 -0
  17. package/dist/server/routes/mcp.js +156 -76
  18. package/dist/server/routes/usage.js +24 -0
  19. package/dist/server/routes/workspaces.js +19 -3
  20. package/dist/server/services/agent/orchestrator.js +27 -17
  21. package/dist/server/services/external-mcp-credentials-service.js +110 -0
  22. package/dist/server/services/forge/github/provider.js +15 -3
  23. package/dist/server/services/forge/types.js +8 -0
  24. package/dist/server/services/internal-mcp-capability-service.js +98 -0
  25. package/dist/server/services/mcp-connection-info-service.js +1 -1
  26. package/dist/server/services/memory-mcp-service.js +9 -7
  27. package/dist/server/services/memory-service.js +15 -0
  28. package/dist/server/services/network-access-service.js +43 -73
  29. package/dist/server/services/network-session-service.js +182 -0
  30. package/dist/server/services/provider-accounts-service.js +202 -0
  31. package/dist/server/services/settings-service.js +29 -7
  32. package/dist/server/services/terminal-websocket-service.js +5 -0
  33. package/dist/server/services/usage/poller.js +44 -3
  34. package/dist/server/services/websocket-service.js +5 -0
  35. package/dist/server/utils/paths.js +2 -1
  36. package/dist/server/utils/startup-display.js +1 -2
  37. package/dist/shared/models.js +6 -0
  38. package/dist/shared/provider-accounts.js +1 -0
  39. package/docker-compose.vps.yml +6 -0
  40. package/docs/deployment-vps.md +24 -12
  41. package/package.json +1 -1
  42. package/src/client/dist/pwa/assets/ActivityFeed-D58VS9o2.js +2 -0
  43. package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-Dafi7Tue.js → AutoLoopFinalReviewSettings-CK3r_fVB.js} +1 -1
  44. package/src/client/dist/pwa/assets/{ChangelogPage-DH-zUtlf.js → ChangelogPage-CAAUEn5X.js} +1 -1
  45. package/src/client/dist/pwa/assets/ComparisonPanel-BCb7vB5E.js +2 -0
  46. package/src/client/dist/pwa/assets/CreatePage-DasH1Uk4.js +2 -0
  47. package/src/client/dist/pwa/assets/{DashboardPage-B8gTooRi.js → DashboardPage-XE52EABP.js} +1 -1
  48. package/src/client/dist/pwa/assets/DiffViewer-CncKlXne.js +8 -0
  49. package/src/client/dist/pwa/assets/{DocumentsPanel-DXjjhGUg.js → DocumentsPanel-L4pbQfz1.js} +1 -1
  50. package/src/client/dist/pwa/assets/{DrawerToggleButton-CiVNMTbq.js → DrawerToggleButton-DiOJm5he.js} +1 -1
  51. package/src/client/dist/pwa/assets/GitPanel-WJlzy2VJ.js +3 -0
  52. package/src/client/dist/pwa/assets/{GitPanel-CRaLJzjc.css → GitPanel-yHvU38dp.css} +1 -1
  53. package/src/client/dist/pwa/assets/{HealthPage-lc2Cuuks.js → HealthPage-hJY5MurK.js} +1 -1
  54. package/src/client/dist/pwa/assets/MainLayout-DgWElK7w.js +2 -0
  55. package/src/client/dist/pwa/assets/{MemoryPanel-SrR94UVW.js → MemoryPanel-DtEvh_b7.js} +1 -1
  56. package/src/client/dist/pwa/assets/{MemoryProposalList-B5uVf3KU.js → MemoryProposalList-kgFCm9zz.js} +1 -1
  57. package/src/client/dist/pwa/assets/{QBtnDropdown-Cxr3lBPB.js → QBtnDropdown-CWPIugs8.js} +1 -1
  58. package/src/client/dist/pwa/assets/{QExpansionItem-xIXDevKz.js → QExpansionItem-D-EGL2b3.js} +1 -1
  59. package/src/client/dist/pwa/assets/{QScrollArea-DeczMHVs.js → QScrollArea-BMrqK-xd.js} +1 -1
  60. package/src/client/dist/pwa/assets/QSelect-DFWyDWCE.js +1 -0
  61. package/src/client/dist/pwa/assets/{QTooltip-be9mqhmB.js → QTooltip-Bk8dLUtU.js} +1 -1
  62. package/src/client/dist/pwa/assets/{SchedulePanel-R6H-D-m5.js → SchedulePanel-CuxmxCKi.js} +1 -1
  63. package/src/client/dist/pwa/assets/{SearchPage-CaeYoIDw.js → SearchPage-DKnQPUqY.js} +1 -1
  64. package/src/client/dist/pwa/assets/{SettingsPage-Dcq1Jeoo.css → SettingsPage-B4rmd2WX.css} +1 -1
  65. package/src/client/dist/pwa/assets/SettingsPage-CO3vIpPd.js +9 -0
  66. package/src/client/dist/pwa/assets/{SplitWorkspacePage-Dw1qUIFU.js → SplitWorkspacePage-WhQyX8fn.js} +1 -1
  67. package/src/client/dist/pwa/assets/{TerminalPanel-Dqw88QqH.js → TerminalPanel-BY0lPq6k.js} +10 -10
  68. package/src/client/dist/pwa/assets/{TerminalPanel-Dz9EE-Ph.css → TerminalPanel-BcbwuFVR.css} +1 -1
  69. package/src/client/dist/pwa/assets/{ToolCallItem-BMn7eela.js → ToolCallItem-DYubi-J0.js} +3 -3
  70. package/src/client/dist/pwa/assets/{TouchPan-BwCpe_Oh.js → TouchPan-DDotyYLN.js} +1 -1
  71. package/src/client/dist/pwa/assets/{TourReplayButton-yeGYf6xT.js → TourReplayButton-kAnEMcy2.js} +1 -1
  72. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BJPw65vU.js → WorkspaceAttentionLabels-DiYt3rP3.js} +1 -1
  73. package/src/client/dist/pwa/assets/WorkspacePage-BEBwwr81.css +1 -0
  74. package/src/client/dist/pwa/assets/WorkspacePage-K-1ce50d.js +4 -0
  75. package/src/client/dist/pwa/assets/{browser-C9U-mSDm.js → browser-DVYLH8vl.js} +1 -1
  76. package/src/client/dist/pwa/assets/{build-path-tree-DYdRW2T4.js → build-path-tree-DEuOdIIk.js} +1 -1
  77. package/src/client/dist/pwa/assets/custom-sounds-Ggw6V9iF.js +1 -0
  78. package/src/client/dist/pwa/assets/de-Bk3zQrin.js +17 -0
  79. package/src/client/dist/pwa/assets/{editor.api-iu5YkEl3.js → editor.api-BC38eXAS.js} +1 -1
  80. package/src/client/dist/pwa/assets/{efforts-ja6uJadn.js → efforts-TGacUPcJ.js} +1 -1
  81. package/src/client/dist/pwa/assets/es-f3xuAWA9.js +17 -0
  82. package/src/client/dist/pwa/assets/{expand-template-BbgnJ8YH.js → expand-template-DM8gq3Mo.js} +1 -1
  83. package/src/client/dist/pwa/assets/{format-CLXLy6Ov.js → format-OwxXBnDJ.js} +1 -1
  84. package/src/client/dist/pwa/assets/{formatters-DeKgxYJe.js → formatters-YgaCZcwJ.js} +1 -1
  85. package/src/client/dist/pwa/assets/fr-B6c21SG3.js +17 -0
  86. package/src/client/dist/pwa/assets/{freemarker2-2IrnDDvu.js → freemarker2-l7R42fI2.js} +1 -1
  87. package/src/client/dist/pwa/assets/{handlebars-CeLN7FCP.js → handlebars-DFmPrgjH.js} +1 -1
  88. package/src/client/dist/pwa/assets/{html-Bn3rhcp3.js → html-CERIAlPM.js} +1 -1
  89. package/src/client/dist/pwa/assets/i18n-WaE4cZK9.js +1 -0
  90. package/src/client/dist/pwa/assets/index-CpWBGKdM.js +20 -0
  91. package/src/client/dist/pwa/assets/it-DUstDpC2.js +17 -0
  92. package/src/client/dist/pwa/assets/{javascript-BMvON8oT.js → javascript-BBYncplF.js} +1 -1
  93. package/src/client/dist/pwa/assets/{liquid-D_AQLvQl.js → liquid-C1HqYyYU.js} +1 -1
  94. package/src/client/dist/pwa/assets/{mdx-DDJzMB8Q.js → mdx-E74dsP22.js} +1 -1
  95. package/src/client/dist/pwa/assets/{monaco-contributions-DFKwR-_X.js → monaco-contributions-DHvm-kp5.js} +1 -1
  96. package/src/client/dist/pwa/assets/{monaco.contribution-4AwFLnjV.js → monaco.contribution-DYdn9CwR.js} +2 -2
  97. package/src/client/dist/pwa/assets/network-auth-DTZUKbny.js +1 -0
  98. package/src/client/dist/pwa/assets/{notify-theme-CMzdjL91.js → notify-theme-FuTL7HuS.js} +1 -1
  99. package/src/client/dist/pwa/assets/{permissionModes-DLWiBfdb.js → permissionModes-D-Nexnoo.js} +1 -1
  100. package/src/client/dist/pwa/assets/{project-color-C5mEKdlH.js → project-color-_snXWYUp.js} +1 -1
  101. package/src/client/dist/pwa/assets/{python-BOqcp9xc.js → python-DNAAGBcX.js} +1 -1
  102. package/src/client/dist/pwa/assets/{razor-BrGwBzd7.js → razor-CB3g5p_T.js} +1 -1
  103. package/src/client/dist/pwa/assets/{typescript-mOGf86uY.js → typescript-0kpz8bqP.js} +1 -1
  104. package/src/client/dist/pwa/assets/{use-search-index-status-P7O7EaV_.js → use-search-index-status-D38yFOD_.js} +1 -1
  105. package/src/client/dist/pwa/assets/{use-tours-CMFPIW6f.js → use-tours-BWkHp34X.js} +2 -2
  106. package/src/client/dist/pwa/assets/{use-worktree-restore-DF6P7h8g.js → use-worktree-restore-DUjZr44N.js} +1 -1
  107. package/src/client/dist/pwa/assets/{workspace-templates-C_bgIQvs.js → workspace-templates-D-lPJQ4G.js} +1 -1
  108. package/src/client/dist/pwa/assets/{xml-YcMBzYAm.js → xml-BbOuqdUe.js} +1 -1
  109. package/src/client/dist/pwa/assets/{yaml-BP_67Sds.js → yaml-gasFQtfk.js} +1 -1
  110. package/src/client/dist/pwa/index.html +1 -1
  111. package/src/client/dist/pwa/sw.js +1 -1
  112. package/src/mcp-server/README.md +13 -19
  113. package/src/mcp-server/kobo-tasks-server.ts +42 -1146
  114. package/src/mcp-server/managed-mcp-dispatcher.ts +1071 -0
  115. package/src/mcp-server/workspace-dialogue-client.ts +26 -1
  116. package/src/client/dist/pwa/assets/ActivityFeed-xJMPnXKA.js +0 -2
  117. package/src/client/dist/pwa/assets/ComparisonPanel-DwaQyEgb.js +0 -2
  118. package/src/client/dist/pwa/assets/CreatePage-DyF_efbs.js +0 -2
  119. package/src/client/dist/pwa/assets/DiffViewer-BivWNITj.js +0 -8
  120. package/src/client/dist/pwa/assets/GitPanel-DQjgGtoN.js +0 -3
  121. package/src/client/dist/pwa/assets/MainLayout-D3pGcTx8.js +0 -2
  122. package/src/client/dist/pwa/assets/QSelect-GQAsf7pB.js +0 -1
  123. package/src/client/dist/pwa/assets/SettingsPage-BCWdQHS5.js +0 -9
  124. package/src/client/dist/pwa/assets/WorkspacePage-BHO1Q8PG.js +0 -4
  125. package/src/client/dist/pwa/assets/WorkspacePage-BnklZAYZ.css +0 -1
  126. package/src/client/dist/pwa/assets/custom-sounds-DPsfYErA.js +0 -1
  127. package/src/client/dist/pwa/assets/de-Bqil7RJE.js +0 -17
  128. package/src/client/dist/pwa/assets/es-CNcMm1Zz.js +0 -17
  129. package/src/client/dist/pwa/assets/fr-BObHEohg.js +0 -17
  130. package/src/client/dist/pwa/assets/i18n-Df_y96N2.js +0 -1
  131. package/src/client/dist/pwa/assets/index-CH4JpI2a.js +0 -20
  132. package/src/client/dist/pwa/assets/it-DDMC7Emk.js +0 -17
  133. package/src/client/dist/pwa/assets/network-auth-CMERg4Qk.js +0 -1
@@ -0,0 +1,202 @@
1
+ import { execFile } from 'node:child_process';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { withUsageAccountSwitch } from './usage/poller.js';
5
+ const ERRORS = {
6
+ accounts_unknown_provider: [404, 'Unknown account provider.'],
7
+ accounts_invalid_account: [400, 'Invalid account selection.'],
8
+ accounts_not_found: [404, 'Account is no longer available.'],
9
+ accounts_disabled: [409, 'Account is disabled.'],
10
+ accounts_unavailable: [503, 'Account switching is unavailable.'],
11
+ accounts_unsupported_environment: [409, 'Account switching is unsupported in this runtime environment.'],
12
+ accounts_command_failed: [502, 'Account operation failed.'],
13
+ accounts_busy: [429, 'Account operations are busy.'],
14
+ };
15
+ export class ProviderAccountsError extends Error {
16
+ code;
17
+ status;
18
+ constructor(code) {
19
+ super(ERRORS[code][1]);
20
+ this.code = code;
21
+ this.status = ERRORS[code][0];
22
+ }
23
+ }
24
+ // ccswap shares backup/config state across providers. Keep validation and the
25
+ // post-switch read in the same queue, not just the mutating child process.
26
+ let operations = Promise.resolve();
27
+ let queued = 0;
28
+ function serialize(operation) {
29
+ if (queued >= 32)
30
+ return Promise.reject(new ProviderAccountsError('accounts_busy'));
31
+ queued++;
32
+ return new Promise((resolve, reject) => {
33
+ let expired = false;
34
+ const timeout = setTimeout(() => {
35
+ expired = true;
36
+ reject(new ProviderAccountsError('accounts_busy'));
37
+ }, 5_000);
38
+ const result = operations
39
+ .then(async () => {
40
+ clearTimeout(timeout);
41
+ // The caller has already received busy; never perform a delayed switch.
42
+ if (expired)
43
+ return;
44
+ resolve(await operation());
45
+ })
46
+ .catch(reject);
47
+ operations = result.finally(() => {
48
+ queued--;
49
+ });
50
+ });
51
+ }
52
+ function providerId(value) {
53
+ if (value !== 'claude-code' && value !== 'codex')
54
+ throw new ProviderAccountsError('accounts_unknown_provider');
55
+ return value;
56
+ }
57
+ function assertRuntime(provider) {
58
+ if (provider !== 'claude-code')
59
+ return;
60
+ // ccswap 0.37 writes the regular active store, not Claude's separate secure
61
+ // store override or macOS's hashed Keychain service for ANY nonempty
62
+ // CLAUDE_CONFIG_DIR, even when it explicitly names the default directory.
63
+ const config = process.env.CLAUDE_CONFIG_DIR;
64
+ if (process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR !== undefined || (process.platform === 'darwin' && config)) {
65
+ throw new ProviderAccountsError('accounts_unsupported_environment');
66
+ }
67
+ }
68
+ function run(args, allowEmpty = false) {
69
+ return new Promise((resolve, reject) => {
70
+ // Inherit the actual server/container identity and homes. Never invoke a
71
+ // shell, docker, SSH, or resolve a binary relative to a workspace checkout.
72
+ execFile('ccswap', args, {
73
+ encoding: 'utf8',
74
+ shell: false,
75
+ killSignal: 'SIGKILL',
76
+ timeout: 30_000,
77
+ maxBuffer: 1024 * 1024,
78
+ cwd: os.homedir(),
79
+ env: process.env,
80
+ }, (error, stdout) => {
81
+ if (error) {
82
+ reject(new ProviderAccountsError(error.code === 'ENOENT' ? 'accounts_unavailable' : 'accounts_command_failed'));
83
+ return;
84
+ }
85
+ if (allowEmpty && !stdout.trim()) {
86
+ resolve(null);
87
+ return;
88
+ }
89
+ try {
90
+ resolve(JSON.parse(stdout));
91
+ }
92
+ catch {
93
+ reject(new ProviderAccountsError('accounts_command_failed'));
94
+ }
95
+ });
96
+ });
97
+ }
98
+ function object(value) {
99
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
100
+ }
101
+ function display(value) {
102
+ return typeof value === 'string'
103
+ ? value
104
+ .replace(/[\p{Cc}\p{Cf}]/gu, '')
105
+ .trim()
106
+ .slice(0, 320)
107
+ : '';
108
+ }
109
+ async function list(provider) {
110
+ const home = provider === 'codex' ? process.env.CODEX_HOME : process.env.CLAUDE_CONFIG_DIR;
111
+ // Agent subprocesses run in their worktrees, the quota provider in the server
112
+ // cwd, and ccswap in HOME. Relative (including empty) overrides cannot name
113
+ // the same credential store reliably. Require an absolute path or no override.
114
+ if (home !== undefined && !path.isAbsolute(home)) {
115
+ throw new ProviderAccountsError('accounts_unsupported_environment');
116
+ }
117
+ const raw = await run(provider === 'codex' ? ['codex', 'list', '--json'] : ['list', '--provider', 'claude', '--json']);
118
+ if (!object(raw) ||
119
+ !Array.isArray(raw.accounts) ||
120
+ raw.accounts.length > 1000 ||
121
+ (provider === 'claude-code' ? raw.schemaVersion !== 1 : raw.provider !== 'codex')) {
122
+ throw new ProviderAccountsError('accounts_command_failed');
123
+ }
124
+ const seen = new Set();
125
+ const accounts = raw.accounts.map((row) => {
126
+ if (!object(row) ||
127
+ typeof row.number !== 'number' ||
128
+ !Number.isSafeInteger(row.number) ||
129
+ row.number < 1 ||
130
+ typeof row.active !== 'boolean' ||
131
+ (row.disabled !== undefined && typeof row.disabled !== 'boolean')) {
132
+ throw new ProviderAccountsError('accounts_command_failed');
133
+ }
134
+ const id = String(row.number);
135
+ if (seen.has(id))
136
+ throw new ProviderAccountsError('accounts_command_failed');
137
+ seen.add(id);
138
+ return {
139
+ id,
140
+ email: display(row.email),
141
+ label: display(provider === 'codex' ? row.label : row.alias) || display(row.organizationName),
142
+ active: row.active,
143
+ disabled: row.disabled === true,
144
+ };
145
+ });
146
+ return { available: true, accounts };
147
+ }
148
+ export async function listProviderAccounts(value) {
149
+ const provider = providerId(value);
150
+ return serialize(async () => {
151
+ try {
152
+ return await list(provider);
153
+ }
154
+ catch (error) {
155
+ if (error instanceof ProviderAccountsError && error.code === 'accounts_unavailable')
156
+ return { available: false, accounts: [] };
157
+ throw error;
158
+ }
159
+ });
160
+ }
161
+ export function validateAccountId(accountId) {
162
+ if (typeof accountId !== 'string' || !/^[1-9]\d{0,15}$/.test(accountId) || !Number.isSafeInteger(Number(accountId))) {
163
+ throw new ProviderAccountsError('accounts_invalid_account');
164
+ }
165
+ return accountId;
166
+ }
167
+ export async function switchProviderAccount(value, accountId) {
168
+ const provider = providerId(value);
169
+ const selectedId = validateAccountId(accountId);
170
+ return serialize(async () => {
171
+ assertRuntime(provider);
172
+ const current = await list(provider);
173
+ const selected = current.accounts.find((account) => account.id === selectedId);
174
+ if (!selected)
175
+ throw new ProviderAccountsError('accounts_not_found');
176
+ if (selected.disabled)
177
+ throw new ProviderAccountsError('accounts_disabled');
178
+ const change = async () => {
179
+ const raw = await run(provider === 'codex' ? ['codex', 'switch', selectedId, '--json'] : ['switch', selectedId, '--json'], provider === 'codex');
180
+ // ccswap 0.37's Codex dispatcher discards the switch return value.
181
+ // Empty stdout is accepted only here; the following list must confirm it.
182
+ if (provider === 'codex' && raw === null)
183
+ return;
184
+ if (!object(raw) ||
185
+ typeof raw.switched !== 'boolean' ||
186
+ !object(raw.to) ||
187
+ raw.to.number !== Number(selectedId) ||
188
+ (provider === 'claude-code' && raw.schemaVersion !== 1)) {
189
+ throw new ProviderAccountsError('accounts_command_failed');
190
+ }
191
+ };
192
+ if (provider === 'claude-code')
193
+ await withUsageAccountSwitch(provider, change);
194
+ else
195
+ await change();
196
+ const updated = await list(provider);
197
+ if (!updated.accounts.some((account) => account.id === selectedId && account.active)) {
198
+ throw new ProviderAccountsError('accounts_command_failed');
199
+ }
200
+ return updated;
201
+ });
202
+ }
@@ -12,6 +12,7 @@ import { getSettingsPath } from '../utils/paths.js';
12
12
  import { InvalidWorktreesPathError, resolveGlobalWorktreesRoot, sanitizeWorktreesPath, validateWorktreesPath, } from '../utils/worktree-paths.js';
13
13
  import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT } from './initial-prompt-template-service.js';
14
14
  import { LEGACY_AGNOSTIC_AUTO_LOOP_REVIEW_GATE, LEGACY_AGNOSTIC_REVIEW_TEMPLATE, LEGACY_CI_FIX_PROMPT_TEMPLATE, LEGACY_FINALIZATION_PROMPT, LEGACY_NOTION_INITIAL_PROMPT, LEGACY_PR_PROMPT_TEMPLATE, LEGACY_SENTRY_INITIAL_PROMPT, } from './legacy-public-prompts.js';
15
+ import { revokeNetworkSessions } from './network-session-service.js';
15
16
  import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from './review-template-service.js';
16
17
  import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT, DEFAULT_MODEL_BY_ENGINE, FRESH_MODEL_BY_ENGINE, } from './settings-defaults.js';
17
18
  import { AGNOSTIC_PROMPTS } from './skill-suite-prompts.js';
@@ -1293,6 +1294,7 @@ function readSettings() {
1293
1294
  }
1294
1295
  function readSettingsFromDisk() {
1295
1296
  ensureSettingsPathInitialized();
1297
+ protectSettingsFiles();
1296
1298
  if (!fs.existsSync(settingsFilePath)) {
1297
1299
  const defaults = defaultSettings();
1298
1300
  writeSettings(defaults);
@@ -1334,6 +1336,22 @@ function readSettingsFromDisk() {
1334
1336
  }
1335
1337
  /** Number of `settings.json.backup-*` files kept; older ones are rotated out. */
1336
1338
  const SETTINGS_BACKUP_KEEP = 5;
1339
+ /** Repair legacy permissions before reading secrets, without following file symlinks. */
1340
+ function protectSettingsFiles() {
1341
+ const dir = path.dirname(settingsFilePath);
1342
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
1343
+ fs.chmodSync(dir, 0o700);
1344
+ const basename = path.basename(settingsFilePath);
1345
+ for (const name of fs.readdirSync(dir)) {
1346
+ if (name !== basename && name !== `${basename}.tmp` && !name.startsWith(`${basename}.backup-`))
1347
+ continue;
1348
+ const file = path.join(dir, name);
1349
+ const stat = fs.lstatSync(file);
1350
+ if (!stat.isFile() || stat.nlink !== 1)
1351
+ throw new Error('[settings] Refusing unsafe settings file');
1352
+ fs.chmodSync(file, 0o600);
1353
+ }
1354
+ }
1337
1355
  /**
1338
1356
  * Keep only the `SETTINGS_BACKUP_KEEP` most recent `settings.json.backup-*`
1339
1357
  * files in `dir`, deleting the rest. Best-effort — never throws.
@@ -1368,7 +1386,7 @@ function createSettingsBackupIfPresent() {
1368
1386
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1369
1387
  settingsBackupSequence += 1;
1370
1388
  const backupPath = path.join(dir, `settings.json.backup-${stamp}-${settingsBackupSequence}`);
1371
- fs.copyFileSync(settingsFilePath, backupPath);
1389
+ fs.writeFileSync(backupPath, fs.readFileSync(settingsFilePath), { mode: 0o600, flag: 'wx' });
1372
1390
  pruneSettingsBackups(dir);
1373
1391
  }
1374
1392
  function writeSettings(settings, options) {
@@ -1376,14 +1394,11 @@ function writeSettings(settings, options) {
1376
1394
  invalidateSettingsCache();
1377
1395
  ensureSettingsPathInitialized();
1378
1396
  const tmpPath = `${settingsFilePath}.tmp`;
1379
- const dir = path.dirname(settingsFilePath);
1380
- if (!fs.existsSync(dir)) {
1381
- fs.mkdirSync(dir, { recursive: true });
1382
- }
1397
+ protectSettingsFiles();
1383
1398
  if (options?.backup) {
1384
1399
  createSettingsBackupIfPresent();
1385
1400
  }
1386
- fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2), 'utf-8');
1401
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
1387
1402
  fs.renameSync(tmpPath, settingsFilePath);
1388
1403
  }
1389
1404
  /** Read and return the full settings object, creating defaults if missing. */
@@ -1823,7 +1838,9 @@ export function updateGlobalSettings(input) {
1823
1838
  * (which would leave the server bound wide but unauthenticatable after a restart).
1824
1839
  */
1825
1840
  export function updateNetworkAccessSettings(patch) {
1826
- const settings = readSettings();
1841
+ const current = readSettings();
1842
+ // Keep cached policy intact until persistence succeeds.
1843
+ const settings = { ...current, global: { ...current.global } };
1827
1844
  if (typeof patch.networkAccessEnabled === 'boolean') {
1828
1845
  settings.global.networkAccessEnabled = patch.networkAccessEnabled;
1829
1846
  }
@@ -1834,6 +1851,11 @@ export function updateNetworkAccessSettings(patch) {
1834
1851
  settings.global.networkAccessBehindProxy = patch.networkAccessBehindProxy;
1835
1852
  }
1836
1853
  writeSettings(settings, { backup: true });
1854
+ if (current.global.networkAccessEnabled !== settings.global.networkAccessEnabled ||
1855
+ current.global.networkAccessToken !== settings.global.networkAccessToken ||
1856
+ current.global.networkAccessBehindProxy !== settings.global.networkAccessBehindProxy) {
1857
+ revokeNetworkSessions();
1858
+ }
1837
1859
  return settings.global;
1838
1860
  }
1839
1861
  function ensureGlobalWorktreesRootExists(worktreesPath) {
@@ -1,4 +1,5 @@
1
1
  import { assertWorkspaceLifecycleAvailable, isWorkspaceLifecycleBusy } from '../utils/workspace-lifecycle-guard.js';
2
+ import { acceptNetworkSocketMessage, isNetworkSocketAuthorized } from './network-session-service.js';
2
3
  import { createTerminal, getTerminal } from './terminal-service.js';
3
4
  import { getWorkspace } from './workspace-service.js';
4
5
  const MAX_BUFFERED_BYTES = 1024 * 1024;
@@ -31,6 +32,8 @@ export function handleTerminalConnection(ws, workspaceId, isShuttingDown = () =>
31
32
  ws.terminate();
32
33
  }
33
34
  function send(data) {
35
+ if (!isNetworkSocketAuthorized(ws))
36
+ return false;
34
37
  if (disposed || ws.readyState !== 1)
35
38
  return false;
36
39
  if (ws.bufferedAmount + Buffer.byteLength(data) > MAX_BUFFERED_BYTES) {
@@ -72,6 +75,8 @@ export function handleTerminalConnection(ws, workspaceId, isShuttingDown = () =>
72
75
  ws.on('close', dispose);
73
76
  ws.on('error', disconnect);
74
77
  ws.on('message', (data, isBinary) => {
78
+ if (!acceptNetworkSocketMessage(ws))
79
+ return;
75
80
  if (disposed || ws.readyState !== 1 || isShuttingDown())
76
81
  return;
77
82
  if (isBinary) {
@@ -8,17 +8,23 @@ export const POLL_INTERVAL_MS = 5 * 60_000;
8
8
  const DEFAULT_PROVIDERS = [createClaudeCodeProvider()];
9
9
  let providers = DEFAULT_PROVIDERS;
10
10
  let intervalHandle = null;
11
+ const generations = new Map();
12
+ const switchingProviders = new Set();
13
+ const latest = new Map();
11
14
  function persistAndBroadcast(snap) {
12
15
  upsertUsageSnapshot(snap);
16
+ latest.set(snap.providerId, snap);
13
17
  broadcastAll('usage:snapshot', { providerId: snap.providerId, snapshot: snap });
14
18
  }
15
19
  async function tick() {
16
20
  for (const provider of providers) {
17
21
  try {
18
- if (!(await provider.isAvailable()))
22
+ const generation = generations.get(provider.id) ?? 0;
23
+ if (switchingProviders.has(provider.id) || !(await provider.isAvailable()))
19
24
  continue;
20
- const snap = await provider.fetchSnapshot();
21
- persistAndBroadcast(snap);
25
+ if (generation !== (generations.get(provider.id) ?? 0))
26
+ continue;
27
+ await refreshNow(provider.id);
22
28
  }
23
29
  catch (err) {
24
30
  console.error('[usage-poller] tick failed for provider', provider.id, err);
@@ -45,11 +51,46 @@ export async function refreshNow(providerId) {
45
51
  const provider = providers.find((p) => p.id === providerId);
46
52
  if (!provider)
47
53
  return null;
54
+ if (switchingProviders.has(providerId))
55
+ return latest.get(providerId) ?? null;
56
+ const generation = generations.get(providerId) ?? 0;
48
57
  const snap = await provider.fetchSnapshot();
58
+ // A request started with old credentials must not restore them in SQLite,
59
+ // WebSocket clients, or its own delayed HTTP refresh response.
60
+ if (generation !== (generations.get(providerId) ?? 0))
61
+ return latest.get(providerId) ?? null;
49
62
  persistAndBroadcast(snap);
50
63
  return snap;
51
64
  }
65
+ /** Invalidate before changing credentials, including uncertain CLI failures. */
66
+ export async function withUsageAccountSwitch(providerId, operation) {
67
+ generations.set(providerId, (generations.get(providerId) ?? 0) + 1);
68
+ switchingProviders.add(providerId);
69
+ try {
70
+ persistAndBroadcast({
71
+ providerId,
72
+ status: 'error',
73
+ buckets: [],
74
+ fetchedAt: new Date().toISOString(),
75
+ errorMessage: 'Account credentials are changing. Refresh usage after the switch.',
76
+ });
77
+ return await operation();
78
+ }
79
+ finally {
80
+ switchingProviders.delete(providerId);
81
+ try {
82
+ await refreshNow(providerId);
83
+ }
84
+ catch {
85
+ // Quota refresh failure must not turn a successful account mutation into
86
+ // a failed switch or replace the original command error. Keep empty data.
87
+ }
88
+ }
89
+ }
52
90
  // Test seam — pass `null` to restore the default provider list.
53
91
  export function _setProvidersForTest(list) {
54
92
  providers = list ?? DEFAULT_PROVIDERS;
93
+ generations.clear();
94
+ switchingProviders.clear();
95
+ latest.clear();
55
96
  }
@@ -1,6 +1,7 @@
1
1
  import { nanoid } from 'nanoid';
2
2
  import { getDb } from '../db/index.js';
3
3
  import { recordActivity } from './activity-service.js';
4
+ import { acceptNetworkSocketMessage, isNetworkSocketAuthorized } from './network-session-service.js';
4
5
  import { getAllPersistedSnapshots } from './usage/db.js';
5
6
  // ── State ──────────────────────────────────────────────────────────────────────
6
7
  /** Maps each WS client to the set of workspaceIds they are subscribed to */
@@ -11,6 +12,8 @@ const clients = new Map();
11
12
  const MAX_BUFFERED_BYTES = 1024 * 1024;
12
13
  /** A dropped frame must end the connection so its replay cursor cannot skip it. */
13
14
  function sendFrame(ws, message) {
15
+ if (!isNetworkSocketAuthorized(ws))
16
+ return;
14
17
  if (ws.readyState !== 1)
15
18
  return;
16
19
  if (ws.bufferedAmount > MAX_BUFFERED_BYTES) {
@@ -92,6 +95,8 @@ export function handleConnection(ws) {
92
95
  console.error('[ws] usage hydration failed:', err);
93
96
  }
94
97
  ws.on('message', (data) => {
98
+ if (!acceptNetworkSocketMessage(ws))
99
+ return;
95
100
  let msg;
96
101
  try {
97
102
  const parsed = JSON.parse(data.toString());
@@ -67,8 +67,9 @@ export function getKoboHome() {
67
67
  export function ensureKoboHome() {
68
68
  const dir = getKoboHome();
69
69
  if (!fs.existsSync(dir)) {
70
- fs.mkdirSync(dir, { recursive: true });
70
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
71
71
  }
72
+ fs.chmodSync(dir, 0o700);
72
73
  return dir;
73
74
  }
74
75
  /**
@@ -32,8 +32,7 @@ export function formatStartupBanner(options) {
32
32
  if (options.networkEnabled) {
33
33
  for (const lanUrl of options.lanUrls ?? [])
34
34
  lines.push(` LAN: ${lanUrl}`);
35
- if (options.token)
36
- lines.push(` Token: ${options.token}`);
35
+ lines.push(' Authentication required; retrieve the token from local settings.');
37
36
  }
38
37
  const release = parseChangelog(options.changelog ?? '').find((entry) => entry.version === options.version);
39
38
  const highlights = (release?.notes ?? '')
@@ -35,6 +35,12 @@ export const CLAUDE_MODELS = [
35
35
  i18nLabelKey: 'model.sonnet55',
36
36
  i18nDescriptionKey: 'model.sonnet55Description',
37
37
  },
38
+ {
39
+ id: 'claude-haiku-5-5',
40
+ label: 'Haiku 5.5',
41
+ i18nLabelKey: 'model.haiku55',
42
+ i18nDescriptionKey: 'model.haiku55Description',
43
+ },
38
44
  {
39
45
  id: 'claude-haiku-4-5-20251001',
40
46
  label: 'Haiku 4.5',
@@ -0,0 +1 @@
1
+ export {};
@@ -46,6 +46,12 @@ services:
46
46
  traefik.http.routers.kobo.entrypoints: ${TRAEFIK_ENTRYPOINT:-websecure}
47
47
  traefik.http.routers.kobo.tls: "true"
48
48
  traefik.http.routers.kobo.tls.certresolver: ${TRAEFIK_CERTRESOLVER:-letsencrypt}
49
+ traefik.http.routers.kobo.middlewares: kobo-security
50
+ traefik.http.middlewares.kobo-security.headers.stsseconds: "31536000"
51
+ traefik.http.middlewares.kobo-security.headers.contenttypenosniff: "true"
52
+ traefik.http.middlewares.kobo-security.headers.referrerpolicy: no-referrer
53
+ traefik.http.middlewares.kobo-security.headers.customframeoptionsvalue: SAMEORIGIN
54
+ traefik.http.middlewares.kobo-security.headers.contentsecuritypolicy: "frame-ancestors 'self'"
49
55
  traefik.http.services.kobo.loadbalancer.server.port: "3000"
50
56
  # No published HTTP or SSH ports. Traefik handles HTTP and WebSockets.
51
57
  stop_grace_period: 180s
@@ -191,13 +191,21 @@ From `/opt/kobo`:
191
191
  docker network inspect web # use your TRAEFIK_NETWORK value
192
192
  docker compose config --quiet
193
193
  docker compose up -d --wait --wait-timeout 180 kobo
194
- docker compose logs kobo | grep Token
194
+ docker compose exec kobo node -e 'const fs = require("node:fs"); console.log(JSON.parse(fs.readFileSync(process.env.KOBO_HOME + "/settings.json", "utf8")).global.networkAccessToken)'
195
195
  ```
196
196
 
197
- Open `https://kobo.example.com/?token=<printed-token>` on your PC. The browser
198
- remembers the token; treat both that URL and startup logs as credentials.
197
+ Open `https://kobo.example.com/` on your PC and paste the token into the login
198
+ dialog. The retrieval command deliberately displays a secret in your local
199
+ terminal; do not copy its output into logs or shared transcripts. Startup logs
200
+ never contain the token. After upgrading from a version that logged it, rotate
201
+ the token and apply your existing log-retention policy to old copies.
199
202
  `KOBO_NETWORK_ACCESS_PROXY_HOST` is populated from `KOBO_DOMAIN` and must match
200
203
  the public hostname. Both network access and behind-proxy mode are enabled.
204
+ Missing or invalid proxy hostname configuration rejects public traffic. Browser
205
+ origins must match the configured scheme and port (HTTPS is the default).
206
+ The Compose example attaches HSTS and browser security headers at the TLS router;
207
+ same-origin split panes remain supported. Keep the Docker socket unmounted unless
208
+ project workflows need its root-equivalent host access.
201
209
 
202
210
  This example publishes no HTTP or SSH ports. Use SSH to the VPS followed by
203
211
  `docker compose exec kobo kobo-vps-entrypoint --exec bash` for a container shell. The VPS launcher prepares the home permissions, then drops root before starting Node. It does not start the image's optional SSH daemon. `--exec` runs administrative shell/CLI commands as the same non-root user without repeating home migration.
@@ -211,15 +219,19 @@ curl -s -o /dev/null -w '%{http_code}\n' https://kobo.example.com/api/workspaces
211
219
 
212
220
  Expect health JSON with `status: "ok"` and the installed version, then `401`
213
221
  without a token. `/api/health` is intentionally public. Ordinary authenticated
214
- REST requests use `X-Kobo-Token`; the external MCP endpoint also supports Bearer
215
- authentication. No additional Traefik route is needed for WebSocket upgrades.
216
-
217
- Behind-proxy mode also protects loopback requests. Kōbō supplies the current
218
- network token to its internal workspace MCP process when launching Claude or
219
- Codex, so memory, progress bars and other backend calls can authenticate without
220
- manual MCP configuration. Memory and progress calls still require their separate
221
- per-launch capabilities. After rotating the network token, stop and resume active
222
- agent sessions so their MCP processes receive the new token.
222
+ REST scripts use the operator token in `X-Kobo-Token`; browser login creates a
223
+ 12-hour HttpOnly session cookie. External MCP clients require a dedicated key,
224
+ with explicitly granted tools/workspaces, even on loopback. See
225
+ [external MCP credentials](../CONFIGURATION.md#external-llm-access-through-mcp).
226
+ No additional Traefik route is needed for WebSocket upgrades: the browser obtains
227
+ a 60-second, single-use ticket instead of placing the operator token in the URL.
228
+
229
+ Rotating the operator token, disabling network access or changing proxy mode
230
+ revokes browser sessions and existing WebSockets, including terminal connections.
231
+ Managed agent MCP processes use a revocable per-launch capability through a
232
+ loopback-only internal endpoint, not the operator token. Plan/read-only launches
233
+ cannot mutate workspace state; only their narrow progress/report exceptions
234
+ remain. Agent processes are not stopped by browser credential revocation.
223
235
 
224
236
  ## 3. Authenticate and choose a project
225
237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "2.1.25",
3
+ "version": "2.1.28",
4
4
  "description": "Kōbō — local multi-workspace orchestrator for Claude Code and OpenAI Codex, with isolated Git worktrees, live review and optional automation.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -0,0 +1,2 @@
1
+ import{M as e,S as t,dt as n,lt as r,p as i,pt as a,v as o}from"./reactivity.esm-bundler-DY1JkAEA.js";import{C as s,F as c,J as l,L as u,N as d,O as f,P as p,T as m,Y as h,_ as g,d as _,f as v,g as y,j as ee,k as b,l as x,p as S,r as C,u as w,y as T,z as te}from"./runtime-core.esm-bundler-Bqg71Vll.js";import{$ as ne,K as re,M as E,f as ie,z as ae}from"./dom-BtYTvLeN.js";import{l as oe,t as D,u as se}from"./QBtn-BUSdPaf4.js";import{Gt as ce,Jt as O,O as le,_t as ue,at as de,en as fe,j as pe,m as me,mt as he,pn as ge,pt as _e,rn as k,st as ve,un as A,v as ye}from"./index-CpWBGKdM.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-DFWyDWCE.js";import{t as P}from"./QItemSection-MdJ1_ZK7.js";import{t as Ce}from"./QExpansionItem-D-EGL2b3.js";import{a as F,c as we,r as I,t as L}from"./ToolCallItem-DYubi-J0.js";import{t as Te}from"./QScrollArea-BMrqK-xd.js";import{a as R}from"./use-tours-BWkHp34X.js";import{f as Ee,g as z,m as De,p as Oe,u as ke}from"./use-worktree-restore-DUjZr44N.js";import{t as B}from"./QTooltip-Bk8dLUtU.js";import{n as V}from"./render-chat-markdown-CafzCcmu.js";import{n as H}from"./WorkspacePage-K-1ce50d.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:ge},setup(e,{slots:n,attrs:r}){let i,a=t(null),o=x(()=>e.itemsSize>=0&&e.itemsFn!==void 0?Number.parseInt(e.itemsSize,10):Array.isArray(e.items)?e.items.length:0),{virtualScrollSliceRange:c,localResetVirtualScroll:u,padVirtualScroll:f,onVirtualScrollEvt:m}=j({virtualScrollLength:o,getVirtualScrollTarget:y,getVirtualScrollEl:v}),h=x(()=>{if(o.value===0)return[];let t=(e,t)=>({index:c.value.from+t,item:e});return e.itemsFn===void 0?e.items.slice(c.value.from,c.value.to).map(t):e.itemsFn(c.value.from,c.value.to-c.value.from).map(t)}),g=x(()=>`q-virtual-scroll q-virtual-scroll`+(e.virtualScrollHorizontal?`--horizontal`:`--vertical`)+(e.scrollTarget===void 0?` scroll`:``)),_=x(()=>e.scrollTarget===void 0?{tabindex:0}:{});l(o,()=>{u()}),l(()=>e.scrollTarget,()=>{C(),S()});function v(){let e=a.value;return e===null?null:e.$el||e}function y(){return i}function S(){let t=v();t!==null&&(i=A(t,e.scrollTarget),i.addEventListener(`scroll`,m,E.passive))}function C(){i!==void 0&&(i.removeEventListener(`scroll`,m,E.passive),i=void 0)}function w(){let t=f(e.type===`list`?`div`:`tbody`,h.value.map(n.default));return n.before!==void 0&&(t=n.before().concat(t)),ie(n.after,t)}return u(),p(()=>{S()}),b(()=>{S()}),d(()=>{C()}),ee(()=>{C()}),()=>{if(n.default===void 0){console.error(`QVirtualScroll: default scoped slot is required for rendering`);return}return e.type===`__qtable`?Ae({ref:a,class:`q-table__middle `+g.value,tabindex:0},w()):s(je[e.type],{...r,ref:a,class:[r.class,g.value],..._.value},w)}}}),Ne=new Set([`setup`,`cleanup`,`archive`]);function Pe(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:Ne.has(e.sender)||le(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function Fe(e){let t=[],n=null,r=null;for(let i of e){let e=Pe(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`:e===`user`&&i.type===`user`&&i.source?JSON.stringify([`mcp`,i.source.transport,i.source.clientName]):e;!n||r!==o||a?(n={speaker:e,ts:i.ts,items:[i]},r=o,t.push(n),a&&(n=null)):n.items.push(i)}return t}function W(e){switch(e.type){case`text`:return`text:${e.messageId}`;case`thinking`:return`thinking:${e.messageId}:${e.eventIds?.[0]??e.ts??``}`;case`tool`:return`tool:${e.toolCallId}`;case`session`:return`session:${e.kind}:${e.eventIds?.[0]??e.ts??``}`;case`user`:return`user:${e.eventIds?.[0]??`${e.sender}:${e.ts??``}`}`;case`error`:return`error:${e.eventIds?.[0]??`${e.category}:${e.ts??``}`}`}}function Ie(e){let t=e.items[0];return`${e.speaker}:${t?W(t):`empty`}`}function Le(e,t){let n=Math.min(t,e.length)-1;for(let t=n;t>=0;t--)if(e[t]?.speaker===`user`)return t;return-1}function Re(e,t,n){return new Promise(r=>{let i=!1,a=null,o=null,s=e=>{i||(i=!0,a!==null&&clearTimeout(a),a=null,o?.(),r(e))};if(o=l(e,e=>{t(e)&&s(!0)},{immediate:!0}),i){o();return}a=setTimeout(()=>s(!1),n)})}var ze={class:`agent-error-item`},Be={class:`agent-error-item__body`},Ve={class:`agent-error-item__title`},G={class:`agent-error-item__message`},He=R(T({__name:`AgentErrorItem`,props:{item:{}},setup(e){let t=e,{te:n}=fe(),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=ce(e,{signal:n.signal}).then(async e=>{if(!e.ok)throw await O(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}=fe(),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(k,{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`},Z={key:2,class:`turn-actions`},$e={class:`turn-body`},et={class:`ellipsis`},tt={class:`q-px-sm q-pb-sm`},nt={key:0,class:`turn-scroll-top`},rt=R(T({__name:`TurnCard`,props:{turn:{},workspaceId:{},highlighted:{type:Boolean}},emits:[`scrollTo`],setup(t,{emit:i}){let s=t,c=i,{t:l}=fe(),d=de(),f=m(F),p=o(null);function ee(){let e=p.value;if(!e)return;let t=e.closest(`.q-scrollarea`)?.querySelector(`.q-scrollarea__content`);if(!t){e.scrollIntoView({behavior:`smooth`,block:`start`});return}let n=e.getBoundingClientRect().top-t.getBoundingClientRect().top;c(`scrollTo`,Math.max(0,n-8))}let b=x(()=>{switch(s.turn.speaker){case`user`:{let e=s.turn.items[0],t=e?.type===`user`?e.source:void 0;return{label:t?l(`chat.externalLlm`,{name:t.clientName}):l(`chat.you`),accent:`var(--kobo-turn-user)`,badgeClass:`turn-badge-user`}}case`agent`:return{label:l(`chat.agent`),accent:`var(--kobo-turn-agent)`,badgeClass:`turn-badge-agent`};case`system-prompt`:return{label:l(`chat.systemPrompt`),accent:`var(--kobo-text-3)`,badgeClass:`turn-badge-system`};case`session`:return{label:l(`chat.session`),accent:`var(--kobo-text-3)`,badgeClass:`turn-badge-session`};case`script`:{let e=s.turn.items[0],t=e?.type===`user`?e.sender:``;return{label:le(t)?l(`chat.hookScript`,{event:t.slice(5)}):l(t===`archive`?`chat.archiveScript`:t===`setup`?`chat.setupScript`:`chat.cleanupScript`),accent:`var(--kobo-success)`,badgeClass:`turn-badge-script`}}}});function T(e,t=!1){if(!e)return``;let n=new Date(e);return Number.isNaN(n.getTime())?``:n.toLocaleTimeString(void 0,t?{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}:{hour:`2-digit`,minute:`2-digit`})}let re=x(()=>T(s.turn.ts)),E=x(()=>s.turn.items.flatMap(e=>e.eventIds??[]).join(` `)),ie=x(()=>{let e=s.turn.items;if(e.length===0)return null;for(let t=e.length-1;t>=0;t--){let n=e[t].ts;if(n)return n}return null}),ae=x(()=>{let e=s.turn.ts,t=ie.value;if(!t||!e||t===e)return``;let n=new Date(e).getTime(),r=new Date(t).getTime();return Number.isNaN(n)||Number.isNaN(r)||r<=n?``:T(t,r-n<6e4)}),se=x(()=>ae.value!==``);function ce(e){return we(e.origin,Object.values(d.subagents[s.workspaceId]??{}),e.type===`tool`?e.toolCallId:void 0)}function O(e){return e||l(`chat.subagentActivity`)}function ue(e){return(e.type===`text`||e.type===`thinking`||e.type===`tool`)&&e.origin?.kind===`subagent`}let pe=x(()=>{let e=[],t=new Map;for(let n of s.turn.items){if(!ue(n)){e.push({type:`item`,key:W(n),item:n});continue}let r=ce(n),i=r?.toolUseId??n.origin?.toolCallId??n.origin?.threadId??W(n),a=t.get(i);if(a){a.items.push(n);continue}let o={type:`subagent`,key:`subagent:${W(n)}`,items:[n],subagentKey:i,subagentName:r?.description||null};t.set(i,o),e.push(o)}return e});function me(e){e.origin&&f?.({origin:e.origin})}let he=x(()=>s.turn.items.filter(e=>e.type===`tool`).length);return(i,o)=>(u(),S(`div`,{ref_key:`cardEl`,ref:p,class:r([`turn-card`,{"turn-card--user":t.turn.speaker===`user`,"turn-card--highlighted":t.highlighted}]),"data-event-ids":E.value,style:n({"--turn-accent":b.value.accent})},[w(`div`,X,[w(`span`,{class:r([`turn-badge`,b.value.badgeClass])},a(b.value.label),3),re.value?(u(),S(`span`,Ze,a(re.value),1)):v(``,!0),se.value?(u(),S(C,{key:1},[g(oe,{name:`arrow_forward`,size:`10px`,color:`kobo-3`,class:`turn-time-arrow`}),w(`span`,Qe,[y(a(ae.value)+` `,1),g(B,null,{default:h(()=>[y(a(e(l)(`chat.lastUpdatedAt`,{time:ae.value})),1)]),_:1})])],64)):v(``,!0),he.value>0?(u(),S(`span`,Z,` · `+a(e(l)(`chat.nActions`,{n:he.value})),1)):v(``,!0)]),w(`div`,$e,[(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`,et,a(O(n.subagentName)),1)]),_:2},1032,[`title`]),g(N,{caption:``},{default:h(()=>[y(a(e(l)(`chat.nActions`,{n:n.items.length})),1)]),_:2},1024)]),_:2},1024),g(P,{side:``},{default:h(()=>[g(D,{flat:``,round:``,dense:``,size:`sm`,icon:`open_in_new`,"aria-label":e(l)(`chat.openSubagentActivity`),onClick:ne(e=>me(n.items[0]),[`stop`])},{default:h(()=>[g(B,null,{default:h(()=>[y(a(e(l)(`chat.openSubagentActivity`)),1)]),_:1})]),_:1},8,[`aria-label`,`onClick`])]),_:2},1024)]),default:h(()=>[w(`div`,tt,[(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`,nt,[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`]]),it={key:0,class:`activity-feed-switching`},at={key:1,class:`activity-feed-wrap`,"data-tour":`ws-chat`},ot={key:0,role:`alert`,class:`q-pa-sm text-negative`},st={key:1,"data-testid":`history-load-error`,role:`alert`,class:`q-pa-sm text-negative`},ct=[`aria-label`],lt={key:0,class:`activity-feed-compacting`},ut={class:`text-caption text-kobo-2`},dt={key:0,class:`text-center q-py-sm text-caption text-kobo-3`},ft={key:1,class:`activity-feed-empty q-pa-md`},pt={class:`activity-feed-empty__title`},mt={class:`activity-feed-empty__hint`},ht={key:2,class:`q-px-md q-pb-md`},gt=[`title`],_t={class:`ellipsis`},vt={class:`activity-feed-nav-cluster`},yt=300,bt=60,xt=200,St=180,Ct=255,wt=3,Tt=60,Et=R(T({__name:`ActivityFeed`,props:{workspaceId:{}},setup(t){let{t:n}=fe(),r=t,i=ue(),s=pe(),d=de(),m=me(),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(),ge=``,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!==ge&&(le=ke(),ge=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,yt)}):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){et();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<=bt,R&&e.verticalPosition<=xt&&!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=``,Mt.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 ce(`/api/workspaces/${e}/events?${c.toString()}`,{signal:a});if(!l.ok)throw await O(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(he(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=Pt();if(await f(),t!==Pt())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<St,t=Pt();J=requestAnimationFrame(()=>{J=null,t===Pt()&&(Ge=performance.now(),q(e?0:180))})}function qe(e){$(),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;$(),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 ce(`/api/workspaces/${a}/events?${e}`,{signal:n});if(!o.ok)throw await O(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(he(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}),Z=null;function $e(){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 et(){Z===null&&(Z=requestAnimationFrame(()=>{Z=null,$e()}))}function tt(e){return e.length<=Ct?e:`${e.slice(0,252)}...`}let nt=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:tt(n)}:null});function Et(e){X.value=e.index,et()}function Dt(){let e=nt.value;e&&($(),M.value=!1,Y.value?.scrollTo(e.index,`start`),X.value=e.index)}let Ot=o(!1);async function kt(){if(Ot.value)return;$(),M.value=!1,Ot.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{Ot.value=!1}}let Q=0;function At(){return new Promise(e=>requestAnimationFrame(()=>e()))}async function jt(){if(!z||!U.value||Ae.value)return;R=!1;let e=++Q;if(await f(),e!==Q||!z||(Xe.value=j.value?.getScrollTarget()??null,await f(),e!==Q||!z))return;k.value.length>0&&Y.value?.scrollTo(k.value.length-1,`end-force`),await At();let t=-1,n=0;for(let r=0;r<Tt&&n<wt;r++){if(e!==Q||!z)return;await q(0),await At();let r=j.value?.getScroll().verticalSize??0;B=r,n=r===t?n+1:0,t=r}e===Q&&!Ne.value&&(R=!0,z=!1)}function $(){z=!1,!R&&(Q++,R=!0)}let Mt=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 jt(),e||f(()=>{Xe.value=j.value?.getScrollTarget()??null,et()})}),l(Ne,(e,t)=>{!e&&t&&z&&U.value&&jt()}),p(()=>{window.addEventListener(`kobo:focus-history-event`,Ye),f(()=>{Xe.value=j.value?.getScrollTarget()??null,et()}),U.value&&jt(),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([Mt,()=>ie.value.length,()=>A.value.length],async([e,t,n])=>{N.value||z&&e+t+n>0&&await jt()}),l([Qe,()=>k.value.length],()=>{f(et)});let Nt=x(()=>i.liveAppendCountFor(r.workspaceId,E)),Pt=()=>`${r.workspaceId}\u0000${d.selectedSessionId??``}`;l([Nt,Pt],([e,t],[n,r])=>{t===r&&e>n&&M.value&&!N.value&&Ke()}),c(()=>{Q++,z=!1,Ue(),window.removeEventListener(`kobo:focus-history-event`,Ye),J!=null&&(cancelAnimationFrame(J),J=null),Z!==null&&cancelAnimationFrame(Z)}),l([()=>r.workspaceId,()=>d.selectedSessionId],()=>{Q++,z=!0,B=null,M.value=!0,R=!1,It(),U.value&&jt()});let Ft=new Set;async function It(){let e=r.workspaceId,t=b.value;if(!t||Mt.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 ce(`/api/workspaces/${e}/events?session=${encodeURIComponent(t)}&limit=500`,{signal:a});if(!n.ok)throw await O(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(he(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,Pt],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`,it,[g(be,{size:`40px`,color:`primary`})])):(u(),S(`div`,at,[e(d).sessionsErrors[r.workspaceId]?(u(),S(`div`,ot,[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`,st,[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,ct),g(re,{name:`fade`},{default:h(()=>[ee.value?(u(),S(`div`,lt,[g(be,{size:`18px`,color:`primary`}),w(`span`,ut,a(t.$t(`activity.compacting`)),1)])):v(``,!0)]),_:1}),g(Te,{ref_key:`scrollRef`,ref:j,class:`activity-feed-scroll`,onScroll:Pe,onWheelPassive:$,onTouchstartPassive:$,onKeydown:$},{default:h(()=>[N.value?(u(),S(`div`,dt,[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:Et},{default:h(({item:t,index:n})=>[(u(),_(rt,{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`,ft,[g(oe,{name:`forum`,size:`28px`}),w(`div`,pt,a(t.$t(`activity.empty`)),1),w(`div`,mt,a(t.$t(`activity.emptyHint`)),1)])):v(``,!0),A.value.length?(u(),S(`div`,ht,[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),nt.value?(u(),S(`button`,{key:2,type:`button`,class:`activity-feed-last-user`,"data-testid":`latest-user-message`,title:nt.value.content,onClick:Dt},[g(oe,{name:`person`,size:`16px`}),w(`span`,_t,a(nt.value.content),1)],8,gt)):v(``,!0),w(`div`,vt,[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:Ot.value,disable:Ot.value,onClick:kt},null,8,[`title`,`aria-label`,`loading`,`disable`])])]))}}),[[`__scopeId`,`data-v-58324159`]]);export{Et as default};