anentrypoint-design 0.0.175 → 0.0.176
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/247420.js +12 -12
- package/package.json +1 -1
- package/src/components/chat.js +71 -8
- package/src/components/interaction-primitives.js +50 -0
- package/src/components.js +1 -1
- package/src/kits/os/freddie/pages-chat.js +112 -33
- package/src/kits/os/freddie-dashboard.css +18 -0
- package/src/kits/os/theme.css +501 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.176",
|
|
4
4
|
"description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/247420.js",
|
package/src/components/chat.js
CHANGED
|
@@ -105,10 +105,50 @@ function CodeNode(p) {
|
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// Freddie-flavored agent parts: collapsible tool-call card, tool-result, and
|
|
109
|
+
// transient thinking indicator. Each renders as a `chat-bubble` variant so the
|
|
110
|
+
// surrounding ChatMessage chrome (avatar/meta/reactions) stays consistent.
|
|
111
|
+
function ToolCallNode(p) {
|
|
112
|
+
const status = p.status || (p.error ? 'error' : (p.result != null ? 'done' : 'running'));
|
|
113
|
+
const argsText = typeof p.args === 'string' ? p.args : JSON.stringify(p.args || {}, null, 2);
|
|
114
|
+
const resultText = p.result == null ? '' : (typeof p.result === 'string' ? p.result : JSON.stringify(p.result, null, 2));
|
|
115
|
+
// Default-open while running or on error so the user sees live progress / failure detail;
|
|
116
|
+
// collapse on success unless the caller explicitly overrides with open:true.
|
|
117
|
+
const defaultOpen = p.open != null ? !!p.open : (status === 'running' || status === 'error');
|
|
118
|
+
const iconName = status === 'running' ? 'refresh' : (status === 'error' ? 'warn' : 'check');
|
|
119
|
+
return h('details', { class: 'chat-bubble chat-tool tool-' + status, open: defaultOpen },
|
|
120
|
+
h('summary', { class: 'chat-tool-head' },
|
|
121
|
+
h('span', { class: 'chat-tool-icon', 'aria-hidden': 'true' }, Icon(iconName, { size: 14 })),
|
|
122
|
+
h('span', { class: 'chat-tool-name' }, p.name || 'tool'),
|
|
123
|
+
p.label ? h('span', { class: 'chat-tool-label' }, p.label) : null,
|
|
124
|
+
h('span', { class: 'chat-tool-status' }, status)
|
|
125
|
+
),
|
|
126
|
+
h('div', { class: 'chat-tool-body' },
|
|
127
|
+
h('div', { class: 'chat-tool-section' },
|
|
128
|
+
h('div', { class: 'chat-tool-section-label' }, 'args'),
|
|
129
|
+
h('pre', { class: 'chat-tool-pre' }, h('code', {}, argsText))),
|
|
130
|
+
resultText ? h('div', { class: 'chat-tool-section' },
|
|
131
|
+
h('div', { class: 'chat-tool-section-label' }, p.error ? 'error' : 'result'),
|
|
132
|
+
h('pre', { class: 'chat-tool-pre' + (p.error ? ' is-error' : '') }, h('code', {}, resultText))) : null
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ThinkingNode(p) {
|
|
138
|
+
return h('div', { class: 'chat-bubble chat-thinking', role: 'status', 'aria-live': 'polite' },
|
|
139
|
+
h('span', { class: 'chat-thinking-dots', 'aria-hidden': 'true' }, h('span'), h('span'), h('span')),
|
|
140
|
+
h('span', { class: 'chat-thinking-text' }, p.text || 'thinking…')
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
108
144
|
const PART_RENDERERS = {
|
|
109
145
|
text: (p) => h('div', { class: 'chat-bubble' }, ...renderInline(p.text || '')),
|
|
110
146
|
md: (p) => MdNode(p),
|
|
111
147
|
code: (p) => CodeNode(p),
|
|
148
|
+
tool: (p) => ToolCallNode(p),
|
|
149
|
+
tool_call: (p) => ToolCallNode(p),
|
|
150
|
+
tool_result: (p) => ToolCallNode({ ...p, name: p.name || 'tool_result', result: p.text != null ? p.text : p.result }),
|
|
151
|
+
thinking: (p) => ThinkingNode(p),
|
|
112
152
|
image: (p) => h('a', { class: 'chat-image', href: p.href || p.src, target: '_blank', rel: 'noopener', 'aria-label': p.alt || `embedded image: ${p.src}` },
|
|
113
153
|
h('img', { src: p.src, alt: p.alt || `embedded image from ${p.src}`, loading: 'lazy' }),
|
|
114
154
|
p.caption ? h('span', { class: 'cap' }, p.caption) : null),
|
|
@@ -146,9 +186,20 @@ function renderPart(p, key) {
|
|
|
146
186
|
|
|
147
187
|
export function ChatMessage({ role, who = 'them', avatar, text, parts, time, typing, key, aicat, reactions, receipt, name }) {
|
|
148
188
|
_stats.messages += 1;
|
|
149
|
-
// Support legacy 'who' prop, prefer 'role' with mapping:
|
|
150
|
-
|
|
151
|
-
|
|
189
|
+
// Support legacy 'who' prop, prefer 'role' with mapping:
|
|
190
|
+
// 'user' -> 'you' (right-aligned, accent bubble)
|
|
191
|
+
// 'assistant' -> 'them' (left-aligned, paper bubble)
|
|
192
|
+
// 'system' -> 'system' (centered, italic muted)
|
|
193
|
+
// 'tool' -> 'tool' (centered, collapsible card chrome)
|
|
194
|
+
// 'thinking' -> 'thinking' (centered, transient typing dots)
|
|
195
|
+
const resolvedWho = role
|
|
196
|
+
? (role === 'user' ? 'you'
|
|
197
|
+
: role === 'assistant' ? 'them'
|
|
198
|
+
: (role === 'system' || role === 'tool' || role === 'thinking') ? role
|
|
199
|
+
: role)
|
|
200
|
+
: who;
|
|
201
|
+
const isCentered = resolvedWho === 'system' || resolvedWho === 'tool' || resolvedWho === 'thinking';
|
|
202
|
+
const cls = 'chat-msg ' + resolvedWho + (aicat && resolvedWho === 'them' ? ' aicat' : '') + (isCentered ? ' centered' : '');
|
|
152
203
|
const fallbackAvatar = avatar != null
|
|
153
204
|
? avatar
|
|
154
205
|
: (resolvedWho === 'you' ? 'u' : (name ? String(name).trim().charAt(0).toUpperCase() || '?' : '?'));
|
|
@@ -171,10 +222,14 @@ export function ChatMessage({ role, who = 'them', avatar, text, parts, time, typ
|
|
|
171
222
|
if (tickNode) metaItems.push(tickNode);
|
|
172
223
|
const meta = metaItems.length ? h('div', { class: 'chat-meta' }, ...metaItems) : null;
|
|
173
224
|
const stack = h('div', { class: 'chat-stack' }, ...bodyNodes, reactionRow, meta);
|
|
225
|
+
// Centered roles (system/tool/thinking) skip the avatar column entirely so
|
|
226
|
+
// the bubble owns the full row — the chrome reads as out-of-band signal,
|
|
227
|
+
// not a participant turn.
|
|
228
|
+
if (isCentered) return h('div', { key, class: cls }, stack);
|
|
174
229
|
return h('div', { key, class: cls }, resolvedWho === 'you' ? stack : av, resolvedWho === 'you' ? av : stack);
|
|
175
230
|
}
|
|
176
231
|
|
|
177
|
-
export function ChatComposer({ value, onInput, onSend, onAttach, onEmoji, onMenu, placeholder = 'message…', disabled }) {
|
|
232
|
+
export function ChatComposer({ value, onInput, onSend, onAttach, onEmoji, onMenu, onCancel, busy, placeholder = 'message…', disabled }) {
|
|
178
233
|
// Keep a handle to the live textarea so send() reads the actual DOM value
|
|
179
234
|
// (not the possibly-lagging `value` prop) and so we can sync the DOM value
|
|
180
235
|
// only when it genuinely differs — re-applying `value` on every parent
|
|
@@ -220,12 +275,14 @@ export function ChatComposer({ value, onInput, onSend, onAttach, onEmoji, onMenu
|
|
|
220
275
|
onAttach ? h('button', { type: 'button', class: 'composer-btn', onclick: (e) => { e.preventDefault(); onAttach(e); }, 'aria-label': 'attach file', title: 'attach file' }, Icon('paperclip')) : null,
|
|
221
276
|
onEmoji ? h('button', { type: 'button', class: 'composer-btn', onclick: (e) => { e.preventDefault(); onEmoji(e); }, 'aria-label': 'emoji picker', title: 'emoji picker (Ctrl+;)' }, Icon('smile')) : null,
|
|
222
277
|
onMenu ? h('button', { type: 'button', class: 'composer-btn', onclick: (e) => { e.preventDefault(); onMenu(e); }, 'aria-label': 'composer menu', title: 'more options' }, Icon('more-horizontal')) : null,
|
|
223
|
-
|
|
278
|
+
busy && onCancel
|
|
279
|
+
? h('button', { type: 'button', class: 'send cancel', onclick: (e) => { e.preventDefault(); onCancel(e); }, 'aria-label': 'stop generating', title: 'stop generating (Esc)' }, Icon('square'))
|
|
280
|
+
: h('button', { type: 'button', class: 'send', disabled: disabled || !(value && value.trim()), onclick: send, 'aria-label': 'send message', title: 'send message (Enter)' }, Icon('arrow-up'))
|
|
224
281
|
)
|
|
225
282
|
);
|
|
226
283
|
}
|
|
227
284
|
|
|
228
|
-
export function Chat({ title = 'chat', sub, messages = [], composer, header } = {}) {
|
|
285
|
+
export function Chat({ title = 'chat', sub, messages = [], composer, header, suggestions, onSuggestionClick } = {}) {
|
|
229
286
|
// Warm markdown/Prism caches once so library loading parallelizes.
|
|
230
287
|
ensureCachesInit();
|
|
231
288
|
const threadRef = makeThreadAutoScroll(() => messages.length);
|
|
@@ -243,8 +300,14 @@ export function Chat({ title = 'chat', sub, messages = [], composer, header } =
|
|
|
243
300
|
h('div', { class: 'chat-thread', ref: threadRef, role: 'log', 'aria-label': 'chat messages' },
|
|
244
301
|
messages.length === 0
|
|
245
302
|
? h('div', { key: '_empty', class: 'chat-empty', role: 'status' },
|
|
246
|
-
h('p', { class: 'chat-empty-title' }, '
|
|
247
|
-
h('p', { class: 'chat-empty-sub' }, '
|
|
303
|
+
h('p', { class: 'chat-empty-title' }, 'start a conversation'),
|
|
304
|
+
h('p', { class: 'chat-empty-sub' }, sub || 'ask anything — i can search, read files, recall context, and call tools'),
|
|
305
|
+
(suggestions && suggestions.length)
|
|
306
|
+
? h('div', { class: 'chat-empty-suggestions' },
|
|
307
|
+
...suggestions.map((s, i) => h('button', { key: 'sug' + i, type: 'button', class: 'chat-empty-suggestion',
|
|
308
|
+
onclick: () => { if (onSuggestionClick) onSuggestionClick(typeof s === 'string' ? s : (s.prompt || s.text || '')); } },
|
|
309
|
+
typeof s === 'string' ? s : (s.label || s.text || s.prompt))))
|
|
310
|
+
: null)
|
|
248
311
|
: null,
|
|
249
312
|
...messages.map((m, i) => ChatMessage({ ...m, key: m.key != null ? m.key : i }))
|
|
250
313
|
),
|
|
@@ -89,6 +89,56 @@ export function useDraggable(el, { data, kind, onDragStart, onDragEnd } = {}) {
|
|
|
89
89
|
}};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// useNumberScrub — pointer-event horizontal drag-to-scrub on a numeric input.
|
|
93
|
+
// Pointer Events (mouse+touch+pen+XR), touch-action:none so a vertical page
|
|
94
|
+
// scroll never steals the gesture. Click-to-edit is preserved: a press that
|
|
95
|
+
// does not cross SCRUB_THRESHOLD leaves the input focusable for typing.
|
|
96
|
+
export function useNumberScrub(el, { getValue, onChange, step = 0.01, threshold = 3 } = {}) {
|
|
97
|
+
if (!el) return { destroy() {} };
|
|
98
|
+
el.style.touchAction = 'none';
|
|
99
|
+
let pid = null, startX = 0, startV = 0, moved = false;
|
|
100
|
+
const onMove = (e) => {
|
|
101
|
+
if (pid == null) return;
|
|
102
|
+
const dx = e.clientX - startX;
|
|
103
|
+
if (!moved) {
|
|
104
|
+
if (Math.abs(dx) < threshold) return;
|
|
105
|
+
moved = true;
|
|
106
|
+
el.setAttribute('data-scrubbing', 'true');
|
|
107
|
+
if (document.activeElement === el) el.blur();
|
|
108
|
+
}
|
|
109
|
+
const v = startV + dx * step;
|
|
110
|
+
if (onChange) onChange(v);
|
|
111
|
+
};
|
|
112
|
+
const onUp = () => {
|
|
113
|
+
if (pid == null) return;
|
|
114
|
+
try { el.releasePointerCapture(pid); } catch {}
|
|
115
|
+
pid = null;
|
|
116
|
+
el.removeAttribute('data-scrubbing');
|
|
117
|
+
window.removeEventListener('pointermove', onMove);
|
|
118
|
+
window.removeEventListener('pointerup', onUp);
|
|
119
|
+
window.removeEventListener('pointercancel', onUp);
|
|
120
|
+
};
|
|
121
|
+
const onDown = (e) => {
|
|
122
|
+
if (e.button != null && e.button !== 0) return;
|
|
123
|
+
// Let a focused input handle caret placement / text selection instead.
|
|
124
|
+
if (document.activeElement === el) return;
|
|
125
|
+
pid = e.pointerId; startX = e.clientX; moved = false;
|
|
126
|
+
const cur = getValue ? getValue() : parseFloat(el.value);
|
|
127
|
+
startV = Number.isFinite(cur) ? cur : 0;
|
|
128
|
+
try { el.setPointerCapture(pid); } catch {}
|
|
129
|
+
window.addEventListener('pointermove', onMove);
|
|
130
|
+
window.addEventListener('pointerup', onUp);
|
|
131
|
+
window.addEventListener('pointercancel', onUp);
|
|
132
|
+
};
|
|
133
|
+
el.addEventListener('pointerdown', onDown);
|
|
134
|
+
return { destroy() {
|
|
135
|
+
el.removeEventListener('pointerdown', onDown);
|
|
136
|
+
window.removeEventListener('pointermove', onMove);
|
|
137
|
+
window.removeEventListener('pointerup', onUp);
|
|
138
|
+
window.removeEventListener('pointercancel', onUp);
|
|
139
|
+
}};
|
|
140
|
+
}
|
|
141
|
+
|
|
92
142
|
export function useDropTarget(el, { accepts = [], onDrop, onDragOver } = {}) {
|
|
93
143
|
if (!el) return { destroy() {} };
|
|
94
144
|
el.setAttribute('data-drop-target', '');
|
package/src/components.js
CHANGED
|
@@ -58,7 +58,7 @@ export {
|
|
|
58
58
|
} from './components/form-primitives.js';
|
|
59
59
|
|
|
60
60
|
export {
|
|
61
|
-
useDraggable, useDropTarget, Reorderable,
|
|
61
|
+
useDraggable, useDropTarget, useNumberScrub, Reorderable,
|
|
62
62
|
useKeyboardShortcut, formatShortcut, ShortcutHint,
|
|
63
63
|
useKeyboardShortcutHelp, ShortcutHelpDialog
|
|
64
64
|
} from './components/interaction-primitives.js';
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
// Chat page —
|
|
1
|
+
// Chat page — dashboard surface for the agent. Renders via the kit's
|
|
2
|
+
// Chat + ChatComposer primitives so the dashboard tab and the OS chat panel
|
|
3
|
+
// share the same bubble / tool-call / empty-state chrome. The bespoke
|
|
4
|
+
// cwd/skill/provider/model selectors live in a collapsible config strip
|
|
5
|
+
// above the thread (mirror of the OS panel cc-strip pattern).
|
|
6
|
+
|
|
2
7
|
import * as webjsx from '../../../../vendor/webjsx/index.js';
|
|
3
8
|
import * as components from '../../../components.js';
|
|
4
|
-
import { getRecentPaths, saveRecentPath, skillLabel
|
|
9
|
+
import { getRecentPaths, saveRecentPath, skillLabel } from './helpers.js';
|
|
10
|
+
import { Chat, ChatComposer } from '../../../components/chat.js';
|
|
5
11
|
|
|
6
12
|
const h = webjsx.createElement;
|
|
7
13
|
const { Panel, Receipt, Chip, Icon } = components;
|
|
@@ -20,6 +26,22 @@ function parseSseEvents(text) {
|
|
|
20
26
|
return events;
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
// Convert the dashboard message shape into the kit ChatMessage shape:
|
|
30
|
+
// { role:'user', content:string } -> { role:'user', text }
|
|
31
|
+
// { role:'assistant', content:string } -> { role:'assistant', parts:[{kind:'md', text}] }
|
|
32
|
+
// { role:'tool', name, argsSummary, content } ->
|
|
33
|
+
// { role:'tool', parts:[{kind:'tool_call', name, label, args, result, status}] }
|
|
34
|
+
// { role:'thinking' } -> { role:'thinking', parts:[{kind:'thinking', text}] }
|
|
35
|
+
function toKitMessage(m) {
|
|
36
|
+
if (m.role === 'tool') {
|
|
37
|
+
const status = m.status || (m.error ? 'error' : (m.content != null ? 'done' : 'running'));
|
|
38
|
+
return { role: 'tool', parts: [{ kind: 'tool_call', name: m.name || 'tool', label: m.argsSummary || '', args: m.args || m.input || {}, result: m.content, status, error: !!m.error, open: status !== 'done' }] };
|
|
39
|
+
}
|
|
40
|
+
if (m.role === 'thinking') return { role: 'thinking', parts: [{ kind: 'thinking', text: m.content || 'thinking…' }] };
|
|
41
|
+
if (m.role === 'assistant' && m.content) return { role: 'assistant', parts: [{ kind: 'md', text: String(m.content) }] };
|
|
42
|
+
return { role: m.role, text: m.content || '' };
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
export function makeChatPage(ctx) {
|
|
24
46
|
return async function chat(h0) {
|
|
25
47
|
const root = ctx.root;
|
|
@@ -28,34 +50,44 @@ export function makeChatPage(ctx) {
|
|
|
28
50
|
const configuredProviders = providers.filter(p => p.configured);
|
|
29
51
|
|
|
30
52
|
const chatState = window.__fd_chatState = window.__fd_chatState || {
|
|
31
|
-
cwd: '', skill: '', provider: '', model: '', messages: [], busy: false, sessionId: null,
|
|
53
|
+
cwd: '', skill: '', provider: '', model: '', messages: [], busy: false, sessionId: null, draft: '', abort: null,
|
|
32
54
|
};
|
|
33
55
|
if (!chatState.cwd) chatState.cwd = (getRecentPaths()[0] || '');
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
// Find the chat container in the live DOM (set on the rendered <ds-chat>).
|
|
58
|
+
const getChatHost = () => root.querySelector('ds-chat.fd-dashboard-chat');
|
|
59
|
+
const syncMessages = () => {
|
|
60
|
+
const host = getChatHost();
|
|
61
|
+
if (host) host.messages = chatState.messages.map(toKitMessage);
|
|
62
|
+
};
|
|
36
63
|
|
|
37
64
|
const newSession = () => {
|
|
38
65
|
if (chatState.busy) return;
|
|
39
66
|
chatState.messages = [];
|
|
40
67
|
chatState.sessionId = null;
|
|
41
|
-
|
|
68
|
+
syncMessages();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const cancelInFlight = () => {
|
|
72
|
+
if (chatState.abort) { try { chatState.abort.abort(); } catch {} chatState.abort = null; }
|
|
73
|
+
chatState.busy = false;
|
|
74
|
+
syncMessages();
|
|
75
|
+
renderPage();
|
|
42
76
|
};
|
|
43
77
|
|
|
44
|
-
const sendChat = async (
|
|
45
|
-
ev.preventDefault();
|
|
78
|
+
const sendChat = async (prompt) => {
|
|
46
79
|
if (chatState.busy) return;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
chatState.messages.push({ role: 'user', content: prompt });
|
|
51
|
-
promptEl.value = '';
|
|
52
|
-
promptEl.style.height = 'auto';
|
|
80
|
+
const trimmed = String(prompt || '').trim();
|
|
81
|
+
if (!trimmed) return;
|
|
82
|
+
chatState.messages.push({ role: 'user', content: trimmed });
|
|
53
83
|
chatState.busy = true;
|
|
84
|
+
chatState.abort = new AbortController();
|
|
54
85
|
saveRecentPath(chatState.cwd);
|
|
55
|
-
|
|
86
|
+
syncMessages();
|
|
87
|
+
renderPage();
|
|
56
88
|
try {
|
|
57
|
-
const body = { prompt, cwd: chatState.cwd || undefined, skill: chatState.skill || undefined, provider: chatState.provider || undefined, model: chatState.model || undefined, sessionId: chatState.sessionId || undefined };
|
|
58
|
-
const resp = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
89
|
+
const body = { prompt: trimmed, cwd: chatState.cwd || undefined, skill: chatState.skill || undefined, provider: chatState.provider || undefined, model: chatState.model || undefined, sessionId: chatState.sessionId || undefined };
|
|
90
|
+
const resp = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: chatState.abort.signal });
|
|
59
91
|
const text = await resp.text();
|
|
60
92
|
const events = parseSseEvents(text);
|
|
61
93
|
let assistantContent = '';
|
|
@@ -71,63 +103,110 @@ export function makeChatPage(ctx) {
|
|
|
71
103
|
if (block.type === 'tool_use') {
|
|
72
104
|
if (assistantContent) { chatState.messages.push({ role: 'assistant', content: assistantContent }); assistantContent = ''; }
|
|
73
105
|
const argsSummary = JSON.stringify(block.input || {}).slice(0, 60);
|
|
74
|
-
chatState.messages.push({ role: 'tool', name: block.name, argsSummary, content: JSON.stringify(block.input || {}, null, 2) });
|
|
106
|
+
chatState.messages.push({ role: 'tool', name: block.name, argsSummary, content: JSON.stringify(block.input || {}, null, 2), status: 'running' });
|
|
107
|
+
syncMessages();
|
|
75
108
|
}
|
|
76
109
|
}
|
|
77
110
|
} else if (role === 'tool') {
|
|
78
111
|
const tc = Array.isArray(data.content) ? data.content[0] : data;
|
|
79
|
-
|
|
112
|
+
// Resolve the last running tool call to done with this result.
|
|
113
|
+
for (let i = chatState.messages.length - 1; i >= 0; i--) {
|
|
114
|
+
const m = chatState.messages[i];
|
|
115
|
+
if (m.role === 'tool' && m.status === 'running') {
|
|
116
|
+
m.content = String(tc?.content || tc?.text || JSON.stringify(tc));
|
|
117
|
+
m.status = 'done';
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
syncMessages();
|
|
80
122
|
}
|
|
81
123
|
}
|
|
82
124
|
if (event === 'done' && data.result) { if (!assistantContent) assistantContent = data.result; }
|
|
83
|
-
if (event === 'error')
|
|
125
|
+
if (event === 'error') {
|
|
126
|
+
const msg = 'error: ' + (data.error || 'unknown');
|
|
127
|
+
// Mark any running tool as errored; record assistant error.
|
|
128
|
+
for (let i = chatState.messages.length - 1; i >= 0; i--) {
|
|
129
|
+
const m = chatState.messages[i];
|
|
130
|
+
if (m.role === 'tool' && m.status === 'running') { m.status = 'error'; m.error = true; m.content = msg; break; }
|
|
131
|
+
}
|
|
132
|
+
if (!assistantContent) assistantContent = msg;
|
|
133
|
+
}
|
|
84
134
|
}
|
|
85
135
|
if (assistantContent) chatState.messages.push({ role: 'assistant', content: assistantContent });
|
|
86
136
|
if (!events.length) chatState.messages.push({ role: 'assistant', content: '(no response)' });
|
|
87
137
|
} catch (e) {
|
|
88
|
-
chatState.messages.push({ role: 'assistant', content: '
|
|
138
|
+
if (e.name === 'AbortError') chatState.messages.push({ role: 'assistant', content: '[cancelled]' });
|
|
139
|
+
else chatState.messages.push({ role: 'assistant', content: 'error: ' + e.message });
|
|
89
140
|
}
|
|
141
|
+
chatState.abort = null;
|
|
90
142
|
chatState.busy = false;
|
|
91
|
-
|
|
143
|
+
syncMessages();
|
|
144
|
+
renderPage();
|
|
92
145
|
};
|
|
93
146
|
|
|
94
147
|
const recentPaths = getRecentPaths();
|
|
95
148
|
const datalistId = 'fd-cwd-list';
|
|
96
149
|
const byCat = skills.reduce((a, s) => { const c = s.category || 'other'; (a[c] = a[c] || []).push(s); return a; }, {});
|
|
97
150
|
|
|
98
|
-
|
|
151
|
+
const renderPage = () => {
|
|
152
|
+
const host = getChatHost();
|
|
153
|
+
if (host) {
|
|
154
|
+
host.busy = chatState.busy;
|
|
155
|
+
host.placeholder = chatState.busy ? 'agent working…' : 'describe what you want to do in the working directory…';
|
|
156
|
+
}
|
|
157
|
+
// Refresh disabled state on header buttons.
|
|
158
|
+
const newBtn = root.querySelector('.fd-chat-new');
|
|
159
|
+
if (newBtn) newBtn.disabled = !!chatState.busy;
|
|
160
|
+
const cancelBtn = root.querySelector('.fd-chat-cancel');
|
|
161
|
+
if (cancelBtn) cancelBtn.style.display = chatState.busy ? '' : 'none';
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// After mount, seed messages onto the ds-chat element + wire the send event.
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
const host = getChatHost();
|
|
167
|
+
if (host && !host._fdBound) {
|
|
168
|
+
host._fdBound = true;
|
|
169
|
+
host.addEventListener('send', (e) => { sendChat(e.detail && e.detail.text); });
|
|
170
|
+
host.messages = chatState.messages.map(toKitMessage);
|
|
171
|
+
host.placeholder = 'describe what you want to do in the working directory…';
|
|
172
|
+
host.sub = chatState.sessionId ? ('session ' + chatState.sessionId.slice(0, 8)) : 'agent';
|
|
173
|
+
}
|
|
174
|
+
renderPage();
|
|
175
|
+
}, 50);
|
|
99
176
|
|
|
100
177
|
const selSkill = h('select', { name: 'skill', onchange: (ev) => { chatState.skill = ev.target.value; } },
|
|
101
|
-
h('option', { value: '' }, '
|
|
178
|
+
h('option', { value: '' }, 'no skill'),
|
|
102
179
|
...Object.entries(byCat).map(([cat, ss]) =>
|
|
103
180
|
h('optgroup', { label: cat },
|
|
104
181
|
...ss.map(s => h('option', { value: s.name, selected: chatState.skill === s.name ? 'true' : null, title: s.description || s.name }, skillLabel(s)))
|
|
105
182
|
)));
|
|
106
183
|
|
|
107
184
|
const selProv = h('select', { name: 'provider', onchange: (ev) => { chatState.provider = ev.target.value; } },
|
|
108
|
-
h('option', { value: '' }, configuredProviders.length ? '
|
|
185
|
+
h('option', { value: '' }, configuredProviders.length ? 'auto' : 'no providers configured'),
|
|
109
186
|
...configuredProviders.map(p => h('option', { value: p.name, selected: chatState.provider === p.name ? 'true' : null }, (p.available ? '(on) ' : '(off) ') + p.name)));
|
|
110
187
|
|
|
111
188
|
return [
|
|
112
189
|
Panel({
|
|
113
190
|
title: 'chat',
|
|
114
|
-
right: h('
|
|
191
|
+
right: h('div', { class: 'fd-chat-actions' },
|
|
192
|
+
h('button', { class: 'btn-secondary fd-btn-mini fd-chat-cancel', style: chatState.busy ? '' : 'display:none', onclick: (ev) => { ev.preventDefault(); cancelInFlight(); } }, 'cancel'),
|
|
193
|
+
h('button', { class: 'btn-primary fd-btn-mini fd-chat-new', onclick: (ev) => { ev.preventDefault(); newSession(); }, disabled: chatState.busy ? 'true' : null }, 'new session')
|
|
194
|
+
),
|
|
115
195
|
children: [
|
|
116
196
|
h('datalist', { id: datalistId }, ...recentPaths.map(p => h('option', { value: p }))),
|
|
117
|
-
h('
|
|
118
|
-
h('div', { class: 'fd-chat-field' },
|
|
197
|
+
h('div', { class: 'fd-chat-config' },
|
|
198
|
+
h('div', { class: 'fd-chat-field fd-chat-field-grow' },
|
|
119
199
|
h('label', {}, 'working directory'),
|
|
120
200
|
h('input', { name: 'cwd', type: 'text', placeholder: 'e.g. C:/dev/myproject or /home/user/project', value: chatState.cwd, list: datalistId, oninput: (ev) => { chatState.cwd = ev.target.value; } })),
|
|
121
201
|
h('div', { class: 'fd-chat-row' },
|
|
122
202
|
h('div', { class: 'fd-chat-field fd-chat-field-grow' }, h('label', {}, 'skill'), selSkill),
|
|
123
203
|
h('div', { class: 'fd-chat-field fd-chat-field-grow' }, h('label', {}, 'provider'), selProv),
|
|
124
204
|
h('div', { class: 'fd-chat-field fd-chat-field-grow' }, h('label', {}, 'model (optional)'),
|
|
125
|
-
h('input', { name: 'model', type: 'text', placeholder: configuredProviders.find(p => p.name === chatState.provider)?.defaultModel || 'default', value: chatState.model, oninput: (ev) => { chatState.model = ev.target.value; } }))),
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
h('div', { id: 'fd-chat-msgs', class: 'fd-chatlog' }),
|
|
205
|
+
h('input', { name: 'model', type: 'text', placeholder: configuredProviders.find(p => p.name === chatState.provider)?.defaultModel || 'default', value: chatState.model, oninput: (ev) => { chatState.model = ev.target.value; } })))),
|
|
206
|
+
// Live chat surface — the kit's ds-chat web component handles
|
|
207
|
+
// layout, scroll, empty state, bubble chrome, tool-call cards,
|
|
208
|
+
// composer focus rings, send/cancel button swap, etc.
|
|
209
|
+
h('ds-chat', { class: 'fd-dashboard-chat ds-247420', title: 'chat', placeholder: 'describe what you want to do in the working directory…' }),
|
|
131
210
|
],
|
|
132
211
|
}),
|
|
133
212
|
configuredProviders.length === 0
|
|
@@ -52,3 +52,21 @@
|
|
|
52
52
|
.fd-chip-wrap { display: flex; flex-wrap: wrap; gap: var(--space-1, 4px); }
|
|
53
53
|
.fd-chip-wrap-padded { padding: var(--space-2, 8px) var(--space-1, 4px); }
|
|
54
54
|
.fd-env-chip { cursor: pointer; }
|
|
55
|
+
|
|
56
|
+
/* dashboard chat tab — config strip above + <ds-chat> thread below
|
|
57
|
+
--------------------------------------------------------------------- */
|
|
58
|
+
.fd-chat-config { display: flex; flex-direction: column; gap: var(--space-2, 8px); padding-bottom: var(--space-3, 12px); border-bottom: 1px solid color-mix(in oklab, var(--fg) 10%, transparent); margin-bottom: var(--space-3, 12px); }
|
|
59
|
+
.fd-chat-config .fd-chat-field { display: flex; flex-direction: column; gap: var(--space-1, 4px); min-width: 120px; }
|
|
60
|
+
.fd-chat-config .fd-chat-field > label { font-family: var(--os-mono, monospace); font-size: 10px; opacity: 0.6; letter-spacing: 0.05em; text-transform: uppercase; }
|
|
61
|
+
.fd-chat-config .fd-chat-field > input,
|
|
62
|
+
.fd-chat-config .fd-chat-field > select { width: 100%; box-sizing: border-box; padding: 6px 8px; background: var(--panel-1, transparent); color: var(--fg, inherit); border: 1px solid color-mix(in oklab, var(--fg) 14%, transparent); border-radius: 6px; font: inherit; font-size: 12px; }
|
|
63
|
+
.fd-chat-config .fd-chat-field > input:focus-visible,
|
|
64
|
+
.fd-chat-config .fd-chat-field > select:focus-visible { outline: 2px solid var(--os-accent, #247420); outline-offset: 0; border-color: color-mix(in oklab, var(--os-accent, #247420) 60%, transparent); }
|
|
65
|
+
.fd-chat-config .fd-chat-row { display: flex; gap: var(--space-2, 8px); flex-wrap: wrap; }
|
|
66
|
+
.fd-chat-config .fd-chat-field-grow { flex: 1 1 140px; min-width: 0; }
|
|
67
|
+
|
|
68
|
+
.app-fd ds-chat.fd-dashboard-chat { flex: 1 1 auto; min-height: 280px; display: flex; flex-direction: column; overflow: hidden; }
|
|
69
|
+
|
|
70
|
+
.fd-chat-actions { display: inline-flex; gap: var(--space-1, 4px); align-items: center; }
|
|
71
|
+
.fd-chat-actions .btn-secondary { background: transparent; color: var(--danger, #c0392b); border: 1px solid color-mix(in oklab, var(--danger, #c0392b) 40%, transparent); cursor: pointer; padding: 2px 8px; border-radius: 4px; font: inherit; font-size: 12px; }
|
|
72
|
+
.fd-chat-actions .btn-secondary:hover { background: color-mix(in oklab, var(--danger, #c0392b) 10%, transparent); }
|