cli-jaw 1.6.10 → 1.6.12

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 (139) hide show
  1. package/dist/bin/commands/reset.js +1 -1
  2. package/dist/bin/commands/reset.js.map +1 -1
  3. package/dist/bin/postinstall.js +103 -14
  4. package/dist/bin/postinstall.js.map +1 -1
  5. package/dist/server.js +27 -3
  6. package/dist/server.js.map +1 -1
  7. package/dist/src/core/db.js +29 -0
  8. package/dist/src/core/db.js.map +1 -1
  9. package/dist/src/core/main-session.js +12 -3
  10. package/dist/src/core/main-session.js.map +1 -1
  11. package/dist/src/memory/heartbeat.js +1 -1
  12. package/dist/src/memory/heartbeat.js.map +1 -1
  13. package/dist/src/memory/indexing.js +1 -0
  14. package/dist/src/memory/indexing.js.map +1 -1
  15. package/dist/src/orchestrator/pipeline.js +3 -1
  16. package/dist/src/orchestrator/pipeline.js.map +1 -1
  17. package/dist/src/prompt/builder.js +6 -0
  18. package/dist/src/prompt/builder.js.map +1 -1
  19. package/package.json +1 -1
  20. package/public/css/chat.css +19 -0
  21. package/public/css/variables.css +15 -0
  22. package/public/dist/assets/architecture-YZFGNWBL-ztGcIsMX.js +1 -0
  23. package/public/dist/assets/architectureDiagram-Q4EWVU46-BeLDNmwN.js +1 -0
  24. package/public/dist/assets/blockDiagram-DXYQGD6D-ChvfvChp.js +1 -0
  25. package/public/dist/assets/c4Diagram-AHTNJAMY-BzjAXXeE.js +1 -0
  26. package/public/dist/assets/classDiagram-6PBFFD2Q-Ce2Zjeeb.js +1 -0
  27. package/public/dist/assets/classDiagram-v2-HSJHXN6E-DGGliiix.js +1 -0
  28. package/public/dist/assets/cose-bilkent-S5V4N54A-T-OwcThC.js +1 -0
  29. package/public/dist/assets/dagre-KV5264BT-DmdK-8pb.js +1 -0
  30. package/public/dist/assets/diagram-5BDNPKRD-Bf8S6ACH.js +1 -0
  31. package/public/dist/assets/diagram-G4DWMVQ6-C37hs32X.js +1 -0
  32. package/public/dist/assets/diagram-MMDJMWI5-9NNeBmGR.js +1 -0
  33. package/public/dist/assets/diagram-TYMM5635-l-8r-KVr.js +1 -0
  34. package/public/dist/assets/{employees-C8NcsoIa.js → employees-CA_DI2gy.js} +1 -1
  35. package/public/dist/assets/erDiagram-SMLLAGMA-BfSqi5hi.js +1 -0
  36. package/public/dist/assets/flowDiagram-DWJPFMVM-B6am8zYz.js +1 -0
  37. package/public/dist/assets/ganttDiagram-T4ZO3ILL-weu0QfVv.js +1 -0
  38. package/public/dist/assets/gitGraph-7Q5UKJZL-BucTBJ0C.js +1 -0
  39. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-D4BnXgVC.js +1 -0
  40. package/public/dist/assets/idb-cache-C7z4qE00.js +1 -0
  41. package/public/dist/assets/idb-cache-DbK81tgv.js +1 -0
  42. package/public/dist/assets/{index-DQ81DqnC.js → index-6UFnW9uO.js} +9 -9
  43. package/public/dist/assets/index-ck7lqnh7.css +1 -0
  44. package/public/dist/assets/info-OMHHGYJF-OpOBLEsS.js +1 -0
  45. package/public/dist/assets/infoDiagram-42DDH7IO-9W0kVtoy.js +1 -0
  46. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-BEgeMMA5.js +1 -0
  47. package/public/dist/assets/journeyDiagram-VCZTEJTY-gIlNwmx5.js +1 -0
  48. package/public/dist/assets/kanban-definition-6JOO6SKY-DV9gfO6_.js +1 -0
  49. package/public/dist/assets/mermaid.core-CYqc8Qyq.js +1 -0
  50. package/public/dist/assets/mindmap-definition-QFDTVHPH-DFHJRlCi.js +1 -0
  51. package/public/dist/assets/packet-4T2RLAQJ-DxyOEAi5.js +1 -0
  52. package/public/dist/assets/pie-ZZUOXDRM-CU7m5wDm.js +1 -0
  53. package/public/dist/assets/pieDiagram-DEJITSTG-BEGiEzHN.js +1 -0
  54. package/public/dist/assets/quadrantDiagram-34T5L4WZ-E5jEMjzC.js +1 -0
  55. package/public/dist/assets/radar-PYXPWWZC-CNpXegnm.js +1 -0
  56. package/public/dist/assets/render-C2tuSVTL.js +25 -0
  57. package/public/dist/assets/requirementDiagram-MS252O5E-B3fjwTBx.js +1 -0
  58. package/public/dist/assets/sankeyDiagram-XADWPNL6-BIyouHFw.js +1 -0
  59. package/public/dist/assets/sequenceDiagram-FGHM5R23-CROknRPY.js +1 -0
  60. package/public/dist/assets/{settings-kyjHk6rj.js → settings-BJcG1r6G.js} +1 -1
  61. package/public/dist/assets/settings-DV5X1U8f.js +1 -0
  62. package/public/dist/assets/{skills-B6bTgYdX.js → skills-D3cWRZOl.js} +1 -1
  63. package/public/dist/assets/skills-idPvxY0n.js +1 -0
  64. package/public/dist/assets/slash-commands-BHtBaFWh.js +1 -0
  65. package/public/dist/assets/{slash-commands-D-vY1ONF.js → slash-commands-PkW1NPle.js} +1 -1
  66. package/public/dist/assets/stateDiagram-FHFEXIEX-DqgrX77_.js +1 -0
  67. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-CMxISNLC.js +1 -0
  68. package/public/dist/assets/timeline-definition-GMOUNBTQ-CheLYWOf.js +1 -0
  69. package/public/dist/assets/treeView-SZITEDCU-CWgmTXw-.js +1 -0
  70. package/public/dist/assets/treemap-W4RFUUIX-CMkxaF1N.js +1 -0
  71. package/public/dist/assets/ui-BdW-cWnY.js +131 -0
  72. package/public/dist/assets/ui-qcenMIau.js +1 -0
  73. package/public/dist/assets/{vendor-mermaid-DV2i2BfY.js → vendor-mermaid-C2RBgdM6.js} +4 -4
  74. package/public/dist/assets/vennDiagram-DHZGUBPP-Cm4YZzbv.js +1 -0
  75. package/public/dist/assets/wardley-RL74JXVD-HPEa5s3y.js +1 -0
  76. package/public/dist/assets/wardleyDiagram-NUSXRM2D-vZvODvIY.js +1 -0
  77. package/public/dist/assets/ws-Dcq99IkD.js +2 -0
  78. package/public/dist/assets/xychartDiagram-5P7HB3ND-DdPllF4_.js +1 -0
  79. package/public/dist/index.html +15 -2
  80. package/public/index.html +13 -0
  81. package/public/js/features/avatar.ts +50 -0
  82. package/public/js/features/chat.ts +1 -1
  83. package/public/js/features/idb-cache.ts +5 -0
  84. package/public/js/main.ts +2 -0
  85. package/public/js/render.ts +19 -9
  86. package/public/js/ui.ts +13 -7
  87. package/public/js/virtual-scroll.ts +63 -23
  88. package/public/js/ws.ts +10 -4
  89. package/public/dist/assets/architecture-YZFGNWBL-BBAoUgDs.js +0 -1
  90. package/public/dist/assets/architectureDiagram-Q4EWVU46-CnCGC-W1.js +0 -1
  91. package/public/dist/assets/blockDiagram-DXYQGD6D-CCKUN-eq.js +0 -1
  92. package/public/dist/assets/c4Diagram-AHTNJAMY-2BznTVIo.js +0 -1
  93. package/public/dist/assets/classDiagram-6PBFFD2Q-CNIdDcl_.js +0 -1
  94. package/public/dist/assets/classDiagram-v2-HSJHXN6E-D5c5cCXM.js +0 -1
  95. package/public/dist/assets/cose-bilkent-S5V4N54A-15-I0CI3.js +0 -1
  96. package/public/dist/assets/dagre-KV5264BT-B-7UZ2bP.js +0 -1
  97. package/public/dist/assets/diagram-5BDNPKRD-CtpSLvPV.js +0 -1
  98. package/public/dist/assets/diagram-G4DWMVQ6-C-FxLjz8.js +0 -1
  99. package/public/dist/assets/diagram-MMDJMWI5-CyWYCa1y.js +0 -1
  100. package/public/dist/assets/diagram-TYMM5635-Bc2Z_pKL.js +0 -1
  101. package/public/dist/assets/erDiagram-SMLLAGMA-BOsddzBo.js +0 -1
  102. package/public/dist/assets/flowDiagram-DWJPFMVM-CNS6NI2y.js +0 -1
  103. package/public/dist/assets/ganttDiagram-T4ZO3ILL-DzeONbMw.js +0 -1
  104. package/public/dist/assets/gitGraph-7Q5UKJZL-D-msAZeS.js +0 -1
  105. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-Ck2kMeHe.js +0 -1
  106. package/public/dist/assets/index-DbSvUuCm.css +0 -1
  107. package/public/dist/assets/info-OMHHGYJF-38sLGsyT.js +0 -1
  108. package/public/dist/assets/infoDiagram-42DDH7IO-BpZBAgqS.js +0 -1
  109. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-CHk3pDry.js +0 -1
  110. package/public/dist/assets/journeyDiagram-VCZTEJTY-BQ1Iwmix.js +0 -1
  111. package/public/dist/assets/kanban-definition-6JOO6SKY-B0_TgypH.js +0 -1
  112. package/public/dist/assets/mermaid.core-C6Yxaqu5.js +0 -1
  113. package/public/dist/assets/mindmap-definition-QFDTVHPH-Di38Ha2d.js +0 -1
  114. package/public/dist/assets/packet-4T2RLAQJ-WhSOczez.js +0 -1
  115. package/public/dist/assets/pie-ZZUOXDRM-BFlknSFa.js +0 -1
  116. package/public/dist/assets/pieDiagram-DEJITSTG-C580ezo8.js +0 -1
  117. package/public/dist/assets/quadrantDiagram-34T5L4WZ-CqL6mFQa.js +0 -1
  118. package/public/dist/assets/radar-PYXPWWZC-C6TfDjQ7.js +0 -1
  119. package/public/dist/assets/render-DLUswUab.js +0 -25
  120. package/public/dist/assets/requirementDiagram-MS252O5E-CJ2UTxBk.js +0 -1
  121. package/public/dist/assets/sankeyDiagram-XADWPNL6-CtV7e3kn.js +0 -1
  122. package/public/dist/assets/sequenceDiagram-FGHM5R23-CK_ww48d.js +0 -1
  123. package/public/dist/assets/settings-BQXndJHI.js +0 -1
  124. package/public/dist/assets/skills-BaSoozYo.js +0 -1
  125. package/public/dist/assets/slash-commands-DwlnxrFi.js +0 -1
  126. package/public/dist/assets/stateDiagram-FHFEXIEX-CY21kttq.js +0 -1
  127. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-Da4cAFSL.js +0 -1
  128. package/public/dist/assets/timeline-definition-GMOUNBTQ-BRNd_2SN.js +0 -1
  129. package/public/dist/assets/treeView-SZITEDCU-C4o4RBOK.js +0 -1
  130. package/public/dist/assets/treemap-W4RFUUIX-BIAsQVaf.js +0 -1
  131. package/public/dist/assets/ui-B4mnMXvw.js +0 -131
  132. package/public/dist/assets/ui-BnZpTNdD.js +0 -1
  133. package/public/dist/assets/vennDiagram-DHZGUBPP-BE5I_z1u.js +0 -1
  134. package/public/dist/assets/wardley-RL74JXVD-BYUgx5S_.js +0 -1
  135. package/public/dist/assets/wardleyDiagram-NUSXRM2D-DZn9qJg1.js +0 -1
  136. package/public/dist/assets/ws-BMpuc7kR.js +0 -2
  137. package/public/dist/assets/xychartDiagram-5P7HB3ND-DIYzBAwh.js +0 -1
  138. /package/public/dist/assets/{constants-74vpnlVG.js → constants-DwGsi0Gn.js} +0 -0
  139. /package/public/dist/assets/{locale-DIXc-_34.js → locale-DVVWjxKN.js} +0 -0
