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.
- package/package.json +1 -1
- package/public/assets/shark.svg +1 -0
- package/public/dist/assets/{employees-zxrU6ZV_.js → employees-V7lNStu1.js} +1 -1
- package/public/dist/assets/{index-D61icK-D.js → index-Cpe1jccL.js} +4 -4
- package/public/dist/assets/render-BoxeLlL9.js +25 -0
- package/public/dist/assets/settings-BcKp6ppP.js +1 -0
- package/public/dist/assets/{settings-Dl3RnWsB.js → settings-CBCg5Jhh.js} +1 -1
- package/public/dist/assets/skills-BuAXFNgp.js +1 -0
- package/public/dist/assets/{skills-DhiCSGws.js → skills-RbauGmBZ.js} +1 -1
- package/public/dist/assets/{slash-commands-B1k1vFJG.js → slash-commands-BgKxc49D.js} +1 -1
- package/public/dist/assets/slash-commands-DXGb_iGA.js +1 -0
- package/public/dist/assets/ui-KQ8_sSP8.js +131 -0
- package/public/dist/assets/ui-rD__Mvbs.js +1 -0
- package/public/dist/assets/vendor-icons-C6LXvgi0.js +1 -0
- package/public/dist/assets/{ws-CleMWrLF.js → ws-BtTpgocf.js} +1 -1
- package/public/dist/index.html +3 -3
- package/public/index.html +2 -2
- package/public/js/features/avatar.ts +5 -3
- package/public/js/icons.ts +10 -4
- package/public/js/ui.ts +87 -73
- package/public/js/virtual-scroll-bootstrap.ts +42 -0
- package/public/js/virtual-scroll.ts +140 -49
- package/public/dist/assets/render-CVr6a-dp.js +0 -25
- package/public/dist/assets/settings-BHIV4l1s.js +0 -1
- package/public/dist/assets/skills-JuDja1UC.js +0 -1
- package/public/dist/assets/slash-commands-DyLS0abr.js +0 -1
- package/public/dist/assets/ui-BXZhbE_1.js +0 -131
- package/public/dist/assets/ui-qR28iS0L.js +0 -1
- 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-
|
|
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};
|
package/public/dist/index.html
CHANGED
|
@@ -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-
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
-
|
|
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 {
|
package/public/js/icons.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
Package,
|
|
30
30
|
ClipboardList,
|
|
31
31
|
Bot,
|
|
32
|
+
CircleUserRound,
|
|
32
33
|
Palette,
|
|
33
34
|
Link,
|
|
34
35
|
HandMetal,
|
|
@@ -54,6 +55,9 @@ import {
|
|
|
54
55
|
Download,
|
|
55
56
|
} from '@lucide/icons';
|
|
56
57
|
|
|
58
|
+
// ── Inline SVG assets (embedded to avoid ?raw import issues in Node.js tests) ──
|
|
59
|
+
const sharkSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12c0 0 2-4 6-4 1 0 2 .5 3 1l3-6c0 0 1 5 3 7 1.5 1.5 5 2 5 2s-1 4-5 4c-1 0-2-.3-3-.8L12 18c0 0-2-1-4-1-4 0-6-5-6-5z"/><circle cx="17" cy="11" r="0.5" fill="currentColor" stroke="none"/><path d="M7 12l1 2"/><path d="M9.5 12l1 2"/></svg>';
|
|
60
|
+
|
|
57
61
|
// ── Size presets ──
|
|
58
62
|
const S = 14; // inline / small
|
|
59
63
|
const M = 16; // default UI
|
|
@@ -62,8 +66,7 @@ function luc(data: Parameters<typeof buildLucideSvg>[0], size = M): string {
|
|
|
62
66
|
return buildLucideSvg(data, { size });
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
// ──
|
|
66
|
-
const SHARK_SVG = '🦈';
|
|
69
|
+
// ── Default avatar icons (Lucide-based, no emoji literals) ──
|
|
67
70
|
|
|
68
71
|
// ── Icon registry ──
|
|
69
72
|
// Keys match the semantic role, NOT the old emoji codepoint.
|
|
@@ -101,8 +104,9 @@ export const ICONS = {
|
|
|
101
104
|
link: luc(Link),
|
|
102
105
|
salute: luc(HandMetal),
|
|
103
106
|
|
|
104
|
-
//
|
|
105
|
-
shark:
|
|
107
|
+
// Avatar defaults
|
|
108
|
+
shark: sharkSvg,
|
|
109
|
+
user: luc(CircleUserRound),
|
|
106
110
|
|
|
107
111
|
// HTML template icons
|
|
108
112
|
paperclip: luc(Paperclip),
|
|
@@ -165,6 +169,7 @@ const iconMap: Partial<Record<IconName, (s: number) => string>> = {
|
|
|
165
169
|
palette: (s) => luc(Palette, s),
|
|
166
170
|
link: (s) => luc(Link, s),
|
|
167
171
|
salute: (s) => luc(HandMetal, s),
|
|
172
|
+
user: (s) => luc(CircleUserRound, s),
|
|
168
173
|
paperclip: (s) => luc(Paperclip, s),
|
|
169
174
|
save: (s) => luc(Save, s),
|
|
170
175
|
gamepad: (s) => luc(Gamepad2, s),
|
|
@@ -197,6 +202,7 @@ const EMOJI_TO_ICON: Record<string, IconName> = {
|
|
|
197
202
|
'⚠️': 'warning',
|
|
198
203
|
'💡': 'lightbulb',
|
|
199
204
|
'🦈': 'shark',
|
|
205
|
+
'👤': 'user',
|
|
200
206
|
'💭': 'thinking',
|
|
201
207
|
'🔍': 'search',
|
|
202
208
|
'🌐': 'web',
|
package/public/js/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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
482
|
-
|
|
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
|
+
}
|