@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.
Files changed (126) hide show
  1. package/AI.md +63 -0
  2. package/README.md +384 -9
  3. package/build/ADAPTERS.md +27 -2
  4. package/build/actions.mjs +5 -0
  5. package/build/analyze.mjs +184 -0
  6. package/build/app-config.mjs +7 -8
  7. package/build/app-static.mjs +12 -7
  8. package/build/app.mjs +23 -10
  9. package/build/build-lock.mjs +6 -6
  10. package/build/bundle-runtime.mjs +20 -5
  11. package/build/check.mjs +56 -11
  12. package/build/compiler.mjs +25 -1
  13. package/build/concurrent.mjs +12 -0
  14. package/build/config.mjs +21 -13
  15. package/build/css-modules.mjs +15 -4
  16. package/build/dev-compiler-health.mjs +11 -0
  17. package/build/dev-css-update.mjs +83 -0
  18. package/build/dev-dependencies.mjs +44 -0
  19. package/build/dev-discovery.mjs +28 -0
  20. package/build/dev-worker.mjs +14 -5
  21. package/build/dev.mjs +55 -4
  22. package/build/edge.mjs +3 -2
  23. package/build/framework-imports.mjs +40 -0
  24. package/build/incremental.mjs +8 -4
  25. package/build/index.mjs +96 -101
  26. package/build/inspect.mjs +3 -0
  27. package/build/inspection-summary.mjs +37 -4
  28. package/build/loader-cache.mjs +53 -26
  29. package/build/metadata-routes.mjs +2 -3
  30. package/build/migrate.mjs +60 -57
  31. package/build/npm-packages.mjs +3 -18
  32. package/build/package-manager.mjs +308 -0
  33. package/build/persistent-cache.mjs +97 -0
  34. package/build/precompress.mjs +62 -30
  35. package/build/prerender-pages.mjs +90 -0
  36. package/build/prerender-worker.mjs +10 -0
  37. package/build/production-builder.mjs +80 -0
  38. package/build/scan.mjs +4 -4
  39. package/build/source-analysis.mjs +42 -0
  40. package/build/static-adapter.mjs +13 -5
  41. package/build/static-input.mjs +48 -0
  42. package/build/styles.mjs +15 -3
  43. package/build/transform-cache.mjs +37 -4
  44. package/build/type-command.mjs +41 -0
  45. package/build/typed-routes.mjs +91 -0
  46. package/build/typescript-dev-worker.mjs +18 -0
  47. package/build/typescript-dev.mjs +22 -0
  48. package/build/typescript-watch.mjs +22 -0
  49. package/build/typescript.mjs +5 -4
  50. package/build/webpack-graph.mjs +164 -27
  51. package/build/worker-preload.mjs +9 -0
  52. package/cli.mjs +105 -121
  53. package/compat/index.d.ts +21 -7
  54. package/compat/link.d.cts +4 -3
  55. package/compat/navigation.d.cts +4 -3
  56. package/compat/router.d.cts +4 -3
  57. package/compat/typed-route.d.cts +17 -0
  58. package/docs/ai/compatibility.md +106 -0
  59. package/docs/ai/data.md +128 -0
  60. package/docs/ai/deployment.md +111 -0
  61. package/docs/ai/manifest.json +11 -0
  62. package/docs/ai/quickstart.md +146 -0
  63. package/docs/ai/routing.md +132 -0
  64. package/docs/ai/troubleshooting.md +120 -0
  65. package/index.mjs +2 -0
  66. package/native/diagnostics.mjs +34 -0
  67. package/native/resolve.mjs +7 -2
  68. package/package.json +25 -7
  69. package/release/ai-docs.mjs +48 -0
  70. package/release/verify.mjs +2 -0
  71. package/runtime/ai-docs.mjs +202 -0
  72. package/runtime/app-client.mjs +17 -6
  73. package/runtime/app-render.mjs +12 -0
  74. package/runtime/cloud/archive.mjs +5 -6
  75. package/runtime/compile-cache.mjs +8 -0
  76. package/runtime/dev-ai-brands.mjs +26 -0
  77. package/runtime/dev-ai-style.mjs +99 -0
  78. package/runtime/dev-build-reasons.mjs +25 -0
  79. package/runtime/dev-cache-control.mjs +53 -0
  80. package/runtime/dev-client.mjs +239 -45
  81. package/runtime/dev-console-copy.mjs +19 -0
  82. package/runtime/dev-console.mjs +106 -0
  83. package/runtime/dev-dependency-graph.mjs +40 -0
  84. package/runtime/dev-element-picker-style.mjs +27 -0
  85. package/runtime/dev-element-picker.mjs +213 -0
  86. package/runtime/dev-error-overlay-style.mjs +41 -0
  87. package/runtime/dev-error-overlay.mjs +167 -0
  88. package/runtime/dev-error-report.mjs +121 -0
  89. package/runtime/dev-file-changes.mjs +1 -1
  90. package/runtime/dev-gateway.mjs +44 -0
  91. package/runtime/dev-host-relay.mjs +66 -0
  92. package/runtime/dev-indicator.mjs +186 -0
  93. package/runtime/dev-issue-motion.mjs +214 -0
  94. package/runtime/dev-lazy-tool.mjs +18 -0
  95. package/runtime/dev-logo.mjs +2 -0
  96. package/runtime/dev-notification.mjs +81 -0
  97. package/runtime/dev-popout.mjs +34 -0
  98. package/runtime/dev-position.mjs +107 -0
  99. package/runtime/dev-preview-touch.mjs +122 -0
  100. package/runtime/dev-runtime-pool.mjs +45 -0
  101. package/runtime/dev-runtime-spare.mjs +57 -0
  102. package/runtime/dev-tools-client.mjs +155 -0
  103. package/runtime/dev-tools-mcp.mjs +126 -0
  104. package/runtime/dev-tools-metrics.mjs +31 -0
  105. package/runtime/dev-tools-model.mjs +42 -0
  106. package/runtime/dev-tools-panel.mjs +341 -0
  107. package/runtime/dev-tools-result.mjs +37 -0
  108. package/runtime/dev-tools-server.mjs +256 -0
  109. package/runtime/dev-tools-source.mjs +104 -0
  110. package/runtime/dev-tooltip.mjs +94 -0
  111. package/runtime/dev-ui-icons.mjs +8 -0
  112. package/runtime/dev-workbench-style.mjs +106 -0
  113. package/runtime/dev-workbench.mjs +428 -0
  114. package/runtime/dev-worker-logs.mjs +31 -0
  115. package/runtime/development.mjs +307 -0
  116. package/runtime/env.mjs +2 -1
  117. package/runtime/pages-client.mjs +10 -1
  118. package/runtime/pages-navigation.mjs +6 -1
  119. package/runtime/persistent/client.mjs +6 -5
  120. package/runtime/persistent/daemon.mjs +66 -56
  121. package/runtime/persistent/native-proxy.mjs +63 -0
  122. package/runtime/preload.mjs +12 -0
  123. package/runtime/rsc-worker.mjs +7 -0
  124. package/runtime/typed-route.mjs +19 -0
  125. package/runtime/watch-project.mjs +12 -5
  126. package/runtime/worker.mjs +36 -2
