cli-jaw 1.6.11 → 1.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/bin/commands/dispatch.js +41 -14
  2. package/dist/bin/commands/dispatch.js.map +1 -1
  3. package/dist/bin/commands/launchd.js +3 -2
  4. package/dist/bin/commands/launchd.js.map +1 -1
  5. package/dist/bin/commands/reset.js +1 -1
  6. package/dist/bin/commands/reset.js.map +1 -1
  7. package/dist/bin/commands/service.js +4 -3
  8. package/dist/bin/commands/service.js.map +1 -1
  9. package/dist/server.js +58 -7
  10. package/dist/server.js.map +1 -1
  11. package/dist/src/agent/lifecycle-handler.js +10 -4
  12. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  13. package/dist/src/agent/spawn.js +48 -13
  14. package/dist/src/agent/spawn.js.map +1 -1
  15. package/dist/src/cli/handlers-runtime.js +4 -1
  16. package/dist/src/cli/handlers-runtime.js.map +1 -1
  17. package/dist/src/cli/handlers.js +4 -4
  18. package/dist/src/cli/handlers.js.map +1 -1
  19. package/dist/src/core/config.js +40 -3
  20. package/dist/src/core/config.js.map +1 -1
  21. package/dist/src/core/db.js +41 -0
  22. package/dist/src/core/db.js.map +1 -1
  23. package/dist/src/core/instance.js +18 -4
  24. package/dist/src/core/instance.js.map +1 -1
  25. package/dist/src/core/main-session.js +12 -3
  26. package/dist/src/core/main-session.js.map +1 -1
  27. package/dist/src/core/runtime-path.js +69 -0
  28. package/dist/src/core/runtime-path.js.map +1 -0
  29. package/dist/src/memory/indexing.js +1 -0
  30. package/dist/src/memory/indexing.js.map +1 -1
  31. package/dist/src/memory/shared.js +48 -15
  32. package/dist/src/memory/shared.js.map +1 -1
  33. package/dist/src/orchestrator/pipeline.js +3 -1
  34. package/dist/src/orchestrator/pipeline.js.map +1 -1
  35. package/dist/src/routes/avatar.js +120 -0
  36. package/dist/src/routes/avatar.js.map +1 -0
  37. package/dist/src/telegram/bot.js +7 -1
  38. package/dist/src/telegram/bot.js.map +1 -1
  39. package/package.json +1 -1
  40. package/public/css/chat.css +46 -2
  41. package/public/css/layout.css +26 -0
  42. package/public/css/variables.css +15 -0
  43. package/public/dist/assets/architecture-YZFGNWBL-ztGcIsMX.js +1 -0
  44. package/public/dist/assets/architectureDiagram-Q4EWVU46-BeLDNmwN.js +1 -0
  45. package/public/dist/assets/blockDiagram-DXYQGD6D-ChvfvChp.js +1 -0
  46. package/public/dist/assets/c4Diagram-AHTNJAMY-BzjAXXeE.js +1 -0
  47. package/public/dist/assets/classDiagram-6PBFFD2Q-Ce2Zjeeb.js +1 -0
  48. package/public/dist/assets/classDiagram-v2-HSJHXN6E-DGGliiix.js +1 -0
  49. package/public/dist/assets/cose-bilkent-S5V4N54A-T-OwcThC.js +1 -0
  50. package/public/dist/assets/dagre-KV5264BT-DmdK-8pb.js +1 -0
  51. package/public/dist/assets/diagram-5BDNPKRD-Bf8S6ACH.js +1 -0
  52. package/public/dist/assets/diagram-G4DWMVQ6-C37hs32X.js +1 -0
  53. package/public/dist/assets/diagram-MMDJMWI5-9NNeBmGR.js +1 -0
  54. package/public/dist/assets/diagram-TYMM5635-l-8r-KVr.js +1 -0
  55. package/public/dist/assets/{employees-km3MrgRX.js → employees-C2G0-Rg9.js} +1 -1
  56. package/public/dist/assets/erDiagram-SMLLAGMA-BfSqi5hi.js +1 -0
  57. package/public/dist/assets/flowDiagram-DWJPFMVM-B6am8zYz.js +1 -0
  58. package/public/dist/assets/ganttDiagram-T4ZO3ILL-weu0QfVv.js +1 -0
  59. package/public/dist/assets/gitGraph-7Q5UKJZL-BucTBJ0C.js +1 -0
  60. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-D4BnXgVC.js +1 -0
  61. package/public/dist/assets/idb-cache-C7z4qE00.js +1 -0
  62. package/public/dist/assets/idb-cache-DbK81tgv.js +1 -0
  63. package/public/dist/assets/{index-DbSvUuCm.css → index-CDdXQQmm.css} +1 -1
  64. package/public/dist/assets/{index-CihhAsFo.js → index-CIWCSFl-.js} +9 -9
  65. package/public/dist/assets/info-OMHHGYJF-OpOBLEsS.js +1 -0
  66. package/public/dist/assets/infoDiagram-42DDH7IO-9W0kVtoy.js +1 -0
  67. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-BEgeMMA5.js +1 -0
  68. package/public/dist/assets/journeyDiagram-VCZTEJTY-gIlNwmx5.js +1 -0
  69. package/public/dist/assets/kanban-definition-6JOO6SKY-DV9gfO6_.js +1 -0
  70. package/public/dist/assets/mermaid.core-CYqc8Qyq.js +1 -0
  71. package/public/dist/assets/mindmap-definition-QFDTVHPH-DFHJRlCi.js +1 -0
  72. package/public/dist/assets/packet-4T2RLAQJ-DxyOEAi5.js +1 -0
  73. package/public/dist/assets/pie-ZZUOXDRM-CU7m5wDm.js +1 -0
  74. package/public/dist/assets/pieDiagram-DEJITSTG-BEGiEzHN.js +1 -0
  75. package/public/dist/assets/quadrantDiagram-34T5L4WZ-E5jEMjzC.js +1 -0
  76. package/public/dist/assets/radar-PYXPWWZC-CNpXegnm.js +1 -0
  77. package/public/dist/assets/{render-CCNMcx8O.js → render-BFAkzW1S.js} +1 -1
  78. package/public/dist/assets/requirementDiagram-MS252O5E-B3fjwTBx.js +1 -0
  79. package/public/dist/assets/sankeyDiagram-XADWPNL6-BIyouHFw.js +1 -0
  80. package/public/dist/assets/sequenceDiagram-FGHM5R23-CROknRPY.js +1 -0
  81. package/public/dist/assets/{settings-de6iL7ha.js → settings-BtX9STQd.js} +1 -1
  82. package/public/dist/assets/settings-DUWhygHi.js +1 -0
  83. package/public/dist/assets/skills-C6aTdbWY.js +1 -0
  84. package/public/dist/assets/{skills-CiZtWh5G.js → skills-C9o5E1Pc.js} +1 -1
  85. package/public/dist/assets/slash-commands-C1p8kRBv.js +1 -0
  86. package/public/dist/assets/{slash-commands-BeZm3K9x.js → slash-commands-DveLHSQt.js} +1 -1
  87. package/public/dist/assets/stateDiagram-FHFEXIEX-DqgrX77_.js +1 -0
  88. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-CMxISNLC.js +1 -0
  89. package/public/dist/assets/timeline-definition-GMOUNBTQ-CheLYWOf.js +1 -0
  90. package/public/dist/assets/treeView-SZITEDCU-CWgmTXw-.js +1 -0
  91. package/public/dist/assets/treemap-W4RFUUIX-CMkxaF1N.js +1 -0
  92. package/public/dist/assets/ui-BpZlLDtM.js +1 -0
  93. package/public/dist/assets/ui-Dx3w-H-4.js +131 -0
  94. package/public/dist/assets/{vendor-mermaid-DV2i2BfY.js → vendor-mermaid-C2RBgdM6.js} +4 -4
  95. package/public/dist/assets/vennDiagram-DHZGUBPP-Cm4YZzbv.js +1 -0
  96. package/public/dist/assets/wardley-RL74JXVD-HPEa5s3y.js +1 -0
  97. package/public/dist/assets/wardleyDiagram-NUSXRM2D-vZvODvIY.js +1 -0
  98. package/public/dist/assets/{ws-DQ4lNPWK.js → ws-D39_cIa_.js} +1 -1
  99. package/public/dist/assets/xychartDiagram-5P7HB3ND-DdPllF4_.js +1 -0
  100. package/public/dist/index.html +25 -2
  101. package/public/index.html +23 -0
  102. package/public/js/features/avatar.ts +224 -0
  103. package/public/js/features/chat.ts +1 -1
  104. package/public/js/features/idb-cache.ts +5 -0
  105. package/public/js/main.ts +2 -0
  106. package/public/js/ui.ts +23 -11
  107. package/public/js/uuid.ts +24 -0
  108. package/public/js/virtual-scroll.ts +21 -5
  109. package/public/js/ws.ts +9 -3
  110. package/public/locales/en.json +2 -3
  111. package/public/locales/ko.json +2 -3
  112. package/public/dist/assets/architecture-YZFGNWBL-BBAoUgDs.js +0 -1
  113. package/public/dist/assets/architectureDiagram-Q4EWVU46-CnCGC-W1.js +0 -1
  114. package/public/dist/assets/blockDiagram-DXYQGD6D-CCKUN-eq.js +0 -1
  115. package/public/dist/assets/c4Diagram-AHTNJAMY-2BznTVIo.js +0 -1
  116. package/public/dist/assets/classDiagram-6PBFFD2Q-CNIdDcl_.js +0 -1
  117. package/public/dist/assets/classDiagram-v2-HSJHXN6E-D5c5cCXM.js +0 -1
  118. package/public/dist/assets/cose-bilkent-S5V4N54A-15-I0CI3.js +0 -1
  119. package/public/dist/assets/dagre-KV5264BT-B-7UZ2bP.js +0 -1
  120. package/public/dist/assets/diagram-5BDNPKRD-CtpSLvPV.js +0 -1
  121. package/public/dist/assets/diagram-G4DWMVQ6-C-FxLjz8.js +0 -1
  122. package/public/dist/assets/diagram-MMDJMWI5-CyWYCa1y.js +0 -1
  123. package/public/dist/assets/diagram-TYMM5635-Bc2Z_pKL.js +0 -1
  124. package/public/dist/assets/erDiagram-SMLLAGMA-BOsddzBo.js +0 -1
  125. package/public/dist/assets/flowDiagram-DWJPFMVM-CNS6NI2y.js +0 -1
  126. package/public/dist/assets/ganttDiagram-T4ZO3ILL-DzeONbMw.js +0 -1
  127. package/public/dist/assets/gitGraph-7Q5UKJZL-D-msAZeS.js +0 -1
  128. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-Ck2kMeHe.js +0 -1
  129. package/public/dist/assets/info-OMHHGYJF-38sLGsyT.js +0 -1
  130. package/public/dist/assets/infoDiagram-42DDH7IO-BpZBAgqS.js +0 -1
  131. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-CHk3pDry.js +0 -1
  132. package/public/dist/assets/journeyDiagram-VCZTEJTY-BQ1Iwmix.js +0 -1
  133. package/public/dist/assets/kanban-definition-6JOO6SKY-B0_TgypH.js +0 -1
  134. package/public/dist/assets/mermaid.core-C6Yxaqu5.js +0 -1
  135. package/public/dist/assets/mindmap-definition-QFDTVHPH-Di38Ha2d.js +0 -1
  136. package/public/dist/assets/packet-4T2RLAQJ-WhSOczez.js +0 -1
  137. package/public/dist/assets/pie-ZZUOXDRM-BFlknSFa.js +0 -1
  138. package/public/dist/assets/pieDiagram-DEJITSTG-C580ezo8.js +0 -1
  139. package/public/dist/assets/quadrantDiagram-34T5L4WZ-CqL6mFQa.js +0 -1
  140. package/public/dist/assets/radar-PYXPWWZC-C6TfDjQ7.js +0 -1
  141. package/public/dist/assets/requirementDiagram-MS252O5E-CJ2UTxBk.js +0 -1
  142. package/public/dist/assets/sankeyDiagram-XADWPNL6-CtV7e3kn.js +0 -1
  143. package/public/dist/assets/sequenceDiagram-FGHM5R23-CK_ww48d.js +0 -1
  144. package/public/dist/assets/settings-Jzr8xYot.js +0 -1
  145. package/public/dist/assets/skills-DGAnOiRN.js +0 -1
  146. package/public/dist/assets/slash-commands-D5ZGh5wk.js +0 -1
  147. package/public/dist/assets/stateDiagram-FHFEXIEX-CY21kttq.js +0 -1
  148. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-Da4cAFSL.js +0 -1
  149. package/public/dist/assets/timeline-definition-GMOUNBTQ-BRNd_2SN.js +0 -1
  150. package/public/dist/assets/treeView-SZITEDCU-C4o4RBOK.js +0 -1
  151. package/public/dist/assets/treemap-W4RFUUIX-BIAsQVaf.js +0 -1
  152. package/public/dist/assets/ui-C1QJBOEL.js +0 -131
  153. package/public/dist/assets/ui-CxS6ErNQ.js +0 -1
  154. package/public/dist/assets/vennDiagram-DHZGUBPP-BE5I_z1u.js +0 -1
  155. package/public/dist/assets/wardley-RL74JXVD-BYUgx5S_.js +0 -1
  156. package/public/dist/assets/wardleyDiagram-NUSXRM2D-DZn9qJg1.js +0 -1
  157. package/public/dist/assets/xychartDiagram-5P7HB3ND-DIYzBAwh.js +0 -1
  158. /package/public/dist/assets/{constants-74vpnlVG.js → constants-DwGsi0Gn.js} +0 -0
  159. /package/public/dist/assets/{locale-DIXc-_34.js → locale-DVVWjxKN.js} +0 -0