@@ -25,9 +25,9 @@
25
25
  href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap"
26
26
  rel="stylesheet">
27
27
  <!-- Vite handles module bundling in dev (HMR) and production (build) -->
28
- <script type="module" crossorigin src="/dist/assets/index-DQ81DqnC.js"></script>
28
+ <script type="module" crossorigin src="/dist/assets/index-6UFnW9uO.js"></script>
29
29
  <link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-Bjnw0wQ6.css">
30
- <link rel="stylesheet" crossorigin href="/dist/assets/index-DbSvUuCm.css">
30
+ <link rel="stylesheet" crossorigin href="/dist/assets/index-ck7lqnh7.css">
31
31
  </head>
32
32
 
33
33
  <body>
@@ -86,6 +86,19 @@
86
86
  class="input-agent-name">
87
87
  <button class="sidebar-hb-btn btn-action-sm w-auto" id="appNameSave">✓</button>
88
88
  </div>
89
+
90
+ <div class="section-title" style="margin-top:12px" data-i18n="sidebar.avatar">아바타</div>
91
+ <div class="flex gap-2 items-center">
92
+ <div class="flex gap-1 items-center flex-1">
93
+ <label class="avatar-label">🤖</label>
94
+ <input type="text" id="agentAvatarInput" value="🦈" class="input-avatar" maxlength="2" placeholder="🦈">
95
+ </div>
96
+ <div class="flex gap-1 items-center flex-1">
97
+ <label class="avatar-label">👤</label>
98
+ <input type="text" id="userAvatarInput" value="👤" class="input-avatar" maxlength="2" placeholder="👤">
99
+ </div>
100
+ <button class="sidebar-hb-btn btn-action-sm w-auto" id="avatarSave">✓</button>
101
+ </div>
89
102
  <button class="sidebar-hb-btn" id="btnClearChat">/clear</button>
