cli-jaw 1.6.4 → 1.6.7
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.
- package/README.md +8 -4
- package/dist/bin/commands/doctor.js +48 -0
- package/dist/bin/commands/doctor.js.map +1 -1
- package/dist/bin/commands/init.js +2 -1
- package/dist/bin/commands/init.js.map +1 -1
- package/dist/bin/postinstall.js +66 -1
- package/dist/bin/postinstall.js.map +1 -1
- package/dist/server.js +8 -3
- package/dist/server.js.map +1 -1
- package/dist/src/agent/spawn.js +2 -5
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/orchestrator/parser.js +2 -1
- package/dist/src/orchestrator/parser.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +5 -96
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/orchestrator/state-machine.js +11 -14
- package/dist/src/orchestrator/state-machine.js.map +1 -1
- package/dist/src/prompt/builder.js +5 -15
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +33 -34
- package/dist/src/prompt/templates/employee.md +5 -3
- package/dist/src/prompt/templates/orchestration.md +13 -37
- package/package.json +7 -1
- package/public/css/chat.css +12 -0
- package/public/dist/assets/employees-B11suLXa.js +39 -0
- package/public/dist/assets/index-C3xIEYRH.css +1 -0
- package/public/dist/assets/index-CMUmeewA.js +49 -0
- package/public/dist/assets/render-C5gpc065.js +25 -0
- package/public/dist/assets/{settings-jLH2evC4.js → settings-BbG1hQmA.js} +14 -14
- package/public/dist/assets/settings-tOEsbIIs.js +1 -0
- package/public/dist/assets/skills-DL7wTirZ.js +12 -0
- package/public/dist/assets/skills-kOx6rq1X.js +1 -0
- package/public/dist/assets/slash-commands-C5vUKzNP.js +1 -0
- package/public/dist/assets/{slash-commands-3SMle5Qk.js → slash-commands-D_tV86er.js} +1 -1
- package/public/dist/assets/ui-ByJAyywC.js +1 -0
- package/public/dist/assets/ui-ORW7tzea.js +131 -0
- package/public/dist/assets/ws-CDrAtY9-.js +2 -0
- package/public/dist/index.html +2 -2
- package/public/js/features/chat.ts +3 -1
- package/public/js/render.ts +44 -15
- package/public/js/streaming-render.ts +33 -10
- package/public/js/ui.ts +45 -13
- package/public/js/virtual-scroll.ts +50 -16
- package/public/js/ws.ts +2 -1
- package/scripts/ensure-native-modules.cjs +54 -0
- package/scripts/install-officecli.ps1 +96 -0
- package/scripts/install-officecli.sh +37 -3
- package/scripts/install-wsl.sh +113 -27
- package/public/dist/assets/employees-C2iuXw4k.js +0 -39
- package/public/dist/assets/index-BkF1Onqv.js +0 -49
- package/public/dist/assets/index-D6ci1wCN.css +0 -1
- package/public/dist/assets/render-HNw-fqbv.js +0 -25
- package/public/dist/assets/settings-GMJkuozJ.js +0 -1
- package/public/dist/assets/skills-CbtCvg1l.js +0 -1
- package/public/dist/assets/skills-DfGrAT8o.js +0 -12
- package/public/dist/assets/slash-commands-AojbjNCT.js +0 -1
- package/public/dist/assets/ui-5JOnDmC_.js +0 -131
- package/public/dist/assets/ui-CaWuOlEV.js +0 -1
- package/public/dist/assets/ws-CF5lzgW9.js +0 -2
package/public/js/render.ts
CHANGED
|
@@ -288,10 +288,18 @@ export function shieldMath(text: string): { text: string; blocks: MathBlock[] }
|
|
|
288
288
|
return { text: processed, blocks };
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
export function unshieldMath(html: string, blocks: MathBlock[]): string {
|
|
291
|
+
export function unshieldMath(html: string, blocks: MathBlock[], isStreaming = false): string {
|
|
292
292
|
return html.replace(/\x00MATH-(\d+)\x00/g, (_, i) => {
|
|
293
293
|
const block = blocks[Number(i)];
|
|
294
294
|
if (!block) return `<code title="math placeholder error">[math error]</code>`;
|
|
295
|
+
|
|
296
|
+
// During streaming: lightweight placeholder, defer KaTeX to finalize
|
|
297
|
+
if (isStreaming) {
|
|
298
|
+
return block.displayMode
|
|
299
|
+
? `<div class="math-placeholder">${escapeHtml(block.tex)}</div>`
|
|
300
|
+
: `<code class="math-placeholder">${escapeHtml(block.tex)}</code>`;
|
|
301
|
+
}
|
|
302
|
+
|
|
295
303
|
try {
|
|
296
304
|
return katex.renderToString(block.tex, {
|
|
297
305
|
displayMode: block.displayMode,
|
|
@@ -397,8 +405,9 @@ async function renderSingleMermaid(el: HTMLElement): Promise<void> {
|
|
|
397
405
|
}
|
|
398
406
|
}
|
|
399
407
|
|
|
400
|
-
async function renderMermaidBlocks(): Promise<void> {
|
|
401
|
-
const
|
|
408
|
+
async function renderMermaidBlocks(scope?: HTMLElement | Document): Promise<void> {
|
|
409
|
+
const root = scope || document;
|
|
410
|
+
const pending = root.querySelectorAll('.mermaid-pending');
|
|
402
411
|
if (!pending.length) return;
|
|
403
412
|
ensureMermaidObserver();
|
|
404
413
|
for (const el of pending) {
|
|
@@ -453,8 +462,9 @@ function ensureMarked(): boolean {
|
|
|
453
462
|
}
|
|
454
463
|
|
|
455
464
|
// ── Rehighlight all code blocks ──
|
|
456
|
-
export function rehighlightAll(): void {
|
|
457
|
-
|
|
465
|
+
export function rehighlightAll(scope?: HTMLElement | Document): void {
|
|
466
|
+
const root = scope || document;
|
|
467
|
+
root.querySelectorAll('.code-block pre code, .code-block-wrapper pre code').forEach(el => {
|
|
458
468
|
if ((el as HTMLElement).dataset.highlighted === 'yes') return;
|
|
459
469
|
const lang = [...el.classList].find(c => c.startsWith('language-'))?.replace('language-', '');
|
|
460
470
|
const raw = el.textContent || '';
|
|
@@ -676,14 +686,14 @@ function unshieldSvgBlocks(html: string, blocks: SvgBlock[]): string {
|
|
|
676
686
|
|
|
677
687
|
// ── Diagram Zoom Overlay ──
|
|
678
688
|
|
|
679
|
-
export function bindDiagramZoom(): void {
|
|
680
|
-
|
|
689
|
+
export function bindDiagramZoom(scope?: HTMLElement | Document): void {
|
|
690
|
+
const root = scope || document;
|
|
691
|
+
root.querySelectorAll('.diagram-zoom-btn').forEach(btn => {
|
|
681
692
|
if ((btn as HTMLElement).dataset.bound) return;
|
|
682
693
|
(btn as HTMLElement).dataset.bound = '1';
|
|
683
694
|
btn.addEventListener('click', () => {
|
|
684
695
|
const container = btn.closest('.diagram-container');
|
|
685
696
|
if (!container) return;
|
|
686
|
-
// Clone without zoom button to prevent nesting
|
|
687
697
|
const clone = container.cloneNode(true) as HTMLElement;
|
|
688
698
|
clone.querySelectorAll('.diagram-zoom-btn, .diagram-copy-btn, .diagram-save-btn').forEach(b => b.remove());
|
|
689
699
|
openDiagramOverlay(clone.innerHTML);
|
|
@@ -773,7 +783,7 @@ export function renderMarkdown(text: string, isStreaming = false): string {
|
|
|
773
783
|
html = html.replace(/<table/g, '<div class="table-wrapper"><table').replace(/<\/table>/g, '</table></div>');
|
|
774
784
|
|
|
775
785
|
// 6. Unshield math
|
|
776
|
-
html = unshieldMath(html, mathBlocks);
|
|
786
|
+
html = unshieldMath(html, mathBlocks, isStreaming);
|
|
777
787
|
|
|
778
788
|
// 7. Sanitize
|
|
779
789
|
html = sanitizeHtml(html);
|
|
@@ -781,15 +791,34 @@ export function renderMarkdown(text: string, isStreaming = false): string {
|
|
|
781
791
|
// 8. Unshield SVGs (after sanitize — SVGs sanitized individually in renderSvgBlock)
|
|
782
792
|
html = unshieldSvgBlocks(html, svgBlocks);
|
|
783
793
|
|
|
784
|
-
// 9. Post-render async tasks
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
bindDiagramZoom();
|
|
789
|
-
});
|
|
794
|
+
// 9. Post-render async tasks — skip during streaming (deferred to finalize)
|
|
795
|
+
if (!isStreaming) {
|
|
796
|
+
schedulePostRender();
|
|
797
|
+
}
|
|
790
798
|
|
|
791
799
|
ensureCopyDelegation();
|
|
792
800
|
ensureDiagramActionDelegation();
|
|
793
801
|
|
|
794
802
|
return html;
|
|
795
803
|
}
|
|
804
|
+
|
|
805
|
+
// ── Batched post-render scheduler ──
|
|
806
|
+
// Coalesces multiple renderMarkdown() calls into a single post-render pass.
|
|
807
|
+
let postRenderRAF: number | null = null;
|
|
808
|
+
|
|
809
|
+
function schedulePostRender(): void {
|
|
810
|
+
if (postRenderRAF) return;
|
|
811
|
+
postRenderRAF = requestAnimationFrame(() => {
|
|
812
|
+
postRenderRAF = null;
|
|
813
|
+
renderMermaidBlocks();
|
|
814
|
+
rehighlightAll();
|
|
815
|
+
bindDiagramZoom();
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function cancelPostRender(): void {
|
|
820
|
+
if (postRenderRAF) {
|
|
821
|
+
cancelAnimationFrame(postRenderRAF);
|
|
822
|
+
postRenderRAF = null;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// ── Streaming rAF Renderer ──
|
|
2
|
-
//
|
|
2
|
+
// Throttled markdown rendering during agent streaming.
|
|
3
3
|
// Prevents flicker by batching renders to 1 per animation frame.
|
|
4
|
+
// Array buffer avoids O(n) string copy per chunk.
|
|
4
5
|
|
|
5
6
|
import { renderMarkdown } from './render.js';
|
|
6
7
|
|
|
7
8
|
export interface StreamState {
|
|
9
|
+
chunks: string[];
|
|
8
10
|
fullText: string;
|
|
11
|
+
textDirty: boolean;
|
|
9
12
|
element: HTMLElement;
|
|
10
13
|
pendingRAF: number | null;
|
|
11
14
|
isFinalized: boolean;
|
|
@@ -16,26 +19,39 @@ const FULL_RENDER_THRESHOLD = 2000;
|
|
|
16
19
|
const THROTTLE_MS = 80; // ~12fps — was 32ms (30fps), reduced to avoid blocking input
|
|
17
20
|
|
|
18
21
|
export function createStreamRenderer(el: HTMLElement): StreamState {
|
|
19
|
-
return {
|
|
22
|
+
return {
|
|
23
|
+
chunks: [], fullText: '', textDirty: false,
|
|
24
|
+
element: el, pendingRAF: null, isFinalized: false, lastRenderTime: 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getFullText(ss: StreamState): string {
|
|
29
|
+
if (ss.textDirty) {
|
|
30
|
+
ss.fullText = ss.chunks.join('');
|
|
31
|
+
ss.textDirty = false;
|
|
32
|
+
}
|
|
33
|
+
return ss.fullText;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
export function appendChunk(ss: StreamState, chunk: string): void {
|
|
23
|
-
ss.
|
|
37
|
+
ss.chunks.push(chunk);
|
|
38
|
+
ss.textDirty = true;
|
|
39
|
+
|
|
24
40
|
if (!ss.pendingRAF && !ss.isFinalized) {
|
|
25
41
|
ss.pendingRAF = requestAnimationFrame(() => {
|
|
26
42
|
ss.pendingRAF = null;
|
|
27
43
|
if (ss.isFinalized) return;
|
|
28
44
|
const now = performance.now();
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
const text = getFullText(ss);
|
|
46
|
+
if (text.length < FULL_RENDER_THRESHOLD || now - ss.lastRenderTime > THROTTLE_MS) {
|
|
47
|
+
ss.element.innerHTML = renderMarkdown(text, true) +
|
|
31
48
|
'<span class="stream-cursor" aria-hidden="true"></span>';
|
|
32
49
|
ss.lastRenderTime = now;
|
|
33
50
|
} else {
|
|
34
|
-
// Throttled — schedule trailing render to ensure latest text paints
|
|
35
51
|
ss.pendingRAF = requestAnimationFrame(() => {
|
|
36
52
|
ss.pendingRAF = null;
|
|
37
53
|
if (ss.isFinalized) return;
|
|
38
|
-
ss.element.innerHTML = renderMarkdown(ss
|
|
54
|
+
ss.element.innerHTML = renderMarkdown(getFullText(ss), true) +
|
|
39
55
|
'<span class="stream-cursor" aria-hidden="true"></span>';
|
|
40
56
|
ss.lastRenderTime = performance.now();
|
|
41
57
|
});
|
|
@@ -44,12 +60,19 @@ export function appendChunk(ss: StreamState, chunk: string): void {
|
|
|
44
60
|
}
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Finalize streaming session. Returns accumulated text.
|
|
65
|
+
* @param skipRender true when caller (finalizeAgent) will do its own full render.
|
|
66
|
+
*/
|
|
67
|
+
export function finalizeStream(ss: StreamState, skipRender = false): string {
|
|
48
68
|
ss.isFinalized = true;
|
|
49
69
|
if (ss.pendingRAF) {
|
|
50
70
|
cancelAnimationFrame(ss.pendingRAF);
|
|
51
71
|
ss.pendingRAF = null;
|
|
52
72
|
}
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
const text = getFullText(ss);
|
|
74
|
+
if (!skipRender) {
|
|
75
|
+
ss.element.innerHTML = renderMarkdown(text);
|
|
76
|
+
}
|
|
77
|
+
return text;
|
|
55
78
|
}
|
package/public/js/ui.ts
CHANGED
|
@@ -85,8 +85,7 @@ export function updateQueueBadge(count: number): void {
|
|
|
85
85
|
function showSkeleton(): void {
|
|
86
86
|
const container = document.getElementById('chatMessages');
|
|
87
87
|
if (!container || container.querySelector('.skeleton-msg')) return;
|
|
88
|
-
|
|
89
|
-
if (vs.active) vs.flushToDOM();
|
|
88
|
+
// No flushToDOM — skeleton goes directly into container as overlay
|
|
90
89
|
hideEmptyState();
|
|
91
90
|
const skel = document.createElement('div');
|
|
92
91
|
skel.className = 'skeleton-msg';
|
|
@@ -236,7 +235,7 @@ export function finalizeAgent(text: string, toolLog?: ToolLogEntry[]): void {
|
|
|
236
235
|
}
|
|
237
236
|
const content = (state.currentAgentDiv as HTMLElement)?.querySelector('.msg-content');
|
|
238
237
|
// Live stream is preview-only; agent_done text stays authoritative.
|
|
239
|
-
const streamedText = currentStream ? finalizeStream(currentStream) : '';
|
|
238
|
+
const streamedText = currentStream ? finalizeStream(currentStream, true) : '';
|
|
240
239
|
const finalText = text || streamedText;
|
|
241
240
|
currentStream = null;
|
|
242
241
|
// Skip static tool HTML when process block already shows tool summary
|
|
@@ -244,6 +243,13 @@ export function finalizeAgent(text: string, toolLog?: ToolLogEntry[]): void {
|
|
|
244
243
|
if (content) content.innerHTML = toolHtml + renderMarkdown(finalText);
|
|
245
244
|
if (content) content.setAttribute('data-raw', stripOrchestration(finalText));
|
|
246
245
|
if (content) activateWidgets(content as HTMLElement);
|
|
246
|
+
|
|
247
|
+
// Promote streaming div from real DOM into VS if active
|
|
248
|
+
const vs = getVirtualScroll();
|
|
249
|
+
if (vs.active && state.currentAgentDiv && state.currentAgentDiv.isConnected) {
|
|
250
|
+
vs.appendLiveItem(state.currentAgentDiv);
|
|
251
|
+
state.currentAgentDiv.remove();
|
|
252
|
+
}
|
|
247
253
|
}
|
|
248
254
|
currentStream = null;
|
|
249
255
|
state.currentAgentDiv = null;
|
|
@@ -257,7 +263,6 @@ export function finalizeAgent(text: string, toolLog?: ToolLogEntry[]): void {
|
|
|
257
263
|
export function addMessage(role: string, text: string, cli?: string | null): HTMLDivElement {
|
|
258
264
|
const container = document.getElementById('chatMessages');
|
|
259
265
|
const vs = getVirtualScroll();
|
|
260
|
-
if (vs.active) vs.flushToDOM();
|
|
261
266
|
hideEmptyState();
|
|
262
267
|
removeSkeleton();
|
|
263
268
|
|
|
@@ -274,8 +279,17 @@ export function addMessage(role: string, text: string, cli?: string | null): HTM
|
|
|
274
279
|
}
|
|
275
280
|
const contentEl = div.querySelector('.msg-content');
|
|
276
281
|
if (contentEl) contentEl.setAttribute('data-raw', stripOrchestration(text));
|
|
277
|
-
|
|
278
|
-
|
|
282
|
+
|
|
283
|
+
// Streaming placeholder (agent + empty text) must stay in real DOM
|
|
284
|
+
// so state.currentAgentDiv reference remains valid during streaming.
|
|
285
|
+
const isStreamingPlaceholder = role === 'agent' && !text;
|
|
286
|
+
|
|
287
|
+
if (vs.active && !isStreamingPlaceholder) {
|
|
288
|
+
vs.appendLiveItem(div);
|
|
289
|
+
} else {
|
|
290
|
+
container?.appendChild(div);
|
|
291
|
+
activateWidgets(div);
|
|
292
|
+
}
|
|
279
293
|
scrollToBottom();
|
|
280
294
|
return div;
|
|
281
295
|
}
|
|
@@ -337,22 +351,40 @@ export async function loadMessages(): Promise<void> {
|
|
|
337
351
|
if (chatEl) chatEl.innerHTML = '';
|
|
338
352
|
|
|
339
353
|
if (msgs.length >= VS_THRESHOLD) {
|
|
354
|
+
// Phase 2: lazy render — store skeleton HTML, render on viewport entry
|
|
340
355
|
for (const m of msgs) {
|
|
341
356
|
const role = m.role === 'assistant' ? 'agent' : m.role;
|
|
342
|
-
const
|
|
357
|
+
const rawContent = stripOrchestration(m.content);
|
|
343
358
|
const label = escapeHtml(role === 'user' ? t('msg.you') : getAppName());
|
|
344
359
|
const tools = m.role === 'assistant' ? parseToolLog(m.tool_log) : [];
|
|
345
360
|
const toolHtml = tools.length > 0 ? buildProcessBlockHtml(toProcessSteps(tools), true) : '';
|
|
361
|
+
// Skeleton placeholder — lazy-pending class triggers render on viewport entry
|
|
362
|
+
const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
|
|
346
363
|
const html = role === 'agent'
|
|
347
|
-
? `<div class="msg msg-agent"><div class="agent-icon" aria-hidden="true">${getAgentIcon(m.cli)}</div><div class="agent-body">${toolHtml}<div class="msg-content" data-raw="${escapeHtml(
|
|
348
|
-
: `<div class="msg msg-${role}"><div class="msg-label">${label}</div><div class="msg-content" data-raw="${escapeHtml(
|
|
364
|
+
? `<div class="msg msg-agent"><div class="agent-icon" aria-hidden="true">${getAgentIcon(m.cli)}</div><div class="agent-body">${toolHtml}<div class="msg-content lazy-pending" data-raw="${escapeHtml(rawContent)}">${skeletonContent}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div></div>`
|
|
365
|
+
: `<div class="msg msg-${role}"><div class="msg-label">${label}</div><div class="msg-content lazy-pending" data-raw="${escapeHtml(rawContent)}">${skeletonContent}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div>`;
|
|
349
366
|
vs.addItem(crypto.randomUUID(), html);
|
|
350
367
|
}
|
|
368
|
+
|
|
369
|
+
// Register lazy render callback
|
|
370
|
+
vs.onLazyRender = (targets: HTMLElement[]) => {
|
|
371
|
+
for (const el of targets) {
|
|
372
|
+
if (!el.classList.contains('lazy-pending')) continue;
|
|
373
|
+
const raw = el.getAttribute('data-raw') || '';
|
|
374
|
+
el.innerHTML = raw ? renderMarkdown(raw) : '';
|
|
375
|
+
el.classList.remove('lazy-pending');
|
|
376
|
+
activateWidgets(el);
|
|
377
|
+
|
|
378
|
+
// Persist rendered HTML back into VS cache
|
|
379
|
+
const msgEl = el.closest('[data-vs-idx]') as HTMLElement | null;
|
|
380
|
+
if (msgEl) {
|
|
381
|
+
const idx = Number(msgEl.dataset.vsIdx);
|
|
382
|
+
vs.updateItemHtml(idx, msgEl.outerHTML);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
351
387
|
vs.scrollToBottom();
|
|
352
|
-
requestAnimationFrame(() => {
|
|
353
|
-
const chatElRef = document.getElementById('chatMessages');
|
|
354
|
-
if (chatElRef) activateWidgets(chatElRef);
|
|
355
|
-
});
|
|
356
388
|
} else {
|
|
357
389
|
msgs.forEach(m => {
|
|
358
390
|
const div = addMessage(m.role === 'assistant' ? 'agent' : m.role, m.content, m.cli);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Activates at THRESHOLD messages to prevent DOM bloat
|
|
3
3
|
// Below threshold: standard DOM append (zero overhead)
|
|
4
4
|
|
|
5
|
-
const THRESHOLD =
|
|
5
|
+
const THRESHOLD = 80;
|
|
6
6
|
const BUFFER = 5;
|
|
7
7
|
const EST_HEIGHT = 80;
|
|
8
8
|
|
|
@@ -12,6 +12,8 @@ export interface VirtualItem {
|
|
|
12
12
|
height: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export type LazyRenderCallback = (targets: HTMLElement[]) => void;
|
|
16
|
+
|
|
15
17
|
export class VirtualScroll {
|
|
16
18
|
private items: VirtualItem[] = [];
|
|
17
19
|
private container: HTMLElement;
|
|
@@ -19,10 +21,14 @@ export class VirtualScroll {
|
|
|
19
21
|
private spacerBottom: HTMLDivElement;
|
|
20
22
|
private viewport: HTMLDivElement;
|
|
21
23
|
private _active = false;
|
|
24
|
+
private _totalHeight = 0;
|
|
22
25
|
private rafId: number | null = null;
|
|
23
26
|
private firstVisible = 0;
|
|
24
27
|
private lastVisible = 0;
|
|
25
28
|
|
|
29
|
+
/** Called after render() mounts items in viewport — for lazy rendering */
|
|
30
|
+
onLazyRender: LazyRenderCallback | null = null;
|
|
31
|
+
|
|
26
32
|
constructor(containerId: string) {
|
|
27
33
|
this.container = document.getElementById(containerId)!;
|
|
28
34
|
this.spacerTop = document.createElement('div');
|
|
@@ -37,22 +43,23 @@ export class VirtualScroll {
|
|
|
37
43
|
get count(): number { return this.items.length; }
|
|
38
44
|
|
|
39
45
|
/** Flush all virtual items to real DOM and deactivate VS.
|
|
40
|
-
* Called
|
|
46
|
+
* Called on conversation clear or explicit reset. */
|
|
41
47
|
flushToDOM(): void {
|
|
42
48
|
if (!this._active) return;
|
|
43
49
|
this.container.removeEventListener('scroll', this.scrollHandler);
|
|
44
50
|
if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; }
|
|
45
|
-
// Render all items as real DOM nodes
|
|
46
51
|
this.container.innerHTML = this.items.map(it => it.html).join('');
|
|
47
52
|
this._active = false;
|
|
48
53
|
this.firstVisible = 0;
|
|
49
54
|
this.lastVisible = 0;
|
|
50
|
-
// Release items to free memory (DOM now owns the content)
|
|
51
55
|
this.items = [];
|
|
56
|
+
this._totalHeight = 0;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
addItem(id: string, html: string): void {
|
|
55
|
-
|
|
60
|
+
const item: VirtualItem = { id, html, height: EST_HEIGHT };
|
|
61
|
+
this.items.push(item);
|
|
62
|
+
this._totalHeight += EST_HEIGHT;
|
|
56
63
|
if (!this._active && this.items.length >= THRESHOLD) {
|
|
57
64
|
this.activate();
|
|
58
65
|
}
|
|
@@ -61,18 +68,38 @@ export class VirtualScroll {
|
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
/** Append a live DOM element while keeping VS active.
|
|
72
|
+
* Serializes to HTML for virtual storage. */
|
|
73
|
+
appendLiveItem(div: HTMLElement): void {
|
|
74
|
+
if (!this._active) return;
|
|
75
|
+
const html = div.outerHTML;
|
|
76
|
+
const id = crypto.randomUUID();
|
|
77
|
+
const item: VirtualItem = { id, html, height: EST_HEIGHT };
|
|
78
|
+
this.items.push(item);
|
|
79
|
+
this._totalHeight += EST_HEIGHT;
|
|
80
|
+
this.scheduleRender();
|
|
81
|
+
this.scrollToBottom();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Update cached HTML for a specific item index (used by lazy render). */
|
|
85
|
+
updateItemHtml(idx: number, html: string): void {
|
|
86
|
+
if (this.items[idx]) {
|
|
87
|
+
this.items[idx].html = html;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
64
91
|
private scrollHandler = () => this.scheduleRender();
|
|
65
92
|
|
|
66
93
|
private activate(): void {
|
|
67
94
|
this._active = true;
|
|
68
|
-
|
|
95
|
+
this._totalHeight = 0;
|
|
69
96
|
const existing = this.container.querySelectorAll('.msg');
|
|
70
97
|
existing.forEach((el, i) => {
|
|
71
98
|
if (this.items[i]) {
|
|
72
99
|
this.items[i].height = el.getBoundingClientRect().height;
|
|
100
|
+
this._totalHeight += this.items[i].height;
|
|
73
101
|
}
|
|
74
102
|
});
|
|
75
|
-
// Replace DOM with virtual structure
|
|
76
103
|
this.container.innerHTML = '';
|
|
77
104
|
this.container.append(this.spacerTop, this.viewport, this.spacerBottom);
|
|
78
105
|
this.container.addEventListener('scroll', this.scrollHandler, { passive: true });
|
|
@@ -91,7 +118,6 @@ export class VirtualScroll {
|
|
|
91
118
|
const scrollTop = this.container.scrollTop;
|
|
92
119
|
const viewHeight = this.container.clientHeight;
|
|
93
120
|
|
|
94
|
-
// Binary-ish search for start index
|
|
95
121
|
let accum = 0;
|
|
96
122
|
let startIdx = 0;
|
|
97
123
|
for (let i = 0; i < this.items.length; i++) {
|
|
@@ -112,12 +138,10 @@ export class VirtualScroll {
|
|
|
112
138
|
}
|
|
113
139
|
const last = Math.min(this.items.length - 1, endIdx + BUFFER);
|
|
114
140
|
|
|
115
|
-
// Skip re-render if range unchanged
|
|
116
141
|
if (first === this.firstVisible && last === this.lastVisible) return;
|
|
117
142
|
this.firstVisible = first;
|
|
118
143
|
this.lastVisible = last;
|
|
119
144
|
|
|
120
|
-
// Compute spacer heights
|
|
121
145
|
let topSpace = 0;
|
|
122
146
|
for (let i = 0; i < first; i++) topSpace += this.items[i].height;
|
|
123
147
|
let bottomSpace = 0;
|
|
@@ -126,7 +150,6 @@ export class VirtualScroll {
|
|
|
126
150
|
this.spacerTop.style.height = `${topSpace}px`;
|
|
127
151
|
this.spacerBottom.style.height = `${bottomSpace}px`;
|
|
128
152
|
|
|
129
|
-
// Render visible items
|
|
130
153
|
const frag = document.createDocumentFragment();
|
|
131
154
|
for (let i = first; i <= last; i++) {
|
|
132
155
|
const item = this.items[i];
|
|
@@ -141,26 +164,36 @@ export class VirtualScroll {
|
|
|
141
164
|
this.viewport.innerHTML = '';
|
|
142
165
|
this.viewport.appendChild(frag);
|
|
143
166
|
|
|
144
|
-
// Re-measure rendered heights
|
|
167
|
+
// Re-measure rendered heights and update totalHeight
|
|
145
168
|
this.viewport.querySelectorAll('[data-vs-idx]').forEach(el => {
|
|
146
169
|
const idx = Number((el as HTMLElement).dataset.vsIdx);
|
|
147
170
|
if (this.items[idx]) {
|
|
148
|
-
this.items[idx].height
|
|
171
|
+
const oldH = this.items[idx].height;
|
|
172
|
+
const newH = el.getBoundingClientRect().height;
|
|
173
|
+
this.items[idx].height = newH;
|
|
174
|
+
this._totalHeight += (newH - oldH);
|
|
149
175
|
}
|
|
150
176
|
});
|
|
177
|
+
|
|
178
|
+
// Fire lazy render callback for newly visible items
|
|
179
|
+
if (this.onLazyRender) {
|
|
180
|
+
const lazyTargets = this.viewport.querySelectorAll<HTMLElement>('.lazy-pending');
|
|
181
|
+
if (lazyTargets.length > 0) {
|
|
182
|
+
this.onLazyRender(Array.from(lazyTargets));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
151
185
|
}
|
|
152
186
|
|
|
153
187
|
scrollToBottom(): void {
|
|
154
|
-
|
|
155
|
-
this.container.scrollTop = total;
|
|
188
|
+
this.container.scrollTop = this._totalHeight;
|
|
156
189
|
this.scheduleRender();
|
|
157
190
|
}
|
|
158
191
|
|
|
159
192
|
clear(): void {
|
|
160
193
|
this.items = [];
|
|
194
|
+
this._totalHeight = 0;
|
|
161
195
|
if (this._active) {
|
|
162
196
|
this.container.removeEventListener('scroll', this.scrollHandler);
|
|
163
|
-
// Restore normal DOM structure
|
|
164
197
|
this.viewport.innerHTML = '';
|
|
165
198
|
this.spacerTop.style.height = '0';
|
|
166
199
|
this.spacerBottom.style.height = '0';
|
|
@@ -169,6 +202,7 @@ export class VirtualScroll {
|
|
|
169
202
|
this._active = false;
|
|
170
203
|
this.firstVisible = 0;
|
|
171
204
|
this.lastVisible = 0;
|
|
205
|
+
this.onLazyRender = null;
|
|
172
206
|
if (this.rafId) {
|
|
173
207
|
cancelAnimationFrame(this.rafId);
|
|
174
208
|
this.rafId = null;
|
package/public/js/ws.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { setStatus, updateQueueBadge, addSystemMsg, appendAgentText, finalizeAge
|
|
|
4
4
|
import { t, getLang } from './features/i18n.js';
|
|
5
5
|
import { getVirtualScroll } from './virtual-scroll.js';
|
|
6
6
|
import { ICONS, emojiToIcon } from './icons.js';
|
|
7
|
-
import { escapeHtml } from './render.js';
|
|
7
|
+
import { escapeHtml, cancelPostRender } from './render.js';
|
|
8
8
|
import type { OrcStateName } from './state.js';
|
|
9
9
|
|
|
10
10
|
const ROADMAP_PHASES = ['P', 'A', 'B', 'C'] as const;
|
|
@@ -258,6 +258,7 @@ export function connect(): void {
|
|
|
258
258
|
} else if (msg.type === 'orchestrate_done') {
|
|
259
259
|
finalizeAgent(msg.text || '');
|
|
260
260
|
} else if (msg.type === 'clear') {
|
|
261
|
+
cancelPostRender();
|
|
261
262
|
cleanupToolActivity();
|
|
262
263
|
getVirtualScroll().clear();
|
|
263
264
|
const el = document.getElementById('chatMessages');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ensure-native-modules.cjs
|
|
4
|
+
*
|
|
5
|
+
* Verifies that native addons needed for local runtime are loadable.
|
|
6
|
+
* If better-sqlite3 exists but was built against a different Node ABI,
|
|
7
|
+
* rebuild it in-place before build/test steps continue.
|
|
8
|
+
*/
|
|
9
|
+
const { execFileSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
12
|
+
|
|
13
|
+
function loadBetterSqlite3() {
|
|
14
|
+
try {
|
|
15
|
+
require('better-sqlite3');
|
|
16
|
+
return null;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isRecoverableNativeMismatch(error) {
|
|
23
|
+
const text = String(error && (error.stack || error.message || error));
|
|
24
|
+
return (
|
|
25
|
+
text.includes('better_sqlite3.node') ||
|
|
26
|
+
text.includes('better-sqlite3') ||
|
|
27
|
+
text.includes('NODE_MODULE_VERSION') ||
|
|
28
|
+
text.includes('ERR_DLOPEN_FAILED')
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const firstError = loadBetterSqlite3();
|
|
33
|
+
if (!firstError) process.exit(0);
|
|
34
|
+
|
|
35
|
+
if (!isRecoverableNativeMismatch(firstError)) {
|
|
36
|
+
console.error('[jaw:native] better-sqlite3 load failed with a non-recoverable error.');
|
|
37
|
+
console.error(firstError);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.warn('[jaw:native] better-sqlite3 load failed — rebuilding native module for current Node runtime...');
|
|
42
|
+
execFileSync(npmBin, ['rebuild', 'better-sqlite3'], {
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
cwd: process.cwd(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const secondError = loadBetterSqlite3();
|
|
48
|
+
if (secondError) {
|
|
49
|
+
console.error('[jaw:native] better-sqlite3 is still not loadable after rebuild.');
|
|
50
|
+
console.error(secondError);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('[jaw:native] better-sqlite3 is ready for this Node runtime.');
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[switch]$Force,
|
|
3
|
+
[switch]$Update,
|
|
4
|
+
[switch]$Upstream,
|
|
5
|
+
[string]$Repo = $env:OFFICECLI_REPO
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = "Stop"
|
|
9
|
+
|
|
10
|
+
function Write-Info($msg) { Write-Host "▸ $msg" -ForegroundColor Cyan }
|
|
11
|
+
function Write-Ok($msg) { Write-Host "✔ $msg" -ForegroundColor Green }
|
|
12
|
+
function Write-Warn($msg) { Write-Host "⚠ $msg" -ForegroundColor Yellow }
|
|
13
|
+
function Fail($msg) { Write-Host "✖ $msg" -ForegroundColor Red; exit 1 }
|
|
14
|
+
|
|
15
|
+
if ($Upstream -and $env:OFFICECLI_REPO) {
|
|
16
|
+
Fail "--upstream cannot be combined with OFFICECLI_REPO"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (-not $Repo) {
|
|
20
|
+
$Repo = "lidge-jun/OfficeCLI"
|
|
21
|
+
}
|
|
22
|
+
if ($Upstream) {
|
|
23
|
+
$Repo = "iOfficeAI/OfficeCLI"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
$installDir = Join-Path $env:LOCALAPPDATA "OfficeCli"
|
|
27
|
+
$targetBin = Join-Path $installDir "officecli.exe"
|
|
28
|
+
$downloadBin = Join-Path $installDir "officecli.download.exe"
|
|
29
|
+
|
|
30
|
+
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
|
|
31
|
+
switch ($arch) {
|
|
32
|
+
"x64" { $asset = "officecli-win-x64.exe" }
|
|
33
|
+
"arm64" { $asset = "officecli-win-arm64.exe" }
|
|
34
|
+
default { Fail "Unsupported Windows architecture: $arch" }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Normalize-Version([string]$version) {
|
|
38
|
+
if (-not $version) { return "" }
|
|
39
|
+
return $version.Trim().TrimStart('v')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Get-LatestTag([string]$repoName) {
|
|
43
|
+
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$repoName/releases/latest" -Headers @{ "User-Agent" = "cli-jaw-postinstall" }
|
|
44
|
+
return [string]$release.tag_name
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Write-Info "Platform: win32/$arch -> $asset"
|
|
48
|
+
|
|
49
|
+
if ((Test-Path $targetBin) -and -not $Force -and -not $Update) {
|
|
50
|
+
try {
|
|
51
|
+
$current = & $targetBin --version 2>$null
|
|
52
|
+
Write-Ok "officecli already installed: v$($current.Trim())"
|
|
53
|
+
Write-Host " Use -Force to reinstall or -Update to refresh only when outdated"
|
|
54
|
+
exit 0
|
|
55
|
+
} catch {
|
|
56
|
+
Write-Warn "Existing officecli is not executable; reinstalling"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ((Test-Path $targetBin) -and -not $Force -and $Update) {
|
|
61
|
+
$current = ""
|
|
62
|
+
$latest = ""
|
|
63
|
+
try { $current = & $targetBin --version 2>$null } catch { }
|
|
64
|
+
try { $latest = Get-LatestTag $Repo } catch { }
|
|
65
|
+
if ($current -and $latest -and (Normalize-Version $current) -eq (Normalize-Version $latest)) {
|
|
66
|
+
Write-Ok "officecli already up to date: v$(Normalize-Version $current)"
|
|
67
|
+
exit 0
|
|
68
|
+
}
|
|
69
|
+
if ($current -and $latest) {
|
|
70
|
+
Write-Info "Updating officecli v$(Normalize-Version $current) -> v$(Normalize-Version $latest)"
|
|
71
|
+
} else {
|
|
72
|
+
Write-Warn "Could not compare installed version with latest release; reinstalling"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
|
|
77
|
+
$downloadUrl = "https://github.com/$Repo/releases/latest/download/$asset"
|
|
78
|
+
|
|
79
|
+
Write-Info "Downloading officecli from $Repo..."
|
|
80
|
+
Invoke-WebRequest -Uri $downloadUrl -OutFile $downloadBin
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
$version = (& $downloadBin --version 2>$null).Trim()
|
|
84
|
+
} catch {
|
|
85
|
+
Remove-Item -Force $downloadBin -ErrorAction SilentlyContinue
|
|
86
|
+
Fail "Binary exists but won't execute"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Move-Item -Force $downloadBin $targetBin
|
|
90
|
+
Write-Ok "officecli v$version installed -> $targetBin"
|
|
91
|
+
|
|
92
|
+
$pathEntries = (($env:PATH -split ';') | ForEach-Object { $_.Trim() }) | Where-Object { $_ }
|
|
93
|
+
if (-not ($pathEntries -contains $installDir)) {
|
|
94
|
+
Write-Warn "officecli is not on PATH. Add this location if you want to run it directly:"
|
|
95
|
+
Write-Host " $installDir"
|
|
96
|
+
}
|