cli-jaw 1.6.14 → 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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/public/assets/shark.svg +1 -0
  3. package/public/dist/assets/{employees-zxrU6ZV_.js → employees-V7lNStu1.js} +1 -1
  4. package/public/dist/assets/{index-D61icK-D.js → index-Cpe1jccL.js} +4 -4
  5. package/public/dist/assets/render-BoxeLlL9.js +25 -0
  6. package/public/dist/assets/settings-BcKp6ppP.js +1 -0
  7. package/public/dist/assets/{settings-Dl3RnWsB.js → settings-CBCg5Jhh.js} +1 -1
  8. package/public/dist/assets/skills-BuAXFNgp.js +1 -0
  9. package/public/dist/assets/{skills-DhiCSGws.js → skills-RbauGmBZ.js} +1 -1
  10. package/public/dist/assets/{slash-commands-B1k1vFJG.js → slash-commands-BgKxc49D.js} +1 -1
  11. package/public/dist/assets/slash-commands-DXGb_iGA.js +1 -0
  12. package/public/dist/assets/ui-KQ8_sSP8.js +131 -0
  13. package/public/dist/assets/ui-rD__Mvbs.js +1 -0
  14. package/public/dist/assets/vendor-icons-C6LXvgi0.js +1 -0
  15. package/public/dist/assets/{ws-CleMWrLF.js → ws-BtTpgocf.js} +1 -1
  16. package/public/dist/index.html +3 -3
  17. package/public/index.html +2 -2
  18. package/public/js/features/avatar.ts +5 -3
  19. package/public/js/icons.ts +10 -4
  20. package/public/js/ui.ts +87 -73
  21. package/public/js/virtual-scroll-bootstrap.ts +42 -0
  22. package/public/js/virtual-scroll.ts +140 -49
  23. package/public/dist/assets/render-CVr6a-dp.js +0 -25
  24. package/public/dist/assets/settings-BHIV4l1s.js +0 -1
  25. package/public/dist/assets/skills-JuDja1UC.js +0 -1
  26. package/public/dist/assets/slash-commands-DyLS0abr.js +0 -1
  27. package/public/dist/assets/ui-BXZhbE_1.js +0 -131
  28. package/public/dist/assets/ui-qR28iS0L.js +0 -1
  29. package/public/dist/assets/vendor-icons-BqxEYYco.js +0 -1