90
103
  <button class="sidebar-hb-btn" id="hbSidebarBtn"><span data-icon="heartPulse"></span> Heartbeat (0)</button>
91
104
  <button class="sidebar-hb-btn" data-action="openTemplates"><span data-icon="plan"></span> 프롬프트 템플릿</button>
package/public/index.html CHANGED
@@ -92,6 +92,19 @@
92
92
  class="input-agent-name">
93
93
  <button class="sidebar-hb-btn btn-action-sm w-auto" id="appNameSave">✓</button>
94
94
  </div>
95
+
96
+ <div class="section-title" style="margin-top:12px" data-i18n="sidebar.avatar">아바타</div>
97
+ <div class="flex gap-2 items-center">
98
+ <div class="flex gap-1 items-center flex-1">
99
+ <label class="avatar-label">🤖</label>
100
+ <input type="text" id="agentAvatarInput" value="🦈" class="input-avatar" maxlength="2" placeholder="🦈">
101
+ </div>
102
+ <div class="flex gap-1 items-center flex-1">
103
+ <label class="avatar-label">👤</label>
104
+ <input type="text" id="userAvatarInput" value="👤" class="input-avatar" maxlength="2" placeholder="👤">
105
+ </div>
106
+ <button class="sidebar-hb-btn btn-action-sm w-auto" id="avatarSave">✓</button>
107
+ </div>
95
108
  <button class="sidebar-hb-btn" id="btnClearChat">/clear</button>
