cli-jaw 2.1.2 → 2.1.3

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 (251) hide show
  1. package/README.md +6 -5
  2. package/dist/bin/cli-jaw.js +14 -2
  3. package/dist/bin/cli-jaw.js.map +1 -1
  4. package/dist/bin/commands/chat-search.js +71 -0
  5. package/dist/bin/commands/chat-search.js.map +1 -0
  6. package/dist/bin/commands/goal.js +48 -3
  7. package/dist/bin/commands/goal.js.map +1 -1
  8. package/dist/bin/commands/history.js +61 -0
  9. package/dist/bin/commands/history.js.map +1 -0
  10. package/dist/bin/commands/lock.js +104 -0
  11. package/dist/bin/commands/lock.js.map +1 -0
  12. package/dist/bin/commands/memory.js +23 -2
  13. package/dist/bin/commands/memory.js.map +1 -1
  14. package/dist/bin/commands/project.js +4 -1
  15. package/dist/bin/commands/project.js.map +1 -1
  16. package/dist/bin/commands/tui/simple-mode.js +0 -1
  17. package/dist/bin/commands/tui/simple-mode.js.map +1 -1
  18. package/dist/lib/upload.js +7 -0
  19. package/dist/lib/upload.js.map +1 -1
  20. package/dist/server.js +127 -19
  21. package/dist/server.js.map +1 -1
  22. package/dist/src/agent/agy-runtime.js +1 -0
  23. package/dist/src/agent/agy-runtime.js.map +1 -1
  24. package/dist/src/agent/agy-transcript-watcher.js +78 -0
  25. package/dist/src/agent/agy-transcript-watcher.js.map +1 -0
  26. package/dist/src/agent/agy-transcript.js +158 -0
  27. package/dist/src/agent/agy-transcript.js.map +1 -0
  28. package/dist/src/agent/args.js +18 -4
  29. package/dist/src/agent/args.js.map +1 -1
  30. package/dist/src/agent/codex-app-client.js +1 -1
  31. package/dist/src/agent/codex-app-client.js.map +1 -1
  32. package/dist/src/agent/cursor-runtime.js +1 -0
  33. package/dist/src/agent/cursor-runtime.js.map +1 -1
  34. package/dist/src/agent/kiro-runtime.js +11 -3
  35. package/dist/src/agent/kiro-runtime.js.map +1 -1
  36. package/dist/src/agent/lifecycle-handler.js +18 -11
  37. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  38. package/dist/src/agent/memory-flush-controller.js +2 -1
  39. package/dist/src/agent/memory-flush-controller.js.map +1 -1
  40. package/dist/src/agent/session-persistence.js +2 -2
  41. package/dist/src/agent/session-persistence.js.map +1 -1
  42. package/dist/src/agent/spawn/queue.js +1 -1
  43. package/dist/src/agent/spawn/queue.js.map +1 -1
  44. package/dist/src/agent/spawn-env.js +1 -1
  45. package/dist/src/agent/spawn-env.js.map +1 -1
  46. package/dist/src/agent/spawn.js +76 -48
  47. package/dist/src/agent/spawn.js.map +1 -1
  48. package/dist/src/agent/watchdog.js +1 -1
  49. package/dist/src/agent/watchdog.js.map +1 -1
  50. package/dist/src/browser/actions.js +0 -8
  51. package/dist/src/browser/actions.js.map +1 -1
  52. package/dist/src/browser/adaptive-fetch/browser-escalation.js +5 -29
  53. package/dist/src/browser/adaptive-fetch/browser-escalation.js.map +1 -1
  54. package/dist/src/browser/adaptive-fetch/browser-runtime.js +5 -14
  55. package/dist/src/browser/adaptive-fetch/browser-runtime.js.map +1 -1
  56. package/dist/src/browser/adaptive-fetch/browser-session.js +14 -28
  57. package/dist/src/browser/adaptive-fetch/browser-session.js.map +1 -1
  58. package/dist/src/browser/adaptive-fetch/challenge-detector.js +4 -26
  59. package/dist/src/browser/adaptive-fetch/challenge-detector.js.map +1 -1
  60. package/dist/src/browser/adaptive-fetch/content-scorer.js +5 -29
  61. package/dist/src/browser/adaptive-fetch/content-scorer.js.map +1 -1
  62. package/dist/src/browser/adaptive-fetch/endpoint-resolvers.js +0 -64
  63. package/dist/src/browser/adaptive-fetch/endpoint-resolvers.js.map +1 -1
  64. package/dist/src/browser/adaptive-fetch/fetcher.js +4 -19
  65. package/dist/src/browser/adaptive-fetch/fetcher.js.map +1 -1
  66. package/dist/src/browser/adaptive-fetch/human-loop.js +0 -26
  67. package/dist/src/browser/adaptive-fetch/human-loop.js.map +1 -1
  68. package/dist/src/browser/adaptive-fetch/index.js +74 -145
  69. package/dist/src/browser/adaptive-fetch/index.js.map +1 -1
  70. package/dist/src/browser/adaptive-fetch/metadata.js +1 -41
  71. package/dist/src/browser/adaptive-fetch/metadata.js.map +1 -1
  72. package/dist/src/browser/adaptive-fetch/output.js +1 -18
  73. package/dist/src/browser/adaptive-fetch/output.js.map +1 -1
  74. package/dist/src/browser/adaptive-fetch/reader-adapters.js +43 -75
  75. package/dist/src/browser/adaptive-fetch/reader-adapters.js.map +1 -1
  76. package/dist/src/browser/adaptive-fetch/safety.js +5 -48
  77. package/dist/src/browser/adaptive-fetch/safety.js.map +1 -1
  78. package/dist/src/browser/adaptive-fetch/third-party-readers.js +0 -11
  79. package/dist/src/browser/adaptive-fetch/third-party-readers.js.map +1 -1
  80. package/dist/src/browser/adaptive-fetch/trace.js +3 -18
  81. package/dist/src/browser/adaptive-fetch/trace.js.map +1 -1
  82. package/dist/src/browser/adaptive-fetch/transforms.js +0 -23
  83. package/dist/src/browser/adaptive-fetch/transforms.js.map +1 -1
  84. package/dist/src/browser/adaptive-fetch/types.js +3 -0
  85. package/dist/src/browser/adaptive-fetch/types.js.map +1 -0
  86. package/dist/src/browser/adaptive-fetch/validators.js +0 -17
  87. package/dist/src/browser/adaptive-fetch/validators.js.map +1 -1
  88. package/dist/src/browser/adaptive-fetch/waf-profiles.js +0 -10
  89. package/dist/src/browser/adaptive-fetch/waf-profiles.js.map +1 -1
  90. package/dist/src/browser/connection.js +1 -1
  91. package/dist/src/browser/connection.js.map +1 -1
  92. package/dist/src/browser/web-ai/chatgpt-response.js +0 -1
  93. package/dist/src/browser/web-ai/chatgpt-response.js.map +1 -1
  94. package/dist/src/browser/web-ai/chatgpt.js +0 -33
  95. package/dist/src/browser/web-ai/chatgpt.js.map +1 -1
  96. package/dist/src/browser/web-ai/diagnostics.js +0 -16
  97. package/dist/src/browser/web-ai/diagnostics.js.map +1 -1
  98. package/dist/src/browser/web-ai/doctor.js.map +1 -1
  99. package/dist/src/browser/web-ai/errors.js +1 -1
  100. package/dist/src/browser/web-ai/errors.js.map +1 -1
  101. package/dist/src/cli/command-context.js.map +1 -1
  102. package/dist/src/cli/commands.js +13 -2
  103. package/dist/src/cli/commands.js.map +1 -1
  104. package/dist/src/cli/compact.js +2 -1
  105. package/dist/src/cli/compact.js.map +1 -1
  106. package/dist/src/cli/handlers/session-handlers.js +34 -0
  107. package/dist/src/cli/handlers/session-handlers.js.map +1 -0
  108. package/dist/src/cli/handlers-project.js.map +1 -1
  109. package/dist/src/cli/handlers-workflows.js +76 -13
  110. package/dist/src/cli/handlers-workflows.js.map +1 -1
  111. package/dist/src/cli/handlers.js +37 -36
  112. package/dist/src/cli/handlers.js.map +1 -1
  113. package/dist/src/cli/registry.js +7 -7
  114. package/dist/src/cli/registry.js.map +1 -1
  115. package/dist/src/core/chat-sessions.js +63 -0
  116. package/dist/src/core/chat-sessions.js.map +1 -0
  117. package/dist/src/core/compact.js +43 -11
  118. package/dist/src/core/compact.js.map +1 -1
  119. package/dist/src/core/config.js +9 -1
  120. package/dist/src/core/config.js.map +1 -1
  121. package/dist/src/core/db.js +48 -15
  122. package/dist/src/core/db.js.map +1 -1
  123. package/dist/src/core/main-session.js +3 -2
  124. package/dist/src/core/main-session.js.map +1 -1
  125. package/dist/src/goal/heartbeat.js +88 -5
  126. package/dist/src/goal/heartbeat.js.map +1 -1
  127. package/dist/src/goal/store.js +38 -3
  128. package/dist/src/goal/store.js.map +1 -1
  129. package/dist/src/manager/lifecycle-store.js.map +1 -1
  130. package/dist/src/manager/lifecycle.js +45 -1
  131. package/dist/src/manager/lifecycle.js.map +1 -1
  132. package/dist/src/manager/preview-link-policy.js +11 -4
  133. package/dist/src/manager/preview-link-policy.js.map +1 -1
  134. package/dist/src/manager/server.js +25 -9
  135. package/dist/src/manager/server.js.map +1 -1
  136. package/dist/src/manager/shutdown.js +8 -3
  137. package/dist/src/manager/shutdown.js.map +1 -1
  138. package/dist/src/memory/indexing.js +49 -1
  139. package/dist/src/memory/indexing.js.map +1 -1
  140. package/dist/src/memory/runtime.js +2 -1
  141. package/dist/src/memory/runtime.js.map +1 -1
  142. package/dist/src/messaging/send.js +66 -7
  143. package/dist/src/messaging/send.js.map +1 -1
  144. package/dist/src/orchestrator/distribute.js +2 -2
  145. package/dist/src/orchestrator/gateway.js +4 -4
  146. package/dist/src/orchestrator/gateway.js.map +1 -1
  147. package/dist/src/orchestrator/pipeline.js +2 -1
  148. package/dist/src/orchestrator/pipeline.js.map +1 -1
  149. package/dist/src/orchestrator/sanitize.js +57 -7
  150. package/dist/src/orchestrator/sanitize.js.map +1 -1
  151. package/dist/src/orchestrator/state-machine.js +15 -8
  152. package/dist/src/orchestrator/state-machine.js.map +1 -1
  153. package/dist/src/prompt/builder.js +171 -86
  154. package/dist/src/prompt/builder.js.map +1 -1
  155. package/dist/src/prompt/templates/a1-system.md +94 -66
  156. package/dist/src/prompt/templates/employee.md +41 -59
  157. package/dist/src/prompt/templates/skills.md +6 -0
  158. package/dist/src/routes/browser.js +3 -3
  159. package/dist/src/routes/browser.js.map +1 -1
  160. package/dist/src/routes/goal.js +38 -2
  161. package/dist/src/routes/goal.js.map +1 -1
  162. package/dist/src/routes/messaging.js +8 -3
  163. package/dist/src/routes/messaging.js.map +1 -1
  164. package/dist/src/routes/orchestrate.js +3 -2
  165. package/dist/src/routes/orchestrate.js.map +1 -1
  166. package/dist/src/routes/quota-agy-reverse.js +6 -0
  167. package/dist/src/routes/quota-agy-reverse.js.map +1 -1
  168. package/dist/src/routes/quota.js +68 -6
  169. package/dist/src/routes/quota.js.map +1 -1
  170. package/dist/src/routes/settings.js +6 -6
  171. package/dist/src/routes/settings.js.map +1 -1
  172. package/dist/src/security/path-guards.js +0 -3
  173. package/dist/src/security/path-guards.js.map +1 -1
  174. package/dist/src/workflows/scope-sandbox.js +17 -5
  175. package/dist/src/workflows/scope-sandbox.js.map +1 -1
  176. package/package.json +1 -1
  177. package/public/css/chat.css +27 -0
  178. package/public/css/orc-state.css +10 -6
  179. package/public/css/variables.css +4 -0
  180. package/public/dist/assets/{Agent-DkH7eoHd.js → Agent-DBKNQ6tp.js} +1 -1
  181. package/public/dist/assets/{FolderPanel-DSkanaGN.js → FolderPanel-DT0fU8f2.js} +1 -1
  182. package/public/dist/assets/{Heartbeat-C3JS6gkF.js → Heartbeat-fsuLzY9c.js} +1 -1
  183. package/public/dist/assets/{Memory-C-EQubN2.js → Memory-CRR8kotI.js} +1 -1
  184. package/public/dist/assets/{ModelProvider-BD2KrypI.js → ModelProvider-DQgISbKw.js} +1 -1
  185. package/public/dist/assets/agent-meta-C4mauPL5.js +1 -0
  186. package/public/dist/assets/app-0OQhPpTG.css +1 -0
  187. package/public/dist/assets/app-CSqIyg9A.js +55 -0
  188. package/public/dist/assets/constants-BHMkzpN_.js +1 -0
  189. package/public/dist/assets/{employees-_A-p_bZg.js → employees-CZdWHH-r.js} +1 -1
  190. package/public/dist/assets/manager-CGTQ5EIm.js +12 -0
  191. package/public/dist/assets/manager-UBunDqMH.css +1 -0
  192. package/public/dist/assets/{memory-D9AUn8fG.js → memory-B3QSmj_e.js} +1 -1
  193. package/public/dist/assets/memory-DOBqiuPc.js +1 -0
  194. package/public/dist/assets/render-J11oxfnl.js +28 -0
  195. package/public/dist/assets/settings-BV_2Bh0_.js +1 -0
  196. package/public/dist/assets/settings-m_cim0ov.js +151 -0
  197. package/public/dist/assets/sidebar-ZBXdBgF7.js +49 -0
  198. package/public/dist/assets/{skills-BDVLIrVT.js → skills-CjaBmIPV.js} +1 -1
  199. package/public/dist/assets/skills-Xm3fl1zr.js +1 -0
  200. package/public/dist/assets/{slash-commands-C5da3q1p.js → slash-commands-71jc07Wf.js} +1 -1
  201. package/public/dist/assets/slash-commands-BO6ssnAz.js +1 -0
  202. package/public/dist/assets/{trace-drawer-5kqBzFMk.js → trace-drawer-Czvf2P2o.js} +1 -1
  203. package/public/dist/assets/ui-CverZJnd.js +1 -0
  204. package/public/dist/assets/ui-qKxy-4p9.js +142 -0
  205. package/public/dist/index.html +2 -2
  206. package/public/dist/manager/index.html +2 -2
  207. package/public/js/constants.ts +4 -4
  208. package/public/js/features/chat-messages.ts +13 -2
  209. package/public/js/features/chat.ts +86 -44
  210. package/public/js/features/media-lightbox.ts +40 -0
  211. package/public/js/features/pending-queue.ts +20 -2
  212. package/public/js/features/settings-cli-status-render.ts +2 -4
  213. package/public/js/features/settings-cli-status.ts +6 -2
  214. package/public/js/features/settings-types.ts +1 -0
  215. package/public/js/features/ui-status.ts +5 -1
  216. package/public/js/main.ts +2 -0
  217. package/public/js/render/markdown.ts +16 -0
  218. package/public/js/ui.ts +8 -4
  219. package/public/js/ws.ts +27 -16
  220. package/public/locales/en.json +1 -0
  221. package/public/locales/ja.json +1 -0
  222. package/public/locales/ko.json +1 -0
  223. package/public/locales/zh.json +1 -0
  224. package/public/manager/src/InstancePreview.tsx +21 -1
  225. package/public/manager/src/SidebarRailRouter.tsx +32 -4
  226. package/public/manager/src/components/InstanceRow.tsx +0 -1
  227. package/public/manager/src/components/WorkbenchHeader.tsx +17 -7
  228. package/public/manager/src/folder-panel/FolderPanel.tsx +13 -2
  229. package/public/manager/src/hooks/useElectronDroppedPaths.ts +89 -0
  230. package/public/manager/src/manager-components.css +34 -0
  231. package/public/manager/src/manager-polish.css +10 -0
  232. package/public/manager/src/panels/desktop-bridge.ts +11 -0
  233. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +4 -4
  234. package/scripts/smoke/agy-transcript-tail-smoke.mjs +89 -0
  235. package/public/dist/assets/agent-meta-B1098B_a.js +0 -1
  236. package/public/dist/assets/app-ByHYOMZE.js +0 -48
  237. package/public/dist/assets/app-CYdhP6Vh.css +0 -1
  238. package/public/dist/assets/constants-s2UJodER.js +0 -1
  239. package/public/dist/assets/manager-B2qEQRxN.css +0 -1
  240. package/public/dist/assets/manager-CR9BA-wO.js +0 -12
  241. package/public/dist/assets/memory-D1RKYvyu.js +0 -1
  242. package/public/dist/assets/render-DFBujF8n.js +0 -28
  243. package/public/dist/assets/settings-B4ZkeaU-.js +0 -1
  244. package/public/dist/assets/settings-D9jTceN0.js +0 -151
  245. package/public/dist/assets/sidebar-DPPRNiTc.js +0 -48
  246. package/public/dist/assets/skills-8nHJkv-r.js +0 -1
  247. package/public/dist/assets/slash-commands-RJWO8wJZ.js +0 -1
  248. package/public/dist/assets/ui-Crnp79bG.js +0 -142
  249. package/public/dist/assets/ui-HtSKByR3.js +0 -1
  250. /package/public/dist/assets/{locale-DT1WRaeJ.js → locale-BHMJIzyw.js} +0 -0
  251. /package/public/dist/assets/{provider-icons-CVVK5xUP.js → provider-icons-Crv6yvlG.js} +0 -0
