@thomas.f/prnext 0.1.16-alpha → 0.1.17-alpha
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 +63 -0
- package/README.md +384 -9
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +5 -0
- package/build/analyze.mjs +184 -0
- package/build/app-config.mjs +7 -8
- package/build/app-static.mjs +12 -7
- package/build/app.mjs +23 -10
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +20 -5
- package/build/check.mjs +56 -11
- package/build/compiler.mjs +25 -1
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +21 -13
- package/build/css-modules.mjs +15 -4
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +83 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +55 -4
- package/build/edge.mjs +3 -2
- package/build/framework-imports.mjs +40 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +96 -101
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +2 -3
- package/build/migrate.mjs +60 -57
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +97 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-pages.mjs +90 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +80 -0
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +15 -3
- package/build/transform-cache.mjs +37 -4
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +91 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +22 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +5 -4
- package/build/webpack-graph.mjs +164 -27
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +105 -121
- package/compat/index.d.ts +21 -7
- package/compat/link.d.cts +4 -3
- package/compat/navigation.d.cts +4 -3
- package/compat/router.d.cts +4 -3
- package/compat/typed-route.d.cts +17 -0
- package/docs/ai/compatibility.md +106 -0
- package/docs/ai/data.md +128 -0
- package/docs/ai/deployment.md +111 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +146 -0
- package/docs/ai/routing.md +132 -0
- package/docs/ai/troubleshooting.md +120 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +25 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +17 -6
- package/runtime/app-render.mjs +12 -0
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +239 -45
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +186 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +18 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +307 -0
- package/runtime/env.mjs +2 -1
- package/runtime/pages-client.mjs +10 -1
- package/runtime/pages-navigation.mjs +6 -1
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/rsc-worker.mjs +7 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +12 -5
- package/runtime/worker.mjs +36 -2
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { createElementPicker } from './dev-element-picker.mjs';
|
|
2
|
+
import { durationLabel, materializeRoute, routeParameters, formatMcpConfig } from './dev-tools-model.mjs';
|
|
3
|
+
import { svg } from './dev-ui-icons.mjs';
|
|
4
|
+
import { workbenchStyle } from './dev-workbench-style.mjs';
|
|
5
|
+
import { devAiStyle } from './dev-ai-style.mjs';
|
|
6
|
+
import { devAiBrands, devMcpBrand } from './dev-ai-brands.mjs';
|
|
7
|
+
|
|
8
|
+
const headings = { requests: 'Requests', performance: 'Performance', routes: 'Routes & cache', ai: 'AI connection' };
|
|
9
|
+
const mcpGuides = {
|
|
10
|
+
codex: { text: 'Merge this TOML into .codex/config.toml in a trusted project, or ~/.codex/config.toml for all projects. Restart your Codex session after saving.', url: 'https://learn.chatgpt.com/docs/extend/mcp?surface=cli' },
|
|
11
|
+
cursor: { text: 'Merge this JSON into .cursor/mcp.json at your project root, then enable the server in Cursor’s MCP settings.', url: 'https://cursor.com/docs/mcp' },
|
|
12
|
+
claude: { text: 'In Claude Desktop, open Settings → Developer → Edit Config. Merge this JSON into claude_desktop_config.json, then fully quit and reopen Claude Desktop.', url: 'https://modelcontextprotocol.io/docs/develop/connect-local-servers' },
|
|
13
|
+
other: { text: 'Add a local stdio MCP server using this command and its arguments. If your client supports mcpServers JSON, merge this entry into its configuration.', url: 'https://modelcontextprotocol.io/docs/develop/connect-local-servers' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createDevToolsPanel({ shadow, tools, restoreFocus }) {
|
|
17
|
+
const document = shadow.ownerDocument, view = document.defaultView;
|
|
18
|
+
const node = (tag, className, text) => { const result = document.createElement(tag); if (className) result.className = className; if (text !== undefined) result.textContent = text; return result; };
|
|
19
|
+
const button = (label, onClick, className = '') => { const result = node('button', className, label); result.type = 'button'; result.onclick = onClick; return result; };
|
|
20
|
+
const dialog = node('dialog', 'wb wb-tools'); dialog.setAttribute('aria-label', 'Development inspector');
|
|
21
|
+
const style = node('style'); style.textContent = workbenchStyle + devAiStyle; shadow.append(style);
|
|
22
|
+
const header = node('div', 'wb-head'), title = node('strong', 'wb-title'), status = node('span', 'wb-context');
|
|
23
|
+
const refresh = button('Refresh', () => load(true), 'wb-push');
|
|
24
|
+
const close = button('', () => dialog.close(), 'wb-close'); close.innerHTML = svg('<path d="m6 6 12 12M6 18 18 6"/>'); close.setAttribute('aria-label', 'Close inspector');
|
|
25
|
+
header.append(title, status, refresh, close);
|
|
26
|
+
const nav = node('nav', 'wb-tools-nav'); nav.setAttribute('aria-label', 'Development panels');
|
|
27
|
+
for (const [key, label] of Object.entries(headings)) { const item = button(label, () => open(key)); item.dataset.panel = key; nav.append(item); }
|
|
28
|
+
const filter = node('input', 'wb-tools-search'); filter.type = 'search'; filter.placeholder = 'Filter requests…'; filter.setAttribute('aria-label', 'Filter requests');
|
|
29
|
+
const content = node('div', 'wb-tools-content');
|
|
30
|
+
const feedback = node('div', 'wb-tools-feedback'); feedback.setAttribute('role', 'status');
|
|
31
|
+
dialog.append(header, nav, filter, content, feedback); shadow.append(dialog);
|
|
32
|
+
let active = 'requests', stop, unsubscribe, disposed = false, busy = false, context, picker, disposeAiView, aiViewId=0;
|
|
33
|
+
let selectedRoute, routeValues = {}, connection, refreshing = false, lastSignature, instruction = '', mcpClient = 'codex', syncAiView, pickEpoch = 0;
|
|
34
|
+
const query = () => filter.value.toLocaleLowerCase();
|
|
35
|
+
const assistantConnected = (data=tools.snapshot()) => data.connection==='connected'&&data.ai?.connected===true;
|
|
36
|
+
function setFeedback(message) { feedback.textContent = message || ''; }
|
|
37
|
+
function disclosure(trigger, reveal, inner, details) {
|
|
38
|
+
let expanded=false,timer;
|
|
39
|
+
const finish=()=>{view.clearTimeout(timer);if(details&&!expanded)details.open=false;reveal.style.height=expanded?'auto':'0px';};
|
|
40
|
+
const settle=()=>{view.clearTimeout(timer);timer=view.setTimeout(finish,260);};
|
|
41
|
+
function set(open,{animate=true}={}) {
|
|
42
|
+
open=Boolean(open);if(open===expanded&&(!details||details.open===open))return;
|
|
43
|
+
const height=reveal.getBoundingClientRect().height;
|
|
44
|
+
expanded=open;if(details){details.open=true;details.dataset.expanded=String(open);}
|
|
45
|
+
if(!open&&reveal.contains(shadow.activeElement))trigger.focus({preventScroll:true});
|
|
46
|
+
reveal.inert=!open;reveal.setAttribute('aria-hidden',String(!open));trigger.setAttribute('aria-expanded',String(open));
|
|
47
|
+
const immediate=!animate||view.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
48
|
+
if(immediate)reveal.style.transition='none';
|
|
49
|
+
reveal.style.height=height+'px';void reveal.offsetHeight;
|
|
50
|
+
reveal.dataset.expanded=String(open);reveal.style.height=(open?inner.getBoundingClientRect().height:0)+'px';
|
|
51
|
+
if(immediate){finish();void reveal.offsetHeight;reveal.style.removeProperty('transition');}else settle();
|
|
52
|
+
}
|
|
53
|
+
const toggle=event=>{event.preventDefault();set(!expanded);};
|
|
54
|
+
const complete=event=>{if(event.target===reveal&&event.propertyName==='height'&&Math.abs(reveal.getBoundingClientRect().height-(expanded?inner.getBoundingClientRect().height:0))<1)finish();};
|
|
55
|
+
const observer=view.ResizeObserver?new view.ResizeObserver(()=>{if(expanded&&reveal.style.height!=='auto'){reveal.style.height=inner.getBoundingClientRect().height+'px';settle();}}):undefined;
|
|
56
|
+
reveal.style.height='0px';reveal.dataset.expanded='false';reveal.inert=true;reveal.setAttribute('aria-hidden','true');trigger.setAttribute('aria-expanded','false');
|
|
57
|
+
if(details){details.dataset.expanded='false';details.prnextExpand=set;}
|
|
58
|
+
trigger.addEventListener('click',toggle);reveal.addEventListener('transitionend',complete);observer?.observe(inner);
|
|
59
|
+
return {set,dispose(){view.clearTimeout(timer);observer?.disconnect();trigger.removeEventListener('click',toggle);reveal.removeEventListener('transitionend',complete);}};
|
|
60
|
+
}
|
|
61
|
+
async function action(control, task, success) {
|
|
62
|
+
if (busy) return; busy = true; control.disabled = true; control.setAttribute('aria-busy', 'true'); setFeedback('');if(active==='ai')syncAiView?.(tools.snapshot());
|
|
63
|
+
try { const result = await task(); if (success) setFeedback(typeof success === 'function' ? success(result) : success); return result; }
|
|
64
|
+
catch (error) { setFeedback(error.message || 'The action failed.'); }
|
|
65
|
+
finally { busy = false; control.disabled = false; control.removeAttribute('aria-busy');if(active==='ai'&&!disposed)syncAiView?.(tools.snapshot()); }
|
|
66
|
+
}
|
|
67
|
+
async function openEditor(args) {
|
|
68
|
+
const result = await tools.call('dev_open_editor', args);
|
|
69
|
+
if (!/^(?:vscode|cursor):\/\//.test(result.editorUrl || '')) throw Error('The editor link is unavailable.');
|
|
70
|
+
const link = node('a'); link.href = result.editorUrl; link.click(); return result;
|
|
71
|
+
}
|
|
72
|
+
function empty(text) { return node('p', 'wb-empty', text); }
|
|
73
|
+
function meta(text) { return node('span', 'wb-muted', text); }
|
|
74
|
+
function pair(label, value) { const row = node('div', 'wb-tools-pair'); row.append(meta(label), node('span', '', value)); return row; }
|
|
75
|
+
function section(label) { const section = node('section', 'wb-tools-section'); section.append(node('h3', '', label)); return section; }
|
|
76
|
+
function tableHead(labels) { const row = node('div', 'wb-request-grid wb-tools-labels'); for (const label of labels) row.append(meta(label)); return row; }
|
|
77
|
+
function requests(data) {
|
|
78
|
+
const records = [...(data.requests || []).filter(entry => entry.source === 'server' || entry.clientId !== data.clientId).map(entry => ({ ...entry, source: entry.source || 'browser' })), ...(data.localRequests || [])]
|
|
79
|
+
.filter(entry => !query() || [entry.method, entry.path, entry.status, entry.source].join(' ').toLocaleLowerCase().includes(query()))
|
|
80
|
+
.sort((a, b) => (b.time || 0) - (a.time || 0)).slice(0, 300);
|
|
81
|
+
const body = node('div'); body.append(tableHead(['Request', 'Status', 'Time', 'Source']));
|
|
82
|
+
for (const record of records) {
|
|
83
|
+
const details = node('details', 'wb-request'), summary = node('summary', 'wb-request-grid'); details.dataset.key = 'request:' + record.source + ':' + record.id;
|
|
84
|
+
const path = node('span', 'wb-request-path'); path.append(node('b', '', record.method || 'GET'), document.createTextNode(' ' + (record.path || '/')));
|
|
85
|
+
const statusText = record.status == null ? record.error || 'Failed' : String(record.status);
|
|
86
|
+
summary.append(path, node('span', record.failed || record.status >= 400 ? 'wb-error-text' : '', statusText), meta(durationLabel(record.duration)), meta(record.source));
|
|
87
|
+
const info = node('div', 'wb-request-info');
|
|
88
|
+
info.append(pair('Started', record.time ? new Date(record.time).toLocaleTimeString() : 'Not recorded'), pair('Kind', record.kind || record.type || 'HTTP'), pair('Duration', durationLabel(record.duration)));
|
|
89
|
+
if (record.id) info.append(pair('Request ID', String(record.id)));
|
|
90
|
+
if (record.error) info.append(node('pre', '', String(record.error)));
|
|
91
|
+
info.append(meta('Headers, request bodies and URL query values are not recorded.'));
|
|
92
|
+
const related = (data.serverLogs || []).filter(entry => record.requestId && entry.requestId === record.requestId);
|
|
93
|
+
if (related.length) info.append(node('pre', '', related.map(entry => entry.text || entry.message).join('\n')));
|
|
94
|
+
details.append(summary, info); body.append(details);
|
|
95
|
+
}
|
|
96
|
+
if (!records.length) body.append(empty(query() ? 'No matching requests.' : 'No requests recorded yet. Use the page to see fetch, XHR and server requests.'));
|
|
97
|
+
return body;
|
|
98
|
+
}
|
|
99
|
+
function metric(label, value, caption) { const metric = node('div', 'wb-metric'); metric.append(meta(label), node('strong', '', durationLabel(value))); if (caption) metric.append(meta(caption)); return metric; }
|
|
100
|
+
function performance(data) {
|
|
101
|
+
const body = node('div'), metrics = node('div', 'wb-metrics');
|
|
102
|
+
const navigation = view.performance.getEntriesByType('navigation')[0];
|
|
103
|
+
const builds = (data.builds || []).slice().reverse(), latest = builds[0];
|
|
104
|
+
metrics.append(metric('Latest build', latest?.duration), metric('Page response', navigation ? navigation.responseStart - navigation.requestStart : undefined, 'Time to first byte'),
|
|
105
|
+
metric('DOM ready', navigation?.domContentLoadedEventEnd || undefined));
|
|
106
|
+
body.append(metrics);
|
|
107
|
+
const build = section('Build timeline');
|
|
108
|
+
if (!builds.length) build.append(empty('Build timings appear after the next compilation.'));
|
|
109
|
+
for (const entry of builds.slice(0, 6)) {
|
|
110
|
+
const details = node('details', 'wb-build'); details.dataset.key = 'build:' + (entry.buildId || entry.startedAt);details.open = entry === latest;
|
|
111
|
+
const summary = node('summary'); summary.append(node('span', '', entry.reason || entry.kind || 'Compilation'), meta(durationLabel(entry.duration)));
|
|
112
|
+
details.append(summary);
|
|
113
|
+
for (const [name, duration] of Object.entries(entry.phases || {})) {
|
|
114
|
+
if (typeof duration !== 'number') continue;
|
|
115
|
+
const phase = node('div', 'wb-phase'), meter = node('span', 'wb-phase-meter'); meter.style.setProperty('--amount', Math.min(100, entry.duration ? duration / entry.duration * 100 : 0) + '%');
|
|
116
|
+
phase.append(node('span', '', name), meter, meta(durationLabel(duration))); details.append(phase);
|
|
117
|
+
}
|
|
118
|
+
if (!Object.keys(entry.phases || {}).length) details.append(meta('This build has no phase measurements.'));
|
|
119
|
+
build.append(details);
|
|
120
|
+
}
|
|
121
|
+
body.append(build);
|
|
122
|
+
const network = section('Slowest resource loads');
|
|
123
|
+
const resources = view.performance.getEntriesByType('resource').filter(entry => !/\/_prnext\/(?:dev|ensure)/.test(entry.name) && (!data.available || !entry.name.startsWith(tools.endpoint || 'http://invalid.invalid')))
|
|
124
|
+
.sort((a, b) => b.duration - a.duration).slice(0, 8);
|
|
125
|
+
for (const resource of resources) { const row = node('div', 'wb-tools-pair'); let path; try { path = new URL(resource.name).pathname; } catch { path = resource.name; } row.append(node('span', 'wb-truncate', path), meta(durationLabel(resource.duration))); network.append(row); }
|
|
126
|
+
if (!resources.length) network.append(empty('No resource timings recorded.'));
|
|
127
|
+
network.append(meta('Browser resource duration includes loading. Server render time is shown only when recorded by the runtime.'));
|
|
128
|
+
body.append(network);
|
|
129
|
+
const runtime = section('Runtime requests');
|
|
130
|
+
const observed = (data.requests || []).filter(entry=>entry.source==='server').slice().sort((a,b)=>(b.duration||0)-(a.duration||0)).slice(0,8);
|
|
131
|
+
for(const entry of observed) { const row=node('div','wb-tools-pair');row.append(node('span','wb-truncate',(entry.kind==='fetch'?'Server fetch · ':'HTTP · ')+(entry.path||'/')),meta(durationLabel(entry.duration)));runtime.append(row); }
|
|
132
|
+
if(!observed.length)runtime.append(empty('No runtime request timings recorded yet.'));
|
|
133
|
+
runtime.append(meta('Measured until response headers are ready; streamed body completion is not included.'));body.append(runtime);return body;
|
|
134
|
+
}
|
|
135
|
+
function routes(data) {
|
|
136
|
+
const body = node('div'), records = (data.routes || []).filter(route => !query() || [route.path, route.kind, route.source].join(' ').toLocaleLowerCase().includes(query()));
|
|
137
|
+
const cache = data.cache || {}, cacheSummary = node('div', 'wb-cache-summary');
|
|
138
|
+
for (const [key, value] of Object.entries(cache)) if (['string', 'number', 'boolean'].includes(typeof value)) cacheSummary.append(pair(key.replace(/([A-Z])/g, ' $1'), String(value)));
|
|
139
|
+
if (cacheSummary.childElementCount) body.append(cacheSummary);
|
|
140
|
+
for (const route of records) {
|
|
141
|
+
const details = node('details', 'wb-route'); details.dataset.key = 'route:' + route.path;details.open = selectedRoute === route.path;
|
|
142
|
+
const summary = node('summary'); summary.append(node('code', '', route.path), meta(route.kind || 'route'), meta(route.compiled ? 'Compiled' : 'On demand'));
|
|
143
|
+
details.addEventListener('toggle', () => { if (details.open) selectedRoute = route.path; else if (selectedRoute === route.path) selectedRoute = undefined; });
|
|
144
|
+
const inner = node('div', 'wb-route-actions'), values = routeValues[route.path] ||= {};
|
|
145
|
+
for (const parameter of routeParameters(route.path)) {
|
|
146
|
+
const label = node('label', 'wb-route-param', parameter.name + (parameter.optional ? ' (optional)' : ''));
|
|
147
|
+
const input = node('input'); input.value = values[parameter.name] || ''; input.placeholder = parameter.catchAll ? 'one/two' : parameter.name; input.autocomplete = 'off';
|
|
148
|
+
input.addEventListener('input', () => { values[parameter.name] = input.value; }); label.append(input); inner.append(label);
|
|
149
|
+
}
|
|
150
|
+
const actions = node('div', 'wb-button-row');
|
|
151
|
+
const open = button('Open route', () => action(open, async () => { const path = materializeRoute(route.path, values); await tools.call('dev_route_ensure', { path }); view.location.assign(path); }));
|
|
152
|
+
const invalidate = button('Clear route cache', () => action(invalidate, async () => {
|
|
153
|
+
const path = materializeRoute(route.path, values), result = await tools.call('dev_cache_invalidate', { path }); await tools.refresh(); return result;
|
|
154
|
+
}, result => result?.invalidated === true ? 'Development cache invalidated for this route.' : result?.reason || 'No active development cache was invalidated.'));
|
|
155
|
+
actions.append(open, invalidate);
|
|
156
|
+
if (route.source) {
|
|
157
|
+
const editor = button('Open source', () => action(editor, () => openEditor({ file: route.source, line: 1, column: 1 }), 'Editor link requested.')); actions.append(editor);
|
|
158
|
+
}
|
|
159
|
+
inner.append(actions); if (route.source) inner.append(meta(route.source)); details.append(summary, inner); body.append(details);
|
|
160
|
+
}
|
|
161
|
+
if (!records.length) body.append(empty(query() ? 'No matching routes.' : data.available ? 'No routes reported yet.' : 'Start PRNext dev to browse routes.'));
|
|
162
|
+
return body;
|
|
163
|
+
}
|
|
164
|
+
const drawings = {
|
|
165
|
+
target:'<path d="M8 3H5a2 2 0 0 0-2 2v3m13-5h3a2 2 0 0 1 2 2v3M3 16v3a2 2 0 0 0 2 2h3m8 0h3a2 2 0 0 0 2-2v-3"/><path d="m9 8 6 4-3 1-1 3-2-8Z"/>',
|
|
166
|
+
copy:'<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V4H4v12h4"/>',
|
|
167
|
+
arrow:'<path d="M5 12h14m-5-5 5 5-5 5"/>',
|
|
168
|
+
check:'<path d="m5 12 4 4L19 6"/>',
|
|
169
|
+
plug:'<path d="M9 3v5m6-5v5M7 8h10v3a5 5 0 0 1-10 0V8Zm5 8v5"/>',
|
|
170
|
+
};
|
|
171
|
+
function mark(paths, className='') {const result=node('span',className);result.innerHTML=svg(paths);return result;}
|
|
172
|
+
async function copyText(text) {
|
|
173
|
+
if (!view.navigator.clipboard?.writeText) throw Error('Clipboard unavailable. Select and copy the text below.');
|
|
174
|
+
await view.navigator.clipboard.writeText(text);
|
|
175
|
+
}
|
|
176
|
+
function selectionView(initial) {
|
|
177
|
+
const selection=node('section','wb-component');selection.setAttribute('aria-label',initial.kind==='error'?'Error context':'Selected element');
|
|
178
|
+
const heading=node('div','wb-component-head'), identity=node('div','wb-component-identity'), name=node('h3'), kind=node('code','wb-component-kind');
|
|
179
|
+
const labels=node('div');labels.append(name,kind);identity.append(mark(drawings.target,'wb-component-mark'),labels);
|
|
180
|
+
const reselect=button('Reselect',()=>pick(),'wb-component-reselect');reselect.setAttribute('aria-label','Select another element');reselect.prepend(mark(drawings.target));heading.append(identity,reselect);
|
|
181
|
+
const visual=node('div','wb-component-visual'), image=node('img','wb-selection-image');image.alt='Captured selected element';
|
|
182
|
+
const placeholder=node('span','wb-component-placeholder');visual.append(image,placeholder);
|
|
183
|
+
const sourceRow=node('div','wb-component-source'), location=node('code','wb-component-location'), editor=node('a','wb-component-editor','Open in editor');sourceRow.append(location,editor);
|
|
184
|
+
const composer=node('div','wb-component-composer'), label=node('label','wb-component-label','What should change?');
|
|
185
|
+
const prompt=node('textarea');prompt.rows=3;prompt.maxLength=4000;prompt.setAttribute('aria-label','Instruction for your assistant');prompt.placeholder='Describe the change…';prompt.value=instruction;prompt.addEventListener('input',()=>{instruction=prompt.value;});label.append(prompt);
|
|
186
|
+
const footer=node('div','wb-component-compose-foot'), parts=node('span','wb-component-parts');
|
|
187
|
+
const send=button('Send context to AI',()=>action(send,()=>tools.sendContext(tools.snapshot().context||context||initial,prompt.value),'Context is available to your assistant through PRNext MCP. Ask it to use the latest context.'),'wb-ai-primary');send.append(mark(drawings.arrow));footer.append(parts,send);composer.append(label,footer);
|
|
188
|
+
const sourceDetails=node('details','wb-component-details');sourceDetails.dataset.key='component-source';sourceDetails.append(node('summary','','Source & styles'));
|
|
189
|
+
const sourceCode=node('pre','wb-source-code'), styles=node('dl','wb-component-styles');sourceDetails.append(sourceCode,styles);
|
|
190
|
+
selection.append(heading,visual,sourceRow,composer,sourceDetails);
|
|
191
|
+
let lastImage,lastStyles;
|
|
192
|
+
function sync(value) {
|
|
193
|
+
const mapped=value.mappedSource,source=mapped?.file||(typeof value.source==='string'?value.source:''),file=source.split(/[\\/]/).pop()?.replace(/:\d+(?::\d+)?$/,'');
|
|
194
|
+
name.textContent=file||(value.kind==='error'?'Runtime error':'Selected element');
|
|
195
|
+
kind.textContent=value.kind==='error'?'Error':`<${value.tag||'element'}>${value.bounds?' · '+value.bounds.width+' × '+value.bounds.height:''}`;
|
|
196
|
+
const png=value.screenshot?.mimeType==='image/png'&&typeof value.screenshot.data==='string'?value.screenshot:undefined;
|
|
197
|
+
image.hidden=!png;placeholder.hidden=!!png;
|
|
198
|
+
if(png&&lastImage!==png.data){image.src='data:image/png;base64,'+png.data;lastImage=png.data;}
|
|
199
|
+
const pending=value.kind==='element'&&!Object.hasOwn(value,'screenshot');
|
|
200
|
+
placeholder.textContent=value.kind==='error'?value.error?.message||value.text||'Runtime error':value.screenshotError||'Preparing preview…';
|
|
201
|
+
visual.dataset.state=pending?'loading':value.kind==='error'?'error':png?'ready':'unavailable';
|
|
202
|
+
location.textContent=mapped?.file?`${mapped.file}:${mapped.line}:${mapped.column}`:source||'Source unavailable';location.title=location.textContent;
|
|
203
|
+
editor.hidden=!/^(?:vscode|cursor):\/\//.test(mapped?.editorUrl||'');if(!editor.hidden)editor.href=mapped.editorUrl;
|
|
204
|
+
sourceCode.textContent=(value.kind==='error'&&value.error?.stack?value.error.stack+'\n\n':'')+(mapped?.frame||(Array.isArray(mapped?.excerpt)?mapped.excerpt.map(line=>`${line.line} ${line.text}`).join('\n'):mapped?.error||'Source is being resolved.'));
|
|
205
|
+
const nextStyles=JSON.stringify(value.styles||{});if(nextStyles!==lastStyles){lastStyles=nextStyles;styles.replaceChildren();for(const [key,text]of Object.entries(value.styles||{}))styles.append(node('dt','',key),node('dd','',text));}
|
|
206
|
+
parts.textContent=pending?'Preparing context…':[mapped?.file&&'Source',value.styles&&'Styles',png&&'Screenshot'].filter(Boolean).join(' · ')||'Error context';
|
|
207
|
+
const connected=assistantConnected();reselect.disabled=busy;
|
|
208
|
+
send.disabled=busy||pending||!connected;send.title=!connected?'Connect your assistant first.':pending?'Wait for the source and preview to finish.':'Queue this context for your assistant';
|
|
209
|
+
}
|
|
210
|
+
sync(initial);return {node:selection,sync};
|
|
211
|
+
}
|
|
212
|
+
function ai(data) {
|
|
213
|
+
const body=node('div','wb-ai-layout'),current={...(connection||{}),...(data.ai||{})},currentContext=data.context||context;
|
|
214
|
+
const state=node('section','wb-ai-connect'),hero=node('div','wb-ai-hero'),heading=node('h3','wb-ai-heading'),description=node('p','wb-ai-description');
|
|
215
|
+
const badge=node('span','wb-ai-status'),badgeText=node('span','wb-ai-status-label');badge.append(node('i'),badgeText);badge.setAttribute('aria-live','polite');badge.setAttribute('aria-atomic','true');
|
|
216
|
+
hero.append(heading,description,badge);state.append(hero);
|
|
217
|
+
const setup=node('details','wb-ai-setup'),setupSummary=node('summary','','Connection settings'),setupContent=node('div','wb-ai-setup-content');setup.dataset.key='ai-setup';setup.append(setupSummary,setupContent);state.append(setup);
|
|
218
|
+
const host=node('div','wb-host-connect'),hostHead=node('div','wb-host-heading');
|
|
219
|
+
hostHead.append(mark(drawings.plug),node('strong','','Use your PRNext MCP'));
|
|
220
|
+
const hostState=node('span','wb-host-state',current.hostRelay?.linked?'Relay ready':'Existing connection');hostHead.append(hostState);
|
|
221
|
+
const command=node('div','wb-host-command');command.append(node('code','','prnext dev --host-ai'));
|
|
222
|
+
const copyCommand=button('',()=>action(copyCommand,()=>copyText('prnext dev --host-ai'),'Host command copied.'),'wb-host-copy');copyCommand.setAttribute('aria-label','Copy Host command');copyCommand.title='Copy command';copyCommand.append(mark(drawings.copy));command.append(copyCommand);
|
|
223
|
+
const relayError=node('p','wb-error-text wb-host-error');relayError.hidden=true;
|
|
224
|
+
host.append(hostHead,command,node('p','wb-host-note','Shares this dev session through your existing PRNext login.'));setupContent.append(host);state.append(relayError);
|
|
225
|
+
const divider=node('div','wb-ai-divider');divider.append(node('span','','Or connect this project'));setupContent.append(divider);
|
|
226
|
+
const chooser=node('div','wb-ai-clients');chooser.setAttribute('role','group');chooser.setAttribute('aria-label','AI client');
|
|
227
|
+
const details=node('details','wb-mcp-config wb-local-setup');details.dataset.key='mcp';
|
|
228
|
+
const summary=node('summary','','Local MCP configuration'),controls=node('div','wb-mcp-controls'),format=node('span','wb-mcp-format'),code=node('pre','wb-mcp-code');
|
|
229
|
+
const reveal=node('div','wb-mcp-reveal'),mcpContent=node('div','wb-mcp-content'),snippet=node('div','wb-mcp-snippet');
|
|
230
|
+
const help=button('',undefined,'wb-mcp-help-toggle');help.setAttribute('aria-label','MCP setup help');help.title='Where to put this configuration';help.append(mark('<circle cx="12" cy="12" r="8"/><path d="M12 11v5m0-9v.01"/>'));
|
|
231
|
+
const helpReveal=node('div','wb-mcp-help-reveal'),helpContent=node('div','wb-mcp-help'),helpText=node('p'),helpNote=node('p','','Keep prnext dev running on this computer.'),helpLink=node('a','','Setup guide ↗');
|
|
232
|
+
helpReveal.id='prnext-mcp-help-'+(++aiViewId);help.setAttribute('aria-controls',helpReveal.id);helpLink.target='_blank';helpLink.rel='noopener noreferrer';helpContent.append(helpText,helpNote,helpLink);helpReveal.append(helpContent);
|
|
233
|
+
code.tabIndex=0;code.setAttribute('role','region');code.setAttribute('aria-label','MCP configuration');
|
|
234
|
+
let mcp=current.mcp,mcpSignature=JSON.stringify(mcp);
|
|
235
|
+
const copy=button('',()=>action(copy,()=>copyText(formatMcpConfig(mcp,mcpClient)),'MCP configuration copied.'),'wb-config-copy');copy.setAttribute('aria-label','Copy MCP configuration');copy.title='Copy configuration';copy.prepend(mark(drawings.copy));
|
|
236
|
+
controls.append(format,help);snippet.append(code,copy);mcpContent.append(controls,helpReveal,snippet);reveal.append(mcpContent);details.append(summary,reveal);
|
|
237
|
+
const configDisclosure=disclosure(summary,reveal,mcpContent,details),helpDisclosure=disclosure(help,helpReveal,helpContent);disposeAiView=()=>{configDisclosure.dispose();helpDisclosure.dispose();};
|
|
238
|
+
const providerButtons=new Map(),formats={codex:'Codex · TOML',cursor:'Cursor · JSON',claude:'Claude Desktop · JSON',other:'Other MCP · JSON'};
|
|
239
|
+
function syncProvider(){
|
|
240
|
+
for(const[id,control]of providerButtons)control.setAttribute('aria-pressed',String(id===mcpClient));
|
|
241
|
+
format.textContent=formats[mcpClient];code.textContent=mcp?formatMcpConfig(mcp,mcpClient):'Start PRNext dev to get the local configuration.';
|
|
242
|
+
const guide=mcpGuides[mcpClient];helpText.textContent=guide.text;helpLink.href=guide.url;code.scrollTop=0;
|
|
243
|
+
copy.disabled=!mcp;
|
|
244
|
+
}
|
|
245
|
+
function choose(id){const changed=id!==mcpClient;mcpClient=id;syncProvider();configDisclosure.set(true);if(changed&&!view.matchMedia('(prefers-reduced-motion: reduce)').matches){mcpContent.style.transition='none';mcpContent.dataset.swapping='true';void mcpContent.offsetHeight;mcpContent.style.removeProperty('transition');mcpContent.dataset.swapping='false';}}
|
|
246
|
+
for(const brand of devAiBrands){
|
|
247
|
+
const label=brand.id==='claude'?'Claude':brand.label,control=button('',()=>choose(brand.id),'wb-ai-client');control.setAttribute('aria-label',label);control.dataset.client=brand.id;
|
|
248
|
+
const logo=node('span','wb-ai-client-logo');logo.innerHTML=brand.icon;
|
|
249
|
+
const tick=mark(drawings.check,'wb-ai-client-check');control.append(logo,node('span','wb-ai-client-label',label),tick);control.disabled=!current.mcp;chooser.append(control);providerButtons.set(brand.id,control);
|
|
250
|
+
}
|
|
251
|
+
const other=button('Other MCP',()=>choose('other'),'wb-ai-other');other.setAttribute('aria-label','Other MCP');const otherLogo=node('span');otherLogo.innerHTML=devMcpBrand.icon;other.prepend(otherLogo);other.disabled=!current.mcp;providerButtons.set('other',other);
|
|
252
|
+
syncProvider();setupContent.append(chooser,other,details);body.append(state);
|
|
253
|
+
const workspace=node('section','wb-ai-workspace'),ready=node('div','wb-ai-ready');
|
|
254
|
+
const select=button('Select an element',()=>pick(),'wb-ai-primary wb-ai-select');select.prepend(mark(drawings.target));
|
|
255
|
+
ready.append(select,node('p','','Choose a component to describe a change.'));workspace.append(ready);body.append(workspace);
|
|
256
|
+
let selection,wasConnected,resumePrompt=false;
|
|
257
|
+
const verify=node('section','wb-ai-verify'),verifyText=node('div');verifyText.append(node('strong','','Check your changes'),node('p','','Confirm the latest build in your browser.'));
|
|
258
|
+
const check=button('Verify page',()=>action(check,()=>{const latest=tools.snapshot();if(!assistantConnected(latest))throw Error('Connect your assistant first.');return tools.call('dev_verify',{path:view.location.pathname,sinceSequence:Math.max(0,(latest.sequence||1)-1),clientId:latest.clientId,timeoutMs:25000});},result=>result.verified===true||result.ok===true||result.state==='verified'?'Verified: the browser applied the build and no current errors were reported.':result.reason||result.message||'Verification is incomplete. Check the browser and current errors.'),'wb-ai-verify-button');check.prepend(mark(drawings.check));verify.append(verifyText,check);body.append(verify);
|
|
259
|
+
syncAiView=latest=>{
|
|
260
|
+
const live={...(connection||{}),...(latest.ai||{})},online=latest.connection==='connected',connected=assistantConnected(latest),value=latest.context||context||currentContext;
|
|
261
|
+
heading.textContent=connected?'Assistant connected':'Connect your assistant';
|
|
262
|
+
const signature=JSON.stringify(live.mcp);if(signature!==mcpSignature){mcp=live.mcp;mcpSignature=signature;syncProvider();for(const control of providerButtons.values())control.disabled=!mcp;}
|
|
263
|
+
description.textContent=connected?String(live.client||'Local MCP session'):!online&&live.connected?'Reconnect your local dev session.':'Connect to send your selection to an assistant.';
|
|
264
|
+
badge.dataset.connected=String(connected);badgeText.textContent=!online?latest.connection==='unavailable'?'Unavailable':'Reconnecting':connected?'Connected':live.hostRelay?.linked?'Waiting for assistant':'Not connected';
|
|
265
|
+
hostState.textContent=online&&live.hostRelay?.linked?'Relay ready':'Existing connection';relayError.textContent=live.hostRelay?.error||'';relayError.hidden=!relayError.textContent;
|
|
266
|
+
let focusSetup=false,focusWorkspace=false;
|
|
267
|
+
if(wasConnected!==connected){
|
|
268
|
+
focusWorkspace=connected&&wasConnected===false&&setup.contains(shadow.activeElement);
|
|
269
|
+
setup.open=!connected;
|
|
270
|
+
if(wasConnected===true&&!connected){
|
|
271
|
+
resumePrompt=workspace.contains(shadow.activeElement);focusSetup=resumePrompt||verify.contains(shadow.activeElement)||shadow.activeElement===setupSummary;
|
|
272
|
+
setFeedback('Assistant disconnected. Reconnect to continue.');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
setupSummary.hidden=!connected;workspace.hidden=!connected;verify.hidden=!connected;check.disabled=!connected||busy;select.disabled=!connected||busy;
|
|
276
|
+
body.dataset.connected=String(connected);body.classList.toggle('wb-ai-has-selection',connected&&!!value);
|
|
277
|
+
if(connected&&value){if(!selection){selection=selectionView(value);workspace.replaceChildren(selection.node);}selection.sync(value);}
|
|
278
|
+
else if(selection&&value)selection.sync(value);
|
|
279
|
+
if(focusSetup)copyCommand.focus({preventScroll:true});
|
|
280
|
+
if(connected&&resumePrompt){(workspace.querySelector('textarea')||workspace.querySelector('button'))?.focus({preventScroll:true});resumePrompt=false;}
|
|
281
|
+
else if(focusWorkspace)(workspace.querySelector('textarea')||workspace.querySelector('button'))?.focus({preventScroll:true});
|
|
282
|
+
if(connected&&wasConnected===false&&/^(?:Assistant disconnected\.|Connect your assistant\b|Your assistant is no longer connected\.)/.test(feedback.textContent))setFeedback('');
|
|
283
|
+
wasConnected=connected;
|
|
284
|
+
};syncAiView(data);return body;
|
|
285
|
+
}
|
|
286
|
+
function render(force = false) {
|
|
287
|
+
if (disposed || !dialog.open) return;
|
|
288
|
+
const data = tools.snapshot(); status.textContent = data.connection === 'connected' ? 'Local' : data.connection === 'unavailable' ? 'Unavailable' : 'Reconnecting';
|
|
289
|
+
const selectionReady=active==='ai'&&assistantConnected(data)&&!!(data.context||context);
|
|
290
|
+
dialog.dataset.panel=active;dialog.dataset.selection=String(selectionReady);
|
|
291
|
+
title.textContent=selectionReady?(data.context||context).kind==='error'?'Debug with AI':'Selected component':headings[active];
|
|
292
|
+
if(active==='ai')syncAiView?.(data);
|
|
293
|
+
// Avoid replacing a focused field or open details every heartbeat.
|
|
294
|
+
const signature = JSON.stringify([active, filter.value, data.requests?.at(-1)?.id, data.localRequests?.at(-1)?.id, data.builds?.at(-1)?.buildId, data.routes, data.cache,
|
|
295
|
+
data.context?.source, data.context?.screenshot?.data?.length, data.context?.screenshotError, data.context?.sent,
|
|
296
|
+
data.ai?.connected, data.ai?.lastActivity, data.ai?.hostRelay, data.ai?.contexts?.at(-1)?.id]);
|
|
297
|
+
if (!force && signature === lastSignature) return;
|
|
298
|
+
if (!force && content.contains(shadow.activeElement)) return;
|
|
299
|
+
lastSignature = signature; const openDetails=new Map([...content.querySelectorAll('details[data-key]')].map(details=>[details.dataset.key,details.prnextExpand?details.dataset.expanded==='true':details.open]));
|
|
300
|
+
disposeAiView?.();disposeAiView=undefined;syncAiView=undefined;
|
|
301
|
+
content.replaceChildren(({ requests, performance, routes, ai })[active](data));
|
|
302
|
+
for(const details of content.querySelectorAll('details[data-key]'))if(openDetails.has(details.dataset.key)){const expanded=openDetails.get(details.dataset.key);if(details.prnextExpand)details.prnextExpand(expanded,{animate:false});else details.open=expanded;}
|
|
303
|
+
if (data.error && !feedback.textContent) setFeedback(data.error);
|
|
304
|
+
}
|
|
305
|
+
async function load(force=false) {
|
|
306
|
+
if (refreshing) return; refreshing = true; refresh.disabled = true;
|
|
307
|
+
try { await tools.refresh(); if (active === 'ai') connection = await tools.call('dev_connection'); render(force); }
|
|
308
|
+
catch (error) { setFeedback(error.message); }
|
|
309
|
+
finally { refreshing = false; refresh.disabled = false; }
|
|
310
|
+
}
|
|
311
|
+
function open(key = 'requests', nextContext) {
|
|
312
|
+
if (disposed) return;
|
|
313
|
+
if (picker) { picker.dispose(); picker = undefined; }
|
|
314
|
+
if (nextContext) { context = nextContext; tools.setContext(nextContext); }
|
|
315
|
+
const changed = active !== key; active = key; title.textContent = headings[key];
|
|
316
|
+
for (const item of nav.children) { item.setAttribute('aria-current', item.dataset.panel === key ? 'page' : 'false'); }
|
|
317
|
+
filter.hidden = !['requests', 'routes'].includes(key); filter.placeholder = key === 'routes' ? 'Find a route…' : 'Filter requests…'; filter.setAttribute('aria-label', key === 'routes' ? 'Find a route' : 'Filter requests');
|
|
318
|
+
if (changed) { filter.value = ''; setFeedback(''); }
|
|
319
|
+
if (!dialog.open) { dialog.showModal(); stop = tools.watch(); unsubscribe = tools.subscribe(() => render()); }
|
|
320
|
+
render(true);
|
|
321
|
+
const selected=nav.querySelector('[aria-current=page]');
|
|
322
|
+
if(selected&&active!=='ai'){const item=selected.getBoundingClientRect(),bounds=nav.getBoundingClientRect();if(item.right>bounds.right-8)nav.scrollLeft+=item.right-bounds.right+8;else if(item.left<bounds.left+8)nav.scrollLeft-=bounds.left+8-item.left;}
|
|
323
|
+
void load(); if (!filter.hidden) filter.focus(); else if(nextContext)(content.querySelector('.wb-ai-workspace:not([hidden]) textarea')||content.querySelector('.wb-host-copy'))?.focus({preventScroll:true});else close.focus();
|
|
324
|
+
}
|
|
325
|
+
function pick() {
|
|
326
|
+
if(disposed)return;
|
|
327
|
+
const epoch=++pickEpoch;
|
|
328
|
+
if (dialog.open) dialog.close();
|
|
329
|
+
let shown = false;
|
|
330
|
+
picker?.dispose(); picker = createElementPicker({ tools, view, onCancel(){picker=undefined;restoreFocus();}, onSelect(selection) {
|
|
331
|
+
if(disposed||epoch!==pickEpoch)return;if(!shown)instruction='';context = selection; picker = undefined;
|
|
332
|
+
if(shown){if(dialog.open&&active==='ai')render();return;}shown=true;open('ai', selection);
|
|
333
|
+
} });
|
|
334
|
+
}
|
|
335
|
+
filter.addEventListener('input', () => render(true));
|
|
336
|
+
content.addEventListener('focusout',()=>view.requestAnimationFrame(()=>render()));
|
|
337
|
+
dialog.addEventListener('close', () => { stop?.(); stop = undefined; unsubscribe?.(); unsubscribe = undefined; restoreFocus(); });
|
|
338
|
+
return { open, pick, hide() { pickEpoch++;if (dialog.open) dialog.close();picker?.dispose(); picker = undefined; },
|
|
339
|
+
dispose() { disposed = true;pickEpoch++;picker?.dispose(); stop?.(); unsubscribe?.();disposeAiView?.(); dialog.remove(); style.remove(); },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Images are MCP image blocks, never base64 text in the model's text context.
|
|
2
|
+
// Browser HTTP snapshots retain the original image for the local inspector.
|
|
3
|
+
export function devToolResult(value) {
|
|
4
|
+
const copy=structuredClone(value),data=copy&&typeof copy==='object'&&!Array.isArray(copy)?copy:{value:copy??null},images=[],candidates=[];
|
|
5
|
+
// Walk JSON iteratively, so deeply nested CLI replies cannot overflow the
|
|
6
|
+
// formatter stack. All image data is removed from text, even malformed data.
|
|
7
|
+
const stack=[data];
|
|
8
|
+
while(stack.length){
|
|
9
|
+
const parent=stack.pop();if(!parent||typeof parent!=='object')continue;
|
|
10
|
+
const entries=Object.entries(parent);
|
|
11
|
+
for(let index=entries.length-1;index>=0;index--){
|
|
12
|
+
const [key,item]=entries[index];
|
|
13
|
+
if(key==='screenshot'&&(typeof item==='string'||item&&typeof item==='object'&&Object.hasOwn(item,'data'))||
|
|
14
|
+
item&&typeof item==='object'&&typeof item.mimeType==='string'&&item.mimeType.startsWith('image/')&&Object.hasOwn(item,'data')){
|
|
15
|
+
candidates.push({parent,key,image:item});continue;
|
|
16
|
+
}
|
|
17
|
+
if(item&&typeof item==='object')stack.push(item);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// The traversal above retains chronological context array order. Prefer its
|
|
21
|
+
// most recent valid image; older/invalid images remain explicit omissions.
|
|
22
|
+
let selected;
|
|
23
|
+
for(const candidate of candidates){
|
|
24
|
+
const image=candidate.image;
|
|
25
|
+
const valid=image?.mimeType==='image/png'&&typeof image.data==='string'&&image.data.length<=350000&&/^[A-Za-z0-9+/]+={0,2}$/.test(image.data)&&
|
|
26
|
+
Buffer.from(image.data,'base64').subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10]));
|
|
27
|
+
candidate.valid=valid;if(valid)selected=candidate;
|
|
28
|
+
}
|
|
29
|
+
for(const candidate of candidates){
|
|
30
|
+
const {parent,key,image,valid}=candidate;
|
|
31
|
+
if(candidate===selected){
|
|
32
|
+
images.push({type:'image',mimeType:'image/png',data:image.data});
|
|
33
|
+
parent[key]={mimeType:'image/png',width:image.width,height:image.height,attached:true};
|
|
34
|
+
}else parent[key]={omitted:true,reason:valid?'Only the latest screenshot is attached.':'Screenshot is invalid, unsupported or exceeds the image limit.'};
|
|
35
|
+
}
|
|
36
|
+
return {content:[{type:'text',text:JSON.stringify(data)},...images],structuredContent:data,isError:false};
|
|
37
|
+
}
|