96
109
  <button class="sidebar-hb-btn" id="hbSidebarBtn"><span data-icon="heartPulse"></span> Heartbeat (0)</button>
97
110
  <button class="sidebar-hb-btn" data-action="openTemplates"><span data-icon="plan"></span> 프롬프트 템플릿</button>
@@ -0,0 +1,50 @@
1
+ // ── Agent & User Avatar ──
2
+ const AGENT_KEY = 'agentAvatar';
3
+ const USER_KEY = 'userAvatar';
4
+ const DEFAULT_AGENT = '🦈';
5
+ const DEFAULT_USER = '👤';
6
+
7
+ let agentAvatar = DEFAULT_AGENT;
8
+ let userAvatar = DEFAULT_USER;
9
+
10
+ export function getAgentAvatar(): string { return agentAvatar; }
11
+ export function getUserAvatar(): string { return userAvatar; }
12
+
13
+ export function setAgentAvatar(emoji: string): void {
14
+ agentAvatar = (emoji || '').trim() || DEFAULT_AGENT;
15
+ localStorage.setItem(AGENT_KEY, agentAvatar);
16
+ document.querySelectorAll('.agent-icon').forEach(el => { el.textContent = agentAvatar; });
17
+ }
18
+
19
+ export function setUserAvatar(emoji: string): void {
20
+ userAvatar = (emoji || '').trim() || DEFAULT_USER;
21
+ localStorage.setItem(USER_KEY, userAvatar);
22
+ document.querySelectorAll('.user-icon').forEach(el => { el.textContent = userAvatar; });
23
+ }
24
+
25
+ export function initAvatar(): void {
26
+ agentAvatar = localStorage.getItem(AGENT_KEY) || DEFAULT_AGENT;
27
+ userAvatar = localStorage.getItem(USER_KEY) || DEFAULT_USER;
28
+
29
+ const ai = document.getElementById('agentAvatarInput') as HTMLInputElement | null;
30
+ const ui = document.getElementById('userAvatarInput') as HTMLInputElement | null;
31
+ if (ai) ai.value = agentAvatar;
32
+ if (ui) ui.value = userAvatar;
33
+
34
+ document.getElementById('avatarSave')?.addEventListener('click', () => {
35
+ const a = document.getElementById('agentAvatarInput') as HTMLInputElement | null;
36
+ const u = document.getElementById('userAvatarInput') as HTMLInputElement | null;
37
+ if (a) setAgentAvatar(a.value);
38
+ if (u) setUserAvatar(u.value);
39
+ });
40
+
41
+ for (const id of ['agentAvatarInput', 'userAvatarInput']) {
42
+ document.getElementById(id)?.addEventListener('keydown', (e: Event) => {
43
+ if ((e as KeyboardEvent).key === 'Enter') {
44
+ (e as KeyboardEvent).preventDefault();
45
+ document.getElementById('avatarSave')?.click();
46
+ (e.target as HTMLInputElement).blur();
47
+ }
48
+ });
49
+ }
50
+ }
@@ -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
+ initAvatar();
434
436
  initSidebar();