@@ -0,0 +1,224 @@
1
+ import { escapeHtml } from '../render.js';
2
+ import { api, getAuthToken } from '../api.js';
3
+
4
+ type AvatarRole = 'agent' | 'user';
5
+ type AvatarServerEntry = {
6
+ kind?: 'emoji' | 'image';
7
+ imageUrl?: string;
8
+ updatedAt?: number | null;
9
+ };
10
+ type AvatarServerState = Record<AvatarRole, AvatarServerEntry>;
11
+ type AvatarState = {
12
+ emoji: string;
13
+ imageUrl: string;
14
+ updatedAt: number | null;
15
+ };
16
+
17
+ const AGENT_KEY = 'agentAvatar';
18
+ const USER_KEY = 'userAvatar';
19
+ const DEFAULT_AGENT = '🦈';
20
+ const DEFAULT_USER = '👤';
21
+
22
+ const avatarState: Record<AvatarRole, AvatarState> = {
23
+ agent: { emoji: DEFAULT_AGENT, imageUrl: '', updatedAt: null },
24
+ user: { emoji: DEFAULT_USER, imageUrl: '', updatedAt: null },
25
+ };
26
+
27
+ let initialized = false;
28
+
29
+ function stateFor(role: AvatarRole): AvatarState {
30
+ return avatarState[role];
31
+ }
32
+
33
+ function defaultEmoji(role: AvatarRole): string {
34
+ return role === 'agent' ? DEFAULT_AGENT : DEFAULT_USER;
35
+ }
36
+
37
+ function storageKey(role: AvatarRole): string {
38
+ return role === 'agent' ? AGENT_KEY : USER_KEY;
39
+ }
40
+
41
+ function inputId(role: AvatarRole): string {
42
+ return role === 'agent' ? 'agentAvatarInput' : 'userAvatarInput';
43
+ }
44
+
45
+ function statusId(role: AvatarRole): string {
46
+ return role === 'agent' ? 'agentAvatarStatus' : 'userAvatarStatus';
47
+ }
48
+
49
+ function iconSelector(role: AvatarRole): string {
50
+ return role === 'agent' ? '.agent-icon' : '.user-icon';
51
+ }
52
+
53
+ function getEmoji(role: AvatarRole): string {
54
+ return stateFor(role).emoji;
55
+ }
56
+
57
+ function syncInputs(role: AvatarRole): void {
58
+ const input = document.getElementById(inputId(role)) as HTMLInputElement | null;
59
+ if (input) input.value = getEmoji(role);
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;
67
+ }
68
+
69
+ function avatarMarkup(role: AvatarRole): string {
70
+ const current = stateFor(role);
71
+ if (current.imageUrl) {
72
+ return `<img class="avatar-image" src="${escapeHtml(current.imageUrl)}" alt="" loading="lazy" decoding="async">`;
73
+ }
74
+ return escapeHtml(current.emoji);
75
+ }
76
+
77
+ function applyAvatar(role: AvatarRole): void {
78
+ const html = avatarMarkup(role);
79
+ const kind = stateFor(role).imageUrl ? 'image' : 'emoji';
80
+ document.querySelectorAll(iconSelector(role)).forEach((el) => {
81
+ el.innerHTML = html;
82
+ el.setAttribute('data-avatar-kind', kind);
83
+ });
84
+ }
85
+
86
+ function setServerAvatar(role: AvatarRole, payload?: AvatarServerEntry | null): void {
87
+ if (payload?.kind === 'image' && payload.imageUrl) {
88
+ stateFor(role).imageUrl = payload.imageUrl;
89
+ stateFor(role).updatedAt = payload.updatedAt ?? Date.now();
90
+ } else {
91
+ stateFor(role).imageUrl = '';
92
+ stateFor(role).updatedAt = payload?.updatedAt ?? null;
93
+ }
94
+ syncInputs(role);
95
+ applyAvatar(role);
96
+ }
97
+
98
+ async function loadServerAvatars(): Promise<void> {
99
+ const payload = await api<AvatarServerState>('/api/avatar');
100
+ if (!payload) return;
101
+ setServerAvatar('agent', payload.agent);
102
+ setServerAvatar('user', payload.user);
103
+ }
104
+
105
+ async function authorizedFetch(path: string, init: RequestInit): Promise<Response> {
106
+ const token = await getAuthToken();
107
+ const headers = new Headers(init.headers || {});
108
+ if (token) headers.set('Authorization', `Bearer ${token}`);
109
+ return fetch(path, { ...init, headers });
110
+ }
111
+
112
+ async function uploadAvatar(role: AvatarRole, file: File): Promise<void> {
113
+ setStatus(role, 'uploading...');
114
+ const res = await authorizedFetch(`/api/avatar/${role}/upload`, {
115
+ method: 'POST',
116
+ headers: { 'X-Filename': encodeURIComponent(file.name) },
117
+ body: file,
118
+ });
119
+ 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
+ }
124
+ setServerAvatar(role, json?.data || json);
125
+ }
126
+
127
+ async function resetAvatarImage(role: AvatarRole): Promise<void> {
128
+ setStatus(role, 'resetting...');
129
+ const res = await authorizedFetch(`/api/avatar/${role}/image`, { method: 'DELETE' });
130
+ 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
+ }
135
+ setServerAvatar(role, json?.data || json);
136
+ }
137
+
138
+ function bindRoleControls(role: AvatarRole): void {
139
+ const uploadBtnId = role === 'agent' ? 'agentAvatarUploadBtn' : 'userAvatarUploadBtn';
140
+ const resetBtnId = role === 'agent' ? 'agentAvatarResetBtn' : 'userAvatarResetBtn';
141
+ const fileInputId = role === 'agent' ? 'agentAvatarFile' : 'userAvatarFile';
142
+
143
+ document.getElementById(uploadBtnId)?.addEventListener('click', () => {
144
+ (document.getElementById(fileInputId) as HTMLInputElement | null)?.click();
145
+ });
146
+
147
+ document.getElementById(resetBtnId)?.addEventListener('click', async () => {
148
+ try {
149
+ await resetAvatarImage(role);
150
+ } catch (error) {
151
+ console.warn('[avatar:reset]', (error as Error).message);
152
+ }
153
+ });
154
+
155
+ document.getElementById(fileInputId)?.addEventListener('change', async (event: Event) => {
156
+ const input = event.target as HTMLInputElement;
157
+ const file = input.files?.[0];
158
+ if (!file) return;
159
+ try {
160
+ await uploadAvatar(role, file);
161
+ } catch (error) {
162
+ console.warn('[avatar:upload]', (error as Error).message);
163
+ } finally {
164
+ input.value = '';
165
+ }
166
+ });
167
+ }
168
+
169
+ export function getAgentAvatar(): string { return getEmoji('agent'); }
170
+ export function getUserAvatar(): string { return getEmoji('user'); }
171
+ export function getAgentAvatarMarkup(): string { return avatarMarkup('agent'); }
172
+ export function getUserAvatarMarkup(): string { return avatarMarkup('user'); }
173
+
174
+ export function setAgentAvatar(emoji: string): void {
175
+ const next = (emoji || '').trim() || DEFAULT_AGENT;
176
+ stateFor('agent').emoji = next;
177
+ localStorage.setItem(storageKey('agent'), next);
178
+ syncInputs('agent');
179
+ if (!stateFor('agent').imageUrl) applyAvatar('agent');
180
+ }
181
+
182
+ export function setUserAvatar(emoji: string): void {
183
+ const next = (emoji || '').trim() || DEFAULT_USER;
184
+ stateFor('user').emoji = next;
185
+ localStorage.setItem(storageKey('user'), next);
186
+ syncInputs('user');
187
+ if (!stateFor('user').imageUrl) applyAvatar('user');
188
+ }
189
+
190
+ export async function initAvatar(): Promise<void> {
191
+ stateFor('agent').emoji = localStorage.getItem(AGENT_KEY) || DEFAULT_AGENT;
192
+ stateFor('user').emoji = localStorage.getItem(USER_KEY) || DEFAULT_USER;
193
+ syncInputs('agent');
194
+ syncInputs('user');
195
+
196
+ if (!initialized) {
197
+ 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
+ bindRoleControls('agent');
218
+ bindRoleControls('user');
219
+ }
220
+
221
+ await loadServerAvatars();
222
+ applyAvatar('agent');
223
+ applyAvatar('user');
224
+ }
@@ -202,7 +202,7 @@ function renderFilePreview(): void {
202
202
  }
