cli-jaw 1.6.13 → 1.6.15
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/memory.js +11 -0
- package/dist/bin/commands/memory.js.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.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/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/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/package.json +1 -1
- package/public/assets/shark.svg +1 -0
- package/public/css/chat.css +20 -3
- package/public/css/layout.css +46 -15
- package/public/css/variables.css +3 -15
- package/public/dist/assets/{employees-C2G0-Rg9.js → employees-V7lNStu1.js} +1 -1
- package/public/dist/assets/index-Cpe1jccL.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-BoxeLlL9.js +25 -0
- package/public/dist/assets/settings-BcKp6ppP.js +1 -0
- package/public/dist/assets/settings-CBCg5Jhh.js +40 -0
- package/public/dist/assets/skills-BuAXFNgp.js +1 -0
- package/public/dist/assets/{skills-C9o5E1Pc.js → skills-RbauGmBZ.js} +1 -1
- package/public/dist/assets/{slash-commands-DveLHSQt.js → slash-commands-BgKxc49D.js} +1 -1
- package/public/dist/assets/slash-commands-DXGb_iGA.js +1 -0
- package/public/dist/assets/ui-KQ8_sSP8.js +131 -0
- package/public/dist/assets/ui-rD__Mvbs.js +1 -0
- package/public/dist/assets/vendor-icons-C6LXvgi0.js +1 -0
- package/public/dist/assets/{ws-D39_cIa_.js → ws-BtTpgocf.js} +1 -1
- package/public/dist/index.html +25 -18
- package/public/index.html +23 -16
- package/public/js/features/avatar.ts +23 -63
- package/public/js/features/memory.ts +17 -5
- package/public/js/features/settings-templates.ts +6 -5
- package/public/js/icons.ts +10 -4
- package/public/js/locale.ts +30 -0
- package/public/js/render.ts +2 -1
- package/public/js/ui.ts +87 -71
- package/public/js/virtual-scroll-bootstrap.ts +42 -0
- package/public/js/virtual-scroll.ts +165 -51
- package/public/dist/assets/index-CDdXQQmm.css +0 -1
- package/public/dist/assets/index-CIWCSFl-.js +0 -50
- package/public/dist/assets/locale-DVVWjxKN.js +0 -1
- package/public/dist/assets/render-BFAkzW1S.js +0 -25
- package/public/dist/assets/settings-BtX9STQd.js +0 -41
- package/public/dist/assets/settings-DUWhygHi.js +0 -1
- package/public/dist/assets/skills-C6aTdbWY.js +0 -1
- package/public/dist/assets/slash-commands-C1p8kRBv.js +0 -1
- package/public/dist/assets/ui-BpZlLDtM.js +0 -1
- package/public/dist/assets/ui-Dx3w-H-4.js +0 -131
- package/public/dist/assets/vendor-icons-BqxEYYco.js +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { escapeHtml } from '../render.js';
|
|
2
2
|
import { api, getAuthToken } from '../api.js';
|
|
3
|
+
import { ICONS } from '../icons.js';
|
|
3
4
|
|
|
4
5
|
type AvatarRole = 'agent' | 'user';
|
|
5
6
|
type AvatarServerEntry = {
|
|
@@ -16,8 +17,8 @@ type AvatarState = {
|
|
|
16
17
|
|
|
17
18
|
const AGENT_KEY = 'agentAvatar';
|
|
18
19
|
const USER_KEY = 'userAvatar';
|
|
19
|
-
const DEFAULT_AGENT =
|
|
20
|
-
const DEFAULT_USER =
|
|
20
|
+
const DEFAULT_AGENT = ICONS.shark;
|
|
21
|
+
const DEFAULT_USER = ICONS.user;
|
|
21
22
|
|
|
22
23
|
const avatarState: Record<AvatarRole, AvatarState> = {
|
|
23
24
|
agent: { emoji: DEFAULT_AGENT, imageUrl: '', updatedAt: null },
|
|
@@ -30,40 +31,25 @@ function stateFor(role: AvatarRole): AvatarState {
|
|
|
30
31
|
return avatarState[role];
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function defaultEmoji(role: AvatarRole): string {
|
|
34
|
-
return role === 'agent' ? DEFAULT_AGENT : DEFAULT_USER;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
function storageKey(role: AvatarRole): string {
|
|
38
35
|
return role === 'agent' ? AGENT_KEY : USER_KEY;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
function inputId(
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function statusId(role: AvatarRole): string {
|
|
46
|
-
return role === 'agent' ? 'agentAvatarStatus' : 'userAvatarStatus';
|
|
38
|
+
function inputId(_role: AvatarRole): string {
|
|
39
|
+
return _role === 'agent' ? 'agentAvatarPreview' : 'userAvatarPreview';
|
|
47
40
|
}
|
|
48
41
|
|
|
49
42
|
function iconSelector(role: AvatarRole): string {
|
|
50
43
|
return role === 'agent' ? '.agent-icon' : '.user-icon';
|
|
51
44
|
}
|
|
52
45
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const status = document.getElementById(statusId(role));
|
|
61
|
-
if (status) status.textContent = stateFor(role).imageUrl ? 'image active' : 'emoji active';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function setStatus(role: AvatarRole, text: string): void {
|
|
65
|
-
const status = document.getElementById(statusId(role));
|
|
66
|
-
if (status) status.textContent = text;
|
|
46
|
+
function syncPreview(role: AvatarRole): void {
|
|
47
|
+
const preview = document.getElementById(inputId(role));
|
|
48
|
+
if (preview) {
|
|
49
|
+
preview.innerHTML = avatarMarkup(role);
|
|
50
|
+
const kind = stateFor(role).imageUrl ? 'image' : 'emoji';
|
|
51
|
+
preview.setAttribute('data-avatar-kind', kind);
|
|
52
|
+
}
|
|
67
53
|
}
|
|
68
54
|
|
|
69
55
|
function avatarMarkup(role: AvatarRole): string {
|
|
@@ -71,7 +57,8 @@ function avatarMarkup(role: AvatarRole): string {
|
|
|
71
57
|
if (current.imageUrl) {
|
|
72
58
|
return `<img class="avatar-image" src="${escapeHtml(current.imageUrl)}" alt="" loading="lazy" decoding="async">`;
|
|
73
59
|
}
|
|
74
|
-
|
|
60
|
+
// Default icons are Lucide SVG strings — render as-is
|
|
61
|
+
return current.emoji;
|
|
75
62
|
}
|
|
76
63
|
|
|
77
64
|
function applyAvatar(role: AvatarRole): void {
|
|
@@ -91,7 +78,7 @@ function setServerAvatar(role: AvatarRole, payload?: AvatarServerEntry | null):
|
|
|
91
78
|
stateFor(role).imageUrl = '';
|
|
92
79
|
stateFor(role).updatedAt = payload?.updatedAt ?? null;
|
|
93
80
|
}
|
|
94
|
-
|
|
81
|
+
syncPreview(role);
|
|
95
82
|
applyAvatar(role);
|
|
96
83
|
}
|
|
97
84
|
|
|
@@ -110,28 +97,20 @@ async function authorizedFetch(path: string, init: RequestInit): Promise<Respons
|
|
|
110
97
|
}
|
|
111
98
|
|
|
112
99
|
async function uploadAvatar(role: AvatarRole, file: File): Promise<void> {
|
|
113
|
-
setStatus(role, 'uploading...');
|
|
114
100
|
const res = await authorizedFetch(`/api/avatar/${role}/upload`, {
|
|
115
101
|
method: 'POST',
|
|
116
102
|
headers: { 'X-Filename': encodeURIComponent(file.name) },
|
|
117
103
|
body: file,
|
|
118
104
|
});
|
|
119
105
|
const json = await res.json().catch(() => null);
|
|
120
|
-
if (!res.ok) {
|
|
121
|
-
setStatus(role, 'upload failed');
|
|
122
|
-
throw new Error(json?.error || `avatar upload failed (${res.status})`);
|
|
123
|
-
}
|
|
106
|
+
if (!res.ok) throw new Error(json?.error || `avatar upload failed (${res.status})`);
|
|
124
107
|
setServerAvatar(role, json?.data || json);
|
|
125
108
|
}
|
|
126
109
|
|
|
127
110
|
async function resetAvatarImage(role: AvatarRole): Promise<void> {
|
|
128
|
-
setStatus(role, 'resetting...');
|
|
129
111
|
const res = await authorizedFetch(`/api/avatar/${role}/image`, { method: 'DELETE' });
|
|
130
112
|
const json = await res.json().catch(() => null);
|
|
131
|
-
if (!res.ok) {
|
|
132
|
-
setStatus(role, 'reset failed');
|
|
133
|
-
throw new Error(json?.error || `avatar reset failed (${res.status})`);
|
|
134
|
-
}
|
|
113
|
+
if (!res.ok) throw new Error(json?.error || `avatar reset failed (${res.status})`);
|
|
135
114
|
setServerAvatar(role, json?.data || json);
|
|
136
115
|
}
|
|
137
116
|
|
|
@@ -166,8 +145,8 @@ function bindRoleControls(role: AvatarRole): void {
|
|
|
166
145
|
});
|
|
167
146
|
}
|
|
168
147
|
|
|
169
|
-
export function getAgentAvatar(): string { return
|
|
170
|
-
export function getUserAvatar(): string { return
|
|
148
|
+
export function getAgentAvatar(): string { return stateFor('agent').emoji; }
|
|
149
|
+
export function getUserAvatar(): string { return stateFor('user').emoji; }
|
|
171
150
|
export function getAgentAvatarMarkup(): string { return avatarMarkup('agent'); }
|
|
172
151
|
export function getUserAvatarMarkup(): string { return avatarMarkup('user'); }
|
|
173
152
|
|
|
@@ -175,7 +154,7 @@ export function setAgentAvatar(emoji: string): void {
|
|
|
175
154
|
const next = (emoji || '').trim() || DEFAULT_AGENT;
|
|
176
155
|
stateFor('agent').emoji = next;
|
|
177
156
|
localStorage.setItem(storageKey('agent'), next);
|
|
178
|
-
|
|
157
|
+
syncPreview('agent');
|
|
179
158
|
if (!stateFor('agent').imageUrl) applyAvatar('agent');
|
|
180
159
|
}
|
|
181
160
|
|
|
@@ -183,37 +162,18 @@ export function setUserAvatar(emoji: string): void {
|
|
|
183
162
|
const next = (emoji || '').trim() || DEFAULT_USER;
|
|
184
163
|
stateFor('user').emoji = next;
|
|
185
164
|
localStorage.setItem(storageKey('user'), next);
|
|
186
|
-
|
|
165
|
+
syncPreview('user');
|
|
187
166
|
if (!stateFor('user').imageUrl) applyAvatar('user');
|
|
188
167
|
}
|
|
189
168
|
|
|
190
169
|
export async function initAvatar(): Promise<void> {
|
|
191
170
|
stateFor('agent').emoji = localStorage.getItem(AGENT_KEY) || DEFAULT_AGENT;
|
|
192
171
|
stateFor('user').emoji = localStorage.getItem(USER_KEY) || DEFAULT_USER;
|
|
193
|
-
|
|
194
|
-
|
|
172
|
+
syncPreview('agent');
|
|
173
|
+
syncPreview('user');
|
|
195
174
|
|
|
196
175
|
if (!initialized) {
|
|
197
176
|
initialized = true;
|
|
198
|
-
|
|
199
|
-
document.getElementById('avatarSave')?.addEventListener('click', () => {
|
|
200
|
-
const agentInput = document.getElementById('agentAvatarInput') as HTMLInputElement | null;
|
|
201
|
-
const userInput = document.getElementById('userAvatarInput') as HTMLInputElement | null;
|
|
202
|
-
if (agentInput) setAgentAvatar(agentInput.value);
|
|
203
|
-
if (userInput) setUserAvatar(userInput.value);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
for (const id of ['agentAvatarInput', 'userAvatarInput']) {
|
|
207
|
-
document.getElementById(id)?.addEventListener('keydown', (e: Event) => {
|
|
208
|
-
const keyEvent = e as KeyboardEvent;
|
|
209
|
-
if (keyEvent.key === 'Enter') {
|
|
210
|
-
keyEvent.preventDefault();
|
|
211
|
-
document.getElementById('avatarSave')?.click();
|
|
212
|
-
(keyEvent.target as HTMLInputElement).blur();
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
177
|
bindRoleControls('agent');
|
|
218
178
|
bindRoleControls('user');
|
|
219
179
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { escapeHtml } from '../render.js';
|
|
3
3
|
import { api, apiJson } from '../api.js';
|
|
4
4
|
import { ICONS } from '../icons.js';
|
|
5
|
+
import { t } from '../locale.js';
|
|
5
6
|
|
|
6
7
|
interface MemoryFile {
|
|
7
8
|
name: string;
|
|
@@ -79,7 +80,7 @@ function syncSidebarBadge(status: AdvancedMemoryStatus | null, basicCount: numbe
|
|
|
79
80
|
const sideBtn = $('memorySidebarBtn');
|
|
80
81
|
if (!sideBtn) return;
|
|
81
82
|
if (status?.enabled && status?.hasSoul === false) {
|
|
82
|
-
sideBtn.innerHTML = `${ICONS.brain} Memory · <span style="color:var(--accent)"
|
|
83
|
+
sideBtn.innerHTML = `${ICONS.brain} Memory · <span style="color:var(--accent)">${t('updateNeeded')}</span>`;
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
const state = status?.indexState === 'ready'
|
|
@@ -101,7 +102,7 @@ function renderStatusBanner(status: AdvancedMemoryStatus | null) {
|
|
|
101
102
|
banner.style.display = '';
|
|
102
103
|
if (status.hasSoul === false) {
|
|
103
104
|
banner.innerHTML = `<span>Memory structure upgrade available.</span>
|
|
104
|
-
<button id="advUpgradeSoulBtn" class="btn-sm" style="margin-left:8px"
|
|
105
|
+
<button id="advUpgradeSoulBtn" class="btn-sm" style="margin-left:8px">${t('memoryUpdateBtn')}</button>`;
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
107
108
|
if (status.state === 'not_initialized') {
|
|
@@ -324,12 +325,23 @@ export async function rerunAdvancedBootstrap(): Promise<void> {
|
|
|
324
325
|
|
|
325
326
|
export async function upgradeSoulMemory(): Promise<void> {
|
|
326
327
|
setAdvBusy(true);
|
|
327
|
-
setAdvBanner('
|
|
328
|
-
await apiJson<{
|
|
328
|
+
setAdvBanner(t('memoryUpdating'), true);
|
|
329
|
+
const result = await apiJson<{
|
|
330
|
+
activated: boolean;
|
|
331
|
+
created: boolean;
|
|
332
|
+
preview: string;
|
|
333
|
+
}>('/api/jaw-memory/soul/activate', 'POST', {});
|
|
329
334
|
setAdvBusy(false);
|
|
330
|
-
|
|
335
|
+
if (result?.created) {
|
|
336
|
+
setAdvBanner('✓ Soul identity created.');
|
|
337
|
+
} else {
|
|
338
|
+
setAdvBanner('✓ Soul already active.');
|
|
339
|
+
}
|
|
331
340
|
await openMemoryModal();
|
|
332
341
|
switchMemTab('status');
|
|
342
|
+
const freshStatus = await apiJson<any>('/api/memory/status');
|
|
343
|
+
syncSidebarBadge(freshStatus, 0);
|
|
344
|
+
renderStatusBanner(freshStatus);
|
|
333
345
|
}
|
|
334
346
|
|
|
335
347
|
export async function reindexAdvancedMemory(): Promise<void> {
|
|
@@ -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/icons.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
Package,
|
|
30
30
|
ClipboardList,
|
|
31
31
|
Bot,
|
|
32
|
+
CircleUserRound,
|
|
32
33
|
Palette,
|
|
33
34
|
Link,
|
|
34
35
|
HandMetal,
|
|
@@ -54,6 +55,9 @@ import {
|
|
|
54
55
|
Download,
|
|
55
56
|
} from '@lucide/icons';
|
|
56
57
|
|
|
58
|
+
// ── Inline SVG assets (embedded to avoid ?raw import issues in Node.js tests) ──
|
|
59
|
+
const sharkSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12c0 0 2-4 6-4 1 0 2 .5 3 1l3-6c0 0 1 5 3 7 1.5 1.5 5 2 5 2s-1 4-5 4c-1 0-2-.3-3-.8L12 18c0 0-2-1-4-1-4 0-6-5-6-5z"/><circle cx="17" cy="11" r="0.5" fill="currentColor" stroke="none"/><path d="M7 12l1 2"/><path d="M9.5 12l1 2"/></svg>';
|
|
60
|
+
|
|
57
61
|
// ── Size presets ──
|
|
58
62
|
const S = 14; // inline / small
|
|
59
63
|
const M = 16; // default UI
|
|
@@ -62,8 +66,7 @@ function luc(data: Parameters<typeof buildLucideSvg>[0], size = M): string {
|
|
|
62
66
|
return buildLucideSvg(data, { size });
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
// ──
|
|
66
|
-
const SHARK_SVG = '🦈';
|
|
69
|
+
// ── Default avatar icons (Lucide-based, no emoji literals) ──
|
|
67
70
|
|
|
68
71
|
// ── Icon registry ──
|
|
69
72
|
// Keys match the semantic role, NOT the old emoji codepoint.
|
|
@@ -101,8 +104,9 @@ export const ICONS = {
|
|
|
101
104
|
link: luc(Link),
|
|
102
105
|
salute: luc(HandMetal),
|
|
103
106
|
|
|
104
|
-
//
|
|
105
|
-
shark:
|
|
107
|
+
// Avatar defaults
|
|
108
|
+
shark: sharkSvg,
|
|
109
|
+
user: luc(CircleUserRound),
|
|
106
110
|
|
|
107
111
|
// HTML template icons
|
|
108
112
|
paperclip: luc(Paperclip),
|
|
@@ -165,6 +169,7 @@ const iconMap: Partial<Record<IconName, (s: number) => string>> = {
|
|
|
165
169
|
palette: (s) => luc(Palette, s),
|
|
166
170
|
link: (s) => luc(Link, s),
|
|
167
171
|
salute: (s) => luc(HandMetal, s),
|
|
172
|
+
user: (s) => luc(CircleUserRound, s),
|
|
168
173
|
paperclip: (s) => luc(Paperclip, s),
|
|
169
174
|
save: (s) => luc(Save, s),
|
|
170
175
|
gamepad: (s) => luc(Gamepad2, s),
|
|
@@ -197,6 +202,7 @@ const EMOJI_TO_ICON: Record<string, IconName> = {
|
|
|
197
202
|
'⚠️': 'warning',
|
|
198
203
|
'💡': 'lightbulb',
|
|
199
204
|
'🦈': 'shark',
|
|
205
|
+
'👤': 'user',
|
|
200
206
|
'💭': 'thinking',
|
|
201
207
|
'🔍': 'search',
|
|
202
208
|
'🌐': 'web',
|
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/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
|
@@ -7,7 +7,8 @@ import { getAgentAvatarMarkup, getUserAvatarMarkup } from './features/avatar.js'
|
|
|
7
7
|
import { t } from './features/i18n.js';
|
|
8
8
|
import { api } from './api.js';
|
|
9
9
|
import { cacheMessages, getCachedMessages, appendCachedMessage, upsertMessage, setMessageScope, getScopedMessages } from './features/idb-cache.js';
|
|
10
|
-
import { getVirtualScroll, VS_THRESHOLD } from './virtual-scroll.js';
|
|
10
|
+
import { getVirtualScroll, VS_THRESHOLD, type VirtualItem } from './virtual-scroll.js';
|
|
11
|
+
import { bootstrapVirtualHistory, BOOTSTRAP_SEED_COUNT, type VirtualHistoryBootstrapDeps } from './virtual-scroll-bootstrap.js';
|
|
11
12
|
import { createStreamRenderer, appendChunk, finalizeStream, type StreamState } from './streaming-render.js';
|
|
12
13
|
import { activateWidgets } from './diagram/iframe-renderer.js';
|
|
13
14
|
import { renderLiveToolActivity, cleanupToolElements, bindToolItemInteractions, type ToolLogEntry } from './features/tool-ui.js';
|
|
@@ -385,6 +386,87 @@ export async function loadStats(): Promise<void> {
|
|
|
385
386
|
updateStatMsgs(msgs.length);
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
// ── Virtual scroll bootstrap helpers ──
|
|
390
|
+
|
|
391
|
+
function buildVirtualHistoryItems(msgs: MessageItem[]): VirtualItem[] {
|
|
392
|
+
const vsItems: VirtualItem[] = [];
|
|
393
|
+
for (const m of msgs) {
|
|
394
|
+
const role = m.role === 'assistant' ? 'agent' : m.role;
|
|
395
|
+
const rawContent = stripOrchestration(m.content);
|
|
396
|
+
const label = escapeHtml(role === 'user' ? t('msg.you') : getAppName());
|
|
397
|
+
const tools = m.role === 'assistant' ? parseToolLog(m.tool_log) : [];
|
|
398
|
+
const toolHtml = tools.length > 0 ? buildProcessBlockHtml(toProcessSteps(tools), true) : '';
|
|
399
|
+
const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
|
|
400
|
+
const html = role === 'agent'
|
|
401
|
+
? `<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>`
|
|
402
|
+
: `<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>`;
|
|
403
|
+
vsItems.push({ id: generateId(), html, height: 80 });
|
|
404
|
+
}
|
|
405
|
+
return vsItems;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function registerVirtualScrollCallbacks(vs: ReturnType<typeof getVirtualScroll>): void {
|
|
409
|
+
vs.onLazyRender = (targets: HTMLElement[]) => {
|
|
410
|
+
for (const el of targets) {
|
|
411
|
+
if (!el.classList.contains('lazy-pending')) continue;
|
|
412
|
+
const raw = el.getAttribute('data-raw') || '';
|
|
413
|
+
el.innerHTML = raw ? renderMarkdown(raw) : '';
|
|
414
|
+
el.classList.remove('lazy-pending');
|
|
415
|
+
activateWidgets(el);
|
|
416
|
+
const msgEl = el.closest('[data-vs-idx]') as HTMLElement | null;
|
|
417
|
+
if (msgEl) {
|
|
418
|
+
const idx = Number(msgEl.dataset.vsIdx);
|
|
419
|
+
vs.updateItemHtml(idx, msgEl.outerHTML);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
vs.onPostRender = (viewport: HTMLElement) => {
|
|
424
|
+
activateWidgets(viewport);
|
|
425
|
+
linkifyFilePaths(viewport);
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function measureTailWindow(
|
|
430
|
+
chatEl: HTMLElement,
|
|
431
|
+
items: VirtualItem[],
|
|
432
|
+
seedCount: number,
|
|
433
|
+
): number[] {
|
|
434
|
+
const start = Math.max(0, items.length - seedCount);
|
|
435
|
+
const slice = items.slice(start);
|
|
436
|
+
if (slice.length === 0) return [];
|
|
437
|
+
|
|
438
|
+
// Render tail items temporarily into empty chatEl, measure, then clear
|
|
439
|
+
const fragment = document.createDocumentFragment();
|
|
440
|
+
for (const item of slice) {
|
|
441
|
+
const wrapper = document.createElement('div');
|
|
442
|
+
wrapper.innerHTML = item.html;
|
|
443
|
+
const el = wrapper.firstElementChild;
|
|
444
|
+
if (el) fragment.appendChild(el);
|
|
445
|
+
}
|
|
446
|
+
chatEl.appendChild(fragment);
|
|
447
|
+
const heights: number[] = [];
|
|
448
|
+
const children = chatEl.children;
|
|
449
|
+
for (let i = 0; i < children.length; i++) {
|
|
450
|
+
heights.push(children[i].getBoundingClientRect().height);
|
|
451
|
+
}
|
|
452
|
+
chatEl.innerHTML = '';
|
|
453
|
+
return heights;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function makeBootstrapDeps(
|
|
457
|
+
vs: ReturnType<typeof getVirtualScroll>,
|
|
458
|
+
chatEl: HTMLElement,
|
|
459
|
+
): VirtualHistoryBootstrapDeps {
|
|
460
|
+
return {
|
|
461
|
+
registerCallbacks: () => registerVirtualScrollCallbacks(vs),
|
|
462
|
+
measureTailWindow: (items, seedCount) => measureTailWindow(chatEl, items, seedCount),
|
|
463
|
+
setItems: (items, opts) => vs.setItems(items, opts),
|
|
464
|
+
seedMeasuredHeights: (start, h) => vs.seedMeasuredHeights(start, h),
|
|
465
|
+
activateIfNeeded: (toBottom) => vs.activateIfNeeded(toBottom),
|
|
466
|
+
scrollToBottom: () => vs.scrollToBottom(),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
388
470
|
export async function loadMessages(): Promise<void> {
|
|
389
471
|
const vs = getVirtualScroll();
|
|
390
472
|
const chatEl = document.getElementById('chatMessages');
|
|
@@ -403,45 +485,8 @@ export async function loadMessages(): Promise<void> {
|
|
|
403
485
|
if (chatEl) chatEl.innerHTML = '';
|
|
404
486
|
|
|
405
487
|
if (msgs.length >= VS_THRESHOLD) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const role = m.role === 'assistant' ? 'agent' : m.role;
|
|
409
|
-
const rawContent = stripOrchestration(m.content);
|
|
410
|
-
const label = escapeHtml(role === 'user' ? t('msg.you') : getAppName());
|
|
411
|
-
const tools = m.role === 'assistant' ? parseToolLog(m.tool_log) : [];
|
|
412
|
-
const toolHtml = tools.length > 0 ? buildProcessBlockHtml(toProcessSteps(tools), true) : '';
|
|
413
|
-
const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
|
|
414
|
-
const html = role === 'agent'
|
|
415
|
-
? `<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>`
|
|
416
|
-
: `<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>`;
|
|
417
|
-
vs.addItem(generateId(), html);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Register lazy render callback
|
|
421
|
-
vs.onLazyRender = (targets: HTMLElement[]) => {
|
|
422
|
-
for (const el of targets) {
|
|
423
|
-
if (!el.classList.contains('lazy-pending')) continue;
|
|
424
|
-
const raw = el.getAttribute('data-raw') || '';
|
|
425
|
-
el.innerHTML = raw ? renderMarkdown(raw) : '';
|
|
426
|
-
el.classList.remove('lazy-pending');
|
|
427
|
-
activateWidgets(el);
|
|
428
|
-
|
|
429
|
-
// Persist rendered HTML back into VS cache
|
|
430
|
-
const msgEl = el.closest('[data-vs-idx]') as HTMLElement | null;
|
|
431
|
-
if (msgEl) {
|
|
432
|
-
const idx = Number(msgEl.dataset.vsIdx);
|
|
433
|
-
vs.updateItemHtml(idx, msgEl.outerHTML);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
// Activate widgets + file path linkification on all VS-rendered items
|
|
439
|
-
vs.onPostRender = (viewport: HTMLElement) => {
|
|
440
|
-
activateWidgets(viewport);
|
|
441
|
-
linkifyFilePaths(viewport);
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
vs.scrollToBottom();
|
|
488
|
+
const vsItems = buildVirtualHistoryItems(msgs);
|
|
489
|
+
bootstrapVirtualHistory(vsItems, makeBootstrapDeps(vs, chatEl!));
|
|
445
490
|
} else {
|
|
446
491
|
msgs.forEach(m => {
|
|
447
492
|
const div = addMessage(m.role === 'assistant' ? 'agent' : m.role, m.content, m.cli);
|
|
@@ -476,37 +521,8 @@ export async function loadMessages(): Promise<void> {
|
|
|
476
521
|
const cached = await getScopedMessages();
|
|
477
522
|
if (cached.length > 0) {
|
|
478
523
|
if (cached.length >= VS_THRESHOLD) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const rawContent = stripOrchestration(m.content);
|
|
482
|
-
const label = escapeHtml(role === 'user' ? t('msg.you') : getAppName());
|
|
483
|
-
const tools = m.role === 'assistant' && m.tool_log ? parseToolLog(m.tool_log) : [];
|
|
484
|
-
const toolHtml = tools.length > 0 ? buildProcessBlockHtml(toProcessSteps(tools), true) : '';
|
|
485
|
-
const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
|
|
486
|
-
const html = role === 'agent'
|
|
487
|
-
? `<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>`
|
|
488
|
-
: `<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>`;
|
|
489
|
-
vs.addItem(generateId(), html);
|
|
490
|
-
}
|
|
491
|
-
vs.onLazyRender = (targets: HTMLElement[]) => {
|
|
492
|
-
for (const el of targets) {
|
|
493
|
-
if (!el.classList.contains('lazy-pending')) continue;
|
|
494
|
-
const raw = el.getAttribute('data-raw') || '';
|
|
495
|
-
el.innerHTML = raw ? renderMarkdown(raw) : '';
|
|
496
|
-
el.classList.remove('lazy-pending');
|
|
497
|
-
activateWidgets(el);
|
|
498
|
-
const msgEl = el.closest('[data-vs-idx]') as HTMLElement | null;
|
|
499
|
-
if (msgEl) {
|
|
500
|
-
const idx = Number(msgEl.dataset.vsIdx);
|
|
501
|
-
vs.updateItemHtml(idx, msgEl.outerHTML);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
vs.onPostRender = (viewport: HTMLElement) => {
|
|
506
|
-
activateWidgets(viewport);
|
|
507
|
-
linkifyFilePaths(viewport);
|
|
508
|
-
};
|
|
509
|
-
vs.scrollToBottom();
|
|
524
|
+
const vsItems = buildVirtualHistoryItems(cached as MessageItem[]);
|
|
525
|
+
bootstrapVirtualHistory(vsItems, makeBootstrapDeps(vs, chatEl!));
|
|
510
526
|
} else {
|
|
511
527
|
cached.forEach(m => {
|
|
512
528
|
const div = addMessage(m.role === 'assistant' ? 'agent' : m.role, m.content, m.cli);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-logic bootstrap orchestrator for virtual scroll.
|
|
3
|
+
* No DOM imports — safe to import in Node test environment.
|
|
4
|
+
*/
|
|
5
|
+
import type { VirtualItem, LazyRenderCallback } from './virtual-scroll.js';
|
|
6
|
+
|
|
7
|
+
export const BOOTSTRAP_SEED_COUNT = 20;
|
|
8
|
+
|
|
9
|
+
export interface VirtualHistoryBootstrapDeps {
|
|
10
|
+
registerCallbacks: () => void;
|
|
11
|
+
measureTailWindow: (items: VirtualItem[], seedCount: number) => number[];
|
|
12
|
+
setItems: (items: VirtualItem[], options?: { autoActivate?: boolean; toBottom?: boolean }) => void;
|
|
13
|
+
seedMeasuredHeights: (startIndex: number, heights: number[]) => void;
|
|
14
|
+
activateIfNeeded: (toBottom: boolean) => void;
|
|
15
|
+
scrollToBottom: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Orchestrates virtual scroll bootstrap in correct order:
|
|
20
|
+
* 1. registerCallbacks (onLazyRender, onPostRender)
|
|
21
|
+
* 2. setItems with autoActivate:false (load all items without triggering activate)
|
|
22
|
+
* 3. measureTailWindow (measure last N items for accurate initial heights)
|
|
23
|
+
* 4. seedMeasuredHeights (feed measured heights back)
|
|
24
|
+
* 5. activateIfNeeded (switch to VS mode with accurate bottom heights)
|
|
25
|
+
* 6. scrollToBottom
|
|
26
|
+
*/
|
|
27
|
+
export function bootstrapVirtualHistory(
|
|
28
|
+
items: VirtualItem[],
|
|
29
|
+
deps: VirtualHistoryBootstrapDeps,
|
|
30
|
+
): void {
|
|
31
|
+
deps.registerCallbacks();
|
|
32
|
+
deps.setItems(items, { autoActivate: false });
|
|
33
|
+
|
|
34
|
+
const seedStart = Math.max(0, items.length - BOOTSTRAP_SEED_COUNT);
|
|
35
|
+
const heights = deps.measureTailWindow(items, BOOTSTRAP_SEED_COUNT);
|
|
36
|
+
if (heights.length > 0) {
|
|
37
|
+
deps.seedMeasuredHeights(seedStart, heights);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
deps.activateIfNeeded(true);
|
|
41
|
+
deps.scrollToBottom();
|
|
42
|
+
}
|