cli-jaw 1.6.12 → 1.6.14
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/dist/bin/commands/dispatch.js +41 -14
- package/dist/bin/commands/dispatch.js.map +1 -1
- package/dist/bin/commands/launchd.js +3 -2
- package/dist/bin/commands/launchd.js.map +1 -1
- package/dist/bin/commands/memory.js +11 -0
- package/dist/bin/commands/memory.js.map +1 -1
- package/dist/bin/commands/service.js +4 -3
- package/dist/bin/commands/service.js.map +1 -1
- package/dist/server.js +34 -6
- package/dist/server.js.map +1 -1
- package/dist/src/agent/lifecycle-handler.js +10 -4
- package/dist/src/agent/lifecycle-handler.js.map +1 -1
- package/dist/src/agent/memory-flush-controller.js +50 -13
- package/dist/src/agent/memory-flush-controller.js.map +1 -1
- package/dist/src/agent/spawn.js +48 -13
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/handlers-runtime.js +4 -1
- package/dist/src/cli/handlers-runtime.js.map +1 -1
- package/dist/src/cli/handlers.js +4 -4
- package/dist/src/cli/handlers.js.map +1 -1
- package/dist/src/core/config.js +40 -3
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/db.js +12 -0
- package/dist/src/core/db.js.map +1 -1
- package/dist/src/core/instance.js +18 -4
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/runtime-path.js +69 -0
- package/dist/src/core/runtime-path.js.map +1 -0
- package/dist/src/memory/identity.js +6 -3
- package/dist/src/memory/identity.js.map +1 -1
- package/dist/src/memory/reflect.js +47 -0
- package/dist/src/memory/reflect.js.map +1 -1
- package/dist/src/memory/shared.js +48 -15
- package/dist/src/memory/shared.js.map +1 -1
- package/dist/src/routes/avatar.js +120 -0
- package/dist/src/routes/avatar.js.map +1 -0
- package/dist/src/routes/jaw-memory.js +25 -1
- package/dist/src/routes/jaw-memory.js.map +1 -1
- package/dist/src/routes/memory.js +4 -1
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/telegram/bot.js +7 -1
- package/dist/src/telegram/bot.js.map +1 -1
- package/package.json +1 -1
- package/public/css/chat.css +47 -5
- package/public/css/layout.css +57 -0
- package/public/css/variables.css +3 -15
- package/public/dist/assets/{employees-CA_DI2gy.js → employees-zxrU6ZV_.js} +1 -1
- package/public/dist/assets/index-D61icK-D.js +50 -0
- package/public/dist/assets/index-DVTRbkJF.css +1 -0
- package/public/dist/assets/locale-CxI5nTcf.js +3 -0
- package/public/dist/assets/render-CVr6a-dp.js +25 -0
- package/public/dist/assets/settings-BHIV4l1s.js +1 -0
- package/public/dist/assets/settings-Dl3RnWsB.js +40 -0
- package/public/dist/assets/{skills-D3cWRZOl.js → skills-DhiCSGws.js} +1 -1
- package/public/dist/assets/skills-JuDja1UC.js +1 -0
- package/public/dist/assets/{slash-commands-PkW1NPle.js → slash-commands-B1k1vFJG.js} +1 -1
- package/public/dist/assets/slash-commands-DyLS0abr.js +1 -0
- package/public/dist/assets/ui-BXZhbE_1.js +131 -0
- package/public/dist/assets/ui-qR28iS0L.js +1 -0
- package/public/dist/assets/{ws-Dcq99IkD.js → ws-CleMWrLF.js} +1 -1
- package/public/dist/index.html +28 -11
- package/public/index.html +26 -9
- package/public/js/features/avatar.ts +165 -33
- package/public/js/features/memory.ts +17 -5
- package/public/js/features/settings-templates.ts +6 -5
- package/public/js/locale.ts +30 -0
- package/public/js/main.ts +1 -1
- package/public/js/render.ts +2 -1
- package/public/js/ui.ts +30 -22
- package/public/js/uuid.ts +24 -0
- package/public/js/virtual-scroll.ts +60 -21
- package/public/js/ws.ts +6 -2
- package/public/locales/en.json +2 -3
- package/public/locales/ko.json +2 -3
- package/public/dist/assets/index-6UFnW9uO.js +0 -50
- package/public/dist/assets/index-ck7lqnh7.css +0 -1
- package/public/dist/assets/locale-DVVWjxKN.js +0 -1
- package/public/dist/assets/render-C2tuSVTL.js +0 -25
- package/public/dist/assets/settings-BJcG1r6G.js +0 -41
- package/public/dist/assets/settings-DV5X1U8f.js +0 -1
- package/public/dist/assets/skills-idPvxY0n.js +0 -1
- package/public/dist/assets/slash-commands-BHtBaFWh.js +0 -1
- package/public/dist/assets/ui-BdW-cWnY.js +0 -131
- package/public/dist/assets/ui-qcenMIau.js +0 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { api, apiJson } from '../api.js';
|
|
3
3
|
import { ICONS } from '../icons.js';
|
|
4
4
|
import { escapeHtml } from '../render.js';
|
|
5
|
+
import { t as i18n, getLang, setLang } from '../locale.js';
|
|
5
6
|
|
|
6
7
|
// ── Prompt Modal ──
|
|
7
8
|
|
|
@@ -80,7 +81,7 @@ function openTemplateEditor(tmpl: TemplateInfo): void {
|
|
|
80
81
|
const saveBtn = document.getElementById('templateSaveBtn');
|
|
81
82
|
if (saveBtn) saveBtn.style.display = 'none';
|
|
82
83
|
const toggle = document.getElementById('templateDevToggle');
|
|
83
|
-
if (toggle) { toggle.style.color = 'var(--text-dim)'; toggle.style.borderColor = 'var(--border)'; toggle.innerHTML = `${ICONS.tool}
|
|
84
|
+
if (toggle) { toggle.style.color = 'var(--text-dim)'; toggle.style.borderColor = 'var(--border)'; toggle.innerHTML = `${ICONS.tool} ${i18n('devMode')}`; }
|
|
84
85
|
const title = document.getElementById('templateModalTitle');
|
|
85
86
|
if (title) title.innerHTML = `${ICONS.file} ${escapeHtml(tmpl.filename)}`;
|
|
86
87
|
showTemplateView('editor');
|
|
@@ -88,7 +89,7 @@ function openTemplateEditor(tmpl: TemplateInfo): void {
|
|
|
88
89
|
|
|
89
90
|
export function toggleDevMode(): void {
|
|
90
91
|
if (!_devMode) {
|
|
91
|
-
if (!confirm('
|
|
92
|
+
if (!confirm(i18n('promptEditWarning'))) return;
|
|
92
93
|
}
|
|
93
94
|
_devMode = !_devMode;
|
|
94
95
|
const editor = document.getElementById('templateEditor') as HTMLTextAreaElement;
|
|
@@ -99,7 +100,7 @@ export function toggleDevMode(): void {
|
|
|
99
100
|
if (toggle) {
|
|
100
101
|
toggle.style.color = _devMode ? 'var(--stop-btn)' : 'var(--text-dim)';
|
|
101
102
|
toggle.style.borderColor = _devMode ? 'var(--stop-btn)' : 'var(--border)';
|
|
102
|
-
toggle.innerHTML = _devMode ? `${ICONS.lockOpen}
|
|
103
|
+
toggle.innerHTML = _devMode ? `${ICONS.lockOpen} ${i18n('devModeOn')}` : `${ICONS.tool} ${i18n('devMode')}`;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -109,7 +110,7 @@ export async function saveTemplateFromModal(): Promise<void> {
|
|
|
109
110
|
if (!id) return;
|
|
110
111
|
await apiJson(`/api/prompt-templates/${id}`, 'PUT', { content: editor.value });
|
|
111
112
|
const label = document.getElementById('templateEditorLabel');
|
|
112
|
-
if (label) { label.innerHTML = `${ICONS.check}
|
|
113
|
+
if (label) { label.innerHTML = `${ICONS.check} ${i18n('savedAndReloaded')}`; setTimeout(() => { label.innerHTML = `${ICONS.file} ${escapeHtml(id)}.md`; }, 2000); }
|
|
113
114
|
const t = _templates.find(x => x.id === id);
|
|
114
115
|
if (t) t.content = editor.value;
|
|
115
116
|
}
|
|
@@ -120,7 +121,7 @@ function showTemplateView(view: 'tree' | 'editor'): void {
|
|
|
120
121
|
if (treeView) treeView.style.display = view === 'tree' ? '' : 'none';
|
|
121
122
|
if (editorView) editorView.style.display = view === 'editor' ? 'flex' : 'none';
|
|
122
123
|
const title = document.getElementById('templateModalTitle');
|
|
123
|
-
if (title && view === 'tree') title.innerHTML = `${ICONS.plan}
|
|
124
|
+
if (title && view === 'tree') title.innerHTML = `${ICONS.plan} ${i18n('promptStructure')}`;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
export function templateGoBack(): void { showTemplateView('tree'); }
|
package/public/js/locale.ts
CHANGED
|
@@ -21,3 +21,33 @@ export function syncStoredLocale(locale: string): void {
|
|
|
21
21
|
}
|
|
22
22
|
} catch { }
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
// ── Translation Map ──
|
|
26
|
+
type Lang = 'ko' | 'en';
|
|
27
|
+
|
|
28
|
+
const STRINGS: Record<string, Record<Lang, string>> = {
|
|
29
|
+
devMode: { ko: '개발자 모드', en: 'Developer Mode' },
|
|
30
|
+
devModeOn: { ko: '개발자 모드 ON', en: 'Developer Mode ON' },
|
|
31
|
+
promptEditWarning: {
|
|
32
|
+
ko: '⚠ 프롬프트를 직접 수정하면 예상치 못한 동작이 발생할 수 있습니다.\n계속하시겠습니까?',
|
|
33
|
+
en: '⚠ Editing prompts directly may cause unexpected behavior.\nContinue?'
|
|
34
|
+
},
|
|
35
|
+
savedAndReloaded: { ko: '저장 + 핫리로드 완료!', en: 'Saved + Hot Reloaded!' },
|
|
36
|
+
promptStructure: { ko: '프롬프트 구조', en: 'Prompt Structure' },
|
|
37
|
+
updateNeeded: { ko: '업데이트 필요', en: 'Update Needed' },
|
|
38
|
+
memoryUpdating: { ko: '메모리 구조를 업데이트하는 중...', en: 'Upgrading memory structure...' },
|
|
39
|
+
memoryUpdateBtn: { ko: '메모리 업데이트하기', en: 'Update Memory' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function detectLang(): Lang {
|
|
43
|
+
const pref = getPreferredLocale();
|
|
44
|
+
return pref.startsWith('ko') ? 'ko' : 'en';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let _lang: Lang = detectLang();
|
|
48
|
+
|
|
49
|
+
export function setLang(lang: Lang) { _lang = lang; syncStoredLocale(lang); }
|
|
50
|
+
export function getLang(): Lang { return _lang; }
|
|
51
|
+
export function t(key: string): string {
|
|
52
|
+
return STRINGS[key]?.[_lang] ?? STRINGS[key]?.['en'] ?? key;
|
|
53
|
+
}
|
package/public/js/main.ts
CHANGED
package/public/js/render.ts
CHANGED
|
@@ -117,6 +117,7 @@ function applyMermaidTheme() {
|
|
|
117
117
|
mermaidModule!.default.initialize({
|
|
118
118
|
startOnLoad: false,
|
|
119
119
|
theme: 'base',
|
|
120
|
+
htmlLabels: false,
|
|
120
121
|
themeVariables: getMermaidThemeVars(),
|
|
121
122
|
securityLevel: 'strict',
|
|
122
123
|
suppressErrorRendering: true,
|
|
@@ -333,7 +334,7 @@ export async function rerenderMermaidDiagrams(): Promise<void> {
|
|
|
333
334
|
try {
|
|
334
335
|
applyMermaidTheme();
|
|
335
336
|
const { svg } = await mm.render(id, code);
|
|
336
|
-
el.innerHTML = svg;
|
|
337
|
+
el.innerHTML = sanitizeMermaidSvg(svg);
|
|
337
338
|
appendMermaidActionBtns(el as HTMLElement);
|
|
338
339
|
} catch { /* keep existing render on failure */ }
|
|
339
340
|
}
|
package/public/js/ui.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// ── UI Utilities ──
|
|
2
2
|
import { state } from './state.js';
|
|
3
3
|
import { renderMarkdown, escapeHtml, stripOrchestration, linkifyFilePaths } from './render.js';
|
|
4
|
+
import { generateId } from './uuid.js';
|
|
4
5
|
import { getAppName } from './features/appname.js';
|
|
5
|
-
import {
|
|
6
|
+
import { getAgentAvatarMarkup, getUserAvatarMarkup } from './features/avatar.js';
|
|
6
7
|
import { t } from './features/i18n.js';
|
|
7
8
|
import { api } from './api.js';
|
|
8
9
|
import { cacheMessages, getCachedMessages, appendCachedMessage, upsertMessage, setMessageScope, getScopedMessages } from './features/idb-cache.js';
|
|
@@ -35,12 +36,12 @@ function parseToolLog(toolLog?: string | null): ToolLogEntry[] {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function getAgentIcon(_cli?: string | null): string {
|
|
38
|
-
return
|
|
39
|
+
return getAgentAvatarMarkup();
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function toProcessSteps(tools: ToolLogEntry[]): ProcessStep[] {
|
|
42
43
|
return tools.map((tool: any) => ({
|
|
43
|
-
id:
|
|
44
|
+
id: generateId(),
|
|
44
45
|
icon: tool.icon ? emojiToIcon(tool.icon) : ICONS.tool,
|
|
45
46
|
label: tool.label || tool.name || 'tool',
|
|
46
47
|
type: tool.toolType || 'tool',
|
|
@@ -295,7 +296,7 @@ export function addMessage(role: string, text: string, cli?: string | null): HTM
|
|
|
295
296
|
div.innerHTML = `<div class="agent-icon" aria-hidden="true">${getAgentIcon(cli)}</div><div class="agent-body"><div class="msg-content">${rendered}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div>`;
|
|
296
297
|
} else {
|
|
297
298
|
div.className = `msg msg-${role}`;
|
|
298
|
-
div.innerHTML = `<div class="user-body"><div class="msg-label">${label}</div><div class="msg-content">${rendered}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div><div class="user-icon" aria-hidden="true">${
|
|
299
|
+
div.innerHTML = `<div class="user-body"><div class="msg-label">${label}</div><div class="msg-content">${rendered}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div><div class="user-icon" aria-hidden="true">${getUserAvatarMarkup()}</div>`;
|
|
299
300
|
}
|
|
300
301
|
const contentEl = div.querySelector('.msg-content');
|
|
301
302
|
if (contentEl) contentEl.setAttribute('data-raw', stripOrchestration(text));
|
|
@@ -316,7 +317,7 @@ export function addMessage(role: string, text: string, cli?: string | null): HTM
|
|
|
316
317
|
if (msgCount >= VS_THRESHOLD) {
|
|
317
318
|
// Feed all existing DOM messages into VS items array
|
|
318
319
|
container.querySelectorAll('.msg').forEach(el => {
|
|
319
|
-
vs.addItem(
|
|
320
|
+
vs.addItem(generateId(), el.outerHTML);
|
|
320
321
|
});
|
|
321
322
|
// Wire widget activation + file path linkification for VS-rendered items
|
|
322
323
|
vs.onPostRender = (viewport: HTMLElement) => {
|
|
@@ -333,6 +334,11 @@ export function addMessage(role: string, text: string, cli?: string | null): HTM
|
|
|
333
334
|
let scrollRAF: number | null = null;
|
|
334
335
|
|
|
335
336
|
export function scrollToBottom(): void {
|
|
337
|
+
const vs = getVirtualScroll();
|
|
338
|
+
if (vs.active) {
|
|
339
|
+
vs.scrollToBottom();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
336
342
|
if (scrollRAF) return;
|
|
337
343
|
scrollRAF = requestAnimationFrame(() => {
|
|
338
344
|
scrollRAF = null;
|
|
@@ -397,21 +403,7 @@ export async function loadMessages(): Promise<void> {
|
|
|
397
403
|
if (chatEl) chatEl.innerHTML = '';
|
|
398
404
|
|
|
399
405
|
if (msgs.length >= VS_THRESHOLD) {
|
|
400
|
-
//
|
|
401
|
-
for (const m of msgs) {
|
|
402
|
-
const role = m.role === 'assistant' ? 'agent' : m.role;
|
|
403
|
-
const rawContent = stripOrchestration(m.content);
|
|
404
|
-
const label = escapeHtml(role === 'user' ? t('msg.you') : getAppName());
|
|
405
|
-
const tools = m.role === 'assistant' ? parseToolLog(m.tool_log) : [];
|
|
406
|
-
const toolHtml = tools.length > 0 ? buildProcessBlockHtml(toProcessSteps(tools), true) : '';
|
|
407
|
-
const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
|
|
408
|
-
const html = role === 'agent'
|
|
409
|
-
? `<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>`
|
|
410
|
-
: `<div class="msg msg-${role}"><div class="user-body"><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><div class="user-icon" aria-hidden="true">${getUserAvatar()}</div></div>`;
|
|
411
|
-
vs.addItem(crypto.randomUUID(), html);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Register lazy render callback
|
|
406
|
+
// RC5 fix: register callbacks BEFORE feeding items so activate() has them
|
|
415
407
|
vs.onLazyRender = (targets: HTMLElement[]) => {
|
|
416
408
|
for (const el of targets) {
|
|
417
409
|
if (!el.classList.contains('lazy-pending')) continue;
|
|
@@ -435,6 +427,22 @@ export async function loadMessages(): Promise<void> {
|
|
|
435
427
|
linkifyFilePaths(viewport);
|
|
436
428
|
};
|
|
437
429
|
|
|
430
|
+
// Bulk-load all items at once — avoids mid-loop activate (RC5 fix)
|
|
431
|
+
const vsItems: import('./virtual-scroll.js').VirtualItem[] = [];
|
|
432
|
+
for (const m of msgs) {
|
|
433
|
+
const role = m.role === 'assistant' ? 'agent' : m.role;
|
|
434
|
+
const rawContent = stripOrchestration(m.content);
|
|
435
|
+
const label = escapeHtml(role === 'user' ? t('msg.you') : getAppName());
|
|
436
|
+
const tools = m.role === 'assistant' ? parseToolLog(m.tool_log) : [];
|
|
437
|
+
const toolHtml = tools.length > 0 ? buildProcessBlockHtml(toProcessSteps(tools), true) : '';
|
|
438
|
+
const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
|
|
439
|
+
const html = role === 'agent'
|
|
440
|
+
? `<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>`
|
|
441
|
+
: `<div class="msg msg-${role}"><div class="user-body"><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><div class="user-icon" aria-hidden="true">${getUserAvatarMarkup()}</div></div>`;
|
|
442
|
+
vsItems.push({ id: generateId(), html, height: 80 });
|
|
443
|
+
}
|
|
444
|
+
vs.setItems(vsItems);
|
|
445
|
+
|
|
438
446
|
vs.scrollToBottom();
|
|
439
447
|
} else {
|
|
440
448
|
msgs.forEach(m => {
|
|
@@ -479,8 +487,8 @@ export async function loadMessages(): Promise<void> {
|
|
|
479
487
|
const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
|
|
480
488
|
const html = role === 'agent'
|
|
481
489
|
? `<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>`
|
|
482
|
-
: `<div class="msg msg-${role}"><div class="user-body"><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><div class="user-icon" aria-hidden="true">${
|
|
483
|
-
vs.addItem(
|
|
490
|
+
: `<div class="msg msg-${role}"><div class="user-body"><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><div class="user-icon" aria-hidden="true">${getUserAvatarMarkup()}</div></div>`;
|
|
491
|
+
vs.addItem(generateId(), html);
|
|
484
492
|
}
|
|
485
493
|
vs.onLazyRender = (targets: HTMLElement[]) => {
|
|
486
494
|
for (const el of targets) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// ── UUID Utility ──
|
|
2
|
+
/**
|
|
3
|
+
* Secure-context-safe UUID v4 generator.
|
|
4
|
+
* crypto.randomUUID() requires Secure Context (HTTPS / localhost).
|
|
5
|
+
* Fallback chain: randomUUID → getRandomValues (RFC 4122) → Math.random.
|
|
6
|
+
* Math.random tier is NOT cryptographically secure — used only for UI element IDs.
|
|
7
|
+
*/
|
|
8
|
+
export function generateId(): string {
|
|
9
|
+
const c = globalThis.crypto;
|
|
10
|
+
if (typeof c?.randomUUID === 'function') return c.randomUUID();
|
|
11
|
+
if (typeof c?.getRandomValues !== 'function') {
|
|
12
|
+
// Last resort: Math.random (never reached in modern browsers)
|
|
13
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, ch => {
|
|
14
|
+
const r = (Math.random() * 16) | 0;
|
|
15
|
+
return (ch === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const bytes = new Uint8Array(16);
|
|
19
|
+
c.getRandomValues(bytes);
|
|
20
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
21
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
22
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
23
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
24
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// ── Virtual Scroll ──
|
|
2
|
+
import { generateId } from './uuid.js';
|
|
2
3
|
// Activates at THRESHOLD messages to prevent DOM bloat
|
|
3
4
|
// Below threshold: standard DOM append (zero overhead)
|
|
4
5
|
|
|
@@ -23,8 +24,8 @@ export class VirtualScroll {
|
|
|
23
24
|
private _active = false;
|
|
24
25
|
private _totalHeight = 0;
|
|
25
26
|
private rafId: number | null = null;
|
|
26
|
-
private firstVisible =
|
|
27
|
-
private lastVisible =
|
|
27
|
+
private firstVisible = -1;
|
|
28
|
+
private lastVisible = -1;
|
|
28
29
|
|
|
29
30
|
/** Called after render() mounts items in viewport — for lazy rendering and widget activation */
|
|
30
31
|
onLazyRender: LazyRenderCallback | null = null;
|
|
@@ -47,22 +48,33 @@ export class VirtualScroll {
|
|
|
47
48
|
* Called on conversation clear or explicit reset. */
|
|
48
49
|
flushToDOM(): void {
|
|
49
50
|
if (!this._active) return;
|
|
51
|
+
this.container.classList.remove('vs-active');
|
|
50
52
|
this.container.removeEventListener('scroll', this.scrollHandler);
|
|
51
53
|
if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; }
|
|
52
54
|
this.container.innerHTML = this.items.map(it => it.html).join('');
|
|
53
55
|
this._active = false;
|
|
54
|
-
this.firstVisible =
|
|
55
|
-
this.lastVisible =
|
|
56
|
+
this.firstVisible = -1;
|
|
57
|
+
this.lastVisible = -1;
|
|
56
58
|
this.items = [];
|
|
57
59
|
this._totalHeight = 0;
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/** Bulk-load items without triggering activate mid-loop (RC5 fix).
|
|
63
|
+
* Call this AFTER registering onLazyRender/onPostRender. */
|
|
64
|
+
setItems(items: VirtualItem[]): void {
|
|
65
|
+
this.items = items;
|
|
66
|
+
this._totalHeight = items.reduce((sum, it) => sum + it.height, 0);
|
|
67
|
+
if (!this._active && this.items.length >= THRESHOLD) {
|
|
68
|
+
this.activate(true);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
addItem(id: string, html: string): void {
|
|
61
73
|
const item: VirtualItem = { id, html, height: EST_HEIGHT };
|
|
62
74
|
this.items.push(item);
|
|
63
75
|
this._totalHeight += EST_HEIGHT;
|
|
64
76
|
if (!this._active && this.items.length >= THRESHOLD) {
|
|
65
|
-
this.activate();
|
|
77
|
+
this.activate(true);
|
|
66
78
|
}
|
|
67
79
|
if (this._active) {
|
|
68
80
|
this.scheduleRender();
|
|
@@ -74,13 +86,12 @@ export class VirtualScroll {
|
|
|
74
86
|
appendLiveItem(div: HTMLElement): void {
|
|
75
87
|
if (!this._active) return;
|
|
76
88
|
const html = div.outerHTML;
|
|
77
|
-
const id =
|
|
89
|
+
const id = generateId();
|
|
78
90
|
const item: VirtualItem = { id, html, height: EST_HEIGHT };
|
|
79
91
|
this.items.push(item);
|
|
80
92
|
this._totalHeight += EST_HEIGHT;
|
|
81
|
-
//
|
|
82
|
-
this.
|
|
83
|
-
this.container.scrollTop = this._totalHeight;
|
|
93
|
+
// RC7 fix: use synchronous scrollToBottom (which handles spacers + render)
|
|
94
|
+
this.scrollToBottom();
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
/** Update cached HTML for a specific item index (used by lazy render). */
|
|
@@ -92,19 +103,30 @@ export class VirtualScroll {
|
|
|
92
103
|
|
|
93
104
|
private scrollHandler = () => this.scheduleRender();
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
/** RC6 fix: activate always scrolls to bottom (new messages are at the end) */
|
|
107
|
+
private activate(toBottom = false): void {
|
|
96
108
|
this._active = true;
|
|
97
109
|
this._totalHeight = 0;
|
|
98
110
|
const existing = this.container.querySelectorAll('.msg');
|
|
99
111
|
existing.forEach((el, i) => {
|
|
100
112
|
if (this.items[i]) {
|
|
101
113
|
this.items[i].height = el.getBoundingClientRect().height;
|
|
102
|
-
this._totalHeight += this.items[i].height;
|
|
103
114
|
}
|
|
104
115
|
});
|
|
105
|
-
|
|
116
|
+
for (const item of this.items) {
|
|
117
|
+
this._totalHeight += item.height;
|
|
118
|
+
}
|
|
119
|
+
this.container.classList.add('vs-active');
|
|
106
120
|
this.container.replaceChildren(this.spacerTop, this.viewport, this.spacerBottom);
|
|
107
121
|
this.container.addEventListener('scroll', this.scrollHandler, { passive: true });
|
|
122
|
+
if (toBottom) {
|
|
123
|
+
// Set spacers for bottom position BEFORE render to avoid top flash
|
|
124
|
+
this.spacerTop.style.height = `${this._totalHeight}px`;
|
|
125
|
+
this.spacerBottom.style.height = '0px';
|
|
126
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
127
|
+
this.firstVisible = -1;
|
|
128
|
+
this.lastVisible = -1;
|
|
129
|
+
}
|
|
108
130
|
this.render();
|
|
109
131
|
}
|
|
110
132
|
|
|
@@ -121,7 +143,7 @@ export class VirtualScroll {
|
|
|
121
143
|
const viewHeight = this.container.clientHeight;
|
|
122
144
|
|
|
123
145
|
let accum = 0;
|
|
124
|
-
let startIdx =
|
|
146
|
+
let startIdx = this.items.length - 1;
|
|
125
147
|
for (let i = 0; i < this.items.length; i++) {
|
|
126
148
|
if (accum + this.items[i].height > scrollTop) {
|
|
127
149
|
startIdx = i;
|
|
@@ -140,10 +162,7 @@ export class VirtualScroll {
|
|
|
140
162
|
}
|
|
141
163
|
const last = Math.min(this.items.length - 1, endIdx + BUFFER);
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
this.firstVisible = first;
|
|
145
|
-
this.lastVisible = last;
|
|
146
|
-
|
|
165
|
+
// RC3 fix: always update spacers even if range unchanged
|
|
147
166
|
let topSpace = 0;
|
|
148
167
|
for (let i = 0; i < first; i++) topSpace += this.items[i].height;
|
|
149
168
|
let bottomSpace = 0;
|
|
@@ -152,6 +171,10 @@ export class VirtualScroll {
|
|
|
152
171
|
this.spacerTop.style.height = `${topSpace}px`;
|
|
153
172
|
this.spacerBottom.style.height = `${bottomSpace}px`;
|
|
154
173
|
|
|
174
|
+
if (first === this.firstVisible && last === this.lastVisible) return;
|
|
175
|
+
this.firstVisible = first;
|
|
176
|
+
this.lastVisible = last;
|
|
177
|
+
|
|
155
178
|
// Build map of currently mounted items by vsIdx
|
|
156
179
|
const mounted = new Map<number, HTMLElement>();
|
|
157
180
|
for (const child of Array.from(this.viewport.children) as HTMLElement[]) {
|
|
@@ -215,6 +238,8 @@ export class VirtualScroll {
|
|
|
215
238
|
/** Batch-read heights from visible elements, batch-write to items array.
|
|
216
239
|
* Separated read/write passes = single forced reflow. */
|
|
217
240
|
private remeasureVisible(): void {
|
|
241
|
+
const wasAtBottom = this.container.scrollHeight - this.container.scrollTop - this.container.clientHeight < 80;
|
|
242
|
+
|
|
218
243
|
const rects: { idx: number; newH: number }[] = [];
|
|
219
244
|
this.viewport.querySelectorAll('[data-vs-idx]').forEach(el => {
|
|
220
245
|
const idx = Number((el as HTMLElement).dataset.vsIdx);
|
|
@@ -222,24 +247,38 @@ export class VirtualScroll {
|
|
|
222
247
|
rects.push({ idx, newH: el.getBoundingClientRect().height });
|
|
223
248
|
}
|
|
224
249
|
});
|
|
250
|
+
let heightChanged = false;
|
|
225
251
|
for (const { idx, newH } of rects) {
|
|
226
252
|
const oldH = this.items[idx].height;
|
|
227
253
|
if (oldH !== newH) {
|
|
228
254
|
this.items[idx].height = newH;
|
|
229
255
|
this._totalHeight += (newH - oldH);
|
|
256
|
+
heightChanged = true;
|
|
230
257
|
}
|
|
231
258
|
}
|
|
259
|
+
if (heightChanged && wasAtBottom) {
|
|
260
|
+
this.scrollToBottom();
|
|
261
|
+
}
|
|
232
262
|
}
|
|
233
263
|
|
|
264
|
+
/** RC2 fix: synchronous scroll — cancel pending RAF, update spacers, render directly */
|
|
234
265
|
scrollToBottom(): void {
|
|
235
|
-
this.
|
|
236
|
-
|
|
266
|
+
if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; }
|
|
267
|
+
// Update spacers to reflect full content height at bottom position
|
|
268
|
+
this.spacerTop.style.height = `${this._totalHeight}px`;
|
|
269
|
+
this.spacerBottom.style.height = '0px';
|
|
270
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
271
|
+
// Reset visible range to force full re-render at new position
|
|
272
|
+
this.firstVisible = -1;
|
|
273
|
+
this.lastVisible = -1;
|
|
274
|
+
this.render();
|
|
237
275
|
}
|
|
238
276
|
|
|
239
277
|
clear(): void {
|
|
240
278
|
this.items = [];
|
|
241
279
|
this._totalHeight = 0;
|
|
242
280
|
if (this._active) {
|
|
281
|
+
this.container.classList.remove('vs-active');
|
|
243
282
|
this.container.removeEventListener('scroll', this.scrollHandler);
|
|
244
283
|
this.viewport.innerHTML = '';
|
|
245
284
|
this.spacerTop.style.height = '0';
|
|
@@ -247,8 +286,8 @@ export class VirtualScroll {
|
|
|
247
286
|
this.container.innerHTML = '';
|
|
248
287
|
}
|
|
249
288
|
this._active = false;
|
|
250
|
-
this.firstVisible =
|
|
251
|
-
this.lastVisible =
|
|
289
|
+
this.firstVisible = -1;
|
|
290
|
+
this.lastVisible = -1;
|
|
252
291
|
this.onLazyRender = null;
|
|
253
292
|
this.onPostRender = null;
|
|
254
293
|
if (this.rafId) {
|
package/public/js/ws.ts
CHANGED
|
@@ -284,8 +284,12 @@ export function connect(): void {
|
|
|
284
284
|
import('./ui.js').then(async m => {
|
|
285
285
|
m.cleanupToolActivity();
|
|
286
286
|
if (!skipReload) {
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
try {
|
|
288
|
+
await m.loadMessages();
|
|
289
|
+
lastLoadTs = Date.now();
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error('[ws] loadMessages failed', error);
|
|
292
|
+
}
|
|
289
293
|
}
|
|
290
294
|
m.setStatus('idle');
|
|
291
295
|
});
|
package/public/locales/en.json
CHANGED
|
@@ -53,9 +53,8 @@
|
|
|
53
53
|
"cmd.skill.resetDone": "Skill reset completed.",
|
|
54
54
|
"cmd.employee.resetUnavailable": "/employee reset is not available in this environment.",
|
|
55
55
|
"cmd.employee.resetDone": "Employees reset to defaults ({count} agents)",
|
|
56
|
-
"cmd.clear.
|
|
57
|
-
"cmd.clear.
|
|
58
|
-
"cmd.clear.done": "Screen cleared. (Chat history preserved)",
|
|
56
|
+
"cmd.clear.remote": "Conversation history cleared. Starting fresh.",
|
|
57
|
+
"cmd.clear.done": "Conversation history cleared and screen reset.",
|
|
59
58
|
"cmd.reset.confirm": "Full reset: MCP, skills, employees, and session will be reset to defaults.\nType /reset confirm to proceed.",
|
|
60
59
|
"cmd.reset.unavailable": "Reset is not supported in this environment.",
|
|
61
60
|
"cmd.reset.done": "Reset complete: {items}",
|
package/public/locales/ko.json
CHANGED
|
@@ -53,9 +53,8 @@
|
|
|
53
53
|
"cmd.skill.resetDone": "스킬 초기화를 실행했습니다.",
|
|
54
54
|
"cmd.employee.resetUnavailable": "이 환경에서는 /employee reset을 사용할 수 없습니다.",
|
|
55
55
|
"cmd.employee.resetDone": "직원 기본값으로 재설정 완료 ({count}명)",
|
|
56
|
-
"cmd.clear.
|
|
57
|
-
"cmd.clear.
|
|
58
|
-
"cmd.clear.done": "화면을 정리했습니다. (대화 기록은 유지됨)",
|
|
56
|
+
"cmd.clear.remote": "대화 히스토리를 초기화했습니다. 새 대화를 시작합니다.",
|
|
57
|
+
"cmd.clear.done": "대화 히스토리를 초기화하고 화면을 정리했습니다.",
|
|
59
58
|
"cmd.reset.confirm": "전체 초기화: MCP, 스킬, 직원, 세션을 기본값으로 재설정합니다.\n실행하려면 /reset confirm 을 입력하세요.",
|
|
60
59
|
"cmd.reset.unavailable": "이 환경에서는 초기화가 지원되지 않습니다.",
|
|
61
60
|
"cmd.reset.done": "초기화 완료: {items}",
|