@@ -12,6 +12,7 @@ import { apiJson } from '../api.js';
12
12
  import { t } from './i18n.js';
13
13
  import { setStatus } from './ui-status.js';
14
14
  import { syncOrchestrateSnapshot } from '../ws.js';
15
+ import { copyText } from './copy-text.js';
15
16
 
16
17
  export interface PendingItem {
17
18
  id: string;
@@ -57,11 +58,13 @@ function renderRow(item: PendingItem): string {
57
58
  const preview = (item.prompt || '').replace(/\s+/g, ' ').trim();
58
59
  const truncated = preview.length > 140 ? preview.slice(0, 140) + '…' : preview;
59
60
  const source = item.source ? `<span class="pending-row-source">${escapeHtml(item.source)}</span>` : '';
61
+ const copyLabel = escapeHtml(t('queue.copy'));
60
62
  const steerLabel = escapeHtml(t('queue.steer'));
61
63
  const deleteLabel = escapeHtml(t('queue.delete'));
62
64
  return `<div class="pending-row" data-pending-id="${escapeHtml(item.id)}" title="${escapeHtml(preview)}">
63
65
  <span class="pending-row-text">${escapeHtml(truncated)}</span>
64
66
  ${source}
67
+ <button class="pending-row-btn pending-row-copy" data-pending-action="copy" title="${copyLabel}" aria-label="${copyLabel}"><span class="pending-btn-content">${ICONS.copy}</span></button>
65
68
  <button class="pending-row-btn pending-row-steer" data-pending-action="steer" data-i18n-title="queue.steer" title="${steerLabel}" aria-label="${steerLabel}"><span class="pending-arm-fill" aria-hidden="true"></span><span class="pending-btn-content"><span class="pending-steer-arrow" aria-hidden="true">↳</span><span class="pending-steer-label">${steerLabel}</span></span></button>
66
69
  <button class="pending-row-btn pending-row-delete" data-pending-action="delete" data-i18n-title="queue.delete" title="${deleteLabel}" aria-label="${deleteLabel}"><span class="pending-arm-fill" aria-hidden="true"></span><span class="pending-btn-content">${ICONS.trash}</span></button>
67
70
  </div>`;
@@ -169,13 +172,28 @@ function handleClick(id: string, action: Action): void {
169
172
  export function initPendingQueue(): void {
170
173
  const host = document.getElementById('pendingQueue');
171
174
  if (!host) return;
172
- host.addEventListener('click', (e) => {
175
+ host.addEventListener('click', async (e) => {
173
176
  const btn = (e.target as HTMLElement)?.closest('[data-pending-action]') as HTMLElement | null;
174
177
  if (!btn) return;
175
178
  const row = btn.closest('.pending-row') as HTMLElement | null;
176
179
  const id = row?.dataset['pendingId'];
177
180
  if (!id) return;
178
- const action: Action = btn.dataset['pendingAction'] === 'steer' ? 'steer' : 'delete';
181
+ const actionStr = btn.dataset['pendingAction'];
182
+ if (actionStr === 'copy') {
183
+ const item = lastItems.find(it => it.id === id);
184
+ if (item?.prompt) {
185
+ const result = await copyText(item.prompt);
186
+ if (result.ok) {
187
+ const content = btn.querySelector('.pending-btn-content');
188
+ if (content) {
189
+ content.innerHTML = ICONS.checkSimple;
190
+ setTimeout(() => { content.innerHTML = ICONS.copy; }, 600);
191
+ }
192
+ }
193
+ }
194
+ return;
195
+ }
196
+ const action: Action = actionStr === 'steer' ? 'steer' : 'delete';
179
197
  handleClick(id, action);
180
198
  });
181
199
  }
@@ -31,12 +31,10 @@ export const QUOTA_SETUP_HINTS: Record<string, QuotaSetupHint> = {
31
31
  ],
32
32
  },
33
33
  grok: {
34
- title: 'Grok Build auth (plan quota not in grok CLI)',
34
+ title: 'Grok login',
35
35
  commands: [
36
- 'grok login --oauth',
37
- 'grok models # verify auth',
36
+ 'progrok login',
38
37
  ],
39
- note: 'Subscription remaining quota: xAI console only. No official grok quota subcommand.',
40
38
  },
41
39
  opencode: {
42
40
  title: 'OpenCode auth + optional plan quota plugin',
@@ -391,12 +391,16 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
391
391
  ? renderSetupHelpMark(name, q)
392
392
  : '';
393
393
 
394
+ const billingLabel = q?.billing?.usedUsd != null && q?.billing?.limitUsd
395
+ ? `<span style="margin-left:auto;font-size:10px;color:var(--text-dim);white-space:nowrap">$${q.billing.usedUsd.toFixed(1)}/$${q.billing.limitUsd}</span>`
396
+ : '';
397
+
394
398
  html += `
395
399
  <div class="settings-group" style="margin-bottom:6px;padding:8px 10px">
396
- <div class="cli-status-row">
400
+ <div class="cli-status-row" style="display:flex;align-items:center">
397
401
  <span class="cli-dot ${dotClass}"></span>
398
402
  <span class="cli-provider-icon" aria-hidden="true">${providerIcon(name) || ''}</span>
399
- <span class="cli-name" style="font-weight:600">${escapeHtml(providerLabel(name))}${quotaHelpMark}</span>${name === 'copilot' ? `<button id="copilotKeychainBtn" style="font-size:9px;margin-left:6px;padding:1px 5px;background:var(--border);color:var(--text-dim);border:1px solid var(--text-dim);border-radius:3px;cursor:pointer;vertical-align:middle;line-height:1" title="${t('copilot.keychainHint')}">${ICONS.key}</button>` : ''}
403
+ <span class="cli-name" style="font-weight:600">${escapeHtml(providerLabel(name))}${quotaHelpMark}</span>${name === 'copilot' ? `<button id="copilotKeychainBtn" style="font-size:9px;margin-left:6px;padding:1px 5px;background:var(--border);color:var(--text-dim);border:1px solid var(--text-dim);border-radius:3px;cursor:pointer;vertical-align:middle;line-height:1" title="${t('copilot.keychainHint')}">${ICONS.key}</button>` : ''}${billingLabel}
400
404
  </div>
401
405
  ${accountLine}
402
406
  ${authHint}
@@ -15,6 +15,7 @@ export interface QuotaEntry {
15
15
  sessionUsageCapable?: boolean;
16
16
  displayTier?: string;
17
17
  delegatedProvider?: string;
18
+ billing?: { usedUsd?: number; limitUsd?: number; percent?: number; periodEnd?: string };
18
19
  sessionUsage?: {
19
20
  contextTokensUsed?: number | null;
20
21
  contextWindowTokens?: number | null;
@@ -9,13 +9,17 @@ export function setStatus(s: string): void {
9
9
  const badge = document.getElementById('statusBadge');
10
10
  const btn = document.getElementById('btnSend');
11
11
  const label = document.getElementById('typingIndicator')?.querySelector('.label') as HTMLElement | null;
12
- state.agentBusy = s === 'running';
12
+ state.agentBusy = s === 'running' || s === 'steering';
13
13
  document.getElementById('typingIndicator')?.classList.toggle('active', state.agentBusy);
14
14
  if (s === 'running') {
15
15
  if (badge) { badge.className = 'status-badge status-running'; badge.textContent = 'running'; }
16
16
  if (btn) { btn.innerHTML = ICONS.stop; btn.title = t('btn.stop'); btn.classList.add('stop-mode'); }
17
17
  if (label) label.textContent = t('status.responding');
18
18
  showSkeleton();
19
+ } else if (s === 'steering') {
20
+ if (badge) { badge.className = 'status-badge status-steering'; badge.textContent = 'steering'; }
21
+ if (btn) { btn.innerHTML = ICONS.stop; btn.title = 'Steering...'; btn.classList.add('stop-mode'); }
22
+ if (label) label.textContent = 'Steering...';
19
23
  } else {
20
24
  if (badge) { badge.className = 'status-badge status-idle'; badge.textContent = 'idle'; }
21
25
  if (btn) { btn.innerHTML = ICONS.send; btn.title = 'Send'; btn.classList.remove('stop-mode'); }
package/public/js/main.ts CHANGED
@@ -90,6 +90,7 @@ import { initPendingQueue } from './features/pending-queue.js';
90
90
  import { initAttentionBadge } from './features/attention-badge.js';
91
91
  import { initHelpDialog } from './features/help-dialog.js';
92
92
  import { initChatSearch, toggleChatSearch, closeChatSearch } from './features/chat-search.js';
93
+ import { initMediaLightbox } from './features/media-lightbox.js';
93
94
 
94
95
  function isLocalPreviewOrigin(origin: string): boolean {
95
96
  if (origin === window.location.origin) return true;
@@ -519,6 +520,7 @@ async function bootstrap(): Promise<void> {
519
520
  bindPerCliControlEvents();
520
521
  initHelpDialog();
521
522
  initChatSearch();
523
+ initMediaLightbox();
522
524
  document.getElementById('chatSearchTrigger')?.addEventListener('click', toggleChatSearch);
523
525
  initAttentionBadge();
524
526
  connect();
@@ -10,6 +10,7 @@ import { unshieldSvgBlocks } from './svg-actions.js';
10
10
  import { highlightCode, ensureHighlightLanguages } from './highlight.js';
11
11
  import { schedulePostRender } from './post-render.js';
12
12
  import { ensureRenderDelegations } from './delegations.js';
13
+ import { API_BASE } from '../api.js';
13
14
 
14
15
  // ── marked.js configuration (ES module — always available) ──
15
16
  let markedReady = false;
@@ -46,6 +47,21 @@ function ensureMarked(): boolean {
46
47
  return `<div class="code-block"><div class="code-header"><span class="code-lang">${langDisplay}</span><button class="code-copy-btn" type="button" aria-label="${escapeHtml(copyLabel)}">${escapeHtml(copyLabel)}</button></div><pre><code class="hljs${lang ? ` language-${escapeHtml(lang)}` : ''}">${highlighted}</code></pre></div>`;
47
48
  };
48
49
 
50
+ // Inline media: rewrite absolute paths to /media/ URL, detect video
51
+ renderer.image = function ({ href, title, text }: { href: string; title?: string | null; text: string }) {
52
+ if (!href) return '';
53
+ const src = (href.startsWith('/') || href.includes('/uploads/'))
54
+ ? `${API_BASE}/media/${encodeURIComponent(href.split('/').pop()!)}`
55
+ : escapeHtml(href);
56
+ const alt = escapeHtml(text || '');
57
+ const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
58
+ const ext = (src.split('.').pop() || '').toLowerCase().split('?')[0] || '';
59
+ if (['mp4', 'webm', 'mov', 'ogg'].includes(ext)) {
60
+ return `<video src="${src}" controls class="chat-inline-video" preload="metadata"${titleAttr}></video>`;
61
+ }
62
+ return `<img src="${src}" alt="${alt}" class="chat-inline-img" loading="lazy"${titleAttr} />`;
63
+ };
64
+
49
65
  renderer.link = function ({ href, title, text }: { href: string; title?: string | null; text: string }) {
50
66
  const safeHref = escapeHtml(href || '');
51
67
  const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
package/public/js/ui.ts CHANGED
@@ -76,6 +76,12 @@ export function cleanupToolActivity(): void {
76
76
  currentStream = null;
77
77
  }
78
78
 
79
+ /** Timestamp of last steer — used to suppress late agent_done after steer. */
80
+ let lastSteerTs = 0;
81
+ export function markSteered(): void { lastSteerTs = Date.now(); }
82
+ export function clearSteer(): void { lastSteerTs = 0; }
83
+ export function isRecentSteer(): boolean { return Date.now() - lastSteerTs < 8000; }
84
+
79
85
  export function showLiveToolActivity(label: string): void {
80
86
  removeSkeleton();
81
87
  if (!state.currentAgentDiv || !state.currentAgentDiv.isConnected) {
@@ -307,11 +313,9 @@ export function finalizeAgent(text: string, toolLog?: ToolLogEntry[]): void {
307
313
  }
308
314
  state.currentAgentDiv.removeAttribute(ACTIVE_RUN_HYDRATED_ATTR);
309
315
  const content = (state.currentAgentDiv as HTMLElement)?.querySelector('.msg-content');
310
- // Live stream is preview-only; agent_done text stays authoritative unless stream is longer.
316
+ // Live stream is preview-only; agent_done text is always authoritative.
311
317
  const streamedText = currentStream ? finalizeStream(currentStream, true) : '';
312
- const finalText = text && streamedText && streamedText.length > text.length
313
- ? streamedText
314
- : (text || streamedText);
318
+ const finalText = text || streamedText;
315
319
  currentStream = null;
316
320
  if (content) content.innerHTML = renderMarkdown(finalText);
317
321
  if (hasTools && state.currentAgentDiv && !hadProcessBlock && !hasAgentToolBlock(state.currentAgentDiv)) {
package/public/js/ws.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // ── WebSocket Connection ──
2
2
  import { state } from './state.js';
3
3
  import { API_BASE } from './api.js';
4
- import { setStatus, updateQueueBadge, addSystemMsg, appendAgentText, finalizeAgent, addMessage, showProcessStep, cleanupToolActivity, showLiveToolActivity, applyQueuedOverlay, hydrateActiveRun, reconcileChatBottomAfterRestore, showChatRestoreIndicator } from './ui.js';
4
+ import { setStatus, updateQueueBadge, addSystemMsg, appendAgentText, finalizeAgent, addMessage, showProcessStep, cleanupToolActivity, applyQueuedOverlay, hydrateActiveRun, reconcileChatBottomAfterRestore, showChatRestoreIndicator, markSteered, clearSteer, isRecentSteer } from './ui.js';
5
5
  import { renderPendingQueue } from './features/pending-queue.js';
6
6
  import { t, getLang } from './features/i18n.js';
7
7
  import { getVirtualScroll } from './virtual-scroll.js';
@@ -107,6 +107,8 @@ interface WsMessage {
107
107
  exitCode?: number;
108
108
  error?: string;
109
109
  message?: string;
110
+ steered?: boolean;
111
+ steerWaitMs?: number;
110
112
  }
111
113
 
112
114
  // Agent phase state (populated by agent_status events from orchestrator)
@@ -134,9 +136,10 @@ async function refreshRuntimeSnapshot(options: { hydrateRun?: boolean } = {}): P
134
136
  renderPendingQueue(snap.queued || []);
135
137
  if (options.hydrateRun) hydrateActiveRun(snap.activeRun);
136
138
  hydrateGoalState();
137
- setStatus(snap.runtime.busy ? 'running' : 'idle');
138
- if (snap.runtime.busy && (snap.activeRun?.cli === 'agy' || snap.activeRun?.cli === 'kiro-code')) {
139
- showLiveToolActivity(`${providerLabel(snap.activeRun.cli)} working...`);
139
+ if (snap.runtime.busy) {
140
+ setStatus('running');
141
+ } else if (!isRecentSteer()) {
142
+ setStatus('idle');
140
143
  }
141
144
  import('./features/employees.js').then(m => {
142
145
  if (typeof m.renderEmployees === 'function') m.renderEmployees();
@@ -379,9 +382,9 @@ function renderInterviewPanel(interview: { known: (string | InterviewEvidenceVie
379
382
  <span class="iv-summary">Interview · <span class="iv-known-count">Known ${interview.known.length}</span> · <span class="iv-unknown-count">Unknown ${interview.unknown.length}</span>${interview.known.filter(k => typeof k === 'object' && k && 'source' in k && (k as InterviewEvidenceView).source === 'assumption').length > 0 ? ` · <span class="iv-assumption-count">⚠️ ${interview.known.filter(k => typeof k === 'object' && k && 'source' in k && (k as InterviewEvidenceView).source === 'assumption').length} assumptions</span>` : ''} · Round ${interview.round}</span>
380
383
  </button>
381
384
  <div class="iv-body" id="interviewBody">
382
- ${interview.assessment ? renderDimensionBars(interview.assessment) : ''}
383
- <div class="iv-section"><strong>Known (${interview.known.length})</strong><ul>${knownHtml}</ul></div>
384
- <div class="iv-section"><strong>Unknown (${interview.unknown.length})</strong><ul>${unknownHtml}</ul></div>
385
+ ${interview.assessment ? `<div class="iv-section iv-section-ambiguity">${renderDimensionBars(interview.assessment)}</div>` : ''}
386
+ <div class="iv-section iv-section-known"><strong>Known (${interview.known.length})</strong><ul>${knownHtml}</ul></div>
387
+ <div class="iv-section iv-section-unknown"><strong>Unknown (${interview.unknown.length})</strong><ul>${unknownHtml}</ul></div>
385
388
  </div>
386
389
  `;
387
390
  if (!panel.dataset['toggleBound']) {
@@ -580,16 +583,12 @@ export function connect(): void {
580
583
  return;
581
584
  }
582
585
  if (msg.type === 'agent_status') {
586
+ if (!msg.running && isRecentSteer()) return;
587
+ if (msg.running && isRecentSteer()) clearSteer();
583
588
  if (msg.running !== undefined) {
584
589
  setStatus(msg.running ? 'running' : 'idle');
585
- if (msg.running && (msg.cli === 'agy' || msg.cli === 'kiro-code')) {
586
- showLiveToolActivity(`${providerLabel(msg.cli)} working...`);
587
- }
588
590
  } else {
589
591
  setStatus(msg.status || 'idle');
590
- if (msg.status === 'running' && (msg.cli === 'agy' || msg.cli === 'kiro-code')) {
591
- showLiveToolActivity(`${providerLabel(msg.cli)} working...`);
592
- }
593
592
  }
594
593
  // Track per-agent phase for badge rendering
595
594
  if (msg.agentId && msg.phase) {
@@ -617,6 +616,7 @@ export function connect(): void {
617
616
  addSystemMsg(t('ws.roundRetry', { round: msg.round || 0 }));
618
617
  }
619
618
  } else if (msg.type === 'agent_tool') {
619
+ if (isRecentSteer()) return;
620
620
  const stepType = msg.toolType === 'thinking' ? 'thinking'
621
621
  : msg.toolType === 'search' ? 'search'
622
622
  : msg.toolType === 'subagent' ? 'subagent' : 'tool';
@@ -638,6 +638,7 @@ export function connect(): void {
638
638
  startTime: Date.now(),
639
639
  });
640
640
  } else if (msg.type === 'agent_output' || msg.type === 'agent_chunk') {
641
+ if (isRecentSteer()) return;
641
642
  appendAgentText(msg.text || '');
642
643
  } else if (msg.type === 'agent_retry') {
643
644
  const retryDelay = Number(msg.delay ?? 0);
@@ -667,8 +668,13 @@ export function connect(): void {
667
668
  const evt = msg.event;
668
669
  if (evt) applyWorkflowEvent(evt);
669
670
  } else if (msg.type === 'agent_done') {
670
- finalizeAgent(msg.text || '', msg.toolLog);
671
- notifyUnreadResponse();
671
+ if (msg.steered || isRecentSteer()) {
672
+ // Suppress agent_done from steered (killed) process.
673
+ // Server sets steered:true; isRecentSteer is fallback for edge cases.
674
+ } else {
675
+ finalizeAgent(msg.text || '', msg.toolLog);
676
+ notifyUnreadResponse();
677
+ }
672
678
  } else if (msg.type === 'orchestrate_done') {
673
679
  finalizeAgent(msg.text || '');
674
680
  notifyUnreadResponse();
@@ -706,7 +712,9 @@ export function connect(): void {
706
712
  } else if (msg.type === 'memory_status') {
707
713
  import('./features/memory.js').then(m => m.refreshMemorySidebar());
708
714
  } else if (msg.type === 'steer_started') {
709
- setStatus('running');
715
+ markSteered();
716
+ finalizeAgent('');
717
+ setStatus('steering');
710
718
  } else if (msg.type === 'new_message' && (msg.source === 'telegram' || msg.source === 'discord' || msg.fromQueue === true)) {
711
719
  addMessage(msg.role === 'assistant' ? 'agent' : (msg.role || 'user'), msg.content || '', msg.cli);
712
720
  } else if (msg.type === 'system_notice') {
@@ -725,6 +733,9 @@ export function connect(): void {
725
733
  addSystemMsg(`⚠️ Goal continuation failed: ${escapeHtml(msg.error || '')}`, 'tool-activity');
726
734
  } else if (msg.type === 'settings_change') {
727
735
  syncOrchestrateSnapshot('settings_change').catch(() => {});
736
+ } else if (msg.type === 'session_switched' || msg.type === 'session_created') {
737
+ // Reload messages for the new active session
738
+ window.location.reload();
728
739
  }
729
740
  };
730
741
  state.ws.onopen = () => {
@@ -267,6 +267,7 @@
267
267
  "btn.attach": "Attach file",
268
268
  "input.placeholder": "Type a message...",
269
269
  "queue.pendingTitle": "Pending",
270
+ "queue.copy": "Copy prompt",
270
271
  "queue.delete": "Remove",
271
272
  "queue.steer": "Run now (steer)",
272
273
  "queue.cancelArm": "Click again to cancel",
@@ -256,6 +256,7 @@
256
256
  "btn.attach": "ファイルを添付",
257
257
  "input.placeholder": "メッセージを入力...",
258
258
  "queue.pendingTitle": "待機中",
259
+ "queue.copy": "コピー",
259
260
  "queue.delete": "削除",
260
261
  "queue.steer": "今すぐ実行(steer)",
261
262
  "queue.cancelArm": "もう一度クリックしてキャンセル",
@@ -267,6 +267,7 @@
267
267
  "btn.attach": "파일 첨부",
268
268
  "input.placeholder": "메시지 입력...",
269
269
  "queue.pendingTitle": "대기 중",
270
+ "queue.copy": "복사",
270
271
  "queue.delete": "삭제",
271
272
  "queue.steer": "지금 실행 (steer)",
272
273
  "queue.cancelArm": "취소 (3초 안에 한 번 더 누르면 취소)",
@@ -256,6 +256,7 @@
256
256
  "btn.attach": "附加文件",
257
257
  "input.placeholder": "输入消息...",
258
258
  "queue.pendingTitle": "待处理",
259
+ "queue.copy": "复制",
259
260
  "queue.delete": "移除",
260
261
  "queue.steer": "立即运行(steer)",
261
262
  "queue.cancelArm": "再次点击取消",
@@ -13,6 +13,7 @@ type InstancePreviewProps = {
13
13
  theme: PreviewTheme;
14
14
  onOpenNotesFromPreview?: (path: string) => void;
15
15
  onOpenDocFromPreview?: (absolutePath: string) => void;
16
+ onPreviewDroppedFiles?: (files: File[]) => void;
16
17
  };
17
18
 
18
19
  type PreviewOpenNotesMessage = {
@@ -26,6 +27,11 @@ type PreviewSendMessage = {
26
27
  prompt?: unknown;
27
28
  };
28
29
 
30
+ type PreviewDroppedFilesMessage = {
31
+ type?: unknown;
32
+ files?: unknown;
33
+ };
34
+
29
35
  function normalizeLoopbackHostname(hostname: string): string {
30
36
  return hostname.replace(/^\[|\]$/g, '').toLowerCase();
31
37
  }
@@ -144,6 +150,11 @@ function isPreviewSttShortcut(event: KeyboardEvent): boolean {
144
150
  return primarySpace || fallbackMic;
145
151
  }
146
152
 
153
+ function extractDroppedFiles(data: PreviewDroppedFilesMessage | null): File[] {
154
+ if (!data || data.type !== 'jaw-preview-dropped-files' || !Array.isArray(data.files)) return [];
155
+ return data.files.filter((item): item is File => item instanceof File);
156
+ }
157
+
147
158
  export function InstancePreview(props: InstancePreviewProps) {
148
159
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
149
160
  const loadedSrcRef = useRef<string | null>(null);
@@ -224,12 +235,21 @@ export function InstancePreview(props: InstancePreviewProps) {
224
235
  props.onOpenDocFromPreview?.(path);
225
236
  }
226
237
  window.addEventListener('message', onPreviewOpenDoc);
238
+ function onPreviewDroppedFiles(event: MessageEvent): void {
239
+ if (event.source !== iframeRef.current?.contentWindow) return;
240
+ if (!state.src || !previewFrameOriginMatches(event.origin, state.src, iframeRef.current)) return;
241
+ const files = extractDroppedFiles(event.data as PreviewDroppedFilesMessage | null);
242
+ if (files.length === 0) return;
243
+ props.onPreviewDroppedFiles?.(files);
244
+ }
245
+ window.addEventListener('message', onPreviewDroppedFiles);
227
246
  return () => {
228
247
  window.removeEventListener('message', onPreviewSend);
229
248
  window.removeEventListener('message', onPreviewOpenNotes);
230
249
  window.removeEventListener('message', onPreviewOpenDoc);
250
+ window.removeEventListener('message', onPreviewDroppedFiles);
231
251
  };
232
- }, [props.enabled, props.instance, props.onOpenNotesFromPreview, props.onOpenDocFromPreview, state.canPreview, state.src]);
252
+ }, [props.enabled, props.instance, props.onOpenNotesFromPreview, props.onOpenDocFromPreview, props.onPreviewDroppedFiles, state.canPreview, state.src]);
233
253
 
234
254
  useEffect(() => {
235
255
  if (!props.active || !props.enabled || !state.canPreview || !state.src) return undefined;
@@ -1,4 +1,4 @@
1
- import { useState, type ReactNode, Suspense } from 'react';
1
+ import { useCallback, useState, type ReactNode, Suspense } from 'react';
2
2
  import { ActivityDock } from './components/ActivityDock';
3
3
  import { InstanceDrawer } from './components/InstanceDrawer';
4
4
  import { InstanceNavigator } from './components/InstanceNavigator';
@@ -31,6 +31,12 @@ import { DashboardScheduleWorkspace } from './dashboard-schedule/DashboardSchedu
31
31
  import { DashboardRemindersSidebar, type RemindersView } from './dashboard-reminders/DashboardRemindersSidebar';
32
32
  import { DashboardRemindersWorkspace } from './dashboard-reminders/DashboardRemindersWorkspace';
33
33
  import { useRemindersFeed } from './dashboard-reminders/useRemindersFeed';
34
+ import {
35
+ firstDirectory,
36
+ firstFile,
37
+ useElectronDroppedPaths,
38
+ type ElectronDroppedPathsEvent,
39
+ } from './hooks/useElectronDroppedPaths';
34
40
  import type { HelpTopicId } from './help/helpContent';
35
41
  import { NotesCommandPalette } from './notes/NotesCommandPalette';
36
42
  import { NotesCommandProvider } from './notes/notes-command-registry';
@@ -140,6 +146,7 @@ type Props = {
140
146
  function renderRightPanelContent(
141
147
  mode: RightPanelMode,
142
148
  previewFilePath: string | null,
149
+ folderRootPath: string | null,
143
150
  onPreviewFile: (path: string) => void,
144
151
  selectedInstance: DashboardInstance | null,
145
152
  dashboardSettingsUi: DashboardRegistryUi,
@@ -149,7 +156,7 @@ function renderRightPanelContent(
149
156
  const fallback = <div style={{ padding: '12px', color: 'var(--text-dim)', fontSize: '12px' }}>Loading...</div>;
150
157
  switch (mode) {
151
158
  case 'diff': return <Suspense fallback={fallback}><DiffPanel selectedInstance={selectedInstance} settings={dashboardSettingsUi} onSettingsPatch={onDashboardSettingsPatch} /></Suspense>;
152
- case 'folder': return <Suspense fallback={fallback}><FolderPanel selectedFilePath={previewFilePath} notesTree={notesModel.tree} notesRoot={notesModel.notesRoot} onPreviewFile={onPreviewFile} /></Suspense>;
159
+ case 'folder': return <Suspense fallback={fallback}><FolderPanel selectedFilePath={previewFilePath} externalRootPath={folderRootPath} notesTree={notesModel.tree} notesRoot={notesModel.notesRoot} onPreviewFile={onPreviewFile} /></Suspense>;
153
160
  case 'doc': return <Suspense fallback={fallback}><DocPanel filePath={previewFilePath ?? undefined} /></Suspense>;
154
161
  case 'browser': return <Suspense fallback={fallback}><BrowserPanel /></Suspense>;
155
162
  default: return null;
@@ -168,6 +175,8 @@ function renderBottomTabContent(tab: BottomPanelTab, controls: BottomPanelRender
168
175
  export function SidebarRailRouter(props: Props) {
169
176
  const panelLayout = usePanelLayout();
170
177
  const [rightPreviewFilePath, setRightPreviewFilePath] = useState<string | null>(null);
178
+ const [rightFolderRootPath, setRightFolderRootPath] = useState<string | null>(null);
179
+ const [, setRecentDroppedPaths] = useState<ElectronDroppedPathsEvent | null>(null);
171
180
  const [remindersView, setRemindersView] = useState<RemindersView>('matrix');
172
181
  const remindersFeed = useRemindersFeed({ active: props.sidebarMode === 'reminders' });
173
182
  const desktopPanelsAvailable = currentManagerSurface() === 'electron';
@@ -184,6 +193,25 @@ export function SidebarRailRouter(props: Props) {
184
193
  panelLayout.dispatch({ type: 'OPEN_RIGHT_PANEL', mode: 'doc', slot: 'bottom' });
185
194
  }
186
195
 
196
+ const handleDroppedPaths = useCallback((event: ElectronDroppedPathsEvent): void => {
197
+ setRecentDroppedPaths(event);
198
+ if (event.source === 'preview') return;
199
+ const directory = firstDirectory(event.entries);
200
+ if (directory) {
201
+ setRightFolderRootPath(directory.path);
202
+ panelLayout.dispatch({ type: 'OPEN_RIGHT_PANEL', mode: 'folder', slot: 'top' });
203
+ return;
204
+ }
205
+ const file = firstFile(event.entries);
206
+ if (file) handleRightPreviewFile(file.path);
207
+ }, [panelLayout]);
208
+
209
+ const electronDrop = useElectronDroppedPaths({ onDroppedPaths: handleDroppedPaths });
210
+
211
+ const handlePreviewDroppedFiles = useCallback((files: File[]): void => {
212
+ void electronDrop.resolveDroppedFiles(files, 'preview');
213
+ }, [electronDrop]);
214
+
187
215
  return (
188
216
  <NotesCommandProvider>
189
217
  {props.jawCeoVoiceOverlay}
@@ -195,7 +223,7 @@ export function SidebarRailRouter(props: Props) {
195
223
  onCloseDrawer={props.onCloseDrawer}
196
224
  rightPanelOpen={rightPanelOpen}
197
225
  rightPanelWidth={panelLayout.state.rightPanel.width}
198
- rightPanelContent={rightPanelOpen ? <RightSidebar renderPanel={mode => renderRightPanelContent(mode, rightPreviewFilePath, handleRightPreviewFile, props.selectedInstance, props.dashboardSettingsUi, props.onDashboardSettingsPatch, props.notesModel)} /> : undefined}
226
+ rightPanelContent={rightPanelOpen ? <RightSidebar renderPanel={mode => renderRightPanelContent(mode, rightPreviewFilePath, rightFolderRootPath, handleRightPreviewFile, props.selectedInstance, props.dashboardSettingsUi, props.onDashboardSettingsPatch, props.notesModel)} /> : undefined}
199
227
  bottomPanelOpen={bottomPanelOpen}
200
228
  bottomPanelHeight={panelLayout.state.bottomPanel.height}
201
229
  bottomPanelContent={bottomPanelOpen && panelLayout.state.bottomPanel.tabs.length > 0 ? <BottomPanel renderTab={renderBottomTabContent} /> : undefined}
@@ -243,7 +271,7 @@ export function SidebarRailRouter(props: Props) {
243
271
  <div className="workspace-surface-layer">
244
272
  <WorkspaceSurface active={props.sidebarMode === 'instances'}>
245
273
  <Workbench mode={props.activeDetailTab} onModeChange={props.onDetailTabChange} header={props.workbenchHeader} modeActions={props.jawCeoWorkbenchButton} overview={props.detailContent('overview')} preview={(
246
- <InstancePreview instance={props.selectedInstance} data={props.data} enabled={props.previewEnabled} active={props.sidebarMode === 'instances' && props.activeDetailTab === 'preview'} refreshKey={props.previewRefreshKey} theme={props.previewTheme} {...(props.onOpenNotesFromPreview ? { onOpenNotesFromPreview: props.onOpenNotesFromPreview } : {})} onOpenDocFromPreview={handleRightPreviewFile} />
274
+ <InstancePreview instance={props.selectedInstance} data={props.data} enabled={props.previewEnabled} active={props.sidebarMode === 'instances' && props.activeDetailTab === 'preview'} refreshKey={props.previewRefreshKey} theme={props.previewTheme} {...(props.onOpenNotesFromPreview ? { onOpenNotesFromPreview: props.onOpenNotesFromPreview } : {})} onOpenDocFromPreview={handleRightPreviewFile} onPreviewDroppedFiles={handlePreviewDroppedFiles} />
247
275
  )} logs={props.detailContent('logs')} settings={props.detailContent('settings')} />
248
276
  </WorkspaceSurface>
249
277
  <WorkspaceSurface active={props.sidebarMode === 'notes'}>
@@ -67,7 +67,6 @@ export function InstanceRow(props: InstanceRowProps) {
67
67
  const transitionLabel = props.transitioning ? TRANSITION_LABELS[props.transitioning] : null;
68
68
  const dotClass = `${statusClass(props.instance.status)}${transitionLabel ? ' is-transitioning' : ''}${props.agentBusy ? ' is-busy' : ''}`;
69
69
  const primaryLabel = props.instance.label || props.profile?.label || props.label;
70
-
71
70
  async function submitLabel(event: FormEvent<HTMLFormElement>): Promise<void> {
72
71
  event.preventDefault();
73
72
  event.stopPropagation();
@@ -11,27 +11,37 @@ type WorkbenchHeaderProps = {
11
11
  onOpenHelpTopic?: (topic: HelpTopicId) => void;
12
12
  };
13
13
 
14
+ function compactPath(path: string): string {
15
+ const parts = path.split(/[/\\]/).filter(Boolean);
16
+ if (parts.length <= 2) return path;
17
+ return `…/${parts.slice(-2).join('/')}`;
18
+ }
19
+
14
20
  export function WorkbenchHeader(props: WorkbenchHeaderProps) {
15
21
  const instance = props.instance;
16
22
  const canPreview = Boolean(instance?.ok);
17
23
  const previewLabel = props.previewEnabled ? 'Preview on' : 'Preview off';
18
24
  const hasActions = Boolean(instance || props.onOpenHelpTopic);
25
+ const projectDirs = instance?.projectDirs?.filter(Boolean) ?? [];
19
26
 
20
27
  return (
21
28
  <div className="detail-header">
22
29
  <div>
23
30
  <p className="eyebrow">Selected instance</p>
24
31
  <h2>{instance ? instanceLabel(instance) : 'No instance selected'}</h2>
25
- <span>{instance?.workingDir || instance?.url || 'Select an online instance to inspect it.'}</span>
26
- {instance?.projectDirs && instance.projectDirs.length > 0 && (
27
- <div className="project-dirs">
28
- <span className="label">Projects:</span>
29
- {instance.projectDirs.map((dir, i) => (
32
+ {instance ? (
33
+ <div className={`project-dirs${projectDirs.length ? '' : ' is-empty'}`}>
34
+ <span className="label">Project</span>
35
+ {projectDirs.length ? projectDirs.map((dir, i) => (
30
36
  <span key={i} className="project-dir" title={dir}>
31
- {dir.split(/[/\\]/).slice(-2).join('/')}
37
+ {compactPath(dir)}
32
38
  </span>
33
- ))}
39
+ )) : (
40
+ <span className="project-dir">Not set</span>
41
+ )}
34
42
  </div>
43
+ ) : (
44
+ <span>Select an online instance to inspect it.</span>
35
45
  )}
36
46
  </div>
37
47
  {hasActions && (
@@ -10,6 +10,7 @@ function getFolderBridge(): FolderBridgeApi | null {
10
10
 
11
11
  type FolderPanelProps = {
12
12
  selectedFilePath?: string | null | undefined;
13
+ externalRootPath?: string | null | undefined;
13
14
  notesTree?: NotesTreeEntry[] | undefined;
14
15
  notesRoot?: string | null | undefined;
15
16
  onPreviewFile?: ((path: string) => void) | undefined;
@@ -73,7 +74,7 @@ export function FolderPanel(props: FolderPanelProps) {
73
74
  }, [loadDir, rootPath, source]);
74
75
 
75
76
  useEffect(() => {
76
- if (rootPath !== null) return;
77
+ if (rootPath !== null || props.externalRootPath) return;
77
78
  let cancelled = false;
78
79
  void (async () => {
79
80
  const nextRoot = await source.getDefaultRoot();
@@ -82,7 +83,17 @@ export function FolderPanel(props: FolderPanelProps) {
82
83
  await loadDir(nextRoot);
83
84
  })();
84
85
  return () => { cancelled = true; };
85
- }, [loadDir, rootPath, source]);
86
+ }, [loadDir, props.externalRootPath, rootPath, source]);
87
+
88
+ useEffect(() => {
89
+ const externalRoot = props.externalRootPath;
90
+ if (!externalRoot || externalRoot === rootPath) return;
91
+ if (rootPath && source.unwatchDir) void source.unwatchDir(rootPath);
92
+ setRootPath(externalRoot);
93
+ setExpanded(new Set());
94
+ setChildrenCache(new Map());
95
+ void loadDir(externalRoot);
96
+ }, [loadDir, props.externalRootPath, rootPath, source]);
86
97
 
87
98
  useEffect(() => {
88
99
  if (!source.watchDir || !source.onDirChange || rootPath === null) return;