@@ -1,2 +1,2 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/vendor-render-Bjnw0wQ6.css"])))=>i.map(i=>d[i]);
2
- import{t as e}from"./state-O6NVkWcL.js";import{Z as t}from"./vendor-mermaid-C2RBgdM6.js";import{f as n,h as r,n as i,s as a,t as o}from"./render-CVr6a-dp.js";import{a as s,d as c,f as l,g as u,i as d,m as f,n as p,r as m,t as h}from"./ui-BXZhbE_1.js";var g=[`P`,`A`,`B`,`C`],_=null;function v(e,t,n){let r=g.indexOf(n);if(r<0)return;let i=e.getBoundingClientRect(),a=t.offsetWidth||36,o=document.getElementById(`dot-${n}`);if(!o)return;let s=g[r+1],c=s?document.getElementById(`dot-${s}`):null;if(c){let e=o.getBoundingClientRect(),n=c.getBoundingClientRect(),r=(e.right+n.left)/2;t.style.left=r-i.left-a/2+`px`}else{let e=o.getBoundingClientRect();t.style.left=e.left-i.left+e.width/2-a/2+`px`}}var y={},b=``,x=0;function S(e){for(let e of Object.keys(y))delete y[e];for(let t of e)t.state===`running`&&t.phase&&(y[t.agentId]={phase:t.phase,phaseLabel:t.phaseLabel||``})}function C(t,n){let r=new Set([`IDLE`,`P`,`A`,`B`,`C`,`D`]).has(t)?t:`IDLE`;if(e.orcState=r,r===`IDLE`||r===`D`)document.body.removeAttribute(`data-orc-state`),document.body.style.removeProperty(`--orc-glow`);else{document.body.setAttribute(`data-orc-state`,r);let e=`--orc-glow-${r}`,t=getComputedStyle(document.documentElement).getPropertyValue(e).trim();document.body.style.setProperty(`--orc-glow`,t)}document.body.classList.add(`orc-pulse`),setTimeout(()=>document.body.classList.remove(`orc-pulse`),700);let i=document.getElementById(`orcStateBadge`);i&&(i.textContent={IDLE:``,P:`PLAN`,A:`AUDIT`,B:`BUILD`,C:`CHECK`,D:`DONE`}[r],i.style.display=r===`IDLE`?`none`:`inline-block`);let a=document.getElementById(`pabcRoadmap`),o=document.getElementById(`sharkRunner`),s=document.getElementById(`pabcBrand`);if(a&&o){if(!a.dataset.resizeObserved){a.dataset.resizeObserved=`1`,new ResizeObserver(()=>{_&&o.classList.contains(`running`)&&v(a,o,_)}).observe(a);let e=0;window.addEventListener(`resize`,()=>{cancelAnimationFrame(e),e=requestAnimationFrame(()=>{_&&o.classList.contains(`running`)&&v(a,o,_)})})}if(r===`IDLE`)a.classList.remove(`visible`,`shimmer-out`),o.classList.remove(`running`),_=null;else if(r===`D`){g.forEach(e=>{let t=document.getElementById(`dot-${e}`);t&&(t.className=`pabc-dot done`,t.setAttribute(`data-phase`,e))});for(let e=0;e<4;e++){let t=document.getElementById(`pabc-conn-${e}`);t&&(t.className=`pabc-connector done`)}o.classList.remove(`running`),_=null,a.classList.add(`shimmer-out`),setTimeout(()=>a.classList.remove(`visible`,`shimmer-out`),1e3)}else{a.classList.remove(`shimmer-out`),a.classList.add(`visible`),o.classList.add(`running`);let e=g.indexOf(r);g.forEach((t,n)=>{let r=document.getElementById(`dot-${t}`);r&&(r.className=`pabc-dot ${n<e?`done`:n===e?`active`:`future`}`,r.setAttribute(`data-phase`,t))});for(let t=0;t<4;t++){let n=document.getElementById(`pabc-conn-${t}`);n&&(n.className=`pabc-connector ${t<e?`done`:``}`)}_=r,requestAnimationFrame(()=>v(a,o,r))}s&&n&&(s.textContent=n)}}function w(){let g=`ws://${location.host}`;e.ws=new WebSocket(`${g}?lang=${n()}`),e.ws.onmessage=e=>{let n;try{n=JSON.parse(e.data)}catch{console.warn(`[ws] malformed message:`,e.data);return}if(!n||typeof n!=`object`||typeof n.type!=`string`){console.warn(`[ws] invalid message shape:`,n);return}if(n.type===`agent_status`)n.running===void 0?c(n.status||`idle`):c(n.running?`running`:`idle`),n.agentId&&n.phase&&(y[n.agentId]={phase:n.phase,phaseLabel:n.phaseLabel||``},t(()=>import(`./employees-zxrU6ZV_.js`).then(e=>e.loadEmployees()),__vite__mapDeps([0])));else if(n.type===`queue_update`)f(n.pending||0);else if(n.type===`worklog_created`)p(`${a.clipboard} Worklog: ${i(n.path||``)}`);else if(n.type===`round_start`){let e=n.agentPhases||n.subtasks||[],t=e.map(e=>i(e.agent||e.name||``)).join(`, `);p(r(`ws.roundStart`,{round:n.round||0,count:e.length,names:t}))}else if(n.type===`round_done`)n.action===`complete`?p(r(`ws.roundDone`,{round:n.round||0})):n.action===`next`?p(r(`ws.roundNext`,{round:n.round||0})):p(r(`ws.roundRetry`,{round:n.round||0}));else if(n.type===`agent_tool`){let e=n.toolType===`thinking`?`thinking`:n.toolType===`search`?`search`:`tool`;l({id:`step-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:e,icon:n.icon||a.tool,label:n.label||``,detail:n.detail||``,stepRef:n.stepRef||``,status:n.status||`running`,startTime:Date.now()})}else if(n.type===`agent_output`)m(n.text||``);else if(n.type===`agent_retry`)p(r(`ws.retry`,{cli:i(n.cli||``),delay:n.delay||10}),`tool-activity`);else if(n.type===`agent_fallback`)p(r(`ws.fallback`,{from:i(n.from||``),to:i(n.to||``)}),`tool-activity`);else if(n.type===`agent_smoke`)p(`${a.warning} ${i(n.cli||`agent`)}: smoke response detected — auto-continuing`,`tool-activity`);else if(n.type===`agent_done`)s(n.text||``,n.toolLog);else if(n.type===`orchestrate_done`)s(n.text||``);else if(n.type===`clear`){o(),d(),u().clear();let e=document.getElementById(`chatMessages`);e&&(e.innerHTML=``),t(()=>import(`./idb-cache-C7z4qE00.js`).then(e=>e.clearCache()),[]).catch(()=>{})}else if(n.type===`session_reset`)p(`${a.refresh} Session reset — history preserved`,`tool-activity`);else if(n.type===`agent_added`||n.type===`agent_updated`||n.type===`agent_deleted`)t(()=>import(`./employees-zxrU6ZV_.js`).then(e=>e.loadEmployees()),__vite__mapDeps([0]));else if(n.type===`orc_state`){if(n.scope&&b&&n.scope!==b)return;C(typeof n.state==`string`?n.state:`IDLE`,n.title)}else n.type===`new_message`&&(n.source===`telegram`||n.source===`discord`)&&h(n.role===`assistant`?`agent`:n.role||`user`,n.content||``,n.cli)},e.ws.onopen=()=>{console.log(`[ws] connected`);let e=Date.now()-x<1e4;t(()=>import(`./ui-qR28iS0L.js`).then(async t=>{if(t.cleanupToolActivity(),!e)try{await t.loadMessages(),x=Date.now()}catch(e){console.error(`[ws] loadMessages failed`,e)}t.setStatus(`idle`)}),__vite__mapDeps([0])),fetch(`/api/orchestrate/snapshot`).then(e=>e.json()).then(e=>{b=String(e.orc.scope||``),C(e.orc.state),S(e.workers),f(e.runtime.queuePending),c(e.runtime.busy?`running`:`idle`),t(()=>import(`./employees-zxrU6ZV_.js`).then(e=>{typeof e.renderEmployees==`function`&&e.renderEmployees()}),__vite__mapDeps([0]))}).catch(()=>{})},e.ws.onclose=()=>{console.log(`[ws] disconnected, reconnecting in 2s...`),t(()=>import(`./ui-qR28iS0L.js`).then(e=>e.cleanupToolActivity()),__vite__mapDeps([0])),c(`idle`),p(`${a.exec} 연결 끊김 — 재연결 중...`,`tool-activity`),setTimeout(w,2e3)}}function T(e){return y[e]||null}export{T as n,w as t};
2
+ import{t as e}from"./state-O6NVkWcL.js";import{Z as t}from"./vendor-mermaid-C2RBgdM6.js";import{f as n,h as r,n as i,s as a,t as o}from"./render-BoxeLlL9.js";import{a as s,d as c,f as l,g as u,i as d,m as f,n as p,r as m,t as h}from"./ui-KQ8_sSP8.js";var g=[`P`,`A`,`B`,`C`],_=null;function v(e,t,n){let r=g.indexOf(n);if(r<0)return;let i=e.getBoundingClientRect(),a=t.offsetWidth||36,o=document.getElementById(`dot-${n}`);if(!o)return;let s=g[r+1],c=s?document.getElementById(`dot-${s}`):null;if(c){let e=o.getBoundingClientRect(),n=c.getBoundingClientRect(),r=(e.right+n.left)/2;t.style.left=r-i.left-a/2+`px`}else{let e=o.getBoundingClientRect();t.style.left=e.left-i.left+e.width/2-a/2+`px`}}var y={},b=``,x=0;function S(e){for(let e of Object.keys(y))delete y[e];for(let t of e)t.state===`running`&&t.phase&&(y[t.agentId]={phase:t.phase,phaseLabel:t.phaseLabel||``})}function C(t,n){let r=new Set([`IDLE`,`P`,`A`,`B`,`C`,`D`]).has(t)?t:`IDLE`;if(e.orcState=r,r===`IDLE`||r===`D`)document.body.removeAttribute(`data-orc-state`),document.body.style.removeProperty(`--orc-glow`);else{document.body.setAttribute(`data-orc-state`,r);let e=`--orc-glow-${r}`,t=getComputedStyle(document.documentElement).getPropertyValue(e).trim();document.body.style.setProperty(`--orc-glow`,t)}document.body.classList.add(`orc-pulse`),setTimeout(()=>document.body.classList.remove(`orc-pulse`),700);let i=document.getElementById(`orcStateBadge`);i&&(i.textContent={IDLE:``,P:`PLAN`,A:`AUDIT`,B:`BUILD`,C:`CHECK`,D:`DONE`}[r],i.style.display=r===`IDLE`?`none`:`inline-block`);let a=document.getElementById(`pabcRoadmap`),o=document.getElementById(`sharkRunner`),s=document.getElementById(`pabcBrand`);if(a&&o){if(!a.dataset.resizeObserved){a.dataset.resizeObserved=`1`,new ResizeObserver(()=>{_&&o.classList.contains(`running`)&&v(a,o,_)}).observe(a);let e=0;window.addEventListener(`resize`,()=>{cancelAnimationFrame(e),e=requestAnimationFrame(()=>{_&&o.classList.contains(`running`)&&v(a,o,_)})})}if(r===`IDLE`)a.classList.remove(`visible`,`shimmer-out`),o.classList.remove(`running`),_=null;else if(r===`D`){g.forEach(e=>{let t=document.getElementById(`dot-${e}`);t&&(t.className=`pabc-dot done`,t.setAttribute(`data-phase`,e))});for(let e=0;e<4;e++){let t=document.getElementById(`pabc-conn-${e}`);t&&(t.className=`pabc-connector done`)}o.classList.remove(`running`),_=null,a.classList.add(`shimmer-out`),setTimeout(()=>a.classList.remove(`visible`,`shimmer-out`),1e3)}else{a.classList.remove(`shimmer-out`),a.classList.add(`visible`),o.classList.add(`running`);let e=g.indexOf(r);g.forEach((t,n)=>{let r=document.getElementById(`dot-${t}`);r&&(r.className=`pabc-dot ${n<e?`done`:n===e?`active`:`future`}`,r.setAttribute(`data-phase`,t))});for(let t=0;t<4;t++){let n=document.getElementById(`pabc-conn-${t}`);n&&(n.className=`pabc-connector ${t<e?`done`:``}`)}_=r,requestAnimationFrame(()=>v(a,o,r))}s&&n&&(s.textContent=n)}}function w(){let g=`ws://${location.host}`;e.ws=new WebSocket(`${g}?lang=${n()}`),e.ws.onmessage=e=>{let n;try{n=JSON.parse(e.data)}catch{console.warn(`[ws] malformed message:`,e.data);return}if(!n||typeof n!=`object`||typeof n.type!=`string`){console.warn(`[ws] invalid message shape:`,n);return}if(n.type===`agent_status`)n.running===void 0?c(n.status||`idle`):c(n.running?`running`:`idle`),n.agentId&&n.phase&&(y[n.agentId]={phase:n.phase,phaseLabel:n.phaseLabel||``},t(()=>import(`./employees-V7lNStu1.js`).then(e=>e.loadEmployees()),__vite__mapDeps([0])));else if(n.type===`queue_update`)f(n.pending||0);else if(n.type===`worklog_created`)p(`${a.clipboard} Worklog: ${i(n.path||``)}`);else if(n.type===`round_start`){let e=n.agentPhases||n.subtasks||[],t=e.map(e=>i(e.agent||e.name||``)).join(`, `);p(r(`ws.roundStart`,{round:n.round||0,count:e.length,names:t}))}else if(n.type===`round_done`)n.action===`complete`?p(r(`ws.roundDone`,{round:n.round||0})):n.action===`next`?p(r(`ws.roundNext`,{round:n.round||0})):p(r(`ws.roundRetry`,{round:n.round||0}));else if(n.type===`agent_tool`){let e=n.toolType===`thinking`?`thinking`:n.toolType===`search`?`search`:`tool`;l({id:`step-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,type:e,icon:n.icon||a.tool,label:n.label||``,detail:n.detail||``,stepRef:n.stepRef||``,status:n.status||`running`,startTime:Date.now()})}else if(n.type===`agent_output`)m(n.text||``);else if(n.type===`agent_retry`)p(r(`ws.retry`,{cli:i(n.cli||``),delay:n.delay||10}),`tool-activity`);else if(n.type===`agent_fallback`)p(r(`ws.fallback`,{from:i(n.from||``),to:i(n.to||``)}),`tool-activity`);else if(n.type===`agent_smoke`)p(`${a.warning} ${i(n.cli||`agent`)}: smoke response detected — auto-continuing`,`tool-activity`);else if(n.type===`agent_done`)s(n.text||``,n.toolLog);else if(n.type===`orchestrate_done`)s(n.text||``);else if(n.type===`clear`){o(),d(),u().clear();let e=document.getElementById(`chatMessages`);e&&(e.innerHTML=``),t(()=>import(`./idb-cache-C7z4qE00.js`).then(e=>e.clearCache()),[]).catch(()=>{})}else if(n.type===`session_reset`)p(`${a.refresh} Session reset — history preserved`,`tool-activity`);else if(n.type===`agent_added`||n.type===`agent_updated`||n.type===`agent_deleted`)t(()=>import(`./employees-V7lNStu1.js`).then(e=>e.loadEmployees()),__vite__mapDeps([0]));else if(n.type===`orc_state`){if(n.scope&&b&&n.scope!==b)return;C(typeof n.state==`string`?n.state:`IDLE`,n.title)}else n.type===`new_message`&&(n.source===`telegram`||n.source===`discord`)&&h(n.role===`assistant`?`agent`:n.role||`user`,n.content||``,n.cli)},e.ws.onopen=()=>{console.log(`[ws] connected`);let e=Date.now()-x<1e4;t(()=>import(`./ui-rD__Mvbs.js`).then(async t=>{if(t.cleanupToolActivity(),!e)try{await t.loadMessages(),x=Date.now()}catch(e){console.error(`[ws] loadMessages failed`,e)}t.setStatus(`idle`)}),__vite__mapDeps([0])),fetch(`/api/orchestrate/snapshot`).then(e=>e.json()).then(e=>{b=String(e.orc.scope||``),C(e.orc.state),S(e.workers),f(e.runtime.queuePending),c(e.runtime.busy?`running`:`idle`),t(()=>import(`./employees-V7lNStu1.js`).then(e=>{typeof e.renderEmployees==`function`&&e.renderEmployees()}),__vite__mapDeps([0]))}).catch(()=>{})},e.ws.onclose=()=>{console.log(`[ws] disconnected, reconnecting in 2s...`),t(()=>import(`./ui-rD__Mvbs.js`).then(e=>e.cleanupToolActivity()),__vite__mapDeps([0])),c(`idle`),p(`${a.exec} 연결 끊김 — 재연결 중...`,`tool-activity`),setTimeout(w,2e3)}}function T(e){return y[e]||null}export{T as n,w as t};
@@ -25,7 +25,7 @@
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-D61icK-D.js"></script>
28
+ <script type="module" crossorigin src="/dist/assets/index-Cpe1jccL.js"></script>
29
29
  <link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-Bjnw0wQ6.css">
30
30
  <link rel="stylesheet" crossorigin href="/dist/assets/index-DVTRbkJF.css">
31
31
  </head>
@@ -90,7 +90,7 @@
90
90
  <div class="section-title" style="margin-top:12px" data-i18n="sidebar.avatar">아바타</div>
91
91
  <div class="avatar-row">
92
92
  <div class="avatar-card">
93
- <div class="avatar-preview agent-icon" id="agentAvatarPreview" aria-hidden="true">🦈</div>
93
+ <div class="avatar-preview agent-icon" id="agentAvatarPreview" data-icon="shark" aria-hidden="true"></div>
94
94
  <span class="avatar-card-label">Agent</span>
95
95
  <div class="avatar-card-actions">
96
96
  <button class="avatar-action-btn" id="agentAvatarUploadBtn" title="Upload image">
@@ -103,7 +103,7 @@
103
103
  <input type="file" id="agentAvatarFile" hidden accept="image/png,image/jpeg,image/webp,image/gif">
104
104
  </div>
105
105
  <div class="avatar-card">
106
- <div class="avatar-preview user-icon" id="userAvatarPreview" aria-hidden="true">👤</div>
106
+ <div class="avatar-preview user-icon" id="userAvatarPreview" data-icon="user" aria-hidden="true"></div>
107
107
  <span class="avatar-card-label">User</span>
108
108
  <div class="avatar-card-actions">
109
109
  <button class="avatar-action-btn" id="userAvatarUploadBtn" title="Upload image">
package/public/index.html CHANGED
@@ -96,7 +96,7 @@
96
96
  <div class="section-title" style="margin-top:12px" data-i18n="sidebar.avatar">아바타</div>
97
97
  <div class="avatar-row">
98
98
  <div class="avatar-card">
99
- <div class="avatar-preview agent-icon" id="agentAvatarPreview" aria-hidden="true">🦈</div>
99
+ <div class="avatar-preview agent-icon" id="agentAvatarPreview" data-icon="shark" aria-hidden="true"></div>
100
100
  <span class="avatar-card-label">Agent</span>
101
101
  <div class="avatar-card-actions">
102
102
  <button class="avatar-action-btn" id="agentAvatarUploadBtn" title="Upload image">
@@ -109,7 +109,7 @@
109
109
  <input type="file" id="agentAvatarFile" hidden accept="image/png,image/jpeg,image/webp,image/gif">
110
110
  </div>
111
111
  <div class="avatar-card">
112
- <div class="avatar-preview user-icon" id="userAvatarPreview" aria-hidden="true">👤</div>
112
+ <div class="avatar-preview user-icon" id="userAvatarPreview" data-icon="user" aria-hidden="true"></div>
113
113
  <span class="avatar-card-label">User</span>
114
114
  <div class="avatar-card-actions">
115
115
  <button class="avatar-action-btn" id="userAvatarUploadBtn" title="Upload image">
@@ -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 },
@@ -56,7 +57,8 @@ function avatarMarkup(role: AvatarRole): string {
56
57
  if (current.imageUrl) {
57
58
  return `<img class="avatar-image" src="${escapeHtml(current.imageUrl)}" alt="" loading="lazy" decoding="async">`;
58
59
  }
59
- return escapeHtml(current.emoji);
60
+ // Default icons are Lucide SVG strings — render as-is
61
+ return current.emoji;
60
62
  }
61
63
 
62
64
  function applyAvatar(role: AvatarRole): void {
@@ -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
- // ── Shark mascot (🦈 emoji — brand identity) ──
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
- // Mascot
105
- shark: SHARK_SVG,
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/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,47 +485,8 @@ export async function loadMessages(): Promise<void> {
403
485
  if (chatEl) chatEl.innerHTML = '';
404
486
 
405
487
  if (msgs.length >= VS_THRESHOLD) {
406
- // RC5 fix: register callbacks BEFORE feeding items so activate() has them
407
- vs.onLazyRender = (targets: HTMLElement[]) => {
408
- for (const el of targets) {
409
- if (!el.classList.contains('lazy-pending')) continue;
410
- const raw = el.getAttribute('data-raw') || '';
411
- el.innerHTML = raw ? renderMarkdown(raw) : '';
412
- el.classList.remove('lazy-pending');
413
- activateWidgets(el);
414
-
415
- // Persist rendered HTML back into VS cache
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
-
424
- // Activate widgets + file path linkification on all VS-rendered items
425
- vs.onPostRender = (viewport: HTMLElement) => {
426
- activateWidgets(viewport);
427
- linkifyFilePaths(viewport);
428
- };
429
-
430
- // Bulk-load all items at once — avoids mid-loop activate (RC5 fix)
431
- const vsItems: import('./virtual-scroll.js').VirtualItem[] = [];
432
- for (const m of msgs) {
433
- const role = m.role === 'assistant' ? 'agent' : m.role;
434
- const rawContent = stripOrchestration(m.content);
435
- const label = escapeHtml(role === 'user' ? t('msg.you') : getAppName());
436
- const tools = m.role === 'assistant' ? parseToolLog(m.tool_log) : [];
437
- const toolHtml = tools.length > 0 ? buildProcessBlockHtml(toProcessSteps(tools), true) : '';
438
- const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
439
- const html = role === 'agent'
440
- ? `<div class="msg msg-agent"><div class="agent-icon" aria-hidden="true">${getAgentIcon(m.cli)}</div><div class="agent-body">${toolHtml}<div class="msg-content lazy-pending" data-raw="${escapeHtml(rawContent)}">${skeletonContent}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div></div>`
441
- : `<div class="msg msg-${role}"><div class="user-body"><div class="msg-label">${label}</div><div class="msg-content lazy-pending" data-raw="${escapeHtml(rawContent)}">${skeletonContent}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div><div class="user-icon" aria-hidden="true">${getUserAvatarMarkup()}</div></div>`;
442
- vsItems.push({ id: generateId(), html, height: 80 });
443
- }
444
- vs.setItems(vsItems);
445
-
446
- vs.scrollToBottom();
488
+ const vsItems = buildVirtualHistoryItems(msgs);
489
+ bootstrapVirtualHistory(vsItems, makeBootstrapDeps(vs, chatEl!));
447
490
  } else {
448
491
  msgs.forEach(m => {
449
492
  const div = addMessage(m.role === 'assistant' ? 'agent' : m.role, m.content, m.cli);
@@ -478,37 +521,8 @@ export async function loadMessages(): Promise<void> {
478
521
  const cached = await getScopedMessages();
479
522
  if (cached.length > 0) {
480
523
  if (cached.length >= VS_THRESHOLD) {
481
- for (const m of cached) {
482
- const role = m.role === 'assistant' ? 'agent' : m.role;
483
- const rawContent = stripOrchestration(m.content);
484
- const label = escapeHtml(role === 'user' ? t('msg.you') : getAppName());
485
- const tools = m.role === 'assistant' && m.tool_log ? parseToolLog(m.tool_log) : [];
486
- const toolHtml = tools.length > 0 ? buildProcessBlockHtml(toProcessSteps(tools), true) : '';
487
- const skeletonContent = '<div class="skeleton-line"></div><div class="skeleton-line"></div>';
488
- const html = role === 'agent'
489
- ? `<div class="msg msg-agent"><div class="agent-icon" aria-hidden="true">${getAgentIcon(m.cli)}</div><div class="agent-body">${toolHtml}<div class="msg-content lazy-pending" data-raw="${escapeHtml(rawContent)}">${skeletonContent}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div></div>`
490
- : `<div class="msg msg-${role}"><div class="user-body"><div class="msg-label">${label}</div><div class="msg-content lazy-pending" data-raw="${escapeHtml(rawContent)}">${skeletonContent}</div><button class="msg-copy" title="Copy" aria-label="Copy message"></button></div><div class="user-icon" aria-hidden="true">${getUserAvatarMarkup()}</div></div>`;
491
- vs.addItem(generateId(), html);
492
- }
493
- vs.onLazyRender = (targets: HTMLElement[]) => {
494
- for (const el of targets) {
495
- if (!el.classList.contains('lazy-pending')) continue;
496
- const raw = el.getAttribute('data-raw') || '';
497
- el.innerHTML = raw ? renderMarkdown(raw) : '';
498
- el.classList.remove('lazy-pending');
499
- activateWidgets(el);
500
- const msgEl = el.closest('[data-vs-idx]') as HTMLElement | null;
501
- if (msgEl) {
502
- const idx = Number(msgEl.dataset.vsIdx);
503
- vs.updateItemHtml(idx, msgEl.outerHTML);
504
- }
505
- }
506
- };
507
- vs.onPostRender = (viewport: HTMLElement) => {
508
- activateWidgets(viewport);
509
- linkifyFilePaths(viewport);
510
- };
511
- vs.scrollToBottom();
524
+ const vsItems = buildVirtualHistoryItems(cached as MessageItem[]);
525
+ bootstrapVirtualHistory(vsItems, makeBootstrapDeps(vs, chatEl!));
512
526
  } else {
513
527
  cached.forEach(m => {
514
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
+ }