435
437
  initMsgCopy();
436
438
  initGestures();
@@ -963,20 +963,30 @@ export function renderMarkdown(text: string, isStreaming = false): string {
963
963
  // ── Batched post-render scheduler ──
964
964
  // Coalesces multiple renderMarkdown() calls into a single post-render pass.
965
965
  let postRenderRAF: number | null = null;
966
+ let postRenderTimer: ReturnType<typeof setTimeout> | null = null;
966
967
 
967
968
  function schedulePostRender(): void {
968
- if (postRenderRAF) return;
969
- postRenderRAF = requestAnimationFrame(() => {
970
- postRenderRAF = null;
971
- renderMermaidBlocks();
972
- rehighlightAll();
973
- bindDiagramZoom();
974
- const msgContainer = document.getElementById('chatMessages');
975
- if (msgContainer) linkifyFilePaths(msgContainer);
976
- });
969
+ // Debounce: coalesce rapid VS render triggers into a single pass
970
+ if (postRenderTimer) clearTimeout(postRenderTimer);
971
+ if (postRenderRAF) { cancelAnimationFrame(postRenderRAF); postRenderRAF = null; }
972
+ postRenderTimer = setTimeout(() => {
973
+ postRenderTimer = null;
974
+ postRenderRAF = requestAnimationFrame(() => {
975
+ postRenderRAF = null;
976
+ renderMermaidBlocks();
977
+ rehighlightAll();
978
+ bindDiagramZoom();
979
+ const msgContainer = document.getElementById('chatMessages');
980
+ if (msgContainer) linkifyFilePaths(msgContainer);
981
+ });
982
+ }, 100);
977
983
  }
978
984
 
979
985
  export function cancelPostRender(): void {
986
+ if (postRenderTimer) {
987
+ clearTimeout(postRenderTimer);
988
+ postRenderTimer = null;
989
+ }
980
990
  if (postRenderRAF) {
981
991
  cancelAnimationFrame(postRenderRAF);
982
992
  postRenderRAF = null;
package/public/js/ui.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { state } from './state.js';
3
3
  import { renderMarkdown, escapeHtml, stripOrchestration, linkifyFilePaths } from './render.js';
4
4
  import { getAppName } from './features/appname.js';
5
+ import { getAgentAvatar, getUserAvatar } from './features/avatar.js';
5
6
  import { t } from './features/i18n.js';
6
7
  import { api } from './api.js';
7
8
  import { cacheMessages, getCachedMessages, appendCachedMessage, upsertMessage, setMessageScope, getScopedMessages } from './features/idb-cache.js';
@@ -34,8 +35,7 @@ function parseToolLog(toolLog?: string | null): ToolLogEntry[] {
34
35
  }
35
36
 
36
37
  function getAgentIcon(_cli?: string | null): string {
37
- // Chat mascot is always the shark — provider icons are for header/sidebar only
38
- return ICONS.shark;
38
+ return getAgentAvatar();
39
39
  }
40
40
 
41
41
  function toProcessSteps(tools: ToolLogEntry[]): ProcessStep[] {
@@ -295,7 +295,7 @@ export function addMessage(role: string, text: string, cli?: string | null): HTM
295
295
  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
296
  } else {
297
297
  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>`;
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">${getUserAvatar()}</div>`;
299
299
  }
300
300
  const contentEl = div.querySelector('.msg-content');
301
301
  if (contentEl) contentEl.setAttribute('data-raw', stripOrchestration(text));
@@ -368,11 +368,15 @@ export function handleSave(): void {
368
368
  }
369
369
  }
370
370
 
371
+ function updateStatMsgs(count: number): void {
372
+ const el = document.getElementById('statMsgs');
373
+ if (el) el.textContent = t('stat.messages', { count });
374
+ }
375
+
371
376
  export async function loadStats(): Promise<void> {
372
377
  const msgs = await api<MessageItem[]>('/api/messages');
373
378
  if (!msgs) return;
374
- const el = document.getElementById('statMsgs');
375
- if (el) el.textContent = t('stat.messages', { count: msgs.length });
379
+ updateStatMsgs(msgs.length);
376
380
  }
377
381
 
378
382
  export async function loadMessages(): Promise<void> {
@@ -403,7 +407,7 @@ export async function loadMessages(): Promise<void> {
403
407
  const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
404
408
  const html = role === 'agent'
405
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>`
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>`;
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>`;
407
411
  vs.addItem(crypto.randomUUID(), html);
408
412
  }
409
413
 
@@ -452,6 +456,7 @@ export async function loadMessages(): Promise<void> {
452
456
  cacheMessages(msgs.map(m => ({
453
457
  role: m.role, content: m.content, cli: m.cli ?? null, tool_log: m.tool_log ?? null, timestamp: Date.now(),
454
458
  }))).catch(() => {});
459
+ updateStatMsgs(msgs.length);
455
460
  showEmptyState();
456
461
  return;
457
462
  }
@@ -474,7 +479,7 @@ export async function loadMessages(): Promise<void> {
474
479
  const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
475
480
  const html = role === 'agent'
476
481
  ? `<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>`;
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">${getUserAvatar()}</div></div>`;
478
483
  vs.addItem(crypto.randomUUID(), html);
479
484
  }
480
485
  vs.onLazyRender = (targets: HTMLElement[]) => {
@@ -513,6 +518,7 @@ export async function loadMessages(): Promise<void> {
513
518
  });
514
519
  }
515
520
  addSystemMsg(`${ICONS.warning} 오프라인 모드 — 캐시된 메시지 표시 중`);
521
+ updateStatMsgs(cached.length);
516
522
  }
517
523
  showEmptyState();
518
524
  }