@@ -0,0 +1,256 @@
1
+ import {createServer} from 'node:http';
2
+ import {randomBytes,timingSafeEqual,randomUUID} from 'node:crypto';
3
+ import {open,lstat,realpath,rename,rm,readFile,mkdir} from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+
7
+ const object={type:'object',additionalProperties:false,properties:{}};
8
+ const pathProperty={type:'string',maxLength:1024,description:'Local pathname, without a host.'};
9
+ const sourceProperties={file:{type:'string',maxLength:2048},source:{type:'string',maxLength:2048},url:{type:'string',maxLength:4096},stack:{type:'string',maxLength:16000},line:{type:'integer',minimum:1},column:{type:'integer',minimum:1},error:{type:'object'},editor:{type:'string',enum:['vscode','cursor']}};
10
+ /** Shared by the local stdio adapter and the optional existing Host MCP relay. */
11
+ export const devToolDefinitions=[
12
+ {name:'dev_snapshot',description:'Current local development generation, errors, browser confirmations, logs, routes and measured build timings.',inputSchema:object},
13
+ {name:'dev_connection',description:'Actual local AI connection activity and stdio setup for this project.',inputSchema:object},
14
+ {name:'dev_logs',description:'Bounded, redacted development server/browser logs and request timings.',inputSchema:{...object,properties:{source:{type:'string',enum:['server','browser','all']},level:{type:'string'},limit:{type:'integer',minimum:1,maximum:500}}}},
15
+ {name:'dev_routes',description:'Discovered routes and which routes are currently compiled.',inputSchema:object},
16
+ {name:'dev_route_ensure',description:'Compile a local development pathname on demand. Does not navigate a browser.',inputSchema:{...object,properties:{path:pathProperty},required:['path']}},
17
+ {name:'dev_cache_invalidate',description:'Expire native data/page cache entries associated with one local route. Preserves unrelated entries, the running development generation, persistent transform caches and project files.',inputSchema:{...object,properties:{path:pathProperty},required:['path']}},
18
+ {name:'dev_verify',description:'Wait for a NEW ready generation after sinceSequence AND a browser confirmation of that exact generation. Missing browser evidence is inconclusive, never success.',inputSchema:{...object,properties:{path:pathProperty,sinceSequence:{type:'integer',minimum:0},clientId:{type:'string',maxLength:100},timeoutMs:{type:'integer',minimum:100,maximum:30000}}}},
19
+ {name:'dev_ai_context',description:'Queue selected element or error context for the connected local assistant. Does not claim the assistant has acted.',inputSchema:{...object,properties:{kind:{type:'string',maxLength:32},context:{type:'object'},prompt:{type:'string',maxLength:4000}}}},
20
+ {name:'dev_source',description:'Resolve a project source location and a small code frame. Secret and external files are excluded.',inputSchema:{...object,properties:sourceProperties}},
21
+ {name:'dev_open_editor',description:'Validate a project source location and return an editor deep link. Never runs a shell.',inputSchema:{...object,properties:sourceProperties}},
22
+ ];
23
+
24
+ export function redactDevText(value,limit=16000){
25
+ return String(value??'').replace(/\u001b\[[0-?]*[ -/]*[@-~]/g,'')
26
+ .replace(/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9_]+\b/g,'[redacted key]')
27
+ .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9+/_=.-]{8,}/gi,'[redacted authorization]')
28
+ .replace(/((?:password|passwd|secret|token|api[_-]?key|authorization|cookie)\s*[=:]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\s,;]+)/gi,'$1[redacted]')
29
+ .replace(/(https?:\/\/)[^/@\s:]+:[^/@\s]+@/g,'$1[redacted]@')
30
+ .slice(0,limit);
31
+ }
32
+ export function devPath(value='/'){
33
+ if(typeof value!=='string'||value.length>1024||!value.startsWith('/')||value.startsWith('//')||/[\u0000-\u001f\\]/.test(value))throw Error('A local pathname is required');
34
+ return new URL(value,'http://local').pathname;
35
+ }
36
+ function requestPath(value){
37
+ if(typeof value!=='string'||value.length>2048||/[\u0000-\u001f\\]/.test(value))return '[unavailable]';
38
+ if(value.startsWith('/')&&!value.startsWith('//'))return devPath(value);
39
+ try{const url=new URL(value);if(!['http:','https:'].includes(url.protocol))return '[unavailable]';return url.origin+url.pathname;}catch{return '[unavailable]';}
40
+ }
41
+ const boundedArray=(value,max)=>Array.isArray(value)?value.slice(-max):[];
42
+ const text=(value,max=256)=>redactDevText(value,max);
43
+ const number=value=>Number.isFinite(value)?value:undefined;
44
+ const plain=value=>value&&typeof value==='object'&&!Array.isArray(value);
45
+ function limitedContext(value,capability){
46
+ let serialized;try{serialized=JSON.stringify(value??{});}catch{throw Error('Context must be JSON');}
47
+ if(Buffer.byteLength(serialized)>400*1024)throw Error('Context is too large');
48
+ const clean=(item,depth=0)=>{
49
+ if(depth>10)return '[depth limit]';
50
+ if(typeof item==='string'){const result=redactDevText(item,16000);return capability?result.replaceAll(capability,'[redacted capability]'):result;}
51
+ if(Array.isArray(item))return item.slice(0,100).map(entry=>clean(entry,depth+1));
52
+ if(item&&typeof item==='object'){
53
+ if(item.mimeType==='image/png'&&typeof item.data==='string'){
54
+ if(item.data.length>350000||!/^[A-Za-z0-9+/]+={0,2}$/.test(item.data)||!Buffer.from(item.data,'base64').subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))throw Error('Screenshot must be a PNG no larger than 256 KiB');
55
+ return {mimeType:'image/png',data:item.data,width:number(item.width),height:number(item.height)};
56
+ }
57
+ return Object.fromEntries(Object.entries(item).slice(0,100).map(([key,entry])=>[key,/password|secret|token|authorization|cookie|api.?key/i.test(key)?'[redacted]':clean(entry,depth+1)]));
58
+ }
59
+ return item;
60
+ };
61
+ return clean(JSON.parse(serialized));
62
+ }
63
+ function validArguments(name,args){
64
+ const definition=devToolDefinitions.find(item=>item.name===name);if(!definition)throw Error('Unknown development tool');
65
+ if(!plain(args))throw Error('Arguments must be an object');
66
+ for(const key of Object.keys(args))if(!Object.hasOwn(definition.inputSchema.properties,key))throw Error('Unknown argument: '+key);
67
+ for(const key of definition.inputSchema.required||[])if(!Object.hasOwn(args,key))throw Error('Missing argument: '+key);
68
+ for(const [key,value] of Object.entries(args)){
69
+ const schema=definition.inputSchema.properties[key];
70
+ if(schema.type==='string'&&(typeof value!=='string'||value.length>(schema.maxLength||4096)))throw Error('Invalid '+key);
71
+ if(schema.type==='integer'&&(!Number.isSafeInteger(value)||value<(schema.minimum??0)||value>(schema.maximum??Number.MAX_SAFE_INTEGER)))throw Error('Invalid '+key);
72
+ if(schema.type==='object'&&!plain(value))throw Error('Invalid '+key);
73
+ if(schema.enum&&!schema.enum.includes(value))throw Error('Invalid '+key);
74
+ }
75
+ }
76
+
77
+ export async function createDevToolsServer({root,session=randomUUID(),snapshot=()=>({}),ensureRoute,invalidateRoute,sourceTools,collectMetrics,now=Date.now}={}){
78
+ root=await realpath(root);
79
+ if(!/^[\w-]{1,100}$/.test(session))throw Error('Invalid development session');
80
+ const token=randomBytes(32).toString('base64url'),tokenBytes=Buffer.from(token),origins=new Set(),browsers=new Map(),assistants=new Map(),serverLogs=[],requests=[],builds=[],contexts=[];
81
+ const assistantLeaseMs=30000;
82
+ const safeText=(value,max=256)=>text(value,max).replaceAll(token,'[redacted capability]');
83
+ const discoveryDirectory=path.join(root,'.prnext-dev'),discoveryFile=path.join(discoveryDirectory,'tools.json');
84
+ let endpoint,closed=false,logId=0,lastActivity=null,lastClient=null,appUrl=null,discoveryOwned=false,inFlight=0,hostRelayState={enabled:false,linked:false};
85
+ const envelope=value=>({version:1,session,...value});
86
+ const prune=()=>{for(const [id,client] of browsers)if(now()-client.lastSeen>45000)browsers.delete(id);for(const [id,client] of assistants)if(client.expiresAt<=now())assistants.delete(id);};
87
+ function connection(){
88
+ prune();const live=[...assistants.values()].sort((a,b)=>b.lastSeen-a.lastSeen),recent=lastActivity!==null&&now()-lastActivity<60000,latest=live[0];
89
+ return {connected:live.length>0||recent,initialized:live.length>0,lastActivity:lastActivity??latest?.initializedAt??null,client:recent?lastClient:latest?.client??null,
90
+ clients:live.map(({client,initializedAt,lastSeen})=>({client,initializedAt,lastSeen})),hostRelay:{...hostRelayState},mcp:{command:process.execPath,args:[fileURLToPath(new URL('../cli.mjs',import.meta.url)),'mcp',root]},endpoint:appUrl};
91
+ }
92
+ function assistantEvent(body){
93
+ prune();
94
+ if(!plain(body)||!['register','heartbeat','unregister'].includes(body.event)||typeof body.instanceId!=='string'||!/^[\w-]{16,100}$/.test(body.instanceId))throw Error('Invalid assistant lifecycle event');
95
+ const allowed=body.event==='register'?['event','instanceId','client']:['event','instanceId','leaseId'];
96
+ if(Object.keys(body).some(key=>!allowed.includes(key)))throw Error('Unknown assistant lifecycle field');
97
+ if(body.event==='register'){
98
+ if(typeof body.client!=='string'||!body.client.trim()||body.client.length>100||/[\u0000-\u001f\u007f]/.test(body.client))throw Error('Invalid assistant client');
99
+ let client=assistants.get(body.instanceId);
100
+ if(!client){if(assistants.size>=16)throw Object.assign(Error('Too many initialized assistants'),{code:'assistant_limit',status:429});client={leaseId:randomBytes(24).toString('base64url'),client:safeText(body.client,100),initializedAt:now()};assistants.set(body.instanceId,client);}
101
+ client.lastSeen=now();client.expiresAt=now()+assistantLeaseMs;
102
+ return envelope({leaseId:client.leaseId,leaseMs:assistantLeaseMs});
103
+ }
104
+ const client=assistants.get(body.instanceId);
105
+ if(!client&&body.event==='unregister')return envelope({accepted:true});
106
+ if(!client||typeof body.leaseId!=='string'||client.leaseId!==body.leaseId)throw Object.assign(Error('Assistant lease expired; initialize again'),{code:'assistant_lease_expired',status:404});
107
+ if(body.event==='unregister')assistants.delete(body.instanceId);
108
+ else {client.lastSeen=now();client.expiresAt=now()+assistantLeaseMs;}
109
+ return envelope({accepted:true,leaseMs:assistantLeaseMs});
110
+ }
111
+ function routes(){return boundedArray(snapshot().routes,2000).map(route=>typeof route==='string'?{path:text(route,1024)}:{path:text(route.path,1024),kind:text(route.kind,40),source:route.source?text(route.source,512):undefined,compiled:!!route.compiled,pinned:!!route.pinned});}
112
+ function compact(value){
113
+ // Keep a single tool response below the Host relay's 512 KiB envelope.
114
+ // Explicit counts disclose trimming; never cut an image or JSON string.
115
+ const shortened={};
116
+ const lists=[['serverLogs',value.serverLogs],['requests',value.requests],['builds',value.builds],['routes',value.routes],['logs',value.logs],['browsers',value.browsers]];
117
+ for(const [name,list] of lists)if(list)shortened[name]=list.length;
118
+ const large=()=>Buffer.byteLength(JSON.stringify(value))>470*1024;
119
+ for(const [name,list] of lists){if(!Array.isArray(list))continue;while(list.length>1&&large())list.splice(0,Math.ceil(list.length/2));if(list.length!==shortened[name])(value.truncated??={})[name]={returned:list.length,total:shortened[name]};}
120
+ if(large()&&value.ai?.contexts?.length>1)value.ai.contexts=value.ai.contexts.slice(-1);
121
+ return value;
122
+ }
123
+ function view(){
124
+ prune();const state=snapshot();
125
+ return compact(envelope({...state,error:state.error?safeText(state.error,8000):undefined,typeErrors:boundedArray(state.typeErrors,30).map(value=>safeText(value,4000)),serverLogs:[...serverLogs],requests:[...requests],builds:[...builds],routes:routes(),cache:state.cache||{},ai:{...connection(),contexts:[...contexts]},browsers:[...browsers.values()].map(({seenLogs,logs,...client})=>({...client,errors:client.errors?.slice(-10).map(error=>({...error,message:safeText(error.message,2000),stack:undefined})),selection:undefined}))}));
126
+ }
127
+ function addServerLog(level,value,extra={}){
128
+ const content=safeText(value,16000);if(!content.trim())return;
129
+ if(!extra.structuredLevel){
130
+ if(/^(?:\(node:\d+\)\s+)?(?:\[[^\]]+\]\s+)?\w*Warning:/.test(content))level='warn';
131
+ else if(/^\(Use .+--trace-warnings/.test(content))level='info';
132
+ else if(/^\d{4}-\d{2}-\d{2}T\S+\s+(ERROR|INFO|DEBUG|TRACE|WARN)\b/.test(content))level=/\sERROR\b/.test(content)?'error':/\sWARN\b/.test(content)?'warn':'info';
133
+ }
134
+ serverLogs.push({id:++logId,time:now(),level:['error','warn','info','log','debug','trace'].includes(level)?level:'info',text:content.replaceAll(token,'[redacted capability]'),source:'server',...extra});if(serverLogs.length>300)serverLogs.splice(0,serverLogs.length-300);
135
+ }
136
+ function recordBuild(value){builds.push({...value});if(builds.length>40)builds.shift();}
137
+ function recordRequest(item){
138
+ if(!plain(item))return;
139
+ requests.push({id:++logId,originalId:item.id,clientId:item.clientId,source:item.source==='server'?'server':'browser',time:number(item.time)||now(),path:requestPath(item.path||'/'),route:item.route?requestPath(item.route):undefined,method:text(item.method||'GET',12),status:number(item.status),duration:number(item.duration),kind:text(item.kind||'fetch',20),buildId:item.buildId,failed:!!item.failed,error:item.error?text(item.error,200):undefined});
140
+ if(requests.length>300)requests.shift();
141
+ }
142
+ function browserEvent(body){
143
+ if(!plain(body)||body.session!==session||typeof body.clientId!=='string'||!/^[\w-]{1,100}$/.test(body.clientId))throw Error('Invalid browser session');
144
+ if(!['heartbeat','applied','errors','request','selection','reload'].includes(body.kind))throw Error('Invalid browser event');
145
+ prune();let client=browsers.get(body.clientId);
146
+ if(!client){if(browsers.size>=8)browsers.delete(browsers.keys().next().value);client={clientId:body.clientId,seenLogs:new Set(),logs:[]};browsers.set(body.clientId,client);}
147
+ client.lastSeen=now();client.path=devPath(body.path||'/');
148
+ client.buildId=text(body.buildId,128);client.sequence=Number.isSafeInteger(body.sequence)?body.sequence:0;
149
+ if(body.kind==='applied'&&body.hydrated===true&&body.building!==true)client.applied={buildId:client.buildId,sequence:client.sequence,time:now()};
150
+ if(body.kind==='reload'){client.applied=undefined;client.reloadReason=text(body.reason||'Full reload required',256);client.reloadAt=now();}
151
+ client.issues=Number.isSafeInteger(body.issues)?Math.max(0,body.issues):0;client.building=body.building===true;
152
+ if(Number.isSafeInteger(body.droppedLogs)&&body.droppedLogs>=0)client.droppedLogs=Math.max(client.droppedLogs||0,body.droppedLogs);
153
+ if(Array.isArray(body.errors))client.errors=body.errors.slice(0,30).map(error=>typeof error==='string'?{message:safeText(error,8000),time:now()}:{name:safeText(error?.name,128),message:safeText(error?.message||error?.text,8000),stack:safeText(error?.stack,16000),time:number(error?.time)||now()});
154
+ for(const log of boundedArray(body.logs,100)){
155
+ if(!plain(log))continue;const id=text(log.id,100);if(!id||client.seenLogs.has(id))continue;
156
+ client.seenLogs.add(id);client.logs.push({id:body.clientId+':'+id,time:number(log.time)||now(),level:text(log.level||'log',16),text:safeText(log.text,8000)});
157
+ }
158
+ if(client.logs.length>100)client.logs.splice(0,client.logs.length-100);
159
+ // Keep a bounded dedup window; client log IDs are monotonically increasing.
160
+ if(client.seenLogs.size>500)client.seenLogs=new Set([...client.seenLogs].slice(-300));
161
+ if(body.kind==='request'&&plain(body.request)){
162
+ recordRequest({...body.request,clientId:body.clientId,source:'browser',buildId:client.buildId});
163
+ }
164
+ if(body.kind==='selection'&&plain(body.selection))client.selection=limitedContext(body.selection,token);
165
+ return envelope({accepted:true});
166
+ }
167
+ async function verify(args){
168
+ const baseline=args.sinceSequence??snapshot().sequence??0,start=now(),clock=performance.now(),timeout=Math.min(args.timeoutMs??10000,30000),pathname=args.path&&devPath(args.path);
169
+ while(!closed&&performance.now()-clock<timeout){
170
+ const state=snapshot();prune();
171
+ const clients=[...browsers.values()].filter(client=>(!args.clientId||client.clientId===args.clientId)&&(!pathname||client.path===pathname));
172
+ const match=clients.find(client=>!client.building&&client.applied?.sequence===state.sequence&&client.applied?.buildId===state.buildId&&client.sequence===state.sequence&&client.buildId===state.buildId);
173
+ if(state.state==='ready'&&state.sequence>baseline&&match&&now()-match.applied.time>=300){
174
+ const since=builds.find(build=>build.buildId===state.buildId)?.startedAt||start;
175
+ const errors=[...boundedArray(state.typeErrors,100).map(value=>safeText(value,4000)),...boundedArray(match.errors,30).filter(error=>error.time>=since),...(match.issues?['Browser reports '+match.issues+' current issue(s).']:[])];
176
+ return envelope({verified:errors.length===0,state:errors.length?'failed':'verified',reason:errors.length?'Current generation has reported errors.':'New generation compiled and confirmed by the browser.',scope:'Observed compilation, TypeScript diagnostics and browser errors; this is not a full test suite.',buildId:state.buildId,sequence:state.sequence,browser:{clientId:match.clientId,path:match.path,confirmedAt:match.applied.time},errors});
177
+ }
178
+ if(state.state==='error')return envelope({verified:false,state:'failed',reason:'Development compilation failed.',errors:[safeText(state.error,8000)],sequence:state.sequence});
179
+ await new Promise(resolve=>setTimeout(resolve,Math.min(100,timeout)));
180
+ }
181
+ const state=snapshot();return envelope({verified:false,state:'inconclusive',reason:closed?'Development session closed.':state.sequence>baseline?'No browser confirmed the current generation before the timeout.':'No new ready generation appeared before the timeout.',buildId:state.buildId,sequence:state.sequence});
182
+ }
183
+ async function executeTool(name,args={},meta={}){
184
+ validArguments(name,args);if(closed)throw Error('Development session is closed');
185
+ if(meta.assistantId){
186
+ prune();const assistant=assistants.get(meta.assistantId);
187
+ if(!assistant||meta.leaseId!==assistant.leaseId)throw Object.assign(Error('Assistant lease expired; initialize again'),{code:'assistant_lease_expired',status:409});
188
+ assistant.lastSeen=now();assistant.expiresAt=now()+assistantLeaseMs;
189
+ }else if(meta.client&&meta.client!=='browser'){lastActivity=now();lastClient=text(meta.client,100);}
190
+ if((name==='dev_snapshot'||name==='dev_logs')&&collectMetrics)await collectMetrics(recordRequest);
191
+ if(name==='dev_snapshot')return view();
192
+ if(name==='dev_connection')return envelope(connection());
193
+ if(name==='dev_routes')return compact(envelope({routes:routes(),cache:snapshot().cache||{}}));
194
+ if(name==='dev_logs'){
195
+ prune();const source=args.source||'all',limit=args.limit||200;
196
+ const logs=[...(source!=='browser'?serverLogs:[]),...(source!=='server'?[...browsers.values()].flatMap(client=>client.logs.map(log=>({...log,source:'browser',clientId:client.clientId}))):[])].filter(log=>!args.level||log.level===args.level).sort((a,b)=>a.time-b.time).slice(-limit);
197
+ return compact(envelope({logs,requests:requests.slice(-limit),droppedBrowserLogs:[...browsers.values()].reduce((count,client)=>count+(client.droppedLogs||0),0)}));
198
+ }
199
+ if(name==='dev_route_ensure'||name==='dev_cache_invalidate'){
200
+ const pathname=devPath(args.path),handler=name==='dev_route_ensure'?ensureRoute:invalidateRoute;if(!handler)throw Error('Development route operation is unavailable');
201
+ const result=await handler(pathname);return envelope({...result,path:pathname,invalidated:result.invalidated??name==='dev_cache_invalidate'});
202
+ }
203
+ if(name==='dev_verify')return verify(args);
204
+ if(name==='dev_ai_context'){
205
+ if(!connection().connected)throw Object.assign(Error('Connect your assistant before sending context.'),{code:'assistant_required',status:409});
206
+ const item={id:randomUUID(),time:now(),kind:text(args.kind||'context',32),prompt:safeText(args.prompt||'',4000),context:limitedContext(args.context,token)};
207
+ // The latest selection owns the one retained image; older text context
208
+ // remains useful without multiplying snapshot/relay payload sizes.
209
+ for(const previous of contexts)if(previous.context?.screenshot)previous.context.screenshot={omitted:true,reason:'A newer context was queued.'};
210
+ contexts.push(item);while(contexts.length>3||Buffer.byteLength(JSON.stringify(contexts))>410*1024)contexts.shift();return envelope({queued:true,id:item.id,connected:connection().connected});
211
+ }
212
+ if(name==='dev_source'||name==='dev_open_editor'){
213
+ const handler=name==='dev_source'?sourceTools?.source:sourceTools?.editor;if(!handler)throw Error('Source inspection is unavailable');return envelope(await handler(args));
214
+ }
215
+ }
216
+ function respond(res,status,value,origin){
217
+ const headers={'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store','X-Content-Type-Options':'nosniff'};
218
+ if(origin&&origins.has(origin)){headers['Access-Control-Allow-Origin']=origin;headers.Vary='Origin';headers['Access-Control-Allow-Methods']='POST, OPTIONS';headers['Access-Control-Allow-Headers']='Authorization, Content-Type, X-PRNext-Client';headers['Access-Control-Allow-Private-Network']='true';}
219
+ res.writeHead(status,headers);res.end(JSON.stringify(value));
220
+ }
221
+ const server=createServer(async(req,res)=>{
222
+ const origin=req.headers.origin;
223
+ if(!['127.0.0.1','::1','::ffff:127.0.0.1'].includes(req.socket.remoteAddress)||req.headers.host!==new URL(endpoint).host||origin&&!origins.has(origin))return respond(res,403,{error:'Local development origin required'});
224
+ if(req.method==='OPTIONS')return respond(res,204,{},origin);
225
+ const supplied=Buffer.from((req.headers.authorization||'').replace(/^Bearer /,''));
226
+ if(supplied.length!==tokenBytes.length||!timingSafeEqual(supplied,tokenBytes))return respond(res,401,{error:'Development capability required'},origin);
227
+ if(req.method!=='POST'||!/^application\/json(?:\s*;|$)/i.test(req.headers['content-type']||''))return respond(res,415,{error:'POST application/json required'},origin);
228
+ if(Number(req.headers['content-length'])>420*1024)return respond(res,413,{error:'Request too large'},origin);
229
+ if(inFlight>=16)return respond(res,429,{error:'Too many pending development requests'},origin);
230
+ inFlight++;
231
+ try{
232
+ let size=0;const chunks=[];for await(const chunk of req){size+=chunk.length;if(size>420*1024){respond(res,413,{error:'Request too large'},origin);req.destroy();return;}chunks.push(chunk);}
233
+ const body=JSON.parse(Buffer.concat(chunks).toString('utf8'));
234
+ const pathname=new URL(req.url,endpoint).pathname;
235
+ if(pathname==='/v1/assistant'&&origin)return respond(res,403,{error:'Only an initialized native MCP client can register an assistant.',code:'native_assistant_required'},origin);
236
+ const result=pathname==='/v1/assistant'?assistantEvent(body):pathname==='/v1/browser'?browserEvent(body):pathname.startsWith('/v1/tools/')?await executeTool(pathname.slice(10),body,{client:origin?'browser':req.headers['x-prnext-client'],...(!origin&&req.headers['x-prnext-assistant']?{assistantId:req.headers['x-prnext-assistant'],leaseId:req.headers['x-prnext-lease']}: {})}):null;
237
+ if(result===null)return respond(res,404,{error:'Unknown development endpoint'},origin);respond(res,200,result,origin);
238
+ }catch(error){respond(res,Number.isInteger(error.status)&&error.status>=400&&error.status<=599?error.status:400,{error:text(error.message,1000),...(typeof error.code==='string'&&/^[a-z_]{1,64}$/.test(error.code)?{code:error.code}:{})},origin);}finally{inFlight--;}
239
+ });
240
+ server.requestTimeout=35000;server.headersTimeout=5000;server.maxRequestsPerSocket=1000;
241
+ await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
242
+ endpoint='http://127.0.0.1:'+server.address().port;
243
+ try{
244
+ await mkdir(discoveryDirectory,{recursive:true,mode:0o700});
245
+ const directoryInfo=await lstat(discoveryDirectory);if(!directoryInfo.isDirectory()||directoryInfo.isSymbolicLink()||await realpath(discoveryDirectory)!==discoveryDirectory)throw Error('Development capability directory must be a real project directory');
246
+ const previous=await lstat(discoveryFile).catch(error=>{if(error.code!=='ENOENT')throw error;});
247
+ if(previous?.isSymbolicLink()||previous&&!previous.isFile())throw Error('Development capability file must be a regular file');
248
+ const temporary=discoveryFile+'.'+session+'.tmp';
249
+ try{const file=await open(temporary,'wx',0o600);try{await file.writeFile(JSON.stringify({version:1,root,session,endpoint,token,pid:process.pid}));}finally{await file.close();}await rename(temporary,discoveryFile);discoveryOwned=true;}finally{await rm(temporary,{force:true});}
250
+ }catch(error){server.closeAllConnections();await new Promise(resolve=>server.close(resolve));throw error;}
251
+ return {endpoint,token,session,executeTool,browserEvent,addServerLog,recordBuild,recordRequest,snapshot:view,bootstrap:{version:1,endpoint,token,session},
252
+ setHostRelayState(value={}){let origin;try{const url=new URL(value.origin);if(['https:','http:'].includes(url.protocol))origin=url.origin;}catch{}hostRelayState={enabled:value.enabled===true,linked:value.linked===true,...(origin?{origin}:{}),...(Number.isFinite(value.lastSeen)?{lastSeen:value.lastSeen}:{}),...(value.error?{error:safeText(value.error,1000)}:{})};},
253
+ setAppUrl(value){appUrl=value;const url=new URL(value);origins.add(url.origin);for(const host of ['127.0.0.1','localhost','[::1]'])origins.add(url.protocol+'//'+host+':'+url.port);},
254
+ async close(){if(closed)return;closed=true;assistants.clear();server.closeAllConnections();await new Promise(resolve=>server.close(resolve));if(discoveryOwned){let owned;try{owned=JSON.parse(await readFile(discoveryFile,'utf8'));}catch{}if(owned?.session===session&&owned?.token===token)await rm(discoveryFile,{force:true});}}
255
+ };
256
+ }
@@ -0,0 +1,104 @@
1
+ import { readFile, realpath, stat, readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { SourceMap } from 'node:module';
5
+ import { readBuildDirectory } from './build-directory.mjs';
6
+
7
+ const allowed = /\.(?:[cm]?jsx?|tsx?|css|s[ac]ss|html|mdx?|json)$/i;
8
+ const privatePart = /^(?:node_modules|\.git|\.codex|secrets?|credentials?|\.ssh)$/i;
9
+ const privateFile = /^(?:\.env(?:\..*)?|.*\.(?:pem|key|p12|pfx)|(?:credentials|secrets?)(?:[.-].*)?)$/i;
10
+ const within = (root, file) => { const relative=path.relative(root,file);return relative!==''&&!path.isAbsolute(relative)&&relative!=='..'&&!relative.startsWith('..'+path.sep); };
11
+ const bounded = (value, fallback=1) => Number.isInteger(Number(value))&&Number(value)>0&&Number(value)<=10_000_000?Number(value):fallback;
12
+ function locations(args) {
13
+ const found=[];
14
+ const add=(file,line,column)=>{if(typeof file==='string'&&file.length<=4096)found.push({file,line:bounded(line),column:bounded(column)});};
15
+ if(args.file||args.url)add(args.file||args.url,args.line,args.column);
16
+ for(const input of [args.source,args.stack,args.error?.stack,typeof args.error==='string'?args.error:undefined]){
17
+ if(typeof input!=='string')continue;
18
+ for(const line of input.slice(0,128*1024).split('\n').slice(0,80)){
19
+ const match=/(?:\bat\s+(?:.*?\s+\()?|^\s*|\s+)((?:https?:\/\/|file:\/\/\/|[A-Za-z]:[\\/]|\.?\.?[\\/])?[^()\n]+?):(\d+):(\d+)(?:\)?(?:\s|:|$))/.exec(line);
20
+ if(match)add(match[1].trim(),match[2],match[3]);
21
+ else {const ts=/^\s*(.+?)\((\d+),(\d+)\)/.exec(line);if(ts)add(ts[1],ts[2],ts[3]);}
22
+ if(found.length>=24)return found;
23
+ }
24
+ }
25
+ return found;
26
+ }
27
+
28
+ /** Read only bounded source inside this dev project. No shell, HTTP fetch or eval. */
29
+ export function createDevSourceTools(projectRoot) {
30
+ const canonical=realpath(projectRoot);
31
+ async function scoped(file,{compiled=false}={}) {
32
+ const root=await canonical;
33
+ if(typeof file!=='string'||/[\x00-\x1f\x7f]/.test(file))throw Error('Source file is not available in this project.');
34
+ const resolved=path.resolve(root,file),relative=path.relative(root,resolved);
35
+ if(!within(root,resolved)||relative.split(/[\\/]/).some(part=>privatePart.test(part)||privateFile.test(part))||!allowed.test(resolved))throw Error('Source file is not available in this project.');
36
+ const dist=compiled?undefined:await readBuildDirectory(root);
37
+ const generated=file=>{const value=path.relative(root,file);return value.split(/[\\/]/).some(part=>part.startsWith('.prnext'))||value===dist||value.startsWith(dist+path.sep);};
38
+ if(!compiled&&generated(resolved))throw Error('Choose an original project source file.');
39
+ const actual=await realpath(resolved);
40
+ if(!within(root,actual)||!compiled&&generated(actual)||path.relative(root,actual).split(/[\\/]/).some(part=>privatePart.test(part)||privateFile.test(part)))throw Error('Source file is not available in this project.');
41
+ const info=await stat(actual);if(!info.isFile()||info.size>1024*1024)throw Error('Source file exceeds the development preview limit.');
42
+ return {root,file:actual,relative:path.relative(root,actual).split(path.sep).join('/')};
43
+ }
44
+ async function mapAsset(location) {
45
+ let pathname=location.file;
46
+ if(/^https?:\/\//.test(pathname)){try{pathname=decodeURIComponent(new URL(pathname).pathname);}catch{return;}}
47
+ const asset=/(?:^|\/)_(?:prnext|next)\/assets\/([^/]+\.m?js)$/.exec(pathname);
48
+ const root=await canonical,dist=await readBuildDirectory(root);
49
+ let logical,original;
50
+ if(asset)logical=path.join(root,dist,'assets',asset[1]);
51
+ else {
52
+ if(pathname.startsWith('file:')){try{pathname=fileURLToPath(pathname);}catch{return;}}
53
+ const generated=path.resolve(root,pathname),relative=path.relative(root,generated).split(path.sep).join('/');
54
+ const suffix=/(?:^|\/)(assets|server|app-ssr|app-ssr-edge|edge)\/([^/]+\.m?js)$/.exec(relative);
55
+ if(!within(root,generated)||!suffix||!(relative.startsWith(dist+'/')||relative.startsWith('.prnext-dev/')))return;
56
+ original=generated;logical=path.join(root,dist,suffix[1],suffix[2]);
57
+ }
58
+ const choices=[...(original?[original]:[]),logical];
59
+ // Source maps refer to the logical build depth even after a generation is
60
+ // copied under .prnext-dev/<session>/<generation> for in-flight requests.
61
+ const home=path.join(root,'.prnext-dev');
62
+ const sessions=await readdir(home,{withFileTypes:true}).catch(()=>[]);
63
+ for(const session of sessions.filter(entry=>entry.isDirectory()&&!entry.isSymbolicLink()).slice(-4)){
64
+ const directory=path.join(home,session.name),generations=await readdir(directory,{withFileTypes:true}).catch(()=>[]);
65
+ const suffix=path.relative(path.join(root,dist),logical);
66
+ for(const generation of generations.filter(entry=>entry.isDirectory()&&!entry.isSymbolicLink()).slice(-8).reverse())choices.push(path.join(directory,generation.name,suffix));
67
+ }
68
+ for(const choice of choices){try{
69
+ const checked=await scoped(choice,{compiled:true});
70
+ const mapFile=await realpath(checked.file+'.map');if(!within(root,mapFile))continue;
71
+ const size=await stat(mapFile);if(size.size>8*1024*1024)continue;
72
+ const payload=JSON.parse(await readFile(mapFile,'utf8'));
73
+ const entry=new SourceMap(payload).findEntry(location.line-1,location.column-1);
74
+ if(!entry?.originalSource)continue;
75
+ const original=entry.originalSource.startsWith('file:')?fileURLToPath(entry.originalSource):path.resolve(path.dirname(logical),payload.sourceRoot||'',entry.originalSource);
76
+ const captured=payload.sourcesContent?.[payload.sources?.indexOf(entry.originalSource)];
77
+ return {...await scoped(original),line:entry.originalLine+1,column:entry.originalColumn+1,mapped:true,...(typeof captured==='string'&&captured.length<=1024*1024?{captured}:{} )};
78
+ }catch{/* Another generation may have retired; try the next retained map. */}}
79
+ }
80
+ async function locate(args) {
81
+ for(const location of locations(args)){
82
+ try {
83
+ const mapped=await mapAsset(location);if(mapped)return mapped;
84
+ let name=location.file;
85
+ if(name.startsWith('file:'))name=fileURLToPath(name);
86
+ if(/^https?:\/\//.test(name))continue;
87
+ return {...await scoped(name),line:location.line,column:location.column,mapped:false};
88
+ } catch {/* Framework and external stack frames are not project source. */}
89
+ }
90
+ throw Error('No original project source was found for this message.');
91
+ }
92
+ async function source(args={}) {
93
+ const found=await locate(args),text=found.captured??await readFile(found.file,'utf8'),lines=text.split(/\r?\n/),line=Math.min(found.line,Math.max(1,lines.length));
94
+ const excerpt=[];for(let current=Math.max(1,line-3);current<=Math.min(lines.length,line+3);current++)excerpt.push({line:current,text:lines[current-1].slice(0,4000)});
95
+ const column=Math.min(found.column,(lines[line-1]?.length||0)+1);
96
+ return {file:found.relative,line,column,mapped:found.mapped,excerpt,frame:excerpt.map(item=>`${item.line===line?'>':' '} ${String(item.line).padStart(4)} | ${item.text}`).join('\n'),editorUrl:editorUrl(found.file,line,column,'vscode')};
97
+ }
98
+ function editorUrl(file,line,column,editor){return `${editor}://file/${file.split(path.sep).map(encodeURIComponent).join('/').replace(/^\//,'')}:${line}:${column}`;}
99
+ async function editor(args={}) {
100
+ const editor=args.editor||'vscode';if(!['vscode','cursor'].includes(editor))throw Error('Choose VS Code or Cursor.');
101
+ const found=await locate(args);return {file:found.relative,line:found.line,column:found.column,editorUrl:editorUrl(found.file,found.line,found.column,editor)};
102
+ }
103
+ return {source,editor};
104
+ }
@@ -0,0 +1,94 @@
1
+ // One tooltip per workbench, rendered above modal clipping in its own window.
2
+ let sequence = 0;
3
+ export function createDevTooltips(shadow) {
4
+ const document=shadow.ownerDocument,view=document.defaultView;
5
+ const controller=new view.AbortController(),events={signal:controller.signal};
6
+ const tooltip=document.createElement('div'),label=document.createElement('span'),detail=document.createElement('span');
7
+ tooltip.className='wb wb-tooltip';tooltip.id=`prnext-dev-tooltip-${++sequence}`;tooltip.popover='manual';tooltip.setAttribute('role','tooltip');
8
+ label.className='wb-tooltip-label';detail.className='wb-tooltip-detail';tooltip.append(label,detail);shadow.append(tooltip);
9
+ let owner,pending,suppressed,hovered,focused,showTimer,hideTimer,exitTimer,lastShown=-Infinity;
10
+ const target=event=>event.composedPath().find(node=>node?.hasAttribute?.('data-wb-tooltip'));
11
+ const contains=(node,other)=>other instanceof view.Node&&node.contains(other);
12
+ function clearTimers(){view.clearTimeout(showTimer);view.clearTimeout(hideTimer);view.clearTimeout(exitTimer);}
13
+ function describe(node,enabled){
14
+ const ids=new Set((node.getAttribute('aria-describedby')||'').split(/\s+/).filter(Boolean));
15
+ if(enabled)ids.add(tooltip.id);else ids.delete(tooltip.id);
16
+ if(ids.size)node.setAttribute('aria-describedby',[...ids].join(' '));else node.removeAttribute('aria-describedby');
17
+ }
18
+ function hide({immediate=false}={}){
19
+ clearTimers();pending=undefined;if(owner)describe(owner,false);owner=undefined;tooltip.dataset.open='false';
20
+ const close=()=>{if(tooltip.matches(':popover-open'))tooltip.hidePopover();};
21
+ if(immediate||view.matchMedia('(prefers-reduced-motion: reduce)').matches)close();else exitTimer=view.setTimeout(close,100);
22
+ }
23
+ function position(node){
24
+ const anchor=node.getBoundingClientRect(),margin=8,gap=8;
25
+ const viewport=view.visualViewport,width=viewport?.width||view.innerWidth,height=viewport?.height||view.innerHeight;
26
+ tooltip.style.maxWidth=Math.max(1,width-2*margin)+'px';
27
+ const bounds=tooltip.getBoundingClientRect();
28
+ const leftEdge=viewport?.offsetLeft||0,topEdge=viewport?.offsetTop||0;
29
+ const below=anchor.bottom+gap,bottom=topEdge+height-margin;
30
+ const top=below+bounds.height<=bottom?below:Math.max(topEdge+margin,anchor.top-gap-bounds.height);
31
+ tooltip.dataset.side=top>=anchor.bottom?'bottom':'top';
32
+ tooltip.style.left=Math.max(leftEdge+margin,Math.min(anchor.x+(anchor.width-bounds.width)/2,leftEdge+width-margin-bounds.width))+'px';
33
+ tooltip.style.top=top+'px';
34
+ }
35
+ function show(node){
36
+ pending=undefined;
37
+ if(!node.isConnected||node===suppressed||!node.getClientRects().length)return;
38
+ clearTimers();if(owner&&owner!==node)describe(owner,false);owner=node;
39
+ label.textContent=node.dataset.wbTooltip;detail.textContent=node.dataset.wbTooltipDetail||'';detail.hidden=!detail.textContent;
40
+ // Keep the popover in the active dialog's non-inert subtree.
41
+ const container=node.closest('dialog')||shadow;if(tooltip.parentNode!==container)container.append(tooltip);
42
+ if(!tooltip.matches(':popover-open')){
43
+ tooltip.dataset.open='false';try{tooltip.showPopover();}catch{owner=undefined;return;}
44
+ }
45
+ position(node);tooltip.getBoundingClientRect();tooltip.dataset.open='true';describe(node,true);lastShown=view.performance.now();
46
+ }
47
+ function schedule(node,keyboard=false){
48
+ if(!node||node===suppressed)return;
49
+ view.clearTimeout(hideTimer);view.clearTimeout(exitTimer);
50
+ if(node===owner){tooltip.dataset.open='true';return;}
51
+ view.clearTimeout(showTimer);pending=node;
52
+ const warm=tooltip.matches(':popover-open')||view.performance.now()-lastShown<600;
53
+ showTimer=view.setTimeout(()=>show(node),keyboard||warm?60:280);
54
+ }
55
+ function maybeHide(node){
56
+ if(node===hovered||node===focused)return;
57
+ if(suppressed===node)suppressed=undefined;
58
+ if(node===pending){view.clearTimeout(showTimer);pending=undefined;}
59
+ if(node===owner){const next=[hovered,focused].find(value=>value&&value!==suppressed);if(next)schedule(next,next===focused);else hideTimer=view.setTimeout(()=>hide(),120);}
60
+ }
61
+ shadow.addEventListener('pointerover',event=>{const node=target(event);if(node&&event.pointerType!=='touch'){hovered=node;schedule(node);}},events);
62
+ shadow.addEventListener('pointerout',event=>{const node=target(event);if(!node||contains(node,event.relatedTarget)||contains(tooltip,event.relatedTarget))return;if(hovered===node)hovered=undefined;maybeHide(node);},events);
63
+ shadow.addEventListener('focusin',event=>{const node=target(event);if(node?.matches(':focus-visible')){focused=node;schedule(node,true);}},events);
64
+ shadow.addEventListener('focusout',event=>{const node=target(event);if(!node||contains(node,event.relatedTarget))return;if(focused===node)focused=undefined;maybeHide(node);},events);
65
+ tooltip.addEventListener('pointerenter',()=>view.clearTimeout(hideTimer),events);
66
+ tooltip.addEventListener('pointerleave',event=>{if(owner&&contains(owner,event.relatedTarget))return;const node=hovered;hovered=undefined;if(node)maybeHide(node);},events);
67
+ const dismiss=event=>{const node=target(event);if(node)suppressed=node;if(event.type==='pointerdown')focused=undefined;hide({immediate:true});};
68
+ shadow.addEventListener('pointerdown',dismiss,{...events,capture:true});
69
+ shadow.addEventListener('click',dismiss,{...events,capture:true});
70
+ shadow.addEventListener('keydown',event=>{
71
+ if(event.key==='Escape'&&(owner||pending)){
72
+ const path=event.composedPath();
73
+ const consume=[owner,pending].some(node=>{
74
+ if(!node)return false;
75
+ const modal=node.closest('dialog[open]');
76
+ return path.includes(node)||Boolean(modal&&path.includes(modal));
77
+ });
78
+ // A hovered control must not steal Escape from another focused control
79
+ // in the nonmodal console. Native modals retain tooltip-first dismissal.
80
+ suppressed=owner||pending;hide({immediate:true});
81
+ if(consume){event.preventDefault();event.stopPropagation();}
82
+ }
83
+ },{...events,capture:true});
84
+ const dismissLayout=()=>hide({immediate:true});
85
+ view.addEventListener('resize',dismissLayout,events);view.addEventListener('blur',dismissLayout,events);
86
+ view.addEventListener('scroll',dismissLayout,{...events,capture:true});
87
+ view.visualViewport?.addEventListener('resize',dismissLayout,events);
88
+ view.visualViewport?.addEventListener('scroll',dismissLayout,events);
89
+ return{
90
+ set(node,text,description=''){node.dataset.wbTooltip=text;node.dataset.wbTooltipDetail=description;node.removeAttribute('title');if(owner===node)show(node);return node;},
91
+ hide:()=>hide({immediate:true}),
92
+ dispose(){hide({immediate:true});controller.abort();tooltip.remove();},
93
+ };
94
+ }
@@ -0,0 +1,8 @@
1
+ export const svg = paths => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
2
+ export const deviceIcons = {
3
+ desktop: svg('<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8m-4-4v4"/>'),
4
+ phone: svg('<rect x="7" y="2" width="10" height="20" rx="2"/><path d="M11 18h2"/>'),
5
+ tablet: svg('<rect x="4" y="2" width="16" height="20" rx="2"/><path d="M11 18h2"/>'),
6
+ };
7
+ export const copyIcon = svg('<rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/>');
8
+ export const chevronIcon = svg('<path d="m8 10 4 4 4-4"/>');
@@ -0,0 +1,106 @@
1
+ // Shared by embedded and detached tools; neutral surfaces match the logo menu.
2
+ export const workbenchStyle = `
3
+ .wb{color-scheme:dark;color:#ededed;font:12px/1.5 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;text-align:left;box-sizing:border-box}
4
+ .wb *,.wb *::before,.wb *::after{box-sizing:border-box}
5
+ .wb button,.wb input,.wb select{font:inherit;color:inherit}
6
+ .wb button,.wb select{cursor:pointer}
7
+ .wb button{border:0;border-radius:8px;background:transparent;padding:6px 10px;min-height:34px;white-space:nowrap}
8
+ .wb button:hover{background:#191919}.wb button[aria-pressed=true]{background:#232323;color:#fff}
9
+ .wb button:focus-visible,.wb input:focus-visible,.wb select:focus-visible,.wb summary:focus-visible,.wb [tabindex]:focus-visible{outline:1px solid #929292;outline-offset:-2px}
10
+ .wb svg{width:16px;height:16px;flex:none}
11
+ .wb .wb-close{display:grid;place-items:center;width:34px;padding:0}.wb .wb-push{margin-left:auto}
12
+ .wb .wb-popout,.wb .wb-icon{display:grid;place-items:center;width:34px;padding:6px;color:#aaa}
13
+ .wb .wb-popout:hover,.wb .wb-icon:hover{color:#fff}.wb button[aria-pressed=true]{color:#fff}
14
+ .wb-popup-feedback{padding:6px 10px;color:#e9c58b;font-size:11px}
15
+ .wb-console{position:fixed;left:18px;bottom:74px;width:min(820px,calc(100vw - 36px));height:min(350px,calc(100dvh - 110px));min-height:180px;display:flex;flex-direction:column;background:#080808;border:1px solid #292929;border-radius:14px;box-shadow:0 12px 40px #0008;overflow:hidden;container-type:inline-size}
16
+ .wb-head,.wb-filters{display:flex;align-items:center;gap:6px;padding:7px 10px;border-bottom:1px solid #202020;flex:none;min-width:0}
17
+ .wb-filters{flex-direction:column;align-items:stretch;gap:8px;padding:10px}.wb-title{font-weight:600}
18
+ .wb .wb-pause{position:relative}.wb-pause svg{grid-area:1/1}.wb-pause .wb-resume-icon{opacity:0;scale:.25;filter:blur(4px)}
19
+ .wb-pause[aria-pressed=true] .wb-pause-icon{opacity:0;scale:.25;filter:blur(4px)}.wb-pause[aria-pressed=true] .wb-resume-icon{opacity:1;scale:1;filter:blur(0)}
20
+ .wb-search-row,.wb-filter-row,.wb-filter-options,.wb-console-utilities{display:flex;align-items:center;gap:6px;min-width:0}
21
+ .wb-search{position:relative;flex:1;min-width:0}.wb-search>svg{position:absolute;left:10px;top:50%;translate:0 -50%;width:14px;height:14px;color:#888;pointer-events:none}
22
+ .wb-filters input{display:block;width:100%;min-width:0;border:1px solid #292929;border-radius:8px;padding:6px 9px 6px 30px;background:#111;min-height:34px}
23
+ .wb-console-utilities{flex:none;gap:2px}.wb-console-utilities button{min-width:48px;color:#aaa}.wb-console-utilities button:hover{color:#ededed}
24
+ .wb-filter-options{flex:1;flex-wrap:wrap}.wb-filter-row{justify-content:space-between;gap:10px}
25
+ .wb-filters select{border:1px solid #292929;border-radius:8px;padding:6px;background:#111;max-width:110px;min-height:34px}.wb-copy{font-size:11px}
26
+ .wb-logs{overflow:auto;overscroll-behavior:contain;flex:1;min-height:0;font:11px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;scrollbar-color:#333 #080808}
27
+ .wb-line{display:grid;grid-template-columns:65px 43px minmax(0,1fr);gap:10px;padding:7px 12px;border-bottom:1px solid #ffffff08}
28
+ .wb-line time,.wb-level{color:#999}.wb-line pre{margin:0;font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text}
29
+ .wb-line[data-level=error]{background:#ce3c4a0e;color:#ffa2ac}.wb-line[data-level=warn]{background:#e6b45a0a;color:#e9c58b}
30
+ .wb-line[data-level=error] .wb-level{color:#ffa2ac}.wb-line[data-level=warn] .wb-level{color:#e9c58b}
31
+ .wb-empty{padding:24px;text-align:center;color:#999}.wb-foot{display:flex;gap:10px;flex-wrap:wrap;padding:6px 12px;color:#999;border-top:1px solid #202020;font-size:11px}
32
+ .wb-entry{border-bottom:1px solid #ffffff08}.wb-entry summary{list-style:none;cursor:pointer;border:0;grid-template-columns:65px 43px minmax(0,1fr) 14px}
33
+ .wb-entry summary::-webkit-details-marker{display:none}.wb-message{white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
34
+ .wb-entry[open] .wb-message{-webkit-line-clamp:1}.wb-entry-caret svg{width:13px;height:16px;color:#888}.wb-entry[open] .wb-entry-caret svg{rotate:180deg}.wb-entry:hover{background:#ffffff04}
35
+ .wb-entry-actions{display:flex;gap:6px;padding:6px 12px 8px 130px}.wb-entry-actions button{display:inline-flex;align-items:center;gap:6px;font:11px/1.5 ui-sans-serif,system-ui;background:#161616}.wb-entry-actions svg{width:13px;height:13px}
36
+ .wb-entry-full{margin:0;padding:0 12px 12px 130px;font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text}
37
+ .wb-ai{display:flex;align-items:stretch;flex:none;margin-left:auto;border-radius:8px;background:#191919}
38
+ .wb-ai button{display:flex;align-items:center;justify-content:center;gap:7px;font-weight:600}.wb-ai button:disabled{opacity:.5;cursor:default}
39
+ .wb-ai-main{border-radius:8px 0 0 8px!important}.wb-ai-toggle{width:28px;padding:4px!important;border-radius:0 8px 8px 0!important}.wb-ai svg{width:14px;height:14px}
40
+ .wb-ai-menu{position:fixed;inset:auto;margin:0;min-width:220px;max-width:calc(100vw - 16px);padding:5px;border:1px solid #292929;border-radius:11px;background:#080808;color:#ededed;box-shadow:0 10px 30px #0008;z-index:2147483647}
41
+ .wb-ai-menu button{display:flex;align-items:center;gap:8px;width:100%;min-height:36px;text-align:left;font-size:12px}.wb-ai-menu button:disabled{opacity:.45;cursor:default}.wb-ai-menu::backdrop{background:transparent}
42
+ dialog.wb-preview{position:fixed;inset:12px;width:calc(100vw - 24px);height:calc(100dvh - 24px);max-width:none;max-height:none;padding:0;margin:0;border:0;border-radius:12px;background:#080808;box-shadow:0 12px 40px #0008;overflow:hidden;overscroll-behavior:contain}
43
+ dialog.wb-preview[open]{display:flex;flex-direction:column}dialog.wb-preview::backdrop{background:#000b}
44
+ .wb-preview-head{display:flex;align-items:center;flex-wrap:wrap;gap:12px;padding:10px 12px;border-bottom:1px solid #202020;flex:none}
45
+ .wb-devices{display:flex;gap:2px;padding:3px;background:#111;border-radius:11px}
46
+ .wb-devices .wb-device{position:relative;display:grid;place-items:center;width:34px;height:34px;padding:0;border-radius:8px;color:#999}
47
+ .wb-device[aria-pressed=true]{background:#272727;color:#fff;box-shadow:inset 0 0 0 1px #ffffff15}
48
+ .wb-tooltip{position:fixed;inset:auto;margin:0;width:max-content;max-width:calc(100vw - 16px);padding:7px 10px;gap:9px;align-items:center;background:#111;color:#ededed;border:1px solid #ffffff12;border-radius:8px;box-shadow:0 4px 16px #0007;font-size:11px;line-height:16px;opacity:0;translate:0 -3px;overflow-wrap:anywhere;z-index:2147483647}
49
+ .wb-tooltip:popover-open{display:flex}.wb-tooltip:not(:popover-open){display:none}.wb-tooltip::backdrop{background:transparent;pointer-events:none}
50
+ .wb-tooltip[data-side=top]{translate:0 3px}.wb-tooltip[data-open=true]{opacity:1;translate:0 0}
51
+ .wb-tooltip-label{font-weight:550;min-width:0}.wb-tooltip-detail{min-width:0;color:#929292;font:10px/16px ui-monospace,SFMono-Regular,Consolas,monospace;font-variant-numeric:tabular-nums}
52
+ .wb-dimensions{font:11px ui-monospace,monospace;color:#999;font-variant-numeric:tabular-nums}
53
+ .wb-preview-actions{display:flex;align-items:center;gap:2px;margin-left:auto;flex:none}
54
+ .wb .wb-frame-toggle{display:flex;align-items:center;justify-content:center;gap:6px;width:auto;padding:6px 9px}
55
+ .wb-preview .wb-console{position:relative;left:auto;bottom:auto;width:100%;height:min(300px,42dvh);min-height:140px;flex:none;border:0;border-top:1px solid #292929;border-radius:0;box-shadow:none}
56
+ .wb-preview-note{display:flex;align-items:center;padding:7px 12px;color:#aaa;font-size:11px;border-top:1px solid #202020}
57
+ .wb-preview-note span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
58
+ .wb-stage{position:relative;flex:1;min-height:0;overflow:hidden;background:#080808}
59
+ .wb-frame-wrap{position:absolute;background:#101010;isolation:isolate;box-shadow:none}
60
+ .wb-frame-wrap[data-frame=true]::before{content:'';position:absolute;inset:0;z-index:3;border:1px solid #303030;border-radius:inherit;pointer-events:none}
61
+ .wb-frame-wrap[data-frame=false]{box-shadow:none;background:transparent}
62
+ .wb-screen{position:absolute;overflow:hidden;isolation:isolate;z-index:1;line-height:0;background:#080808;border:0;outline:0;box-shadow:none}
63
+ .wb-frame{position:absolute;left:0;top:0;display:block;border:0;border-radius:0;outline:0;box-shadow:none;transform-origin:top left;background:transparent}
64
+ .wb .wb-frame:focus-visible{outline:0}
65
+ .wb-device-top,.wb-device-home{position:absolute;pointer-events:none;z-index:2}
66
+ .wb-frame-wrap[data-frame=false] .wb-device-top,.wb-frame-wrap[data-frame=false] .wb-device-home{display:none}
67
+ .wb-device-top{left:50%;top:calc(4px * var(--s));translate:-50% 0;width:calc(68px * var(--s));height:calc(4px * var(--s));background:transparent}
68
+ .wb-device-top::before,.wb-device-top::after{content:'';position:absolute;border-radius:999px;background:#292929}
69
+ .wb-device-top::before{left:calc(7px * var(--s));top:calc(1px * var(--s));width:calc(42px * var(--s));height:calc(2px * var(--s))}
70
+ .wb-device-top::after{right:calc(7px * var(--s));top:0;width:calc(4px * var(--s));height:calc(4px * var(--s));background:#222}
71
+ .wb-device-home{bottom:calc(4px * var(--s));left:50%;translate:-50% 0;width:calc(88px * var(--s));height:calc(3px * var(--s));border-radius:99px;background:#555}
72
+ .wb-frame-wrap[data-device=tablet] .wb-device-top{width:calc(4px * var(--s));height:calc(4px * var(--s));top:calc(7px * var(--s));border-radius:50%;background:#292929}
73
+ .wb-frame-wrap[data-device=tablet] .wb-device-top::before,.wb-frame-wrap[data-device=tablet] .wb-device-top::after{display:none}
74
+ .wb-frame-wrap[data-device=tablet] .wb-device-home{display:none}
75
+ .wb-frame-wrap[data-device=phone][data-rotated=true] .wb-device-top{left:calc(4px * var(--s));top:50%;translate:0 -50%;width:calc(4px * var(--s));height:calc(68px * var(--s))}
76
+ .wb-frame-wrap[data-device=phone][data-rotated=true] .wb-device-top::before{left:calc(1px * var(--s));top:calc(19px * var(--s));width:calc(2px * var(--s));height:calc(42px * var(--s))}
77
+ .wb-frame-wrap[data-device=phone][data-rotated=true] .wb-device-top::after{left:0;right:auto;top:calc(7px * var(--s));bottom:auto}
78
+ .wb-frame-wrap[data-device=phone][data-rotated=true] .wb-device-home{bottom:auto;left:auto;right:calc(4px * var(--s));top:50%;translate:0 -50%;width:calc(3px * var(--s));height:calc(88px * var(--s))}
79
+ .wb-frame-wrap[data-device=tablet][data-rotated=true] .wb-device-top{left:calc(7px * var(--s));top:50%;translate:0 -50%}
80
+ .wb-frame-wrap[data-frame=true][data-device=desktop] .wb-device-top{left:0;top:0;translate:0;width:100%;height:calc(32px * var(--s));display:flex;align-items:center;gap:calc(5px * var(--s));padding:0 calc(12px * var(--s));border-radius:inherit;background:transparent}
81
+ .wb-frame-wrap[data-device=desktop] .wb-device-top::before,.wb-frame-wrap[data-device=desktop] .wb-device-top::after{display:none}
82
+ .wb-device-top i{display:none}.wb-frame-wrap[data-device=desktop] .wb-device-top i{display:block;width:calc(6px * var(--s));height:calc(6px * var(--s));border-radius:50%;background:#555}
83
+ .wb-frame-wrap[data-device=desktop] .wb-device-home{display:none}
84
+ .wb .wb-resize{position:absolute;right:-17px;top:50%;translate:0 -50%;width:26px;height:64px;padding:0;border-radius:8px;cursor:ew-resize;background:transparent!important;touch-action:none;display:grid;place-items:center;z-index:3}
85
+ .wb-resize::before{content:'';width:3px;height:38px;border-radius:3px;background:#666}.wb-stage[data-resizing=true] iframe{pointer-events:none}.wb-stage[data-resizing=true]{cursor:ew-resize}
86
+ dialog.wb-tools{inset:0;margin:auto;width:min(920px,calc(100vw - 32px));height:min(680px,calc(100dvh - 48px));max-width:none;max-height:none;padding:0;border:0;border-radius:14px;background:#080808;color:#ededed;box-shadow:0 18px 60px #000a;overflow:hidden;overscroll-behavior:contain}
87
+ dialog.wb-tools[open]{display:flex;flex-direction:column}dialog.wb-tools::backdrop{background:#000b}
88
+ .wb-tools-nav{display:flex;gap:3px;padding:7px 10px;overflow:auto;flex:none;border-bottom:1px solid #202020}.wb-tools-nav button{color:#aaa}.wb-tools-nav [aria-current=page]{background:#202020;color:#fff}
89
+ .wb-editor-link{display:inline-flex;align-items:center;min-height:34px;padding:6px 10px;border-radius:8px;background:#191919;color:#ededed;text-decoration:none}.wb-editor-link:focus-visible{outline:1px solid #929292;outline-offset:-2px}
90
+ .wb-mcp-controls{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:10px}.wb-mcp-controls select{border:1px solid #292929;border-radius:7px;padding:7px 9px;background:#111;min-height:34px;max-width:100%}
91
+ .wb-tools-search{margin:12px 14px 0;padding:8px 10px;background:#111;border:1px solid #292929;border-radius:8px;flex:none;min-width:0}
92
+ .wb-tools-content{padding:14px;overflow:auto;min-height:0;flex:1;scrollbar-color:#333 #080808}.wb-tools-feedback{padding:8px 14px;font-size:12px;color:#ccc;flex:none}.wb-tools-feedback:empty{display:none}
93
+ .wb-muted{color:#999;font-size:11px}.wb-truncate{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0}.wb-tools-labels{padding:6px 10px;color:#999;font-size:11px}
94
+ .wb-request-grid{display:grid;grid-template-columns:minmax(0,1fr) 65px 80px 60px;align-items:center;gap:12px}.wb-request{border-bottom:1px solid #ffffff09}.wb-request summary{list-style:none;cursor:pointer;padding:10px}.wb-request summary::-webkit-details-marker{display:none}.wb-request-path{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font:11px ui-monospace,monospace}.wb-request-path b{font-weight:600;color:#aaa;margin-right:8px}.wb-error-text{color:#ffa2ac}.wb-request-info{padding:0 10px 12px}.wb-request-info pre,.wb-source-code{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 ui-monospace,monospace}
95
+ .wb-tools-pair{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:5px 0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.wb-tools-section{padding:8px 0 18px}.wb-tools-section+.wb-tools-section{border-top:1px solid #202020}.wb-tools-section h3{margin:4px 0 14px;font:600 13px/1.5 ui-sans-serif,system-ui}.wb-tools-section p{margin:8px 0 14px}
96
+ .wb-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;padding:6px 0 20px}.wb-metric{display:flex;flex-direction:column;gap:5px}.wb-metric strong{font-size:22px;font-weight:550;font-variant-numeric:tabular-nums}.wb-build{padding:8px 0;border-bottom:1px solid #ffffff09}.wb-build summary{display:flex;justify-content:space-between;gap:12px;cursor:pointer;list-style:none}.wb-build summary::-webkit-details-marker{display:none}.wb-build[open] summary{margin-bottom:12px}.wb-phase{display:grid;grid-template-columns:85px minmax(0,1fr) 70px;align-items:center;gap:10px;font-size:11px;padding:5px 0}.wb-phase-meter{height:4px;border-radius:4px;background:#202020;overflow:hidden}.wb-phase-meter::before{content:'';display:block;width:var(--amount);height:100%;border-radius:inherit;background:#999}
97
+ .wb-cache-summary{display:flex;flex-wrap:wrap;gap:12px;padding-bottom:12px}.wb-cache-summary .wb-tools-pair{gap:8px}.wb-route{border-bottom:1px solid #ffffff09}.wb-route summary{display:flex;align-items:center;gap:12px;padding:12px 4px;cursor:pointer;list-style:none}.wb-route summary::-webkit-details-marker{display:none}.wb-route summary code{flex:1;min-width:0;overflow-wrap:anywhere;font-size:12px}.wb-route-actions{padding:0 4px 14px}.wb-route-param{display:inline-flex;flex-direction:column;gap:5px;margin:0 8px 12px 0;color:#aaa;font-size:11px;max-width:100%}.wb-route-param input{background:#111;border:1px solid #292929;border-radius:7px;padding:7px 9px;min-width:0;color:#eee}.wb-button-row{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.wb-button-row button,.wb-tools-section>button{background:#191919}.wb-context-source{display:block;overflow-wrap:anywhere}.wb-selection-text{color:#aaa;max-height:80px;overflow:auto;font-size:11px}.wb-selection-image{display:block;max-width:100%;max-height:180px;object-fit:contain;object-position:left;margin:10px 0;border-radius:6px}.wb-instruction{display:flex;flex-direction:column;gap:8px;margin:12px 0;font-size:11px;color:#aaa}.wb-instruction textarea{resize:vertical;min-height:70px;font:12px/1.5 ui-sans-serif,system-ui;background:#111;color:#eee;border:1px solid #292929;border-radius:8px;padding:9px}.wb-instruction textarea:focus-visible{outline:1px solid #929292;outline-offset:-2px}.wb-mcp-config{margin-top:10px}.wb-mcp-config summary{cursor:pointer;color:#999;font-size:11px}.wb-mcp-config pre{overflow:auto;font:11px/1.6 ui-monospace,monospace;background:#111;padding:10px;border-radius:8px}
98
+ @media(max-width:500px){dialog.wb-tools{width:calc(100vw - 16px);height:calc(100dvh - 32px)}.wb-tools-content{padding:10px}.wb-tools-nav{padding:6px;gap:0}.wb-tools-nav button{padding:6px 8px}.wb-request-grid{grid-template-columns:minmax(0,1fr) 48px 58px;gap:5px}.wb-request-grid>:last-child{display:none}.wb-request-grid{font-size:11px}.wb-metrics{gap:8px}.wb-metric strong{font-size:17px}.wb-route summary{gap:8px}.wb-route summary>.wb-muted:last-child{display:none}}
99
+ @media(min-width:1000px){dialog.wb-preview:has(>.wb-console:not([hidden])){display:grid;grid-template-columns:minmax(0,1fr) minmax(360px,40%);grid-template-rows:auto minmax(0,1fr) auto}.wb-preview-head{grid-column:1/-1}.wb-stage{grid-column:1;grid-row:2}.wb-preview-note{grid-column:1;grid-row:3}.wb-preview .wb-console{grid-column:2;grid-row:2/4;height:100%;min-height:0;border-top:0;border-left:1px solid #292929}}
100
+ @media(max-width:640px){.wb-preview-head{gap:6px;padding:7px 8px}.wb-dimensions{margin-left:auto}.wb-preview-actions{width:100%;justify-content:flex-end}.wb-preview-actions .wb-preview-reload{margin-right:auto}.wb .wb-frame-toggle{width:34px;padding:6px}.wb-frame-toggle span{display:none}}
101
+ @media(max-width:500px){.wb-console{left:8px;bottom:70px;width:calc(100vw - 16px)}.wb-line,.wb-entry summary{grid-template-columns:38px minmax(0,1fr) 14px;gap:8px}.wb-line time{display:none}.wb-entry-actions,.wb-entry-full{padding-left:12px}.wb-copy{max-width:70px}}
102
+ @container(max-width:359px){.wb-filter-row{gap:6px}.wb-ai-main [data-copy-label]{display:none}.wb-ai .wb-ai-main{min-width:34px;padding:6px}.wb-filter-options{gap:6px}.wb-filters select{flex:1;min-width:0;max-width:none}.wb-console-utilities button{padding:6px 8px}}
103
+ @media(pointer:coarse){.wb button,.wb input,.wb select{min-height:44px}.wb .wb-close,.wb .wb-icon,.wb .wb-popout,.wb-devices .wb-device{width:44px}.wb-frame-toggle span{display:none}}
104
+ @media(prefers-reduced-motion:no-preference){.wb button{transition:background-color 120ms,color 120ms}.wb-pause svg{transition:opacity 180ms cubic-bezier(.2,0,0,1),scale 180ms cubic-bezier(.2,0,0,1),filter 180ms cubic-bezier(.2,0,0,1)}.wb-tooltip{transition:opacity 100ms ease-out,translate 100ms ease-out}.wb-console{animation:wb-enter 130ms ease-out}@keyframes wb-enter{from{opacity:0;translate:0 4px}to{opacity:1;translate:0 0}}}
105
+ @media(forced-colors:active){.wb-console,dialog.wb-preview,.wb input,.wb select{border-color:ButtonText}.wb-frame-wrap[data-frame=true]::before{border-color:CanvasText}.wb-frame-wrap[data-frame=false] .wb-screen{outline:1px solid CanvasText;outline-offset:-1px}.wb-device-top::before,.wb-device-top::after,.wb-device-home,.wb-frame-wrap[data-device=tablet] .wb-device-top,.wb-frame-wrap[data-device=desktop] .wb-device-top i{background:CanvasText}.wb-device[aria-pressed=true]{outline:1px solid Highlight}.wb button:focus-visible,.wb input:focus-visible,.wb select:focus-visible,.wb summary:focus-visible,.wb [tabindex]:focus-visible{outline:2px solid Highlight;outline-offset:-3px}}
106
+ `;