pith-ui 0.0.0-stage → 0.1.0
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/AI.md +67 -0
- package/API.md +3281 -0
- package/LICENSE +661 -0
- package/README.md +80 -2
- package/dist/composer.d.ts +38 -0
- package/dist/composer.d.ts.map +1 -0
- package/dist/composer.js +114 -0
- package/dist/composer.js.map +1 -0
- package/dist/content/conversation-reader.d.ts +19 -0
- package/dist/content/conversation-reader.d.ts.map +1 -0
- package/dist/content/conversation-reader.js +280 -0
- package/dist/content/conversation-reader.js.map +1 -0
- package/dist/content/conversation-view.d.ts +48 -0
- package/dist/content/conversation-view.d.ts.map +1 -0
- package/dist/content/conversation-view.js +107 -0
- package/dist/content/conversation-view.js.map +1 -0
- package/dist/content/image-viewer.d.ts +23 -0
- package/dist/content/image-viewer.d.ts.map +1 -0
- package/dist/content/image-viewer.js +101 -0
- package/dist/content/image-viewer.js.map +1 -0
- package/dist/content/markdown.d.ts +12 -0
- package/dist/content/markdown.d.ts.map +1 -0
- package/dist/content/markdown.js +68 -0
- package/dist/content/markdown.js.map +1 -0
- package/dist/content/paged-preview.d.ts +15 -0
- package/dist/content/paged-preview.d.ts.map +1 -0
- package/dist/content/paged-preview.js +105 -0
- package/dist/content/paged-preview.js.map +1 -0
- package/dist/content/views.d.ts +9 -0
- package/dist/content/views.d.ts.map +1 -0
- package/dist/content/views.js +53 -0
- package/dist/content/views.js.map +1 -0
- package/dist/content/welcome.d.ts +18 -0
- package/dist/content/welcome.d.ts.map +1 -0
- package/dist/content/welcome.js +45 -0
- package/dist/content/welcome.js.map +1 -0
- package/dist/core.d.ts +36 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +44 -0
- package/dist/core.js.map +1 -0
- package/dist/dom.d.ts +14 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +69 -0
- package/dist/dom.js.map +1 -0
- package/dist/files/panels.d.ts +95 -0
- package/dist/files/panels.d.ts.map +1 -0
- package/dist/files/panels.js +245 -0
- package/dist/files/panels.js.map +1 -0
- package/dist/files/systems.d.ts +91 -0
- package/dist/files/systems.d.ts.map +1 -0
- package/dist/files/systems.js +256 -0
- package/dist/files/systems.js.map +1 -0
- package/dist/forms/actions.d.ts +27 -0
- package/dist/forms/actions.d.ts.map +1 -0
- package/dist/forms/actions.js +19 -0
- package/dist/forms/actions.js.map +1 -0
- package/dist/forms/custom-connection.d.ts +61 -0
- package/dist/forms/custom-connection.d.ts.map +1 -0
- package/dist/forms/custom-connection.js +65 -0
- package/dist/forms/custom-connection.js.map +1 -0
- package/dist/forms/dialog-frame.d.ts +39 -0
- package/dist/forms/dialog-frame.d.ts.map +1 -0
- package/dist/forms/dialog-frame.js +75 -0
- package/dist/forms/dialog-frame.js.map +1 -0
- package/dist/forms/history.d.ts +16 -0
- package/dist/forms/history.d.ts.map +1 -0
- package/dist/forms/history.js +21 -0
- package/dist/forms/history.js.map +1 -0
- package/dist/forms/identity.d.ts +4 -0
- package/dist/forms/identity.d.ts.map +1 -0
- package/dist/forms/identity.js +21 -0
- package/dist/forms/identity.js.map +1 -0
- package/dist/forms/model-editor.d.ts +35 -0
- package/dist/forms/model-editor.d.ts.map +1 -0
- package/dist/forms/model-editor.js +78 -0
- package/dist/forms/model-editor.js.map +1 -0
- package/dist/forms/panels.d.ts +57 -0
- package/dist/forms/panels.d.ts.map +1 -0
- package/dist/forms/panels.js +224 -0
- package/dist/forms/panels.js.map +1 -0
- package/dist/forms/provider-login.d.ts +22 -0
- package/dist/forms/provider-login.d.ts.map +1 -0
- package/dist/forms/provider-login.js +114 -0
- package/dist/forms/provider-login.js.map +1 -0
- package/dist/forms/resource-editor.d.ts +23 -0
- package/dist/forms/resource-editor.d.ts.map +1 -0
- package/dist/forms/resource-editor.js +24 -0
- package/dist/forms/resource-editor.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/context-menu.d.ts +25 -0
- package/dist/navigation/context-menu.d.ts.map +1 -0
- package/dist/navigation/context-menu.js +67 -0
- package/dist/navigation/context-menu.js.map +1 -0
- package/dist/navigation/navigation.d.ts +57 -0
- package/dist/navigation/navigation.d.ts.map +1 -0
- package/dist/navigation/navigation.js +185 -0
- package/dist/navigation/navigation.js.map +1 -0
- package/dist/primitives.d.ts +307 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +741 -0
- package/dist/primitives.js.map +1 -0
- package/dist/styles.css +849 -0
- package/dist/tasks/approval.d.ts +25 -0
- package/dist/tasks/approval.d.ts.map +1 -0
- package/dist/tasks/approval.js +61 -0
- package/dist/tasks/approval.js.map +1 -0
- package/dist/tasks/permission.d.ts +35 -0
- package/dist/tasks/permission.d.ts.map +1 -0
- package/dist/tasks/permission.js +67 -0
- package/dist/tasks/permission.js.map +1 -0
- package/dist/tasks/runtime.d.ts +31 -0
- package/dist/tasks/runtime.d.ts.map +1 -0
- package/dist/tasks/runtime.js +34 -0
- package/dist/tasks/runtime.js.map +1 -0
- package/dist/theme.d.ts +4 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +6 -0
- package/dist/theme.js.map +1 -0
- package/dist/workspace.d.ts +38 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +183 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +27 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { UIBlock } from '../core.js';
|
|
2
|
+
import { type MarkdownOptions } from './markdown.js';
|
|
3
|
+
/** UI data only. Hosts decide copy/edit/branch eligibility and resolve images. */
|
|
4
|
+
export interface ConversationMessage {
|
|
5
|
+
id: string;
|
|
6
|
+
role: string;
|
|
7
|
+
text: string;
|
|
8
|
+
timestamp?: number;
|
|
9
|
+
toolName?: string;
|
|
10
|
+
toolCallId?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
images?: Array<{
|
|
13
|
+
index: number;
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
}>;
|
|
16
|
+
command?: {
|
|
17
|
+
text: string;
|
|
18
|
+
cwd: string;
|
|
19
|
+
exitCode?: number;
|
|
20
|
+
};
|
|
21
|
+
actions?: {
|
|
22
|
+
copy?: boolean | string;
|
|
23
|
+
edit?: boolean;
|
|
24
|
+
branch?: {
|
|
25
|
+
key: string;
|
|
26
|
+
label?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface ConversationViewState {
|
|
31
|
+
id: string;
|
|
32
|
+
messages: readonly ConversationMessage[];
|
|
33
|
+
working: boolean;
|
|
34
|
+
folds?: ReadonlyMap<string, boolean>;
|
|
35
|
+
}
|
|
36
|
+
export interface ConversationAction {
|
|
37
|
+
action: 'copy' | 'edit' | 'branch';
|
|
38
|
+
messageId: string;
|
|
39
|
+
branchKey?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ConversationViewOptions {
|
|
42
|
+
container?: HTMLElement;
|
|
43
|
+
markdown?: (conversationId: string) => MarkdownOptions;
|
|
44
|
+
onAction?: (action: ConversationAction) => void;
|
|
45
|
+
onCopyCode?: (text: string) => void;
|
|
46
|
+
}
|
|
47
|
+
export declare function createConversationView(options?: ConversationViewOptions): UIBlock<ConversationViewState>;
|
|
48
|
+
//# sourceMappingURL=conversation-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-view.d.ts","sourceRoot":"","sources":["../../src/content/conversation-view.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrE,kFAAkF;AAClF,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CACjG;AACD,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AACD,MAAM,WAAW,kBAAkB;IAAG,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AACjH,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,eAAe,CAAC;IACvD,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAChD,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAGD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAqD5G"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { escape, icon } from '../dom.js';
|
|
2
|
+
import { renderMarkdown } from './markdown.js';
|
|
3
|
+
import { reconcileViews, clearViews } from './views.js';
|
|
4
|
+
export function createConversationView(options = {}) {
|
|
5
|
+
const container = options.container || document.createElement('div');
|
|
6
|
+
container.classList.add('pui-conversation-messages');
|
|
7
|
+
let destroyed = false;
|
|
8
|
+
const click = (event) => {
|
|
9
|
+
const target = event.target.closest('[data-message-action], [data-copy-code]');
|
|
10
|
+
if (!target || !container.contains(target) || target.disabled)
|
|
11
|
+
return;
|
|
12
|
+
if (target.hasAttribute('data-copy-code')) {
|
|
13
|
+
const text = target.closest('.pui-code-block')?.querySelector('pre > code')?.textContent || '';
|
|
14
|
+
options.onCopyCode?.(text);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
options.onAction?.({ action: target.dataset.messageAction, messageId: target.dataset.messageActionId, branchKey: target.dataset.branchKey });
|
|
18
|
+
};
|
|
19
|
+
container.addEventListener('click', click);
|
|
20
|
+
let activeId = '';
|
|
21
|
+
function update(id, messages, working, folds) {
|
|
22
|
+
if (destroyed)
|
|
23
|
+
return;
|
|
24
|
+
if (id !== activeId) {
|
|
25
|
+
clearViews(container);
|
|
26
|
+
activeId = id;
|
|
27
|
+
}
|
|
28
|
+
const views = [];
|
|
29
|
+
const messageView = (message, key = message.id, foldKey = '') => ({
|
|
30
|
+
key, signature: JSON.stringify(message), html: () => renderMessage(id, message, folds.get(foldKey), foldKey, (text, conversationId) => renderMarkdown(text, options.markdown?.(conversationId))),
|
|
31
|
+
});
|
|
32
|
+
let groupIndex = 0;
|
|
33
|
+
for (let index = 0; index < messages.length;) {
|
|
34
|
+
if (messages[index].role !== 'tool') {
|
|
35
|
+
views.push(messageView(messages[index++]));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const start = index;
|
|
39
|
+
while (index < messages.length && messages[index].role === 'tool')
|
|
40
|
+
index++;
|
|
41
|
+
const tools = messages.slice(start, index), key = `group-${groupIndex++}`;
|
|
42
|
+
const running = tools.filter((tool) => tool.status === 'running').length;
|
|
43
|
+
const failed = tools.filter((tool) => tool.status === 'error').length;
|
|
44
|
+
const label = toolGroupLabel(tools);
|
|
45
|
+
views.push({ key, signature: JSON.stringify([label, tools.length, failed, running]),
|
|
46
|
+
html: () => `<details class="pui-tool-group" data-tool-fold="${key}" ${folds.get(key) ? 'open' : ''}><summary>${icon('terminal')}<span class="pui-tool-group-label">${label}</span><span class="pui-tool-group-count">${tools.length} ${tools.length === 1 ? 'call' : 'calls'}</span>${failed ? `<span class="pui-tool-group-failed">${failed} failed</span>` : ''}${running ? `<span class="pui-tool-group-running">${running} running</span>` : ''}${icon('down')}</summary><div class="pui-tool-group-content" data-view-children></div></details>`,
|
|
47
|
+
children: tools.map((tool) => { const toolKey = `${key}-tool-${tool.toolCallId || tool.id}`; return messageView(tool, toolKey, toolKey); }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (working)
|
|
51
|
+
views.push({ key: 'working', signature: 'working', html: () => '<div class="pui-agent-working" role="status"><span class="pui-working-dots" aria-hidden="true"><i></i><i></i><i></i></span><span>Working…</span></div>' });
|
|
52
|
+
reconcileViews(container, views);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
element: container,
|
|
56
|
+
update(state) {
|
|
57
|
+
const folds = state.folds || new Map(Array.from(container.querySelectorAll('[data-tool-fold]'), (element) => [element.dataset.toolFold, element.open]));
|
|
58
|
+
update(state.id, state.messages, state.working, folds);
|
|
59
|
+
},
|
|
60
|
+
destroy() {
|
|
61
|
+
if (destroyed)
|
|
62
|
+
return;
|
|
63
|
+
destroyed = true;
|
|
64
|
+
container.removeEventListener('click', click);
|
|
65
|
+
clearViews(container);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function toolGroupLabel(tools) {
|
|
70
|
+
if (tools.length === 1)
|
|
71
|
+
return { list_files: 'Listed files', read_file: 'Read a file', run_command: 'Ran a command', write_file: 'Wrote a file', edit_file: 'Edited a file', tool_search: 'Searched tools', codemode: 'Ran code' }[tools[0].toolName || ''] || 'Used a tool';
|
|
72
|
+
const hasCommands = tools.some((tool) => tool.toolName === 'run_command');
|
|
73
|
+
const exploredFiles = tools.every((tool) => ['list_files', 'read_file', 'grep', 'find', 'ls', 'run_command'].includes(tool.toolName || ''));
|
|
74
|
+
if (tools.every((tool) => tool.toolName === 'run_command'))
|
|
75
|
+
return 'Ran commands';
|
|
76
|
+
if (exploredFiles)
|
|
77
|
+
return hasCommands ? 'Ran commands and explored files' : 'Explored files';
|
|
78
|
+
return 'Used tools';
|
|
79
|
+
}
|
|
80
|
+
function renderMessage(conversationId, message, expanded = false, foldKey = '', markdown = (text) => renderMarkdown(text)) {
|
|
81
|
+
const text = message.text ?? '';
|
|
82
|
+
const images = (message.images || []).map((image) => `<img class="pui-history-image" data-image-key="${escape(`${conversationId}/${message.id}/${image.index}`)}" data-message-id="${escape(message.id)}" data-image-index="${image.index}" alt="Image ${image.index + 1}" role="button" tabindex="0" title="Click to enlarge" loading="lazy" />`).join('');
|
|
83
|
+
const gallery = images ? `<div class="pui-message-images">${images}</div>` : '';
|
|
84
|
+
const copyLabel = typeof message.actions?.copy === 'string' ? message.actions.copy : 'Copy message';
|
|
85
|
+
const copy = message.actions?.copy ? `<button type="button" class="pui-quiet-icon pui-message-action" data-message-action="copy" data-message-action-id="${escape(message.id)}" title="${escape(copyLabel)}" aria-label="${escape(copyLabel)}">${icon('copy')}</button>` : '';
|
|
86
|
+
const edit = message.actions?.edit ? `<button type="button" class="pui-quiet-icon pui-message-action" data-message-action="edit" data-message-action-id="${escape(message.id)}" title="Edit message" aria-label="Edit message">${icon('edit')}</button>` : '';
|
|
87
|
+
const branchAction = message.actions?.branch;
|
|
88
|
+
const branch = branchAction ? `<button type="button" class="pui-quiet-icon pui-message-action" data-message-action="branch" data-message-action-id="${escape(message.id)}" data-branch-key="${escape(branchAction.key)}" title="${escape(branchAction.label || 'Branch from this message')}" aria-label="${escape(branchAction.label || 'Branch from this message')}">${icon('branch')}</button>` : '';
|
|
89
|
+
const actions = copy || edit || branch ? `<div class="pui-message-actions">${copy}${edit}${branch}</div>` : '';
|
|
90
|
+
const date = new Date(message.timestamp || NaN);
|
|
91
|
+
const time = Number.isFinite(date.getTime()) ? `<time class="pui-message-time" datetime="${date.toISOString()}" title="${escape(date.toLocaleString())}">${escape(date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }))}</time>` : '';
|
|
92
|
+
const footer = time || actions ? `<div class="pui-message-footer">${time}${actions}</div>` : '';
|
|
93
|
+
if (message.role === 'tool') {
|
|
94
|
+
const command = message.command;
|
|
95
|
+
const details = command ? `<div class="pui-command-details"><span class="pui-tool-field-label">Command</span><pre class="pui-command-text">${escape(command.text)}</pre><div class="pui-command-directory"><span class="pui-tool-field-label">Directory</span><code>${escape(command.cwd)}</code></div></div>` : '';
|
|
96
|
+
const status = command ? { running: 'Running', done: 'Completed', error: 'Failed' }[message.status || ''] || message.status || 'Result' : message.status || 'Result';
|
|
97
|
+
const exit = command?.exitCode !== undefined ? ` · Exit ${command.exitCode}` : '';
|
|
98
|
+
const output = command ? `<div class="pui-command-output"><span class="pui-tool-field-label">Output</span><pre>${escape(text)}</pre></div>` : `<pre>${escape(text)}</pre>`;
|
|
99
|
+
return `<details class="pui-tool-message" data-reading-id="${escape(message.id)}" data-tool-fold="${escape(foldKey)}" ${expanded ? 'open' : ''}><summary>${icon('terminal')}<span>${command ? 'Command' : escape(message.toolName || 'Tool result')}</span><span class="pui-tool-status${message.status === 'error' ? ' pui-tool-status-error' : ''}">${escape(status + exit)}</span>${icon('down')}</summary>${details}${output}${gallery}</details>`;
|
|
100
|
+
}
|
|
101
|
+
if (message.role === 'user')
|
|
102
|
+
return `<article class="pui-message pui-user-message" data-reading-id="${escape(message.id)}" aria-label="Your message"><div class="pui-message-content">${gallery}${text ? `<div class="pui-user-text">${escape(text)}</div>` : ''}${footer}</div></article>`;
|
|
103
|
+
if (message.role === 'system')
|
|
104
|
+
return `<div class="pui-system-message" data-reading-id="${escape(message.id)}">${escape(text)}</div>`;
|
|
105
|
+
return `<article class="pui-message pui-assistant-message" data-reading-id="${escape(message.id)}" aria-label="Agent response"><div class="pui-message-content"><div class="pui-markdown">${markdown(text, conversationId)}</div>${message.status === 'error' ? '<span class="pui-message-error-label">Response interrupted</span>' : ''}${footer}</div></article>`;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=conversation-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-view.js","sourceRoot":"","sources":["../../src/content/conversation-view.ts"],"names":[],"mappings":"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","sourcesContent":["import { escape, icon } from '../dom.js';\nimport type { UIBlock } from '../core.js';\nimport { renderMarkdown, type MarkdownOptions } from './markdown.js';\nimport { reconcileViews, clearViews, type View } from './views.js';\n\n/** UI data only. Hosts decide copy/edit/branch eligibility and resolve images. */\nexport interface ConversationMessage {\n id: string;\n role: string;\n text: string;\n timestamp?: number;\n toolName?: string;\n toolCallId?: string;\n status?: string;\n images?: Array<{ index: number; mimeType?: string }>;\n command?: { text: string; cwd: string; exitCode?: number };\n actions?: { copy?: boolean | string; edit?: boolean; branch?: { key: string; label?: string } };\n}\nexport interface ConversationViewState {\n id: string;\n messages: readonly ConversationMessage[];\n working: boolean;\n folds?: ReadonlyMap<string, boolean>;\n}\nexport interface ConversationAction { action: 'copy' | 'edit' | 'branch'; messageId: string; branchKey?: string }\nexport interface ConversationViewOptions {\n container?: HTMLElement;\n markdown?: (conversationId: string) => MarkdownOptions;\n onAction?: (action: ConversationAction) => void;\n onCopyCode?: (text: string) => void;\n}\n\n\nexport function createConversationView(options: ConversationViewOptions = {}): UIBlock<ConversationViewState> {\n const container = options.container || document.createElement('div');\n container.classList.add('pui-conversation-messages');\n let destroyed = false;\n const click = (event: Event) => {\n const target = (event.target as Element).closest<HTMLElement>('[data-message-action], [data-copy-code]');\n if (!target || !container.contains(target) || (target as HTMLButtonElement).disabled) return;\n if (target.hasAttribute('data-copy-code')) {\n const text = target.closest('.pui-code-block')?.querySelector('pre > code')?.textContent || '';\n options.onCopyCode?.(text);\n return;\n }\n options.onAction?.({ action: target.dataset.messageAction as ConversationAction['action'], messageId: target.dataset.messageActionId!, branchKey: target.dataset.branchKey });\n };\n container.addEventListener('click', click);\n let activeId = '';\n function update(id: string, messages: readonly ConversationMessage[], working: boolean, folds: ReadonlyMap<string, boolean>): void {\n if (destroyed) return;\n if (id !== activeId) { clearViews(container); activeId = id; }\n const views: View[] = [];\n const messageView = (message: ConversationMessage, key = message.id, foldKey = ''): View => ({\n key, signature: JSON.stringify(message), html: () => renderMessage(id, message, folds.get(foldKey), foldKey, (text, conversationId) => renderMarkdown(text, options.markdown?.(conversationId))),\n });\n let groupIndex = 0;\n for (let index = 0; index < messages.length;) {\n if (messages[index].role !== 'tool') { views.push(messageView(messages[index++])); continue; }\n const start = index;\n while (index < messages.length && messages[index].role === 'tool') index++;\n const tools = messages.slice(start, index), key = `group-${groupIndex++}`;\n const running = tools.filter((tool) => tool.status === 'running').length;\n const failed = tools.filter((tool) => tool.status === 'error').length;\n const label = toolGroupLabel(tools);\n views.push({ key, signature: JSON.stringify([label, tools.length, failed, running]),\n html: () => `<details class=\"pui-tool-group\" data-tool-fold=\"${key}\" ${folds.get(key) ? 'open' : ''}><summary>${icon('terminal')}<span class=\"pui-tool-group-label\">${label}</span><span class=\"pui-tool-group-count\">${tools.length} ${tools.length === 1 ? 'call' : 'calls'}</span>${failed ? `<span class=\"pui-tool-group-failed\">${failed} failed</span>` : ''}${running ? `<span class=\"pui-tool-group-running\">${running} running</span>` : ''}${icon('down')}</summary><div class=\"pui-tool-group-content\" data-view-children></div></details>`,\n children: tools.map((tool) => { const toolKey = `${key}-tool-${tool.toolCallId || tool.id}`; return messageView(tool, toolKey, toolKey); }),\n });\n }\n if (working) views.push({ key: 'working', signature: 'working', html: () => '<div class=\"pui-agent-working\" role=\"status\"><span class=\"pui-working-dots\" aria-hidden=\"true\"><i></i><i></i><i></i></span><span>Working…</span></div>' });\n reconcileViews(container, views);\n }\n return {\n element: container,\n update(state): void {\n const folds = state.folds || new Map(Array.from(container.querySelectorAll<HTMLDetailsElement>('[data-tool-fold]'), (element) => [element.dataset.toolFold!, element.open] as const));\n update(state.id, state.messages, state.working, folds);\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n container.removeEventListener('click', click);\n clearViews(container);\n },\n };\n}\n\nfunction toolGroupLabel(tools: readonly ConversationMessage[]): string {\n if (tools.length === 1) return ({ list_files: 'Listed files', read_file: 'Read a file', run_command: 'Ran a command', write_file: 'Wrote a file', edit_file: 'Edited a file', tool_search: 'Searched tools', codemode: 'Ran code' } as Record<string, string>)[tools[0].toolName || ''] || 'Used a tool';\n const hasCommands = tools.some((tool) => tool.toolName === 'run_command');\n const exploredFiles = tools.every((tool) => ['list_files', 'read_file', 'grep', 'find', 'ls', 'run_command'].includes(tool.toolName || ''));\n if (tools.every((tool) => tool.toolName === 'run_command')) return 'Ran commands';\n if (exploredFiles) return hasCommands ? 'Ran commands and explored files' : 'Explored files';\n return 'Used tools';\n}\n\nfunction renderMessage(conversationId: string, message: ConversationMessage, expanded = false, foldKey = '', markdown: (value: string, conversationId: string) => string = (text) => renderMarkdown(text)): string {\n const text = message.text ?? '';\n const images = (message.images || []).map((image) => `<img class=\"pui-history-image\" data-image-key=\"${escape(`${conversationId}/${message.id}/${image.index}`)}\" data-message-id=\"${escape(message.id)}\" data-image-index=\"${image.index}\" alt=\"Image ${image.index + 1}\" role=\"button\" tabindex=\"0\" title=\"Click to enlarge\" loading=\"lazy\" />`).join('');\n const gallery = images ? `<div class=\"pui-message-images\">${images}</div>` : '';\n const copyLabel = typeof message.actions?.copy === 'string' ? message.actions.copy : 'Copy message';\n const copy = message.actions?.copy ? `<button type=\"button\" class=\"pui-quiet-icon pui-message-action\" data-message-action=\"copy\" data-message-action-id=\"${escape(message.id)}\" title=\"${escape(copyLabel)}\" aria-label=\"${escape(copyLabel)}\">${icon('copy')}</button>` : '';\n const edit = message.actions?.edit ? `<button type=\"button\" class=\"pui-quiet-icon pui-message-action\" data-message-action=\"edit\" data-message-action-id=\"${escape(message.id)}\" title=\"Edit message\" aria-label=\"Edit message\">${icon('edit')}</button>` : '';\n const branchAction = message.actions?.branch;\n const branch = branchAction ? `<button type=\"button\" class=\"pui-quiet-icon pui-message-action\" data-message-action=\"branch\" data-message-action-id=\"${escape(message.id)}\" data-branch-key=\"${escape(branchAction.key)}\" title=\"${escape(branchAction.label || 'Branch from this message')}\" aria-label=\"${escape(branchAction.label || 'Branch from this message')}\">${icon('branch')}</button>` : '';\n const actions = copy || edit || branch ? `<div class=\"pui-message-actions\">${copy}${edit}${branch}</div>` : '';\n const date = new Date(message.timestamp || NaN);\n const time = Number.isFinite(date.getTime()) ? `<time class=\"pui-message-time\" datetime=\"${date.toISOString()}\" title=\"${escape(date.toLocaleString())}\">${escape(date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }))}</time>` : '';\n const footer = time || actions ? `<div class=\"pui-message-footer\">${time}${actions}</div>` : '';\n if (message.role === 'tool') {\n const command = message.command;\n const details = command ? `<div class=\"pui-command-details\"><span class=\"pui-tool-field-label\">Command</span><pre class=\"pui-command-text\">${escape(command.text)}</pre><div class=\"pui-command-directory\"><span class=\"pui-tool-field-label\">Directory</span><code>${escape(command.cwd)}</code></div></div>` : '';\n const status = command ? ({ running: 'Running', done: 'Completed', error: 'Failed' } as Record<string, string>)[message.status || ''] || message.status || 'Result' : message.status || 'Result';\n const exit = command?.exitCode !== undefined ? ` · Exit ${command.exitCode}` : '';\n const output = command ? `<div class=\"pui-command-output\"><span class=\"pui-tool-field-label\">Output</span><pre>${escape(text)}</pre></div>` : `<pre>${escape(text)}</pre>`;\n return `<details class=\"pui-tool-message\" data-reading-id=\"${escape(message.id)}\" data-tool-fold=\"${escape(foldKey)}\" ${expanded ? 'open' : ''}><summary>${icon('terminal')}<span>${command ? 'Command' : escape(message.toolName || 'Tool result')}</span><span class=\"pui-tool-status${message.status === 'error' ? ' pui-tool-status-error' : ''}\">${escape(status + exit)}</span>${icon('down')}</summary>${details}${output}${gallery}</details>`;\n }\n if (message.role === 'user') return `<article class=\"pui-message pui-user-message\" data-reading-id=\"${escape(message.id)}\" aria-label=\"Your message\"><div class=\"pui-message-content\">${gallery}${text ? `<div class=\"pui-user-text\">${escape(text)}</div>` : ''}${footer}</div></article>`;\n if (message.role === 'system') return `<div class=\"pui-system-message\" data-reading-id=\"${escape(message.id)}\">${escape(text)}</div>`;\n return `<article class=\"pui-message pui-assistant-message\" data-reading-id=\"${escape(message.id)}\" aria-label=\"Agent response\"><div class=\"pui-message-content\"><div class=\"pui-markdown\">${markdown(text, conversationId)}</div>${message.status === 'error' ? '<span class=\"pui-message-error-label\">Response interrupted</span>' : ''}${footer}</div></article>`;\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ImageViewerOptions {
|
|
2
|
+
dialog?: HTMLDialogElement;
|
|
3
|
+
headingId?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ImageViewerInput {
|
|
6
|
+
src: string;
|
|
7
|
+
alt?: string;
|
|
8
|
+
returnFocus?: HTMLElement;
|
|
9
|
+
}
|
|
10
|
+
/** A modal above either a generated-file preview or a conversation attachment. */
|
|
11
|
+
export declare function createImageViewer(options?: ImageViewerOptions): {
|
|
12
|
+
element: HTMLDialogElement;
|
|
13
|
+
open(target: HTMLImageElement | ImageViewerInput): void;
|
|
14
|
+
close: () => void;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
};
|
|
17
|
+
export declare function installImageViewer(dialog: HTMLDialogElement): {
|
|
18
|
+
element: HTMLDialogElement;
|
|
19
|
+
open(target: HTMLImageElement | ImageViewerInput): void;
|
|
20
|
+
close: () => void;
|
|
21
|
+
destroy(): void;
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=image-viewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-viewer.d.ts","sourceRoot":"","sources":["../../src/content/image-viewer.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IAAG,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AACtF,MAAM,WAAW,gBAAgB;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,WAAW,CAAA;CAAE;AAG1F,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB;;iBA8CjD,gBAAgB,GAAG,gBAAgB,GAAG,IAAI;iBAHvC,IAAI;eAqBT,IAAI;EAalB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB;;iBAjC3C,gBAAgB,GAAG,gBAAgB,GAAG,IAAI;iBAHvC,IAAI;eAqBT,IAAI;EAiBlB"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { escape, icon } from '../dom.js';
|
|
2
|
+
let sequence = 0;
|
|
3
|
+
/** A modal above either a generated-file preview or a conversation attachment. */
|
|
4
|
+
export function createImageViewer(options = {}) {
|
|
5
|
+
const dialog = options.dialog || document.createElement('dialog');
|
|
6
|
+
const headingId = options.headingId || `pui-image-title-${++sequence}`;
|
|
7
|
+
dialog.classList.add('pui-modal', 'pui-image-viewer');
|
|
8
|
+
dialog.setAttribute('aria-labelledby', headingId);
|
|
9
|
+
dialog.innerHTML = `<div class="pui-modal-heading"><h2 id="${escape(headingId)}">Image</h2><button type="button" class="pui-quiet-icon" data-image-close aria-label="Close image">${icon('close')}</button></div>
|
|
10
|
+
<div class="pui-image-viewer-viewport"><img alt="" /></div>
|
|
11
|
+
<div class="pui-image-viewer-toolbar"><span data-image-scale role="status"></span><div><button type="button" class="pui-secondary-button" data-image-out aria-label="Zoom out">−</button><button type="button" class="pui-secondary-button" data-image-in aria-label="Zoom in">+</button><button type="button" class="pui-secondary-button" data-image-fit>Fit</button><button type="button" class="pui-secondary-button" data-image-original>Original size</button></div></div>`;
|
|
12
|
+
const image = dialog.querySelector('img');
|
|
13
|
+
const viewport = dialog.querySelector('.pui-image-viewer-viewport');
|
|
14
|
+
const title = dialog.querySelector('h2');
|
|
15
|
+
const status = dialog.querySelector('[data-image-scale]');
|
|
16
|
+
const listeners = new AbortController();
|
|
17
|
+
const signal = listeners.signal;
|
|
18
|
+
let destroyed = false;
|
|
19
|
+
let scale = 1, fitted = true, source = null;
|
|
20
|
+
function update() {
|
|
21
|
+
if (destroyed || !image.naturalWidth)
|
|
22
|
+
return;
|
|
23
|
+
if (fitted)
|
|
24
|
+
scale = Math.min(1, viewport.clientWidth / image.naturalWidth, viewport.clientHeight / image.naturalHeight);
|
|
25
|
+
image.style.width = `${image.naturalWidth * scale}px`;
|
|
26
|
+
image.style.height = `${image.naturalHeight * scale}px`;
|
|
27
|
+
status.textContent = `${Math.round(scale * 100)}% · ${image.naturalWidth} × ${image.naturalHeight}`;
|
|
28
|
+
}
|
|
29
|
+
function resize(factor) { fitted = false; const next = scale * factor; if (Number.isFinite(next) && next > 0)
|
|
30
|
+
scale = next; update(); }
|
|
31
|
+
dialog.querySelector('[data-image-fit]').addEventListener('click', () => { fitted = true; update(); }, { signal });
|
|
32
|
+
dialog.querySelector('[data-image-original]').addEventListener('click', () => { fitted = false; scale = 1; update(); }, { signal });
|
|
33
|
+
dialog.querySelector('[data-image-in]').addEventListener('click', () => resize(1.25), { signal });
|
|
34
|
+
dialog.querySelector('[data-image-out]').addEventListener('click', () => resize(1 / 1.25), { signal });
|
|
35
|
+
dialog.querySelector('[data-image-close]').addEventListener('click', () => dialog.close(), { signal });
|
|
36
|
+
image.addEventListener('load', update, { signal });
|
|
37
|
+
const observer = new ResizeObserver(() => { if (dialog.open && fitted)
|
|
38
|
+
update(); });
|
|
39
|
+
observer.observe(viewport);
|
|
40
|
+
dialog.addEventListener('close', () => {
|
|
41
|
+
image.removeAttribute('src');
|
|
42
|
+
image.removeAttribute('style');
|
|
43
|
+
source?.focus({ preventScroll: true });
|
|
44
|
+
source = null;
|
|
45
|
+
}, { signal });
|
|
46
|
+
document.addEventListener('keydown', (event) => {
|
|
47
|
+
if (!dialog.open || event.isComposing || event.metaKey || event.ctrlKey || event.altKey)
|
|
48
|
+
return;
|
|
49
|
+
if (event.key === '+' || event.key === '=') {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
resize(1.25);
|
|
52
|
+
}
|
|
53
|
+
if (event.key === '-') {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
resize(1 / 1.25);
|
|
56
|
+
}
|
|
57
|
+
}, { signal });
|
|
58
|
+
function close() { if (dialog.open)
|
|
59
|
+
dialog.close(); }
|
|
60
|
+
return {
|
|
61
|
+
element: dialog,
|
|
62
|
+
open(target) {
|
|
63
|
+
if (destroyed)
|
|
64
|
+
return;
|
|
65
|
+
const input = target instanceof HTMLImageElement
|
|
66
|
+
? target.getAttribute('src') && !target.classList.contains('pui-image-unavailable') ? { src: target.src, alt: target.alt, returnFocus: target } : null
|
|
67
|
+
: target;
|
|
68
|
+
if (!input?.src)
|
|
69
|
+
return;
|
|
70
|
+
source = input.returnFocus || null;
|
|
71
|
+
title.textContent = input.alt || 'Image';
|
|
72
|
+
image.alt = input.alt || '';
|
|
73
|
+
scale = 1;
|
|
74
|
+
fitted = true;
|
|
75
|
+
status.textContent = 'Loading…';
|
|
76
|
+
image.src = input.src;
|
|
77
|
+
if (!dialog.open)
|
|
78
|
+
dialog.showModal();
|
|
79
|
+
viewport.scrollTop = viewport.scrollLeft = 0;
|
|
80
|
+
update();
|
|
81
|
+
},
|
|
82
|
+
close,
|
|
83
|
+
destroy() {
|
|
84
|
+
if (destroyed)
|
|
85
|
+
return;
|
|
86
|
+
close();
|
|
87
|
+
image.removeAttribute('src');
|
|
88
|
+
image.removeAttribute('style');
|
|
89
|
+
source?.focus({ preventScroll: true });
|
|
90
|
+
source = null;
|
|
91
|
+
destroyed = true;
|
|
92
|
+
observer.disconnect();
|
|
93
|
+
listeners.abort();
|
|
94
|
+
dialog.replaceChildren();
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function installImageViewer(dialog) {
|
|
99
|
+
return createImageViewer({ dialog, headingId: dialog.getAttribute('aria-labelledby') || undefined });
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=image-viewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-viewer.js","sourceRoot":"","sources":["../../src/content/image-viewer.ts"],"names":[],"mappings":"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","sourcesContent":["import { escape, icon } from '../dom.js';\n\nexport interface ImageViewerOptions { dialog?: HTMLDialogElement; headingId?: string }\nexport interface ImageViewerInput { src: string; alt?: string; returnFocus?: HTMLElement }\nlet sequence = 0;\n\n/** A modal above either a generated-file preview or a conversation attachment. */\nexport function createImageViewer(options: ImageViewerOptions = {}) {\n const dialog = options.dialog || document.createElement('dialog');\n const headingId = options.headingId || `pui-image-title-${++sequence}`;\n dialog.classList.add('pui-modal', 'pui-image-viewer');\n dialog.setAttribute('aria-labelledby', headingId);\n dialog.innerHTML = `<div class=\"pui-modal-heading\"><h2 id=\"${escape(headingId)}\">Image</h2><button type=\"button\" class=\"pui-quiet-icon\" data-image-close aria-label=\"Close image\">${icon('close')}</button></div>\n <div class=\"pui-image-viewer-viewport\"><img alt=\"\" /></div>\n <div class=\"pui-image-viewer-toolbar\"><span data-image-scale role=\"status\"></span><div><button type=\"button\" class=\"pui-secondary-button\" data-image-out aria-label=\"Zoom out\">−</button><button type=\"button\" class=\"pui-secondary-button\" data-image-in aria-label=\"Zoom in\">+</button><button type=\"button\" class=\"pui-secondary-button\" data-image-fit>Fit</button><button type=\"button\" class=\"pui-secondary-button\" data-image-original>Original size</button></div></div>`;\n const image = dialog.querySelector<HTMLImageElement>('img')!;\n const viewport = dialog.querySelector<HTMLElement>('.pui-image-viewer-viewport')!;\n const title = dialog.querySelector<HTMLElement>('h2')!;\n const status = dialog.querySelector<HTMLElement>('[data-image-scale]')!;\n const listeners = new AbortController();\n const signal = listeners.signal;\n let destroyed = false;\n let scale = 1, fitted = true, source: HTMLElement | null = null;\n function update(): void {\n if (destroyed || !image.naturalWidth) return;\n if (fitted) scale = Math.min(1, viewport.clientWidth / image.naturalWidth, viewport.clientHeight / image.naturalHeight);\n image.style.width = `${image.naturalWidth * scale}px`;\n image.style.height = `${image.naturalHeight * scale}px`;\n status.textContent = `${Math.round(scale * 100)}% · ${image.naturalWidth} × ${image.naturalHeight}`;\n }\n function resize(factor: number): void { fitted = false; const next = scale * factor; if (Number.isFinite(next) && next > 0) scale = next; update(); }\n dialog.querySelector('[data-image-fit]')!.addEventListener('click', () => { fitted = true; update(); }, { signal });\n dialog.querySelector('[data-image-original]')!.addEventListener('click', () => { fitted = false; scale = 1; update(); }, { signal });\n dialog.querySelector('[data-image-in]')!.addEventListener('click', () => resize(1.25), { signal });\n dialog.querySelector('[data-image-out]')!.addEventListener('click', () => resize(1 / 1.25), { signal });\n dialog.querySelector('[data-image-close]')!.addEventListener('click', () => dialog.close(), { signal });\n image.addEventListener('load', update, { signal });\n const observer = new ResizeObserver(() => { if (dialog.open && fitted) update(); });\n observer.observe(viewport);\n dialog.addEventListener('close', () => {\n image.removeAttribute('src');\n image.removeAttribute('style');\n source?.focus({ preventScroll: true });\n source = null;\n }, { signal });\n document.addEventListener('keydown', (event) => {\n if (!dialog.open || event.isComposing || event.metaKey || event.ctrlKey || event.altKey) return;\n if (event.key === '+' || event.key === '=') { event.preventDefault(); resize(1.25); }\n if (event.key === '-') { event.preventDefault(); resize(1 / 1.25); }\n }, { signal });\n function close(): void { if (dialog.open) dialog.close(); }\n return {\n element: dialog,\n open(target: HTMLImageElement | ImageViewerInput): void {\n if (destroyed) return;\n const input = target instanceof HTMLImageElement\n ? target.getAttribute('src') && !target.classList.contains('pui-image-unavailable') ? { src: target.src, alt: target.alt, returnFocus: target } : null\n : target;\n if (!input?.src) return;\n source = input.returnFocus || null;\n title.textContent = input.alt || 'Image';\n image.alt = input.alt || '';\n scale = 1;\n fitted = true;\n status.textContent = 'Loading…';\n image.src = input.src;\n if (!dialog.open) dialog.showModal();\n viewport.scrollTop = viewport.scrollLeft = 0;\n update();\n },\n close,\n destroy(): void {\n if (destroyed) return;\n close();\n image.removeAttribute('src');\n image.removeAttribute('style');\n source?.focus({ preventScroll: true });\n source = null;\n destroyed = true;\n observer.disconnect();\n listeners.abort();\n dialog.replaceChildren();\n },\n };\n}\n\nexport function installImageViewer(dialog: HTMLDialogElement) {\n return createImageViewer({ dialog, headingId: dialog.getAttribute('aria-labelledby') || undefined });\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LocalContentLink {
|
|
2
|
+
path: string;
|
|
3
|
+
conversationId: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MarkdownOptions {
|
|
6
|
+
/** The host validates its own file identity and download permissions. */
|
|
7
|
+
resolveLocalLink?: (href: string) => LocalContentLink | null;
|
|
8
|
+
}
|
|
9
|
+
/** Sanitized content only: embedded images, executable markup and forms are excluded. */
|
|
10
|
+
export declare function renderMarkdown(value: string, options?: MarkdownOptions): string;
|
|
11
|
+
export declare function renderDiff(diff: string): string;
|
|
12
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/content/markdown.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE;AAC1E,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,gBAAgB,GAAG,IAAI,CAAC;CAC9D;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAqDnF;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/C"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
import { marked, Renderer } from 'marked';
|
|
3
|
+
import { escape } from '../dom.js';
|
|
4
|
+
/** Sanitized content only: embedded images, executable markup and forms are excluded. */
|
|
5
|
+
export function renderMarkdown(value, options = {}) {
|
|
6
|
+
const localLinks = new Map();
|
|
7
|
+
const renderer = new Renderer();
|
|
8
|
+
renderer.link = function (token) {
|
|
9
|
+
const local = options.resolveLocalLink?.(token.href);
|
|
10
|
+
if (local) {
|
|
11
|
+
const marker = `#pui-file-${localLinks.size}`;
|
|
12
|
+
localLinks.set(marker, local);
|
|
13
|
+
return Renderer.prototype.link.call(this, { ...token, href: marker });
|
|
14
|
+
}
|
|
15
|
+
return Renderer.prototype.link.call(this, token);
|
|
16
|
+
};
|
|
17
|
+
const html = marked.parse(value, { async: false, breaks: true, renderer });
|
|
18
|
+
const safe = DOMPurify.sanitize(html, {
|
|
19
|
+
FORBID_TAGS: ['img', 'video', 'audio', 'iframe', 'object', 'embed', 'svg', 'math', 'style', 'form', 'input', 'button'],
|
|
20
|
+
FORBID_ATTR: ['style', 'src', 'srcset'],
|
|
21
|
+
ALLOW_DATA_ATTR: false,
|
|
22
|
+
});
|
|
23
|
+
const container = document.createElement('div');
|
|
24
|
+
container.innerHTML = safe;
|
|
25
|
+
for (const link of container.querySelectorAll('a')) {
|
|
26
|
+
const href = link.getAttribute('href') ?? '';
|
|
27
|
+
const local = localLinks.get(href) || options.resolveLocalLink?.(href);
|
|
28
|
+
if (local) {
|
|
29
|
+
link.setAttribute('href', '#');
|
|
30
|
+
link.dataset.downloadPath = local.path;
|
|
31
|
+
link.dataset.downloadConversation = local.conversationId;
|
|
32
|
+
link.title = `Save file: ${local.path}`;
|
|
33
|
+
}
|
|
34
|
+
else if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) {
|
|
35
|
+
link.setAttribute('target', '_blank');
|
|
36
|
+
link.setAttribute('rel', 'noopener noreferrer');
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
link.replaceWith(...link.childNodes);
|
|
40
|
+
}
|
|
41
|
+
for (const code of container.querySelectorAll('pre > code')) {
|
|
42
|
+
const pre = code.parentElement;
|
|
43
|
+
const block = document.createElement('div');
|
|
44
|
+
block.className = 'pui-code-block';
|
|
45
|
+
const toolbar = document.createElement('div');
|
|
46
|
+
toolbar.className = 'pui-code-toolbar';
|
|
47
|
+
toolbar.dataset.findIgnore = '';
|
|
48
|
+
const language = document.createElement('span');
|
|
49
|
+
language.textContent = [...code.classList].find((name) => name.startsWith('language-'))?.slice(9) || 'Code';
|
|
50
|
+
const copy = document.createElement('button');
|
|
51
|
+
copy.type = 'button';
|
|
52
|
+
copy.className = 'pui-secondary-button pui-code-copy';
|
|
53
|
+
copy.dataset.copyCode = '';
|
|
54
|
+
copy.textContent = 'Copy';
|
|
55
|
+
copy.setAttribute('aria-label', 'Copy code');
|
|
56
|
+
toolbar.append(language, copy);
|
|
57
|
+
pre.replaceWith(block);
|
|
58
|
+
block.append(toolbar, pre);
|
|
59
|
+
}
|
|
60
|
+
return container.innerHTML;
|
|
61
|
+
}
|
|
62
|
+
export function renderDiff(diff) {
|
|
63
|
+
return diff.split('\n').map((line) => {
|
|
64
|
+
const kind = line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@') ? 'meta' : line.startsWith('+') ? 'add' : line.startsWith('-') ? 'remove' : 'context';
|
|
65
|
+
return `<span class="pui-diff-line pui-diff-${kind}">${escape(line) || ' '}</span>`;
|
|
66
|
+
}).join('');
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/content/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["import DOMPurify from 'dompurify';\nimport { marked, Renderer } from 'marked';\nimport { escape } from '../dom.js';\n\nexport interface LocalContentLink { path: string; conversationId: string }\nexport interface MarkdownOptions {\n /** The host validates its own file identity and download permissions. */\n resolveLocalLink?: (href: string) => LocalContentLink | null;\n}\n\n/** Sanitized content only: embedded images, executable markup and forms are excluded. */\nexport function renderMarkdown(value: string, options: MarkdownOptions = {}): string {\n const localLinks = new Map<string, LocalContentLink>();\n const renderer = new Renderer();\n renderer.link = function(token) {\n const local = options.resolveLocalLink?.(token.href);\n if (local) {\n const marker = `#pui-file-${localLinks.size}`;\n localLinks.set(marker, local);\n return Renderer.prototype.link.call(this, { ...token, href: marker });\n }\n return Renderer.prototype.link.call(this, token);\n };\n const html = marked.parse(value, { async: false, breaks: true, renderer }) as string;\n const safe = DOMPurify.sanitize(html, {\n FORBID_TAGS: ['img', 'video', 'audio', 'iframe', 'object', 'embed', 'svg', 'math', 'style', 'form', 'input', 'button'],\n FORBID_ATTR: ['style', 'src', 'srcset'],\n ALLOW_DATA_ATTR: false,\n });\n const container = document.createElement('div');\n container.innerHTML = safe;\n for (const link of container.querySelectorAll('a')) {\n const href = link.getAttribute('href') ?? '';\n const local = localLinks.get(href) || options.resolveLocalLink?.(href);\n if (local) {\n link.setAttribute('href', '#');\n link.dataset.downloadPath = local.path;\n link.dataset.downloadConversation = local.conversationId;\n link.title = `Save file: ${local.path}`;\n } else if (/^https?:\\/\\//i.test(href) || /^mailto:/i.test(href)) {\n link.setAttribute('target', '_blank');\n link.setAttribute('rel', 'noopener noreferrer');\n } else link.replaceWith(...link.childNodes);\n }\n for (const code of container.querySelectorAll('pre > code')) {\n const pre = code.parentElement!;\n const block = document.createElement('div');\n block.className = 'pui-code-block';\n const toolbar = document.createElement('div');\n toolbar.className = 'pui-code-toolbar';\n toolbar.dataset.findIgnore = '';\n const language = document.createElement('span');\n language.textContent = [...code.classList].find((name) => name.startsWith('language-'))?.slice(9) || 'Code';\n const copy = document.createElement('button');\n copy.type = 'button';\n copy.className = 'pui-secondary-button pui-code-copy';\n copy.dataset.copyCode = '';\n copy.textContent = 'Copy';\n copy.setAttribute('aria-label', 'Copy code');\n toolbar.append(language, copy);\n pre.replaceWith(block);\n block.append(toolbar, pre);\n }\n return container.innerHTML;\n}\n\nexport function renderDiff(diff: string): string {\n return diff.split('\\n').map((line) => {\n const kind = line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@') ? 'meta' : line.startsWith('+') ? 'add' : line.startsWith('-') ? 'remove' : 'context';\n return `<span class=\"pui-diff-line pui-diff-${kind}\">${escape(line) || ' '}</span>`;\n }).join('');\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UIBlock } from '../core.js';
|
|
2
|
+
export declare const previewPageBytes: number;
|
|
3
|
+
export declare function previewPageRanges(bytes: Uint8Array, window?: number): Array<[number, number]>;
|
|
4
|
+
export type PreviewKind = 'text' | 'markdown' | 'diff';
|
|
5
|
+
export interface PagedPreviewState {
|
|
6
|
+
text: string;
|
|
7
|
+
kind: PreviewKind;
|
|
8
|
+
}
|
|
9
|
+
export interface PagedPreviewOptions extends PagedPreviewState {
|
|
10
|
+
container?: HTMLElement;
|
|
11
|
+
onCopyCode?: (text: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function createPagedPreview(options: PagedPreviewOptions): UIBlock<PagedPreviewState>;
|
|
14
|
+
export declare function mountPagedPreview(container: HTMLElement, text: string, kind: PreviewKind): void;
|
|
15
|
+
//# sourceMappingURL=paged-preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paged-preview.d.ts","sourceRoot":"","sources":["../../src/content/paged-preview.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,eAAO,MAAM,gBAAgB,QAAa,CAAC;AAE3C,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,SAAmB,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAgBvG;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;AACvD,MAAM,WAAW,iBAAiB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE;AACtE,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAAG,SAAS,CAAC,EAAE,WAAW,CAAC;IAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE;AAE/H,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0B3F;AAGD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAG/F"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { renderDiff, renderMarkdown } from './markdown.js';
|
|
2
|
+
// A rendering window, not a file acceptance cap. Large Markdown is shown as
|
|
3
|
+
// original text so page boundaries cannot change the meaning of fenced code.
|
|
4
|
+
export const previewPageBytes = 256 * 1024;
|
|
5
|
+
export function previewPageRanges(bytes, window = previewPageBytes) {
|
|
6
|
+
if (window < 4)
|
|
7
|
+
throw new RangeError('A preview window must fit a UTF-8 character');
|
|
8
|
+
const ranges = [];
|
|
9
|
+
for (let start = 0; start < bytes.length;) {
|
|
10
|
+
let end = Math.min(start + window, bytes.length);
|
|
11
|
+
while (end < bytes.length && (bytes[end] & 0xc0) === 0x80)
|
|
12
|
+
end--;
|
|
13
|
+
if (end < bytes.length) {
|
|
14
|
+
// Prefer complete lines without turning a long line into tiny pages.
|
|
15
|
+
for (let i = end - 1; i >= start + window / 2 - 1; i--) {
|
|
16
|
+
if (bytes[i] === 10) {
|
|
17
|
+
end = i + 1;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
ranges.push([start, end]);
|
|
23
|
+
start = end;
|
|
24
|
+
}
|
|
25
|
+
return ranges.length ? ranges : [[0, 0]];
|
|
26
|
+
}
|
|
27
|
+
export function createPagedPreview(options) {
|
|
28
|
+
const element = options.container || document.createElement('div');
|
|
29
|
+
let current = { text: options.text, kind: options.kind };
|
|
30
|
+
let disposed = false;
|
|
31
|
+
let dispose = renderPreview(element, current.text, current.kind);
|
|
32
|
+
const click = (event) => {
|
|
33
|
+
const button = event.target.closest('[data-copy-code]');
|
|
34
|
+
if (button && !button.disabled && element.contains(button))
|
|
35
|
+
options.onCopyCode?.(button.closest('.pui-code-block')?.querySelector('code')?.textContent || '');
|
|
36
|
+
};
|
|
37
|
+
if (options.onCopyCode)
|
|
38
|
+
element.addEventListener('click', click);
|
|
39
|
+
return {
|
|
40
|
+
element,
|
|
41
|
+
update(state) {
|
|
42
|
+
if (disposed || (state.text === current.text && state.kind === current.kind))
|
|
43
|
+
return;
|
|
44
|
+
dispose();
|
|
45
|
+
current = { ...state };
|
|
46
|
+
dispose = renderPreview(element, state.text, state.kind);
|
|
47
|
+
},
|
|
48
|
+
destroy() {
|
|
49
|
+
if (disposed)
|
|
50
|
+
return;
|
|
51
|
+
disposed = true;
|
|
52
|
+
dispose();
|
|
53
|
+
element.removeEventListener('click', click);
|
|
54
|
+
element.replaceChildren();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const mounted = new WeakMap();
|
|
59
|
+
export function mountPagedPreview(container, text, kind) {
|
|
60
|
+
mounted.get(container)?.destroy();
|
|
61
|
+
mounted.set(container, createPagedPreview({ container, text, kind }));
|
|
62
|
+
}
|
|
63
|
+
function renderPreview(container, text, kind) {
|
|
64
|
+
const bytes = new TextEncoder().encode(text);
|
|
65
|
+
const ranges = previewPageRanges(bytes);
|
|
66
|
+
const content = document.createElement(kind === 'markdown' && ranges.length === 1 ? 'div' : 'pre');
|
|
67
|
+
content.className = kind === 'diff' ? 'pui-approval-diff' : content.tagName === 'DIV' ? 'pui-markdown' : 'pui-file-preview-text';
|
|
68
|
+
if (kind === 'diff')
|
|
69
|
+
content.setAttribute('aria-label', 'Proposed file changes');
|
|
70
|
+
const toolbar = document.createElement('div');
|
|
71
|
+
toolbar.className = 'pui-feature-toolbar pui-preview-pagination';
|
|
72
|
+
const previous = document.createElement('button');
|
|
73
|
+
const next = document.createElement('button');
|
|
74
|
+
const status = document.createElement('span');
|
|
75
|
+
status.setAttribute('aria-live', 'polite');
|
|
76
|
+
for (const button of [previous, next]) {
|
|
77
|
+
button.type = 'button';
|
|
78
|
+
button.className = 'pui-secondary-button';
|
|
79
|
+
}
|
|
80
|
+
previous.textContent = 'Previous';
|
|
81
|
+
next.textContent = 'Next';
|
|
82
|
+
toolbar.append(previous, status, next);
|
|
83
|
+
let page = 0;
|
|
84
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
85
|
+
const render = () => {
|
|
86
|
+
const [start, end] = ranges[page];
|
|
87
|
+
const value = decoder.decode(bytes.subarray(start, end));
|
|
88
|
+
if (kind === 'markdown' && ranges.length === 1)
|
|
89
|
+
content.innerHTML = renderMarkdown(value);
|
|
90
|
+
else if (kind === 'diff')
|
|
91
|
+
content.innerHTML = renderDiff(value);
|
|
92
|
+
else
|
|
93
|
+
content.textContent = value;
|
|
94
|
+
previous.disabled = page === 0;
|
|
95
|
+
next.disabled = page === ranges.length - 1;
|
|
96
|
+
status.textContent = `${page + 1} / ${ranges.length}${kind === 'markdown' && ranges.length > 1 ? ' · Plain text preview' : ''}`;
|
|
97
|
+
content.scrollTop = 0;
|
|
98
|
+
};
|
|
99
|
+
previous.onclick = () => { page--; render(); };
|
|
100
|
+
next.onclick = () => { page++; render(); };
|
|
101
|
+
container.replaceChildren(...(ranges.length > 1 ? [toolbar, content] : [content]));
|
|
102
|
+
render();
|
|
103
|
+
return () => { previous.onclick = null; next.onclick = null; };
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=paged-preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paged-preview.js","sourceRoot":"","sources":["../../src/content/paged-preview.ts"],"names":[],"mappings":"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","sourcesContent":["import { renderDiff, renderMarkdown } from './markdown.js';\nimport type { UIBlock } from '../core.js';\n\n// A rendering window, not a file acceptance cap. Large Markdown is shown as\n// original text so page boundaries cannot change the meaning of fenced code.\nexport const previewPageBytes = 256 * 1024;\n\nexport function previewPageRanges(bytes: Uint8Array, window = previewPageBytes): Array<[number, number]> {\n if (window < 4) throw new RangeError('A preview window must fit a UTF-8 character');\n const ranges: Array<[number, number]> = [];\n for (let start = 0; start < bytes.length;) {\n let end = Math.min(start + window, bytes.length);\n while (end < bytes.length && (bytes[end] & 0xc0) === 0x80) end--;\n if (end < bytes.length) {\n // Prefer complete lines without turning a long line into tiny pages.\n for (let i = end - 1; i >= start + window / 2 - 1; i--) {\n if (bytes[i] === 10) { end = i + 1; break; }\n }\n }\n ranges.push([start, end]);\n start = end;\n }\n return ranges.length ? ranges : [[0, 0]];\n}\n\nexport type PreviewKind = 'text' | 'markdown' | 'diff';\nexport interface PagedPreviewState { text: string; kind: PreviewKind }\nexport interface PagedPreviewOptions extends PagedPreviewState { container?: HTMLElement; onCopyCode?: (text: string) => void }\n\nexport function createPagedPreview(options: PagedPreviewOptions): UIBlock<PagedPreviewState> {\n const element = options.container || document.createElement('div');\n let current = { text: options.text, kind: options.kind };\n let disposed = false;\n let dispose = renderPreview(element, current.text, current.kind);\n const click = (event: Event) => {\n const button = (event.target as Element).closest<HTMLButtonElement>('[data-copy-code]');\n if (button && !button.disabled && element.contains(button)) options.onCopyCode?.(button.closest('.pui-code-block')?.querySelector('code')?.textContent || '');\n };\n if (options.onCopyCode) element.addEventListener('click', click);\n return {\n element,\n update(state): void {\n if (disposed || (state.text === current.text && state.kind === current.kind)) return;\n dispose();\n current = { ...state };\n dispose = renderPreview(element, state.text, state.kind);\n },\n destroy(): void {\n if (disposed) return;\n disposed = true;\n dispose();\n element.removeEventListener('click', click);\n element.replaceChildren();\n },\n };\n}\n\nconst mounted = new WeakMap<HTMLElement, UIBlock<PagedPreviewState>>();\nexport function mountPagedPreview(container: HTMLElement, text: string, kind: PreviewKind): void {\n mounted.get(container)?.destroy();\n mounted.set(container, createPagedPreview({ container, text, kind }));\n}\n\nfunction renderPreview(container: HTMLElement, text: string, kind: PreviewKind): () => void {\n const bytes = new TextEncoder().encode(text);\n const ranges = previewPageRanges(bytes);\n const content = document.createElement(kind === 'markdown' && ranges.length === 1 ? 'div' : 'pre');\n content.className = kind === 'diff' ? 'pui-approval-diff' : content.tagName === 'DIV' ? 'pui-markdown' : 'pui-file-preview-text';\n if (kind === 'diff') content.setAttribute('aria-label', 'Proposed file changes');\n const toolbar = document.createElement('div');\n toolbar.className = 'pui-feature-toolbar pui-preview-pagination';\n const previous = document.createElement('button');\n const next = document.createElement('button');\n const status = document.createElement('span');\n status.setAttribute('aria-live', 'polite');\n for (const button of [previous, next]) { button.type = 'button'; button.className = 'pui-secondary-button'; }\n previous.textContent = 'Previous'; next.textContent = 'Next';\n toolbar.append(previous, status, next);\n let page = 0;\n const decoder = new TextDecoder('utf-8', { fatal: true });\n const render = () => {\n const [start, end] = ranges[page];\n const value = decoder.decode(bytes.subarray(start, end));\n if (kind === 'markdown' && ranges.length === 1) content.innerHTML = renderMarkdown(value);\n else if (kind === 'diff') content.innerHTML = renderDiff(value);\n else content.textContent = value;\n previous.disabled = page === 0; next.disabled = page === ranges.length - 1;\n status.textContent = `${page + 1} / ${ranges.length}${kind === 'markdown' && ranges.length > 1 ? ' · Plain text preview' : ''}`;\n content.scrollTop = 0;\n };\n previous.onclick = () => { page--; render(); };\n next.onclick = () => { page++; render(); };\n container.replaceChildren(...(ranges.length > 1 ? [toolbar, content] : [content]));\n render();\n return () => { previous.onclick = null; next.onclick = null; };\n}\n"]}
|