@@ -102,8 +102,8 @@ export class VirtualScroll {
102
102
  this._totalHeight += this.items[i].height;
103
103
  }
104
104
  });
105
- this.container.innerHTML = '';
106
- this.container.append(this.spacerTop, this.viewport, this.spacerBottom);
105
+ // Atomic swap — avoids visible blank frame during activation
106
+ this.container.replaceChildren(this.spacerTop, this.viewport, this.spacerBottom);
107
107
  this.container.addEventListener('scroll', this.scrollHandler, { passive: true });
108
108
  this.render();
109
109
  }
@@ -152,32 +152,50 @@ export class VirtualScroll {
152
152
  this.spacerTop.style.height = `${topSpace}px`;
153
153
  this.spacerBottom.style.height = `${bottomSpace}px`;
154
154
 
155
- const frag = document.createDocumentFragment();
155
+ // Build map of currently mounted items by vsIdx
156
+ const mounted = new Map<number, HTMLElement>();
157
+ for (const child of Array.from(this.viewport.children) as HTMLElement[]) {
158
+ const idx = Number(child.dataset.vsIdx);
159
+ if (!isNaN(idx)) mounted.set(idx, child);
160
+ }
161
+
162
+ // Remove items no longer in range
163
+ for (const [idx, el] of mounted) {
164
+ if (idx < first || idx > last) {
165
+ el.remove();
166
+ mounted.delete(idx);
167
+ }
168
+ }
169
+
170
+ // Build ordered list — reuse existing or create new
171
+ const ordered: HTMLElement[] = [];
156
172
  for (let i = first; i <= last; i++) {
157
- const item = this.items[i];
158
- const div = document.createElement('div');
159
- div.innerHTML = item.html;
160
- const el = div.firstElementChild as HTMLElement;
161
- if (el) {
162
- el.dataset.vsIdx = String(i);
163
- frag.appendChild(el);
173
+ const existing = mounted.get(i);
174
+ if (existing) {
175
+ ordered.push(existing);
176
+ } else {
177
+ const item = this.items[i];
178
+ const div = document.createElement('div');
179
+ div.innerHTML = item.html;
180
+ const el = div.firstElementChild as HTMLElement;
181
+ if (el) {
182
+ el.dataset.vsIdx = String(i);
183
+ ordered.push(el);
184
+ }
164
185
  }
165
186
  }
166
- this.viewport.innerHTML = '';
167
- this.viewport.appendChild(frag);
168
187
 
169
- // Re-measure rendered heights and update totalHeight
170
- this.viewport.querySelectorAll('[data-vs-idx]').forEach(el => {
171
- const idx = Number((el as HTMLElement).dataset.vsIdx);
172
- if (this.items[idx]) {
173
- const oldH = this.items[idx].height;
174
- const newH = el.getBoundingClientRect().height;
175
- this.items[idx].height = newH;
176
- this._totalHeight += (newH - oldH);
188
+ // Reorder viewport children to match (minimal DOM moves)
189
+ let nodeRef = this.viewport.firstChild as HTMLElement | null;
190
+ for (const el of ordered) {
191
+ if (el !== nodeRef) {
192
+ this.viewport.insertBefore(el, nodeRef);
193
+ } else {
194
+ nodeRef = nodeRef.nextSibling as HTMLElement | null;
177
195
  }
178
- });
196
+ }
179
197
 
180
- // Fire lazy render callback for newly visible items
198
+ // Fire lazy render callback FIRST (replaces skeleton with real content)
181
199
  if (this.onLazyRender) {
182
200
  const lazyTargets = this.viewport.querySelectorAll<HTMLElement>('.lazy-pending');
183
201
  if (lazyTargets.length > 0) {
@@ -185,10 +203,32 @@ export class VirtualScroll {
185
203
  }
186
204
  }
187
205
 
188
- // Fire post-render callback for widget activation on all mounted items
206
+ // Fire post-render callback for widget activation
189
207
  if (this.onPostRender) {
190
208
  this.onPostRender(this.viewport);
191
209
  }
210
+
211
+ // Batch-read heights AFTER lazy render + widget activation
212
+ this.remeasureVisible();
213
+ }
214
+
215
+ /** Batch-read heights from visible elements, batch-write to items array.
216
+ * Separated read/write passes = single forced reflow. */
217
+ private remeasureVisible(): void {
218
+ const rects: { idx: number; newH: number }[] = [];
219
+ this.viewport.querySelectorAll('[data-vs-idx]').forEach(el => {
220
+ const idx = Number((el as HTMLElement).dataset.vsIdx);
221
+ if (this.items[idx]) {
222
+ rects.push({ idx, newH: el.getBoundingClientRect().height });
223
+ }
224
+ });
225
+ for (const { idx, newH } of rects) {
226
+ const oldH = this.items[idx].height;
227
+ if (oldH !== newH) {
228
+ this.items[idx].height = newH;
229
+ this._totalHeight += (newH - oldH);
230
+ }
231
+ }
192
232
  }
193
233
 
194
234
  scrollToBottom(): void {
package/public/js/ws.ts CHANGED
@@ -72,6 +72,7 @@ interface WsMessage {
72
72
  const agentPhaseState: Record<string, { phase: string; phaseLabel: string }> = {};
73
73
 
74
74
  let currentOrcScope = '';
75
+ let lastLoadTs = 0;
75
76
 
76
77
  /** Hydrate agent phase cache from snapshot (used after reconnect) */
77
78
  export function hydrateAgentPhases(workers: Array<{
@@ -263,6 +264,8 @@ export function connect(): void {
263
264
  getVirtualScroll().clear();
264
265
  const el = document.getElementById('chatMessages');
265
266
  if (el) el.innerHTML = '';
267
+ // Intentional clear — also wipe IndexedDB cache
268
+ import('./features/idb-cache.js').then(m => m.clearCache()).catch(() => {});
266
269
  } else if (msg.type === 'session_reset') {
267
270
  addSystemMsg(`${ICONS.refresh} Session reset — history preserved`, 'tool-activity');
268
271
  } else if (msg.type === 'agent_added' || msg.type === 'agent_updated' || msg.type === 'agent_deleted') {
@@ -276,11 +279,14 @@ export function connect(): void {
276
279
  };
277
280
  state.ws.onopen = () => {
278
281
  console.log('[ws] connected');
279
- // Reload messages — loadMessages() handles DOM clearing internally
280
- // (only clears after successful fetch to prevent blank screen)
281
- import('./ui.js').then(m => {
282
+ const now = Date.now();
283
+ const skipReload = now - lastLoadTs < 10000;
284
+ import('./ui.js').then(async m => {
282
285
  m.cleanupToolActivity();
283
- m.loadMessages();
286
+ if (!skipReload) {
287
+ await m.loadMessages();
288
+ lastLoadTs = Date.now();
289
+ }
284
290
  m.setStatus('idle');
285
291
  });
286
292
 
@@ -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};