cli-jaw 2.0.3 → 2.0.4

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 (99) hide show
  1. package/README.ja.md +6 -2
  2. package/README.ko.md +6 -2
  3. package/README.md +6 -2
  4. package/dist/src/agent/alert-escalation.js +12 -1
  5. package/dist/src/agent/alert-escalation.js.map +1 -1
  6. package/dist/src/agent/error-classifier.js +14 -8
  7. package/dist/src/agent/error-classifier.js.map +1 -1
  8. package/dist/src/agent/lifecycle-handler.js +81 -4
  9. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  10. package/dist/src/agent/session-persistence.js +2 -0
  11. package/dist/src/agent/session-persistence.js.map +1 -1
  12. package/dist/src/agent/spawn.js +70 -9
  13. package/dist/src/agent/spawn.js.map +1 -1
  14. package/dist/src/browser/connection.js +69 -15
  15. package/dist/src/browser/connection.js.map +1 -1
  16. package/dist/src/browser/runtime-diagnostics.js +39 -0
  17. package/dist/src/browser/runtime-diagnostics.js.map +1 -1
  18. package/dist/src/cli/compact.js +5 -1
  19. package/dist/src/cli/compact.js.map +1 -1
  20. package/dist/src/core/compact.js +5 -1
  21. package/dist/src/core/compact.js.map +1 -1
  22. package/dist/src/manager/lifecycle.js +1 -1
  23. package/dist/src/manager/lifecycle.js.map +1 -1
  24. package/dist/src/manager/notes/routes.js +7 -0
  25. package/dist/src/manager/notes/routes.js.map +1 -1
  26. package/dist/src/manager/notes/search.js +282 -0
  27. package/dist/src/manager/notes/search.js.map +1 -0
  28. package/dist/src/manager/preview-origin-proxy.js +12 -2
  29. package/dist/src/manager/preview-origin-proxy.js.map +1 -1
  30. package/dist/src/memory/bootstrap.js +8 -1
  31. package/dist/src/memory/bootstrap.js.map +1 -1
  32. package/dist/src/memory/indexing.js +221 -134
  33. package/dist/src/memory/indexing.js.map +1 -1
  34. package/dist/src/memory/keyword-expand.js +26 -4
  35. package/dist/src/memory/keyword-expand.js.map +1 -1
  36. package/dist/src/memory/reflect.js +119 -8
  37. package/dist/src/memory/reflect.js.map +1 -1
  38. package/dist/src/memory/synonyms.js +60 -0
  39. package/dist/src/memory/synonyms.js.map +1 -0
  40. package/dist/src/orchestrator/gateway.js +1 -1
  41. package/dist/src/orchestrator/gateway.js.map +1 -1
  42. package/dist/src/orchestrator/pipeline.js +8 -3
  43. package/dist/src/orchestrator/pipeline.js.map +1 -1
  44. package/dist/src/orchestrator/state-machine.js +12 -2
  45. package/dist/src/orchestrator/state-machine.js.map +1 -1
  46. package/package.json +1 -1
  47. package/public/dist/assets/{MilkdownWysiwygEditor-Cm3uXfWf.js → MilkdownWysiwygEditor-DIebNZF7.js} +1 -1
  48. package/public/dist/assets/{app-Be58Cs3Y.js → app-DJ8ys0j5.js} +4 -4
  49. package/public/dist/assets/{employees-CxdghzoD.js → employees-RJ_wRL09.js} +1 -1
  50. package/public/dist/assets/insert-image-markdown-kk053MvN.js +22 -0
  51. package/public/dist/assets/manager-DAe38I94.js +25 -0
  52. package/public/dist/assets/{manager-DEiyrWDP.css → manager-fQR46YFa.css} +1 -1
  53. package/public/dist/assets/{memory-CsMNkYtv.js → memory-dJGp6QBv.js} +1 -1
  54. package/public/dist/assets/memory-w3yQettQ.js +1 -0
  55. package/public/dist/assets/{render-DGQX46ei.js → render-KVGsbWj1.js} +1 -1
  56. package/public/dist/assets/{settings-BH213Yv3.js → settings-C7QWaUHB.js} +1 -1
  57. package/public/dist/assets/settings-DmUCo6lz.js +1 -0
  58. package/public/dist/assets/{skills-CQtCtHPA.js → skills-CHkTgM7L.js} +1 -1
  59. package/public/dist/assets/skills-SxG_nfwn.js +1 -0
  60. package/public/dist/assets/{slash-commands-Dzk1xHWS.js → slash-commands-2ThyUGvX.js} +1 -1
  61. package/public/dist/assets/slash-commands-BxJkKdhB.js +1 -0
  62. package/public/dist/assets/{trace-drawer-SRKcfm2S.js → trace-drawer-Dis80M6X.js} +1 -1
  63. package/public/dist/assets/ui-LhD1VfQs.js +1 -0
  64. package/public/dist/assets/ui-kS1ZJfez.js +143 -0
  65. package/public/dist/assets/{ws-CTHQFzM1.js → ws-DVE3eWRj.js} +2 -2
  66. package/public/dist/index.html +1 -1
  67. package/public/dist/manager/index.html +2 -2
  68. package/public/js/features/chat.ts +6 -1
  69. package/public/js/features/process-block.ts +34 -6
  70. package/public/js/features/process-step-match.ts +2 -1
  71. package/public/js/ui.ts +100 -13
  72. package/public/js/virtual-scroll-bootstrap.ts +8 -1
  73. package/public/js/virtual-scroll.ts +83 -13
  74. package/public/js/ws.ts +3 -3
  75. package/public/locales/en.json +2 -1
  76. package/public/locales/ja.json +2 -1
  77. package/public/locales/ko.json +2 -1
  78. package/public/locales/zh.json +2 -1
  79. package/public/manager/src/App.tsx +10 -3
  80. package/public/manager/src/api.ts +17 -0
  81. package/public/manager/src/main.tsx +1 -0
  82. package/public/manager/src/notes/NotesFileTree.tsx +14 -22
  83. package/public/manager/src/notes/NotesSearchSidebar.tsx +118 -0
  84. package/public/manager/src/notes/NotesSidebar.tsx +65 -23
  85. package/public/manager/src/notes/NotesWorkspace.tsx +13 -0
  86. package/public/manager/src/notes/notes-api.ts +1 -0
  87. package/public/manager/src/notes/notes-search.css +90 -0
  88. package/public/manager/src/notes/notes-types.ts +2 -0
  89. package/public/manager/src/preview.ts +20 -1
  90. package/public/manager/src/types.ts +8 -0
  91. package/scripts/install-wsl.sh +48 -14
  92. package/public/dist/assets/insert-image-markdown-DIEa-zjk.js +0 -22
  93. package/public/dist/assets/manager-UEXd1_9T.js +0 -25
  94. package/public/dist/assets/memory-DXad_DPO.js +0 -1
  95. package/public/dist/assets/settings-DXT87G2U.js +0 -1
  96. package/public/dist/assets/skills-5o_1v0nz.js +0 -1
  97. package/public/dist/assets/slash-commands-D4-hrrmh.js +0 -1
  98. package/public/dist/assets/ui-CdRKN2S6.js +0 -141
  99. package/public/dist/assets/ui-n43jmg_f.js +0 -1