203
203
 
204
204
  export async function clearChat(): Promise<void> {
205
- apiFire('/api/clear', 'POST');
205
+ // UI-only clear — do NOT call /api/clear (it deletes DB messages)
206
206
  cancelPostRender();
207
207
  getVirtualScroll().clear();
208
208
  const chatEl = document.getElementById('chatMessages');
@@ -74,6 +74,11 @@ function openDB(): Promise<IDBDatabase> {
74
74
 
75
75
  export async function cacheMessages(messages: CachedMessage[]): Promise<void> {
76
76
  try {
77
+ // Guard: never wipe the cache with an empty array — only a deliberate
78
+ // clearCache() call should empty the store. This prevents data loss when
79
+ // the server briefly returns [] during restarts or scope mismatches.
80
+ if (messages.length === 0) return;
81
+
77
82
  const db = await openDB();
78
83
  const tx = db.transaction(STORE, 'readwrite');
79
84
  const store = tx.objectStore(STORE);
package/public/js/main.ts CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  import { state } from './state.js';
50
50
  import { loadCliRegistry, getCliKeys } from './constants.js';
51
51
  import { initAppName } from './features/appname.js';
52
+ import { initAvatar } from './features/avatar.js';
52
53
  import { initSidebar, toggleLeft, toggleRight } from './features/sidebar.js';
53
54
  import { initTheme } from './features/theme.js';
54
55
  import { initGestures } from './features/gesture.js';
@@ -431,6 +432,7 @@ async function bootstrap(): Promise<void> {
431
432
  loadEmployees();
432
433
  initHeartbeatBadge();
433
434
  initAppName();
435
+ await initAvatar();
434
436
  initSidebar();
435
437
  initMsgCopy();
436
438
  initGestures();
package/public/js/ui.ts CHANGED
@@ -1,7 +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';
6
+ import { getAgentAvatarMarkup, getUserAvatarMarkup } from './features/avatar.js';
5
7
  import { t } from './features/i18n.js';
6
8
  import { api } from './api.js';
7
9
  import { cacheMessages, getCachedMessages, appendCachedMessage, upsertMessage, setMessageScope, getScopedMessages } from './features/idb-cache.js';
@@ -34,13 +36,12 @@ function parseToolLog(toolLog?: string | null): ToolLogEntry[] {
34
36
  }
35
37
 
36
38
  function getAgentIcon(_cli?: string | null): string {
37
- // Chat mascot is always the shark — provider icons are for header/sidebar only
38
- return ICONS.shark;
39
+ return getAgentAvatarMarkup();
39
40
  }
40
41
 
41
42
  function toProcessSteps(tools: ToolLogEntry[]): ProcessStep[] {
42
43
  return tools.map((tool: any) => ({
43
- id: crypto.randomUUID(),
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="msg-label">${label}</div><div class="msg-content">${rendered}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button>`;
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(crypto.randomUUID(), el.outerHTML);
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;
@@ -368,11 +374,15 @@ export function handleSave(): void {
368
374
  }
369
375
  }
370
376
 
377
+ function updateStatMsgs(count: number): void {
378
+ const el = document.getElementById('statMsgs');
379
+ if (el) el.textContent = t('stat.messages', { count });
380
+ }
381
+
371
382
  export async function loadStats(): Promise<void> {
372
383
  const msgs = await api<MessageItem[]>('/api/messages');
373
384
  if (!msgs) return;
374
- const el = document.getElementById('statMsgs');
375
- if (el) el.textContent = t('stat.messages', { count: msgs.length });
385
+ updateStatMsgs(msgs.length);
376
386
  }
377
387
 
378
388
  export async function loadMessages(): Promise<void> {
@@ -403,8 +413,8 @@ export async function loadMessages(): Promise<void> {
403
413
  const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
404
414
  const html = role === 'agent'
405
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>`
406
- : `<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>`;
407
- vs.addItem(crypto.randomUUID(), html);
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);
408
418
  }
409
419
 
410
420
  // Register lazy render callback
@@ -452,6 +462,7 @@ export async function loadMessages(): Promise<void> {
452
462
  cacheMessages(msgs.map(m => ({
453
463
  role: m.role, content: m.content, cli: m.cli ?? null, tool_log: m.tool_log ?? null, timestamp: Date.now(),
454
464
  }))).catch(() => {});
465
+ updateStatMsgs(msgs.length);
455
466
  showEmptyState();
456
467
  return;
457
468
  }
@@ -474,8 +485,8 @@ export async function loadMessages(): Promise<void> {
474
485
  const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
475
486
  const html = role === 'agent'
476
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>`
477
- : `<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>`;
478
- vs.addItem(crypto.randomUUID(), html);
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);
479
490
  }
480
491
  vs.onLazyRender = (targets: HTMLElement[]) => {
481
492
  for (const el of targets) {
@@ -513,6 +524,7 @@ export async function loadMessages(): Promise<void> {
513
524
  });
514
525
  }
515
526
  addSystemMsg(`${ICONS.warning} 오프라인 모드 — 캐시된 메시지 표시 중`);
527
+ updateStatMsgs(cached.length);
516
528
  }
517
529
  showEmptyState();
518
530
  }
@@ -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
 
@@ -47,6 +48,7 @@ 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('');
@@ -74,13 +76,13 @@ export class VirtualScroll {
74
76
  appendLiveItem(div: HTMLElement): void {
75
77
  if (!this._active) return;
76
78
  const html = div.outerHTML;
77
- const id = crypto.randomUUID();
79
+ const id = generateId();
78
80
  const item: VirtualItem = { id, html, height: EST_HEIGHT };
79
81
  this.items.push(item);
80
82
  this._totalHeight += EST_HEIGHT;
81
83
  // Render immediately then scroll again after height is remeasured
82
84
  this.render();
83
- this.container.scrollTop = this._totalHeight;
85
+ this.scrollToBottom();
84
86
  }
85
87
 
86
88
  /** Update cached HTML for a specific item index (used by lazy render). */
@@ -99,10 +101,14 @@ export class VirtualScroll {
99
101
  existing.forEach((el, i) => {
100
102
  if (this.items[i]) {
101
103
  this.items[i].height = el.getBoundingClientRect().height;
102
- this._totalHeight += this.items[i].height;
103
104
  }
104
105
  });
106
+ // Rebuild _totalHeight from items array (covers both DOM-measured and estimated heights)
107
+ for (const item of this.items) {
108
+ this._totalHeight += item.height;
109
+ }
105
110
  // Atomic swap — avoids visible blank frame during activation
111
+ this.container.classList.add('vs-active');
106
112
  this.container.replaceChildren(this.spacerTop, this.viewport, this.spacerBottom);
107
113
  this.container.addEventListener('scroll', this.scrollHandler, { passive: true });
108
114
  this.render();
@@ -121,7 +127,7 @@ export class VirtualScroll {
121
127
  const viewHeight = this.container.clientHeight;
122
128
 
123
129
  let accum = 0;
124
- let startIdx = 0;
130
+ let startIdx = this.items.length - 1; // fallback to bottom, not top
125
131
  for (let i = 0; i < this.items.length; i++) {
126
132
  if (accum + this.items[i].height > scrollTop) {
127
133
  startIdx = i;
@@ -215,6 +221,9 @@ export class VirtualScroll {
215
221
  /** Batch-read heights from visible elements, batch-write to items array.
216
222
  * Separated read/write passes = single forced reflow. */
217
223
  private remeasureVisible(): void {
224
+ // Capture bottom-proximity BEFORE heights change
225
+ const wasAtBottom = this.container.scrollHeight - this.container.scrollTop - this.container.clientHeight < 80;
226
+
218
227
  const rects: { idx: number; newH: number }[] = [];
219
228
  this.viewport.querySelectorAll('[data-vs-idx]').forEach(el => {
220
229
  const idx = Number((el as HTMLElement).dataset.vsIdx);
@@ -222,17 +231,23 @@ export class VirtualScroll {
222
231
  rects.push({ idx, newH: el.getBoundingClientRect().height });
223
232
  }
224
233
  });
234
+ let heightChanged = false;
225
235
  for (const { idx, newH } of rects) {
226
236
  const oldH = this.items[idx].height;
227
237
  if (oldH !== newH) {
228
238
  this.items[idx].height = newH;
229
239
  this._totalHeight += (newH - oldH);
240
+ heightChanged = true;
230
241
  }
231
242
  }
243
+ // Re-snap to bottom if user was there before heights grew
244
+ if (heightChanged && wasAtBottom) {
245
+ this.scrollToBottom();
246
+ }
232
247
  }
233
248
 
234
249
  scrollToBottom(): void {
235
- this.container.scrollTop = this._totalHeight;
250
+ this.container.scrollTop = this.container.scrollHeight;
236
251
  this.scheduleRender();
237
252
  }
238
253
 
@@ -240,6 +255,7 @@ export class VirtualScroll {
240
255
  this.items = [];
241
256
  this._totalHeight = 0;
242
257
  if (this._active) {
258
+ this.container.classList.remove('vs-active');
243
259
  this.container.removeEventListener('scroll', this.scrollHandler);
244
260
  this.viewport.innerHTML = '';
245
261
  this.spacerTop.style.height = '0';
package/public/js/ws.ts CHANGED
@@ -264,6 +264,8 @@ export function connect(): void {
264
264
  getVirtualScroll().clear();
265
265
  const el = document.getElementById('chatMessages');
266
266
  if (el) el.innerHTML = '';
267
+ // Intentional clear — also wipe IndexedDB cache
268
+ import('./features/idb-cache.js').then(m => m.clearCache()).catch(() => {});
267
269
  } else if (msg.type === 'session_reset') {
268
270
  addSystemMsg(`${ICONS.refresh} Session reset — history preserved`, 'tool-activity');
269
271
  } else if (msg.type === 'agent_added' || msg.type === 'agent_updated' || msg.type === 'agent_deleted') {
@@ -278,12 +280,16 @@ export function connect(): void {
278
280
  state.ws.onopen = () => {
279
281
  console.log('[ws] connected');
280
282
  const now = Date.now();
281
- const skipReload = now - lastLoadTs < 3000;
283
+ const skipReload = now - lastLoadTs < 10000;
282
284
  import('./ui.js').then(async m => {
283
285
  m.cleanupToolActivity();
284
286
  if (!skipReload) {
285
- await m.loadMessages();
286
- lastLoadTs = Date.now();
287
+ try {
288
+ await m.loadMessages();
289
+ lastLoadTs = Date.now();
290
+ } catch (error) {
291
+ console.error('[ws] loadMessages failed', error);
292
+ }
287
293
  }
288
294
  m.setStatus('idle');
289
295
  });
@@ -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.telegram": "/clear only shows a notice on Telegram (no screen clearing).",
57
- "cmd.clear.discord": "/clear only shows a notice on Discord (no screen clearing).",
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}",
@@ -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.telegram": "Telegram에서는 /clear가 화면 정리 없이 안내만 합니다.",
57
- "cmd.clear.discord": "Discord에서는 /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}",
@@ -1 +0,0 @@
1
- import{X as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as createArchitectureServices};
@@ -1 +0,0 @@
1
- import{M as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{j as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{A as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{k as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{O as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{D as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as render};
@@ -1 +0,0 @@
1
- import{E as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as render};
@@ -1 +0,0 @@
1
- import{T as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{w as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{C as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{S as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{x as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{b as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{y as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{J as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as createGitGraphServices};
@@ -1 +0,0 @@
1
- import{v as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{K as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as createInfoServices};
@@ -1 +0,0 @@
1
- import{_ as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{g as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{h as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{m as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{t as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as default};
@@ -1 +0,0 @@
1
- import{p as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};
@@ -1 +0,0 @@
1
- import{W as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as createPacketServices};
@@ -1 +0,0 @@
1
- import{H as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as createPieServices};
@@ -1 +0,0 @@
1
- import{f as e}from"./vendor-mermaid-DV2i2BfY.js";export{e as diagram};