@@ -1,11 +1,18 @@
1
1
  import { escapeHtml } from '../render.js';
2
2
  import { ICONS } from '../icons.js';
3
+
4
+ declare global {
5
+ interface Window {
6
+ __jawProcessBlockLayoutMutation?: (anchor: Element | null, mutate: () => void) => void;
7
+ }
8
+ }
3
9
  export interface ProcessStep {
4
10
  id: string;
5
11
  type: 'tool' | 'thinking' | 'search' | 'subagent';
6
12
  icon: string;
7
13
  rawIcon?: string | undefined;
8
14
  label: string;
15
+ isEmployee?: boolean | undefined;
9
16
  detail?: string;
10
17
  detailPreview?: string | undefined;
11
18
  detailLength?: number | undefined;
@@ -36,6 +43,7 @@ export interface StoredProcessStepMeta {
36
43
  icon: string;
37
44
  rawIcon?: string | undefined;
38
45
  label: string;
46
+ isEmployee?: boolean | undefined;
39
47
  stepRef?: string | undefined;
40
48
  traceRunId?: string | undefined; traceSeq?: number | undefined; detailAvailable?: boolean | undefined; detailBytes?: number | undefined; rawRetentionStatus?: string | undefined;
41
49
  status: ProcessStep['status'];
@@ -117,6 +125,7 @@ export function compactProcessStepForStorage(step: ProcessStep): ProcessStep {
117
125
  icon: step.icon,
118
126
  rawIcon: step.rawIcon,
119
127
  label: step.label,
128
+ isEmployee: step.isEmployee,
120
129
  stepRef: step.stepRef,
121
130
  traceRunId: step.traceRunId, traceSeq: step.traceSeq, detailAvailable: step.detailAvailable,
122
131
  detailBytes: step.detailBytes, rawRetentionStatus: step.rawRetentionStatus,
@@ -164,6 +173,7 @@ function updateStoredStepMeta(step: ProcessStep): void {
164
173
  icon: compact.icon,
165
174
  rawIcon: compact.rawIcon,
166
175
  label: compact.label,
176
+ isEmployee: compact.isEmployee,
167
177
  stepRef: compact.stepRef,
168
178
  traceRunId: compact.traceRunId, traceSeq: compact.traceSeq, detailAvailable: compact.detailAvailable,
169
179
  detailBytes: compact.detailBytes, rawRetentionStatus: compact.rawRetentionStatus,
@@ -204,6 +214,9 @@ function renderStep(step: ProcessStep): string {
204
214
  const badgeClass = `process-step-badge ${step.type}`;
205
215
  const badgeText = step.type.toUpperCase();
206
216
  const label = escapeHtml(step.label || step.icon || '');
217
+ const employeeMarker = step.isEmployee
218
+ ? '<span class="process-step-origin" aria-label="Employee tool">(E)</span>'
219
+ : '';
207
220
  const icon = renderTrustedIcon(step.icon);
208
221
  const detail = step.detailPreview || step.detail || '';
209
222
  const detailId = `process-detail-${step.id}`;
@@ -217,6 +230,7 @@ function renderStep(step: ProcessStep): string {
217
230
  data-step-id="${step.id}"
218
231
  data-type="${escapeHtml(step.type)}"
219
232
  data-status="${escapeHtml(step.status)}"
233
+ data-is-employee="${step.isEmployee ? 'true' : ''}"
220
234
  data-step-ref="${escapeHtml(step.stepRef || '')}"
221
235
  data-trace-run-id="${escapeHtml(step.traceRunId || '')}"
222
236
  data-trace-seq="${String(step.traceSeq || '')}"
@@ -226,6 +240,7 @@ function renderStep(step: ProcessStep): string {
226
240
  <span class="process-step-icon" aria-hidden="true">${icon}</span>
227
241
  <span class="${badgeClass}">${badgeText}</span>
228
242
  <span class="process-step-main">
243
+ ${employeeMarker}
229
244
  <span class="process-step-label">${label}</span>
230
245
  ${snippetHtml}
231
246
  </span>
@@ -300,6 +315,15 @@ function toggleStepDetails(toggle: HTMLElement): void {
300
315
  if (chevron) chevron.innerHTML = expanding ? ICONS.chevronDown : ICONS.chevronRight;
301
316
  }
302
317
 
318
+ function withProcessBlockLayoutMutation(anchor: Element | null, mutate: () => void): void {
319
+ const hook = window.__jawProcessBlockLayoutMutation;
320
+ if (typeof hook === 'function') {
321
+ hook(anchor, mutate);
322
+ return;
323
+ }
324
+ mutate();
325
+ }
326
+
303
327
  export function bindProcessBlockInteractions(root: HTMLElement): void {
304
328
  if (root.dataset['processBlockBound'] === '1') return;
305
329
  root.addEventListener('click', (event) => {
@@ -318,7 +342,9 @@ export function bindProcessBlockInteractions(root: HTMLElement): void {
318
342
 
319
343
  const stepToggle = target.closest('.process-step-toggle') as HTMLElement | null;
320
344
  if (stepToggle) {
321
- toggleStepDetails(stepToggle);
345
+ withProcessBlockLayoutMutation(stepToggle.closest('.process-step, .process-block'), () => {
346
+ toggleStepDetails(stepToggle);
347
+ });
322
348
  return;
323
349
  }
324
350
 
@@ -326,11 +352,13 @@ export function bindProcessBlockInteractions(root: HTMLElement): void {
326
352
  if (summary) {
327
353
  const block = summary.closest('.process-block');
328
354
  if (!block) return;
329
- const expanding = block.classList.contains('collapsed');
330
- block.classList.toggle('collapsed', !expanding);
331
- summary.setAttribute('aria-expanded', expanding ? 'true' : 'false');
332
- const chevron = summary.querySelector('.process-chevron');
333
- if (chevron) chevron.innerHTML = expanding ? ICONS.chevronDown : ICONS.chevronRight;
355
+ withProcessBlockLayoutMutation(block, () => {
356
+ const expanding = block.classList.contains('collapsed');
357
+ block.classList.toggle('collapsed', !expanding);
358
+ summary.setAttribute('aria-expanded', expanding ? 'true' : 'false');
359
+ const chevron = summary.querySelector('.process-chevron');
360
+ if (chevron) chevron.innerHTML = expanding ? ICONS.chevronDown : ICONS.chevronRight;
361
+ });
334
362
  }
335
363
  });
336
364
  root.dataset['processBlockBound'] = '1';
@@ -4,7 +4,8 @@ export function findLegacyRunningMatch(steps: ProcessStep[], step: ProcessStep):
4
4
  const matches = steps.filter(s => s.status === 'running'
5
5
  && !s.stepRef
6
6
  && s.label === step.label
7
- && s.type === step.type);
7
+ && s.type === step.type
8
+ && Boolean(s.isEmployee) === Boolean(step.isEmployee));
8
9
  return matches.length === 1 ? matches[0]! : null;
9
10
  }
10
11
 
package/public/js/ui.ts CHANGED
@@ -42,6 +42,12 @@ interface MessageItem { role: string; content: string; tool_log?: string | null;
42
42
  interface QueuedOverlayItem { id: string; prompt: string; source?: string; ts?: number; }
43
43
  interface ActiveRunSnapshot { running?: boolean; cli?: string; text?: string; toolLog?: ToolLogEntry[]; startedAt?: number; }
44
44
 
45
+ declare global {
46
+ interface Window {
47
+ __jawProcessBlockLayoutMutation?: (anchor: Element | null, mutate: () => void) => void;
48
+ }
49
+ }
50
+
45
51
  function processStepType(toolType?: string): ProcessStep['type'] {
46
52
  return toolType === 'thinking' || toolType === 'search' || toolType === 'subagent'
47
53
  ? toolType
@@ -82,6 +88,7 @@ function toProcessSteps(tools: ToolLogEntry[], runStartedAt?: number): ProcessSt
82
88
  icon: tool.icon ? emojiToIcon(tool.icon) : ICONS.tool,
83
89
  rawIcon: tool.rawIcon || tool.icon || '',
84
90
  label: fallbackToolLabel(tool),
91
+ isEmployee: tool.isEmployee === true,
85
92
  type: processStepType(tool.toolType),
86
93
  detail: tool.detail || '',
87
94
  stepRef: tool.stepRef || '',
@@ -170,6 +177,7 @@ function processStepFromDom(row: HTMLElement): ProcessStep | null {
170
177
  icon: storedMeta?.icon || icon,
171
178
  rawIcon: storedMeta?.rawIcon,
172
179
  label: storedMeta?.label || label,
180
+ isEmployee: storedMeta?.isEmployee === true || row.dataset['isEmployee'] === 'true',
173
181
  detail,
174
182
  detailPreview: storedMeta?.preview,
175
183
  detailLength: storedMeta?.detailLength,
@@ -205,6 +213,7 @@ function processStepToToolLog(step: ProcessStep, finalize = false): ToolLogEntry
205
213
  icon: step.rawIcon || step.icon || ICONS.tool,
206
214
  rawIcon: step.rawIcon || step.icon || '',
207
215
  label: step.label || 'tool',
216
+ isEmployee: step.isEmployee === true,
208
217
  detail,
209
218
  toolType: step.type,
210
219
  stepRef: step.stepRef || '',
@@ -225,6 +234,7 @@ function processStepFromMeta(stepId: string, finalize = false): ToolLogEntry | n
225
234
  icon: meta.rawIcon || meta.icon || ICONS.tool,
226
235
  rawIcon: meta.rawIcon || meta.icon || '',
227
236
  label: meta.label || 'tool',
237
+ isEmployee: meta.isEmployee === true,
228
238
  detail: getStoredProcessStepDetail(stepId) || meta.preview || '',
229
239
  toolType: meta.type,
230
240
  stepRef: meta.stepRef || '',
@@ -473,6 +483,7 @@ export function showProcessStep(step: ProcessStep): void {
473
483
  .find(s => s.status === 'running'
474
484
  && s.label === step.label
475
485
  && s.type === step.type
486
+ && Boolean(s.isEmployee) === Boolean(step.isEmployee)
476
487
  && !s.detail);
477
488
  if (ghost) {
478
489
  replaceStep(state.currentProcessBlock, ghost.id, step);
@@ -762,19 +773,74 @@ export function addMessage(role: string, text: string, cli?: string | null): HTM
762
773
 
763
774
  let scrollRAF: number | null = null;
764
775
  let userNearBottom = true;
776
+ type ScrollIntent = 'unknown' | 'following' | 'pinnedAway';
777
+ let scrollIntent: ScrollIntent = 'unknown';
765
778
  let scrollTrackingBound = false;
766
779
  const SCROLL_BOTTOM_THRESHOLD = 80; // px
767
780
  const RESTORE_INDICATOR_SETTLE_MS = 1100;
768
781
  let chatRestoreIndicatorHideTimer: number | null = null;
782
+ const chatRestorePassTimers = new Set<number>();
783
+ const chatRestorePassRafs = new Set<number>();
784
+
785
+ function canFollowAfterRestore(): boolean {
786
+ return scrollIntent !== 'pinnedAway';
787
+ }
788
+
789
+ function markFollowingBottom(): void {
790
+ userNearBottom = true;
791
+ scrollIntent = 'following';
792
+ }
793
+
794
+ function updateScrollIntentFromDistance(dist: number): void {
795
+ userNearBottom = dist < SCROLL_BOTTOM_THRESHOLD;
796
+ scrollIntent = userNearBottom ? 'following' : 'pinnedAway';
797
+ if (scrollIntent === 'pinnedAway') cancelPendingChatRestorePasses();
798
+ }
799
+
800
+ function cancelPendingChatRestorePasses(): void {
801
+ for (const timer of chatRestorePassTimers) window.clearTimeout(timer);
802
+ chatRestorePassTimers.clear();
803
+ for (const raf of chatRestorePassRafs) cancelAnimationFrame(raf);
804
+ chatRestorePassRafs.clear();
805
+ }
806
+
807
+ function requestChatRestoreFrame(callback: () => void): void {
808
+ const raf = requestAnimationFrame(() => {
809
+ chatRestorePassRafs.delete(raf);
810
+ callback();
811
+ });
812
+ chatRestorePassRafs.add(raf);
813
+ }
814
+
815
+ function trackChatRestoreTimer(timer: number): void {
816
+ chatRestorePassTimers.add(timer);
817
+ }
818
+
819
+ function scheduleChatRestoreTimer(callback: () => void, delayMs: number): void {
820
+ const timer = window.setTimeout(() => {
821
+ chatRestorePassTimers.delete(timer);
822
+ callback();
823
+ }, delayMs);
824
+ trackChatRestoreTimer(timer);
825
+ }
769
826
 
770
827
  function ensureScrollTracking(): void {
828
+ getVirtualScroll().setRestoreFollowPredicate(canFollowAfterRestore);
829
+ window.__jawProcessBlockLayoutMutation = (anchor, mutate) => {
830
+ const vs = getVirtualScroll();
831
+ if (vs.active) {
832
+ vs.preserveScrollDuringMutation(anchor, mutate);
833
+ return;
834
+ }
835
+ mutate();
836
+ };
771
837
  if (scrollTrackingBound) return;
772
838
  const c = document.getElementById('chatMessages');
773
839
  if (!c) return;
774
840
  scrollTrackingBound = true;
775
841
  c.addEventListener('scroll', () => {
776
842
  const dist = c.scrollHeight - c.scrollTop - c.clientHeight;
777
- userNearBottom = dist < SCROLL_BOTTOM_THRESHOLD;
843
+ updateScrollIntentFromDistance(dist);
778
844
  }, { passive: true });
779
845
  }
780
846
 
@@ -791,7 +857,7 @@ export function isChatNearBottom(): boolean {
791
857
  export function reconcileChatBottomAfterLayout(shouldFollow = isChatNearBottom()): void {
792
858
  ensureScrollTracking();
793
859
  if (!shouldFollow) return;
794
- userNearBottom = true;
860
+ markFollowingBottom();
795
861
  const vs = getVirtualScroll();
796
862
  if (vs.active) {
797
863
  vs.reconcileBottomAfterLayout('reconnect', true);
@@ -847,22 +913,36 @@ export function reconcileChatBottomAfterRestore(reason: string): void {
847
913
  showChatRestoreIndicator(reason);
848
914
  hideChatRestoreIndicatorAfterSettle();
849
915
  ensureScrollTracking();
850
- userNearBottom = true;
851
916
  const vs = getVirtualScroll();
852
917
  if (vs.active) {
853
- vs.forceBottomAfterRestore(reason as RestoreReason);
918
+ vs.reconcileAfterRestore(reason as RestoreReason, canFollowAfterRestore);
854
919
  return;
855
920
  }
856
- const scroll = () => {
921
+ if (!canFollowAfterRestore()) return;
922
+ const scrollIfFollowing = () => {
923
+ if (!canFollowAfterRestore()) {
924
+ cancelPendingChatRestorePasses();
925
+ return;
926
+ }
857
927
  const c = document.getElementById('chatMessages');
858
- if (c) c.scrollTop = c.scrollHeight;
928
+ if (c) {
929
+ c.scrollTop = c.scrollHeight;
930
+ markFollowingBottom();
931
+ }
932
+ };
933
+ const runRestorePass = () => {
934
+ if (!canFollowAfterRestore()) {
935
+ cancelPendingChatRestorePasses();
936
+ return;
937
+ }
938
+ requestChatRestoreFrame(scrollIfFollowing);
859
939
  };
860
- scroll();
861
- requestAnimationFrame(scroll);
862
- requestAnimationFrame(() => requestAnimationFrame(scroll));
863
- window.setTimeout(scroll, 250);
864
- window.setTimeout(scroll, 1000);
865
- void document.fonts?.ready.then(scroll);
940
+ runRestorePass();
941
+ requestChatRestoreFrame(runRestorePass);
942
+ requestChatRestoreFrame(() => requestChatRestoreFrame(runRestorePass));
943
+ scheduleChatRestoreTimer(runRestorePass, 250);
944
+ scheduleChatRestoreTimer(runRestorePass, 1000);
945
+ void document.fonts?.ready.then(runRestorePass);
866
946
  }
867
947
 
868
948
  /** Scroll chat to bottom.
@@ -872,7 +952,7 @@ export function scrollToBottom(force = false): void {
872
952
  if (!force && !userNearBottom) return;
873
953
  // After force scroll, mark as near-bottom so subsequent
874
954
  // streaming chunks keep auto-scrolling until user scrolls up
875
- if (force) userNearBottom = true;
955
+ if (force) markFollowingBottom();
876
956
 
877
957
  const vs = getVirtualScroll();
878
958
  if (vs.active) {
@@ -993,6 +1073,13 @@ function makeBootstrapDeps(
993
1073
  setItems: (items, opts) => vs.setItems(items, opts),
994
1074
  activateIfNeeded: (toBottom) => vs.activateIfNeeded(toBottom),
995
1075
  scrollToBottom: () => vs.scrollToBottom(),
1076
+ shouldFollowBottom: canFollowAfterRestore,
1077
+ onBeforeVirtualHistoryBootstrap: () => {
1078
+ ensureScrollTracking();
1079
+ },
1080
+ onAfterVirtualHistoryBottomed: () => {
1081
+ markFollowingBottom();
1082
+ },
996
1083
  };
997
1084
  }
998
1085
 
@@ -9,6 +9,9 @@ export interface VirtualHistoryBootstrapDeps {
9
9
  setItems: (items: VirtualItem[], options?: { autoActivate?: boolean; toBottom?: boolean }) => void;
10
10
  activateIfNeeded: (toBottom: boolean) => void;
11
11
  scrollToBottom: () => void;
12
+ shouldFollowBottom?: () => boolean;
13
+ onBeforeVirtualHistoryBootstrap?: () => void;
14
+ onAfterVirtualHistoryBottomed?: () => void;
12
15
  }
13
16
 
14
17
  /**
@@ -22,8 +25,12 @@ export function bootstrapVirtualHistory(
22
25
  items: VirtualItem[],
23
26
  deps: VirtualHistoryBootstrapDeps,
24
27
  ): void {
28
+ deps.onBeforeVirtualHistoryBootstrap?.();
25
29
  deps.registerCallbacks();
26
30
  deps.setItems(items, { autoActivate: false });
27
- deps.activateIfNeeded(true);
31
+ const shouldFollowBottom = deps.shouldFollowBottom?.() ?? true;
32
+ deps.activateIfNeeded(shouldFollowBottom);
33
+ if (!shouldFollowBottom) return;
28
34
  deps.scrollToBottom();
35
+ deps.onAfterVirtualHistoryBottomed?.();
29
36
  }
@@ -34,7 +34,12 @@ export interface VirtualItem {
34
34
  }
35
35
 
36
36
  export type LazyRenderCallback = (targets: HTMLElement[]) => void;
37
+ export type RestoreFollowPredicate = () => boolean;
37
38
  type MeasurableVirtualElement = Pick<HTMLElement, 'getBoundingClientRect'>;
39
+ interface ScrollAnchor {
40
+ el: HTMLElement;
41
+ top: number;
42
+ }
38
43
 
39
44
  function readMeasuredHeight(el: MeasurableVirtualElement): number {
40
45
  const height = Math.ceil(el.getBoundingClientRect().height);
@@ -73,6 +78,7 @@ export class VirtualScroll {
73
78
  private mounted = new Map<number, HTMLElement>();
74
79
  private itemGap = 0;
75
80
  private restorePassTimers = new Set<number>();
81
+ private shouldFollowAfterRestore: RestoreFollowPredicate = () => true;
76
82
 
77
83
  onLazyRender: LazyRenderCallback | null = null;
78
84
  onPostRender: ((viewport: HTMLElement) => void) | null = null;
@@ -193,6 +199,10 @@ export class VirtualScroll {
193
199
  return dist < threshold;
194
200
  }
195
201
 
202
+ setRestoreFollowPredicate(predicate: RestoreFollowPredicate | null): void {
203
+ this.shouldFollowAfterRestore = predicate ?? (() => true);
204
+ }
205
+
196
206
  reconcileBottomAfterLayout(reason: RestoreReason, shouldFollow = this.isNearBottom()): void {
197
207
  if (!shouldFollow) return;
198
208
  void reason;
@@ -204,37 +214,98 @@ export class VirtualScroll {
204
214
  });
205
215
  }
206
216
 
217
+ reconcileAfterRestore(reason: RestoreReason, shouldFollow: RestoreFollowPredicate = this.shouldFollowAfterRestore): void {
218
+ if (!shouldFollow()) {
219
+ this.invalidateLayout();
220
+ return;
221
+ }
222
+ this.scheduleRestoreReconcile(reason, shouldFollow);
223
+ }
224
+
207
225
  forceBottomAfterRestore(reason: RestoreReason): void {
208
226
  this.scheduleRestoreReconcile(reason);
209
227
  }
210
228
 
211
- private scheduleRestoreReconcile(reason: RestoreReason): void {
212
- this.runRestoreReconcilePass(reason);
213
- requestAnimationFrame(() => this.runRestoreReconcilePass(reason));
229
+ cancelRestoreReconcile(_reason?: RestoreReason): void {
230
+ this.clearRestoreTimers();
231
+ }
232
+
233
+ preserveScrollDuringMutation<T>(anchorEl: Element | null, mutate: () => T): T {
234
+ const wasNearBottom = this.isNearBottom();
235
+ const anchor = this.captureScrollAnchor(anchorEl);
236
+ const result = mutate();
237
+ this.invalidateLayout();
238
+ if (wasNearBottom) {
239
+ this.reconcileBottomAfterLayout('manual', true);
240
+ return result;
241
+ }
242
+ this.restoreScrollAnchor(anchor);
243
+ return result;
244
+ }
245
+
246
+ private scheduleRestoreReconcile(reason: RestoreReason, shouldFollow?: RestoreFollowPredicate): void {
247
+ this.runRestoreReconcilePass(reason, shouldFollow);
248
+ requestAnimationFrame(() => this.runRestoreReconcilePass(reason, shouldFollow));
214
249
  requestAnimationFrame(() => {
215
- requestAnimationFrame(() => this.runRestoreReconcilePass(reason));
250
+ requestAnimationFrame(() => this.runRestoreReconcilePass(reason, shouldFollow));
216
251
  });
217
- this.scheduleRestoreTimer(reason, 250);
218
- this.scheduleRestoreTimer(reason, 1000);
219
- void document.fonts?.ready.then(() => this.runRestoreReconcilePass(reason));
252
+ this.scheduleRestoreTimer(reason, 250, shouldFollow);
253
+ this.scheduleRestoreTimer(reason, 1000, shouldFollow);
254
+ void document.fonts?.ready.then(() => this.runRestoreReconcilePass(reason, shouldFollow));
220
255
  }
221
256
 
222
- private scheduleRestoreTimer(reason: RestoreReason, delayMs: number): void {
257
+ private scheduleRestoreTimer(reason: RestoreReason, delayMs: number, shouldFollow?: RestoreFollowPredicate): void {
223
258
  const timer = window.setTimeout(() => {
224
259
  this.restorePassTimers.delete(timer);
225
- this.runRestoreReconcilePass(reason);
260
+ this.runRestoreReconcilePass(reason, shouldFollow);
226
261
  }, delayMs);
227
262
  this.restorePassTimers.add(timer);
228
263
  }
229
264
 
230
- private runRestoreReconcilePass(reason: RestoreReason): void {
265
+ private runRestoreReconcilePass(reason: RestoreReason, shouldFollow?: RestoreFollowPredicate): void {
231
266
  if (!this.virtualizer) return;
232
267
  void reason;
268
+ if (shouldFollow && !shouldFollow()) {
269
+ this.cancelRestoreReconcile(reason);
270
+ this.invalidateLayout();
271
+ return;
272
+ }
233
273
  this.invalidateLayout();
234
274
  remeasureMountedVirtualItems(this.items, this.mounted, this.virtualizer);
235
275
  this.scrollToBottom();
236
276
  }
237
277
 
278
+ private captureScrollAnchor(preferred: Element | null): ScrollAnchor | null {
279
+ const preferredEl = preferred instanceof HTMLElement ? preferred : null;
280
+ const chosen = preferredEl && this.isVisibleInContainer(preferredEl)
281
+ ? preferredEl
282
+ : this.firstVisibleMountedItem();
283
+ return chosen ? { el: chosen, top: chosen.getBoundingClientRect().top } : null;
284
+ }
285
+
286
+ private restoreScrollAnchor(anchor: ScrollAnchor | null): void {
287
+ if (!anchor || !anchor.el.isConnected) return;
288
+ const afterTop = anchor.el.getBoundingClientRect().top;
289
+ const delta = afterTop - anchor.top;
290
+ if (Number.isFinite(delta) && delta !== 0) {
291
+ this.container.scrollTop += delta;
292
+ }
293
+ }
294
+
295
+ private firstVisibleMountedItem(): HTMLElement | null {
296
+ const mounted = Array.from(this.mounted.entries()).sort(([a], [b]) => a - b);
297
+ for (const [, el] of mounted) {
298
+ if (this.isVisibleInContainer(el)) return el;
299
+ }
300
+ return null;
301
+ }
302
+
303
+ private isVisibleInContainer(el: HTMLElement): boolean {
304
+ const rect = el.getBoundingClientRect();
305
+ const containerRect = this.container.getBoundingClientRect();
306
+ return rect.bottom > containerRect.top && rect.top < containerRect.bottom;
307
+ }
308
+
238
309
  private clearRestoreTimers(): void {
239
310
  for (const timer of this.restorePassTimers) {
240
311
  window.clearTimeout(timer);
@@ -337,10 +408,9 @@ export class VirtualScroll {
337
408
 
338
409
  // ── Browser restore reconciliation ──
339
410
  // Resume/discard paths can restore stale virtualizer measurements.
340
- // Product policy: browser restore/reconnect forces the newest message.
341
411
  const restoreBottomAfterLayout = (reason: RestoreReason) => {
342
412
  if (!this.virtualizer) return;
343
- this.forceBottomAfterRestore(reason);
413
+ this.reconcileAfterRestore(reason, this.shouldFollowAfterRestore);
344
414
  };
345
415
  const onPageShow = (e: PageTransitionEvent) => {
346
416
  if (!e.persisted) return;
@@ -394,7 +464,7 @@ export class VirtualScroll {
394
464
  const wasDiscarded = 'wasDiscarded' in document
395
465
  && Boolean((document as Document & { wasDiscarded?: boolean }).wasDiscarded);
396
466
  if (wasDiscarded) {
397
- this.forceBottomAfterRestore('discard');
467
+ restoreBottomAfterLayout('discard');
398
468
  }
399
469
  }
400
470
 
package/public/js/ws.ts CHANGED
@@ -66,7 +66,7 @@ interface WsMessage {
66
66
  detailBytes?: number;
67
67
  rawRetentionStatus?: string;
68
68
  text?: string;
69
- toolLog?: { icon: string; label: string; detail?: string; toolType?: string; stepRef?: string; traceRunId?: string; traceSeq?: number; detailAvailable?: boolean; detailBytes?: number; rawRetentionStatus?: string }[];
69
+ toolLog?: { icon: string; label: string; detail?: string; toolType?: string; stepRef?: string; isEmployee?: boolean; traceRunId?: string; traceSeq?: number; detailAvailable?: boolean; detailBytes?: number; rawRetentionStatus?: string }[];
70
70
  from?: string;
71
71
  to?: string;
72
72
  source?: string;
@@ -377,7 +377,6 @@ export function connect(): void {
377
377
  addSystemMsg(t('ws.roundRetry', { round: msg.round || 0 }));
378
378
  }
379
379
  } else if (msg.type === 'agent_tool') {
380
- const empPrefix = msg.isEmployee ? '(E) ' : '';
381
380
  const stepType = msg.toolType === 'thinking' ? 'thinking'
382
381
  : msg.toolType === 'search' ? 'search'
383
382
  : msg.toolType === 'subagent' ? 'subagent' : 'tool';
@@ -386,7 +385,8 @@ export function connect(): void {
386
385
  type: stepType,
387
386
  icon: msg.icon || ICONS.tool,
388
387
  rawIcon: msg.rawIcon || msg.icon || '',
389
- label: empPrefix + (msg.label || ''),
388
+ label: msg.label || '',
389
+ isEmployee: msg.isEmployee === true,
390
390
  detail: msg.detail || '',
391
391
  stepRef: msg.stepRef || '',
392
392
  traceRunId: msg.traceRunId || '',
@@ -141,7 +141,8 @@
141
141
  "chat.file.sentWithMsg": "\n\nUser message: {text}",
142
142
  "chat.file.uploadFail": "File upload failed: {msg}",
143
143
  "chat.requestFail": "Request failed ({status})",
144
- "chat.continue": "Continuing from previous worklog.",
144
+ "chat.continue": "Continuing the current task.",
145
+ "chat.noPendingContinue": "No pending work to continue.",
145
146
  "chat.voice.label": "🎤 [Voice message]",
146
147
  "skill.loadFail": "Failed to load skills",
147
148
  "skill.count": "{active} active / {total} total",
@@ -141,7 +141,8 @@
141
141
  "chat.file.sentWithMsg": "\n\nユーザーメッセージ: {text}",
142
142
  "chat.file.uploadFail": "ファイルのアップロードに失敗しました: {msg}",
143
143
  "chat.requestFail": "リクエストに失敗しました({status})",
144
- "chat.continue": "前回の worklog から続行しています。",
144
+ "chat.continue": "現在の作業を続行しています。",
145
+ "chat.noPendingContinue": "続行できる保留中の作業はありません。",
145
146
  "chat.voice.label": "🎤 [音声メッセージ]",
146
147
  "skill.loadFail": "スキルを読み込めませんでした",
147
148
  "skill.count": "有効 {active} 件 / 全 {total} 件",
@@ -141,7 +141,8 @@
141
141
  "chat.file.sentWithMsg": "\n\n사용자 메시지: {text}",
142
142
  "chat.file.uploadFail": "파일 업로드 실패: {msg}",
143
143
  "chat.requestFail": "요청 실패 ({status})",
144
- "chat.continue": "이전 worklog 기준으로 이어서 진행합니다.",
144
+ "chat.continue": "현재 작업을 이어서 진행합니다.",
145
+ "chat.noPendingContinue": "이어갈 작업이 없습니다.",
145
146
  "chat.voice.label": "🎤 [음성 메시지]",
146
147
  "skill.loadFail": "스킬 로드 실패",
147
148
  "skill.count": "활성 {active}개 / 전체 {total}개",
@@ -141,7 +141,8 @@
141
141
  "chat.file.sentWithMsg": "\n\n用户消息:{text}",
142
142
  "chat.file.uploadFail": "文件上传失败:{msg}",
143
143
  "chat.requestFail": "请求失败({status})",
144
- "chat.continue": "正在从上一个 worklog 继续。",
144
+ "chat.continue": "正在继续当前任务。",
145
+ "chat.noPendingContinue": "没有可继续的待处理工作。",
145
146
  "chat.voice.label": "🎤 [语音消息]",
146
147
  "skill.loadFail": "无法加载技能",
147
148
  "skill.count": "{active} 个启用 / 共 {total} 个",
@@ -24,7 +24,7 @@ import { DashboardSettingsSidebar, type DashboardSettingsSection } from './dashb
24
24
  import { DashboardSettingsWorkspace } from './dashboard-settings/DashboardSettingsWorkspace';
25
25
  import { summarizeActivityTitleSupport } from './dashboard-settings/activity-title-support';
26
26
  import { dashboardSettingsUiFromView } from './dashboard-settings/dashboard-settings-ui';
27
- import { NotesSidebar } from './notes/NotesSidebar';
27
+ import { NotesSidebar, type NotesSidebarMode } from './notes/NotesSidebar';
28
28
  import { NotesWorkspace } from './notes/NotesWorkspace';
29
29
  import { useNotesModel } from './notes/useNotesModel';
30
30
  import { publishInvalidation } from './sync/invalidation-bus';
@@ -84,6 +84,8 @@ export function App() {
84
84
  const [activeProfileIds, setActiveProfileIds] = useState<string[]>([]);
85
85
  const [settingsDirty, setSettingsDirty] = useState(false);
86
86
  const [notesDirtyPath, setNotesDirtyPath] = useState<string | null>(null);
87
+ const [notesSidebarMode, setNotesSidebarMode] = useState<NotesSidebarMode>('files');
88
+ const [notesSearchFocusToken, setNotesSearchFocusToken] = useState(0);
87
89
  const [dashboardSettingsSection, setDashboardSettingsSection] = useState<DashboardSettingsSection>('display');
88
90
  const [boardView, setBoardView] = useState<BoardView>({ kind: 'overall' });
89
91
  const [scheduleGroup, setScheduleGroup] = useState<ScheduleGroup>('today');
@@ -341,6 +343,11 @@ export function App() {
341
343
  view.setNotesSelectedPath(path); void saveUi({ notesSelectedPath: path });
342
344
  }
343
345
 
346
+ function openNotesSidebarSearch(): void {
347
+ setNotesSidebarMode('search');
348
+ setNotesSearchFocusToken(token => token + 1);
349
+ }
350
+
344
351
  function handleNotesViewModeChange(mode: DashboardNotesViewMode): void {
345
352
  view.setNotesViewMode(mode); void saveUi({ notesViewMode: mode });
346
353
  }
@@ -517,7 +524,7 @@ export function App() {
517
524
  {view.sidebarMode === 'settings' ? (
518
525
  <DashboardSettingsSidebar activeSection={dashboardSettingsSection} locale={view.locale} onSectionChange={setDashboardSettingsSection} />
519
526
  ) : view.sidebarMode === 'notes' ? (
520
- <NotesSidebar tree={notesModel.tree} loading={notesModel.loading} error={notesModel.error} notesRoot={notesModel.notesRoot} selectedPath={view.notesSelectedPath} dirtyPath={notesDirtyPath} treeWidth={view.notesTreeWidth} onSelectedPathChange={handleNotesSelectedPathChange} onRefreshTree={notesModel.refresh} />
527
+ <NotesSidebar tree={notesModel.tree} loading={notesModel.loading} error={notesModel.error} notesRoot={notesModel.notesRoot} selectedPath={view.notesSelectedPath} dirtyPath={notesDirtyPath} treeWidth={view.notesTreeWidth} mode={notesSidebarMode} searchFocusToken={notesSearchFocusToken} onModeChange={setNotesSidebarMode} onOpenSearch={openNotesSidebarSearch} onSelectedPathChange={handleNotesSelectedPathChange} onRefreshTree={notesModel.refresh} />
521
528
  ) : view.sidebarMode === 'board' ? (
522
529
  <DashboardBoardSidebar view={boardView} onViewChange={setBoardView} instances={instances} titlesByPort={messageActivity.titlesByPort} busyPorts={messageActivity.busyPorts} />
523
530
  ) : SCHEDULE_WORKSPACE_ENABLED && view.sidebarMode === 'schedule' ? (
@@ -552,7 +559,7 @@ export function App() {
552
559
  )} logs={detailContent('logs')} settings={detailContent('settings')} />
553
560
  </WorkspaceSurface>
554
561
  <WorkspaceSurface active={view.sidebarMode === 'notes'}>
555
- <NotesWorkspace active={view.sidebarMode === 'notes'} selectedPath={view.notesSelectedPath} vaultIndex={notesModel.index} viewMode={view.notesViewMode} authoringMode={view.notesAuthoringMode} wordWrap={view.notesWordWrap} treeWidth={view.notesTreeWidth} onSelectedPathChange={handleNotesSelectedPathChange} onDirtyPathChange={setNotesDirtyPath} onViewModeChange={handleNotesViewModeChange} onAuthoringModeChange={handleNotesAuthoringModeChange} onWordWrapChange={handleNotesWordWrapChange} onTreeWidthChange={handleNotesTreeWidthChange} />
562
+ <NotesWorkspace active={view.sidebarMode === 'notes'} selectedPath={view.notesSelectedPath} vaultIndex={notesModel.index} viewMode={view.notesViewMode} authoringMode={view.notesAuthoringMode} wordWrap={view.notesWordWrap} treeWidth={view.notesTreeWidth} onOpenSidebarSearch={openNotesSidebarSearch} onSelectedPathChange={handleNotesSelectedPathChange} onDirtyPathChange={setNotesDirtyPath} onViewModeChange={handleNotesViewModeChange} onAuthoringModeChange={handleNotesAuthoringModeChange} onWordWrapChange={handleNotesWordWrapChange} onTreeWidthChange={handleNotesTreeWidthChange} />
556
563
  </WorkspaceSurface>
557
564
  <WorkspaceSurface active={view.sidebarMode === 'settings'}>
558
565
  <DashboardSettingsWorkspace activeSection={dashboardSettingsSection} ui={dashboardSettingsUi} titleSupport={titleSupport} onUiPatch={handleDashboardSettingsPatch} />