diff --git a/plugin/chatgpt-extension/BUILD.bazel b/plugin/chatgpt-extension/BUILD.bazel
index 7970655e4b..5aed6ab7e1 100644
--- a/plugin/chatgpt-extension/BUILD.bazel
+++ b/plugin/chatgpt-extension/BUILD.bazel
@@ -21,7 +21,7 @@ tsc_bin.tsc_test(
vitest_bin.vitest_test(
name = "adapter_test",
data = [":sources", ":node_modules", "//sdks/typescript:dist"],
- args = ["run", "test/adapter.test.ts"],
+ args = ["run", "test/adapter.test.ts", "test/project-discovery.test.ts", "test/project-picker.test.ts"],
chdir = package_name(),
)
diff --git a/plugin/chatgpt-extension/README.md b/plugin/chatgpt-extension/README.md
index 4837c48211..b20ae3c27a 100644
--- a/plugin/chatgpt-extension/README.md
+++ b/plugin/chatgpt-extension/README.md
@@ -9,7 +9,8 @@ contracts. It adds no DTOs, database, policy store, or write API.
From a ChatGPT conversation or the plugin's global entrypoint you can:
-- Pick a project from the daemon's isolated profile registry.
+- Pick a recent project or search the daemon's isolated profile registry by
+ project path, alias, id, or default branch.
- Search code symbols by name.
- Inspect a symbol with its callers, callees, impact, and a bounded call
graph.
@@ -44,7 +45,7 @@ not-found-or-not-authorized problem rather than an empty success.
Inside the server a `DaemonBridge` reads through `@tracedecay/sdk` (the
workspace's generated operation contracts, never hand-edited):
-- Registry operations (`project_list`, `project_context`) use a projectless
+- Registry operations (`project_list`, `project_search`, `project_context`) use a projectless
`RegistryClient` over a profile-scoped `tracedecay serve` session; it takes
no base URL, project id, or token.
- MCP-transport project operations (`search`, `node`, `impact`,
@@ -64,12 +65,21 @@ workspace's generated operation contracts, never hand-edited):
- `tracedecay_workspace` (global UI entrypoint): daemon state + project list.
- `tracedecay_thread_panel` (thread/conversation UI entrypoint).
- `tracedecay_list_projects`: up to 10 recent registered projects (app-only).
+- `tracedecay_search_projects`: up to 10 matching registered projects
+ (app-only); an empty query returns to recent projects. Listings preserve
+ the daemon's limit and truncation signal. Summary counts describe the
+ returned page. Refine a search when more matches exist than are shown.
- `tracedecay_search_code`: symbol search, returns node ids, generation, and
freshness.
- `tracedecay_inspect_symbol`: symbol detail with callers/callees/impact,
bounded graph, provenance, and an `openai/deepLink` back into the app.
- `search_mentions`: `@`-mention search, where the host supports mentions.
+The picker keeps search available while a request is pending. Replacing a
+query or navigating cancels the prior MCP request; late responses cannot
+replace the current results or selected project. Mention search continues
+to read only four recent projects and up to eight symbols per project.
+
## Packaging
`plugin/plugin.json` + `plugin/mcp.json` are the portable Agent Plugins
@@ -89,12 +99,17 @@ staged bundle runs against the exact binary that installed it.
## Verification
-- `pnpm typecheck` runs `tsc --noEmit` over server, app, and tests.
+- `pnpm typecheck` runs `tsc --noEmit` over server and shared view types.
+- `pnpm exec vitest run test/adapter.test.ts test/project-discovery.test.ts
+ test/project-picker.test.ts test/serve-client.test.ts
+ test/bridge-lifecycle.test.ts` checks the extension adapters, project
+ discovery, DOM interactions, cancellation, and shutdown without a daemon.
- `pnpm test` runs the vitest integration suite against a real daemon,
covering project registration, search, symbol inspection, callers/callees,
denied/not-found/disconnected states, and the authority token never
- leaving the server. Requires `target/debug/tracedecay`
- (`cargo build -p tracedecay-cli --bin tracedecay`) or `TRACEDECAY_BIN`.
+ leaving the server, including discovery beyond the recent-project page.
+ Set `TRACEDECAY_BIN` to the CLI built with
+ `bazel build //crates/tracedecay-cli:tracedecay__test`.
- `pnpm test:browser` runs the Playwright suite against the real `embedded` app
inside a sandboxed iframe on the maintained `AppBridge` host. It covers
project connection, search, symbol inspection, graph browsing, sending
diff --git a/plugin/chatgpt-extension/embedded/app.html b/plugin/chatgpt-extension/embedded/app.html
index 80f6919e7e..45df05e6d1 100644
--- a/plugin/chatgpt-extension/embedded/app.html
+++ b/plugin/chatgpt-extension/embedded/app.html
@@ -4,15 +4,15 @@
TraceDecay Code Explorer
-
+
-
+- "app": Tool callable by the app from this server only`),csp:n0().optional(),permissions:n0().optional()}),n9({mimeTypes:n2(nm()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')}),n9({method:il("ui/download-file"),params:n9({contents:n2(n3([a2,a4])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})}),n9({method:il("ui/message"),params:n9({role:il("user").describe('Message role, currently only "user" is supported.'),content:n2(a9).describe("Message content blocks (text, image, etc.).")})}),n9({method:il("ui/notifications/sandbox-resource-ready"),params:n9({html:nm().describe("HTML content to load into the inner iframe."),sandbox:nm().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:ur.optional().describe("CSP configuration from resource metadata."),permissions:uo.optional().describe("Sandbox permissions from resource metadata.")})}),n9({method:il("ui/notifications/tool-result"),params:so.describe("Standard MCP tool execution result.")})),uy=n9({toolInfo:n9({id:iG.optional().describe("JSON-RPC id of the tools/call request."),tool:se.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:c6.optional().describe("Current color theme preference."),styles:ul.optional().describe("Style configuration for theming the app."),displayMode:c3.optional().describe("How the UI is currently displayed."),availableDisplayModes:n2(c3).optional().describe("Display modes the host supports."),containerDimensions:n3([n9({height:nL().describe("Fixed container height in pixels.")}),n9({maxHeight:n3([nL(),nZ()]).optional().describe("Maximum container height in pixels.")})]).and(n3([n9({width:nL().describe("Fixed container width in pixels.")}),n9({maxWidth:n3([nL(),nZ()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
+container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:nm().optional().describe("User's language and region preference in BCP 47 format."),timeZone:nm().optional().describe("User's timezone in IANA format."),userAgent:nm().optional().describe("Host application identifier."),platform:n3([il("web"),il("desktop"),il("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:n9({touch:nD().optional().describe("Whether the device supports touch input."),hover:nD().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:n9({top:nL().describe("Top safe area inset in pixels."),right:nL().describe("Right safe area inset in pixels."),bottom:nL().describe("Bottom safe area inset in pixels."),left:nL().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),uv=n9({method:il("ui/notifications/host-context-changed"),params:uy.describe("Partial context update containing only changed fields.")}),uS=(n9({method:il("ui/update-model-context"),params:n9({content:n2(a9).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:io(nm(),nY().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})}),n9({method:il("ui/initialize"),params:n9({appInfo:i7.describe("App identification (name and version)."),appCapabilities:uh.describe("Features and capabilities this app provides."),protocolVersion:nm().describe("Protocol version this app supports.")})}),n9({protocolVersion:nm().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:i7.describe("Host application identification and version."),hostCapabilities:up.describe("Features and capabilities provided by the host."),hostContext:uy.describe("Rich context about the host environment.")}).passthrough()),ub={target:"draft-2020-12"};async function u_(e,t){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[t](ub);throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function uw(e,t,r=""){let o=await e["~standard"].validate(t);if(o.issues)throw Error(r+o.issues.map(e=>{let t=e.path?.map(e=>"object"==typeof e?e.key:e).join(".");return t?`${t}: ${e.message}`:e.message}).join("; "));return o.value}class uR extends cL{_appInfo;_appCapabilities;options;_hostCapabilities;_hostInfo;_hostContext;_registeredTools={};_initializedSent=!1;_registeredEvents=new Set;_events=new c1;_methods=new c2;setRequestHandler=(...e)=>{let[t,...r]=c9("request",e)??e;this._methods.claim(t,"setRequestHandler"),super.setRequestHandler.call(this,t,...r)};setNotificationHandler=(...e)=>{let[t,...r]=c9("notification",e)??e;this._methods.claim(t,"setNotificationHandler"),super.setNotificationHandler.call(this,t,...r)};removeRequestHandler=e=>{this._methods.release(e),super.removeRequestHandler(e)};removeNotificationHandler=e=>{this._methods.release(e),super.removeNotificationHandler(e)};replaceRequestHandler=(e,...t)=>{this._methods.replace(e),super.setRequestHandler.call(this,e,...t)};_assertInitialized(e){if(this._initializedSent)return;let t=`[ext-apps] App.${e}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if(this.options?.strict)throw Error(t);console.warn(`${t}. This will throw in a future release.`)}eventSchemas={toolinput:un,toolinputpartial:ui,toolresult:ug,toolcancelled:ua,hostcontextchanged:uv};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(e){if(!uR.ONE_SHOT_EVENTS.has(e)||this._everHadListener.has(e)||(this._everHadListener.add(e),!this._initializedSent))return;let t=`[ext-apps] "${String(e)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if(this.options?.strict)throw Error(t);console.warn(t)}setEventHandler(e,t){t&&this._assertHandlerTiming(e),this._ensureEventSlot(e),this._events.setHandler(e,t)}addEventListener(e,t){this._assertHandlerTiming(e),this._ensureEventSlot(e),this._events.addEventListener(e,t)}removeEventListener(e,t){this._events.removeEventListener(e,t)}_ensureEventSlot(e){if(this._registeredEvents.has(e))return;let t=this.eventSchemas[e];if(!t)throw Error(`Unknown event: ${String(e)}`);this.setNotificationHandler(t.shape.method.value,{params:t.shape.params},t=>{"hostcontextchanged"===e&&(this._hostContext={...this._hostContext,...t}),this._events.dispatch(e,t)}),this._registeredEvents.add(e)}getEventHandler(e){return this._events.getHandler(e)}warnIfRequestHandlerReplaced(e,t,r){t&&r&&console.warn(`[MCP Apps] ${e} handler replaced. Previous handler will no longer be called.`)}constructor(e,t={},r={autoResize:!0}){super(r),this._appInfo=e,this._appCapabilities=t,this.options=r,this._methods.replace("notifications/cancelled"),this._methods.replace("notifications/progress"),r.allowUnsafeEval||eE({jitless:!0}),this.replaceRequestHandler("ping",e=>(console.log("Received ping:",e.params),{})),this.setEventHandler("hostcontextchanged",void 0)}buildContext(e){return e}registerAppCapabilities(e){var t;let r;if(this.transport)throw Error("Cannot register capabilities after transport is established");this._appCapabilities=(t=this._appCapabilities,r={...t,...e},void 0!==e.experimental&&(r.experimental={...t.experimental,...e.experimental}),void 0!==e.tools&&(r.tools={...t.tools,...e.tools}),r)}registerTool(e,t,r){if(this._registeredTools[e])throw Error(`Tool ${e} is already registered`);let o=this,n=()=>{o._initializedSent&&o._appCapabilities.tools?.listChanged&&o.sendToolListChanged()},i=void 0!==t.inputSchema,a={title:t.title,description:t.description,inputSchema:t.inputSchema,outputSchema:t.outputSchema,annotations:t.annotations,_meta:t._meta,enabled:!0,enable(){this.enabled=!0,n()},disable(){this.enabled=!1,n()},update(e){Object.assign(this,e),n()},remove(){o._registeredTools[e]===a&&(delete o._registeredTools[e],n())},handler:async(t,o)=>{let n;if(!a.enabled)throw Error(`Tool ${e} is disabled`);if(i){let i=a.inputSchema,s=i?await uw(i,t??{},`Invalid input for tool ${e}: `):t??{};n=await r(s,o)}else n=await r(o);return a.outputSchema&&!n.isError&&(n.structuredContent=await uw(a.outputSchema,n.structuredContent,`Invalid output for tool ${e}: `)),n}};return this._registeredTools[e]=a,this._appCapabilities.tools||this.transport||this.registerAppCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),n(),a}_toolHandlersInitialized=!1;ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(e,t)=>{let r=this._registeredTools[e.name];if(!r)throw Error(`Tool ${e.name} not found`);return r.handler(e.arguments,t)},this.onlisttools=async(e,t)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([e,t])=>t.enabled).map(async([e,t])=>{let r={name:e,title:t.title,description:t.description,inputSchema:t.inputSchema?await u_(t.inputSchema,"input"):{type:"object",properties:{}}};return t.outputSchema&&(r.outputSchema=await u_(t.outputSchema,"output")),t.annotations&&(r.annotations=t.annotations),t._meta&&(r._meta=t._meta),r}))}))}async sendToolListChanged(e={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:e})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(e){this.setEventHandler("toolinput",e)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(e){this.setEventHandler("toolinputpartial",e)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(e){this.setEventHandler("toolresult",e)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(e){this.setEventHandler("toolcancelled",e)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(e){this.setEventHandler("hostcontextchanged",e)}_onteardown;get onteardown(){return this._onteardown}set onteardown(e){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,e),this._onteardown=e,this.replaceRequestHandler("ui/resource-teardown",{params:uc.shape.params,result:uu},(e,t)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(e,t)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(e){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,e),this._oncalltool=e,this.replaceRequestHandler("tools/call",(e,t)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(e.params,t)})}_onlisttools;get onlisttools(){return this._onlisttools}set onlisttools(e){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,e),this._onlisttools=e,this.replaceRequestHandler("tools/list",(e,t)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(e.params,t)})}assertCapabilityForMethod(e){if("sampling/createMessage"===e&&!this._hostCapabilities?.sampling)throw Error(`Host does not support sampling (required for ${e})`)}assertRequestHandlerCapability(e){switch(e){case"tools/call":case"tools/list":if(!this._appCapabilities.tools)throw Error(`Client does not support tool capability (required for ${e})`);return;default:return}}assertNotificationCapability(e){}async callServerTool(e,t){if(this._assertInitialized("callServerTool"),"string"==typeof e)throw Error(`callServerTool() expects an object as its first argument, but received a string ("${e}"). Did you mean: callServerTool({ name: "${e}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:e},{onprogress:()=>{},resetTimeoutOnProgress:!0,...t})}async readServerResource(e,t){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:e},t)}async listServerResources(e,t){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:e},t)}async createSamplingMessage(e,t){return this._assertInitialized("createSamplingMessage"),await this.request({method:"sampling/createMessage",params:e},t)}sendMessage(e,t){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:e},ut,t)}sendLog(e){return this.notification({method:"notifications/message",params:e})}updateModelContext(e,t){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:e},i4,t)}openLink(e,t){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:e},c7,t)}sendOpenLink=this.openLink;downloadFile(e,t){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:e},ue,t)}requestTeardown(e={}){return this.notification({method:"ui/notifications/request-teardown",params:e})}requestDisplayMode(e,t){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:e},um,t)}sendSizeChanged(e){return this.notification({method:"ui/notifications/size-changed",params:e})}setupSizeChangedNotifications(){let e=!1,t=0,r=0,o=()=>{e||(e=!0,requestAnimationFrame(()=>{e=!1;let o=document.documentElement,n=o.style.height;o.style.height="max-content";let i=Math.ceil(o.getBoundingClientRect().height);o.style.height=n;let a=Math.ceil(window.innerWidth);(a!==t||i!==r)&&(t=a,r=i,this.sendSizeChanged({width:a,height:i}))}))};o();let n=new ResizeObserver(o);return n.observe(document.documentElement),n.observe(document.body),()=>n.disconnect()}async connect(e=new c5(window.parent,window.parent),t){if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1;try{await super.connect(e);let r=await this.request({method:"ui/initialize",params:{appCapabilities:this._appCapabilities,appInfo:this._appInfo,protocolVersion:"2026-01-26"}},uS,t);if(void 0===r)throw Error(`Server sent invalid initialize result: ${r}`);this._hostCapabilities=r.hostCapabilities,this._hostInfo=r.hostInfo,this._hostContext=r.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(e){throw this.close(),e}}}let uk=new Set(["projects","search","symbol","failure"]);function uE(e){switch(e.kind){case"projects":return"/";case"project":return`/projects/${encodeURIComponent(e.project_id)}`;case"search":return`/projects/${encodeURIComponent(e.project_id)}/search?q=${encodeURIComponent(e.query)}`;case"symbol":return`/projects/${encodeURIComponent(e.project_id)}/symbols/${encodeURIComponent(e.node_id)}`}}let ux=document.getElementById("root");if(null===ux)throw Error("app root element missing");let uT=new uR({name:"tracedecay-code-explorer",version:"0.0.0"},{availableDisplayModes:["inline","fullscreen"]}),uP={phase:"connecting"},uC=[],uA=null,uO=null,uI=null;function u$(){let e=uO;uO=null,e?.abort()}function uz(e){uP=e,u6()}function uq(){return"view"===uP.phase?uP.view:"loading"===uP.phase?uP.previous:null}function uj(e){u$(),uI="projects"===e.page?e.query:null,"projects"===e.page&&(uC="ready"===e.projects.state?e.projects.data.projects:[]),uz({phase:"view",view:e}),uH(e)}function uN(e){var t;return"object"==typeof(t=e.structuredContent)&&null!==t&&"page"in t&&"string"==typeof t.page&&uk.has(t.page)?e.structuredContent:null}async function uM(e,t,r,o){u$();let n=new AbortController;uO=n,void 0!==o&&(uI=o),uz({phase:"loading",label:r,previous:uq(),projectQuery:o});try{let r=await uT.callServerTool({name:e,arguments:t},{signal:n.signal});if(uO!==n)return;uO=null;let o=uN(r);if(null===o)return void uz({phase:"host_error",message:`${e} returned no view payload`});uj(o)}catch(e){if(uO!==n)return;uO=null,uz({phase:"host_error",message:e instanceof Error?e.message:String(e)})}}async function uH(e){let t=function(e){switch(e.page){case"projects":return null;case"search":var t;return{content:[{type:"text",text:`TraceDecay search in ${e.project.label} for "${e.query}": ${function(e,t){switch(e.state){case"ready":return t(e.data);case"empty":return e.message;case"failed":return`${e.failure.kind}: ${e.failure.message}`}}(e.results,e=>`${e.hits.length} hit(s), recall ${e.recall}${e.undisplayable.length>0?`, ${e.undisplayable.length} undisplayable`:""}`)}${null===(t=e.provenance)?"":` [reported project status: branch ${t.branch??"?"} commit ${t.commit??"?"} generation ${t.generation??"?"} freshness ${t.freshness.state}]`}`}],structuredContent:{page:"search",project_id:e.project.project_id,query:e.query,deep_link:uE({kind:"search",project_id:e.project.project_id,query:e.query}),provenance:e.provenance}};case"symbol":if(null===e.evidence)return null;return{content:[{type:"text",text:e.evidence.markdown}],structuredContent:e.evidence.structured};case"failure":return{content:[{type:"text",text:`TraceDecay could not answer (${e.failure.kind}/${e.failure.code}): ${e.failure.message}`}],structuredContent:{page:"failure",failure:e.failure}}}}(e);if(null!==t)try{await uT.updateModelContext(t)}catch{}}function uL(){uM("tracedecay_workspace",{},"Loading registered projects…","")}function uJ(e){let t=e.trim();uM("tracedecay_search_projects",{query:t},""===t?"Loading recent projects…":`Searching projects for "${t}"…`,t)}function uU(e){uj({page:"search",project:e,query:"",results:{state:"empty",message:"Type a symbol name or phrase to search this project."},provenance:null})}function uW(e,t){uM("tracedecay_search_code",{project_id:e,query:t},`Searching for "${t}"…`)}function uD(e,t){uM("tracedecay_inspect_symbol",{project_id:e,node_id:t},"Loading symbol…")}function uK(e){let t,r="string"==typeof(t=Reflect.get(e,"openai/deepLink"))?t:"object"==typeof t&&null!==t&&"url"in t&&"string"==typeof t.url?t.url:null;if(null===r||r===uA)return;uA=r;let o=function(e){let t;try{t=new URL(e,"https://tracedecay.invalid")}catch{return null}let r=t.pathname.split("/").filter(e=>e.length>0).map(decodeURIComponent);if(0===r.length)return{kind:"projects"};if("projects"!==r[0]||void 0===r[1])return null;let o=r[1];return 2===r.length?{kind:"project",project_id:o}:"search"===r[2]&&3===r.length?{kind:"search",project_id:o,query:t.searchParams.get("q")??""}:"symbols"===r[2]&&void 0!==r[3]&&4===r.length?{kind:"symbol",project_id:o,node_id:r[3]}:null}(r);null!==o&&function(e){switch(e.kind){case"projects":uL();return;case"project":{let t=uC.find(t=>t.project_id===e.project_id);void 0!==t?uU(t):uW(e.project_id,"");return}case"search":uW(e.project_id,e.query);return;case"symbol":uD(e.project_id,e.node_id);return}}(o)}function uZ(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=document.createElement(e);for(let[e,r]of Object.entries(t))o.setAttribute(e,r);return o.append(...r),o}function uF(e,t){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=uZ("button",{type:"button",...r},[e]);return o.addEventListener("click",t),o}function uB(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"neutral",r=arguments.length>2?arguments[2]:void 0,o={class:"neutral"===t?"badge":`badge ${t}`};return void 0!==r&&(o["data-testid"]=r),uZ("span",o,[e])}function uV(e){return"stale"===e.kind||"unavailable"===e.kind?"warn":"bad"}function uG(e){switch(e){case"current":case"fresh":return"ok";case"stale":case"possibly_stale":case"warming":case"restoring":return"warn";case"parked":case"unavailable":return"bad"}}function uX(e,t,r){return uZ("p",{class:"neutral"===t?"notice":`notice ${t}`,"data-testid":r},[e])}function uY(e,t){let r=uX(`${e.message} (${e.code})`,uV(e),t);return r.prepend(uB(e.kind,uV(e),`${t}-kind`)," "),r}function uQ(e,t,r){switch(e.state){case"ready":return r(e.data);case"empty":return uX(e.message,"neutral",`${t}-empty`);case"failed":return uY(e.failure,`${t}-failed`)}}function u0(e,t,r){let o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:[];return uZ("section",{class:"panel","data-testid":t},[uZ("h2",{},[e,...o]),...r])}function u1(e,t){return uF(t.qualified_name||t.name,()=>uD(e,t.node_id),{class:"mono","data-testid":"symbol-link","data-node-id":t.node_id})}function u2(e){let t=null===e.start_line?"":`:${e.start_line}`;return uZ("span",{class:"loc"},[`${e.file}${t}`])}function u4(e,t){let r,o,n,i="connected"===e.daemon.state?uZ("p",{class:"notice","data-testid":"daemon-state"},[uB("connected","ok","daemon-badge"),` tracedecay ${e.daemon.version} (pid ${e.daemon.pid}) \xb7 profile `,uZ("code",{},[e.daemon.profile_root])]):uY(e.daemon.failure,"daemon-state"),a=uQ(e.projects,"projects",t=>{let r=uZ("ul",{class:"list"},t.projects.map(e=>uZ("li",{"data-testid":"project-row"},[uF(e.label,()=>uU(e),{class:"primary","data-testid":"project-open","data-project-id":e.project_id}),uZ("span",{class:"loc"},[e.project_root]),null===e.head_branch?"":uB(e.head_branch)]))),o=`${t.projects.length} project(s) shown (up to ${t.limit}).`;return uZ("div",{},[uX(t.truncated?`${o} More ${""===e.query?"registered projects are available; search to find projects outside this recent list":"matches are available; refine your search to find a specific project"}.`:`${o} ${""===e.query?"All registered projects are shown":"All matching projects are shown"}.`,t.truncated?"warn":"neutral","projects-completeness"),r])}),s=u0(""===e.query?"Recent registered projects":`Projects matching "${e.query}"`,"projects-panel",[a]);return[i,(r=uZ("input",{type:"search",name:"project-query",placeholder:"Find a registered project…",value:uI??t??e.query,"data-testid":"project-search-input","aria-label":"Find registered projects"}),o=()=>uJ(r.value),n=uZ("form",{class:"search project-search","data-testid":"project-search-form",role:"search","aria-label":"Registered projects"},[r,uF("Find projects",o,{class:"primary","data-testid":"project-search-submit"}),uF("Recent projects",()=>uJ(""),{"data-testid":"project-search-recent"})]),r.addEventListener("input",()=>{if(uI=r.value,"loading"===uP.phase&&void 0!==uP.projectQuery&&uP.previous?.page==="projects"){let e=uP.previous;u$(),uz({phase:"view",view:e})}}),r.addEventListener("keydown",e=>{"Enter"===e.key&&(e.preventDefault(),o())}),n.addEventListener("submit",e=>e.preventDefault()),n),...void 0===t?[]:[uZ("p",{class:"loading",role:"status","data-testid":"loading"},[`${"loading"===uP.phase?uP.label:"Loading projects…"} Previous results remain below.`])],s,uZ("p",{class:"notice"},["Search registered project paths, aliases, IDs, or default branches. Only projects registered in this TraceDecay profile (via `tracedecay init`) can be explored."])]}function u9(e,t){let r=uZ("input",{type:"search",name:"q",placeholder:`Search ${e.label}…`,value:t,"data-testid":"search-input","aria-label":"Search query"}),o=()=>{let t=r.value.trim();t.length>0&&uW(e.project_id,t)},n=uZ("form",{class:"search","data-testid":"search-form"},[r,uF("Search",o,{class:"primary","data-testid":"search-submit"})]);return r.addEventListener("keydown",e=>{"Enter"===e.key&&(e.preventDefault(),o())}),n.addEventListener("submit",e=>e.preventDefault()),n}function u5(e,t){if(null===e)return u0("Project status","provenance",[uX("Project status is reported once the project answers a request.","neutral","provenance-pending")]);let r=[["project",uZ("code",{},[e.project_id])],["worktree",uZ("code",{},[e.worktree_id??e.project_root])],["repository",uZ("code",{},[e.repository_id??"—"])],["ref",e.reference??e.branch??"—"],["commit",uZ("code",{"data-testid":"provenance-commit"},[e.commit??"—"])],["generation",uZ("code",{},[e.generation??"—"])],["coverage",uZ("span",{},[uB(e.coverage.recall,"full"===e.coverage.recall?"ok":"warn","coverage-badge"),null===e.coverage.detail?"":` ${e.coverage.detail}`])],["daemon",null===e.authority?"unavailable":`${e.authority.daemon_version} \xb7 pid ${e.authority.daemon_pid}`],["deep link",uZ("code",{"data-testid":"deep-link"},[uE(t)])]],o=uZ("dl",{class:"prov"},r.flatMap(e=>{let[t,r]=e;return[uZ("dt",{},[t]),uZ("dd",{},[r])]})),n=uB(e.freshness.state,uG(e.freshness.state),"freshness-badge"),i=[o];return null!==e.freshness.detail&&i.unshift(uX(e.freshness.detail,"ok"===uG(e.freshness.state)?"neutral":"warn","freshness-detail")),u0("Project status (separate read)","provenance",i,[n])}function u6(){let e=document.activeElement,t=e instanceof HTMLElement&&null!==e.closest('[data-testid="project-search-form"]')?e.dataset.testid:void 0,r=e instanceof HTMLInputElement&&"project-search-input"===e.dataset.testid?e:null,o=null===r?null:[r.selectionStart,r.selectionEnd,r.selectionDirection];if(ux.replaceChildren(function(e){let t=uZ("nav",{class:"crumbs","aria-label":"Breadcrumb"});t.append(uF("Projects",uL,{"data-testid":"crumb-projects"}));let r=null!==e&&"projects"!==e.page?e.project:null;null!==r&&t.append("/",uF(r.label,()=>uU(r),{"data-testid":"crumb-project"})),e?.page==="search"&&""!==e.query&&t.append("/",uZ("span",{},[`"${e.query}"`])),e?.page==="symbol"&&"ready"===e.symbol.state&&t.append("/",uZ("span",{class:"mono"},[e.symbol.data.name]));let o=uZ("header",{class:"bar"},[uZ("h1",{},["TraceDecay"]),t,uZ("span",{class:"spacer"})]),n=uT.getHostContext()?.displayMode;if(void 0!==n){let e="fullscreen"===n?"inline":"fullscreen";o.append(uF("fullscreen"===e?"Expand":"Collapse",()=>void uT.requestDisplayMode({mode:e}),{"data-testid":"toggle-display-mode"}))}return o}(uq()),...function(){switch(uP.phase){case"connecting":return[uZ("p",{class:"loading","data-testid":"connecting"},["Connecting to ChatGPT…"])];case"loading":if(uP.previous?.page==="projects"&&void 0!==uP.projectQuery)return u4(uP.previous,uP.projectQuery);return[uZ("p",{class:"loading","data-testid":"loading"},[uP.label])];case"host_error":return[uY({kind:"protocol",code:"host_call_failed",message:uP.message},"host-error"),uZ("div",{class:"actions"},[uF("Back to projects",uL,{class:"primary","data-testid":"back-to-projects"})])];case"view":var e,t,r;switch(uP.view.page){case"projects":return u4(uP.view);case"search":let o;return o=uQ((e=uP.view).results,"results",t=>{let r=[uZ("ul",{class:"list"},t.hits.map(t=>uZ("li",{"data-testid":"search-hit"},[uB(t.kind),u1(e.project.project_id,t),u2(t)])))];if("partial"===t.recall&&r.push(uX("Partial recall: the index is still converging, so some matches may be missing.","warn","results-partial")),t.truncated&&r.push(uX("Results were truncated to the first page.","neutral","results-truncated")),t.undisplayable.length>0){let e=[...new Set(t.undisplayable)].join(", ");r.push(uX(`${t.undisplayable.length} candidate(s) could not be displayed (${e}).`,"warn","results-undisplayable"))}return"ready"===e.results.state&&void 0!==e.results.generation&&e.provenance?.generation!==void 0&&e.results.generation!==e.provenance.generation&&r.push(uX(`Results were served from generation ${e.results.generation}, which differs from the reported project generation ${e.provenance.generation}.`,"warn","results-generation")),uZ("div",{},r)}),[u9(e.project,e.query),u0(""===e.query?"Search":`Results for "${e.query}"`,"results-panel",[o]),u5(e.provenance,{kind:"search",project_id:e.project.project_id,query:e.query})];case"symbol":let n,i,a,s,l,c,u;return n=(t=uP.view).project.project_id,i=uQ(t.symbol,"symbol",e=>{let t=[uZ("p",{},[uB(e.kind)," ",uZ("code",{"data-testid":"symbol-qualified-name"},[e.qualified_name])," ",u2(e)])];return null!==e.signature&&t.push(uZ("pre",{class:"mono"},[e.signature])),null!==e.complexity&&t.push(uZ("p",{class:"notice"},[`Cyclomatic complexity ${e.complexity}`])),e.unavailable_fields.length>0&&t.push(uX(`Not available for this symbol: ${e.unavailable_fields.join(", ")}`,"neutral","symbol-unavailable-fields")),uZ("div",{},t)}),a=(e,r)=>uQ(e,r,o=>{let i=[uZ("ul",{class:"list"},o.map(e=>uZ("li",{"data-testid":`${r}-row`},[uB(e.edge_kind),u1(n,e.symbol),u2(e.symbol),e.dispatch_via_trait?uB("via trait"):""])))];return"ready"===e.state&&!0===e.truncated&&i.push(uX("Only the first page of relations is shown.","neutral",`${r}-truncated`)),"ready"===e.state&&void 0!==e.generation&&t.provenance?.generation!==void 0&&e.generation!==t.provenance.generation&&i.push(uX(`Served from generation ${e.generation}, which differs from the reported project generation ${t.provenance.generation}.`,"warn",`${r}-generation`)),uZ("div",{},i)}),s=uQ(t.impact,"impact",e=>{let t=[uZ("ul",{class:"list"},e.nodes.map(e=>uZ("li",{"data-testid":"impact-row"},[uB(`depth ${e.depth}`),uF(e.name,()=>uD(n,e.node_id),{class:"mono"}),uZ("span",{class:"loc"},[`${e.file}:${e.line}`])])))];return t.push(e.complete?uX(`Complete within depth ${e.max_depth}.`,"neutral","impact-complete"):uX(`Partial: traversal stopped at depth ${e.max_depth}.`,"warn","impact-partial")),uZ("div",{},t)}),l=uQ(t.graph,"graph",e=>(function(e,t){let r=t.nodes.filter(e=>"caller"===e.role),o=t.nodes.filter(e=>"callee"===e.role),n=t.nodes.find(e=>"focus"===e.role),i=28*Math.max(r.length,o.length,1)+20,a="http://www.w3.org/2000/svg",s=document.createElementNS(a,"svg");s.setAttribute("class","graph"),s.setAttribute("viewBox",`0 0 640 ${i}`),s.setAttribute("role","img"),s.setAttribute("aria-label","Bounded caller/callee graph"),s.dataset.testid="graph";let l={caller:10,focus:220,callee:430},c=new Map,u=(t,r,o)=>{let n=l[o],u="focus"===o?i/2-10:10+28*r;c.set(t.node_id,{x:n,y:u});let d=document.createElementNS(a,"g");d.setAttribute("class",`node ${t.role}`),d.dataset.testid="graph-node",d.dataset.nodeId=t.node_id;let p=document.createElementNS(a,"rect");p.setAttribute("x",String(n)),p.setAttribute("y",String(u)),p.setAttribute("width","200"),p.setAttribute("height","20"),p.setAttribute("rx","4");let h=document.createElementNS(a,"text");h.setAttribute("x",String(n+6)),h.setAttribute("y",String(u+14)),h.textContent=t.name.length>28?`${t.name.slice(0,27)}…`:t.name;let m=document.createElementNS(a,"title");m.textContent=`${t.qualified_name} (${t.file})`,d.append(m,p,h),"focus"!==t.role&&d.addEventListener("click",()=>uD(e,t.node_id)),s.append(d)};for(let e of(r.forEach((e,t)=>u(e,t,"caller")),o.forEach((e,t)=>u(e,t,"callee")),void 0!==n&&u(n,0,"focus"),t.edges)){let t=c.get(e.from),r=c.get(e.to);if(void 0===t||void 0===r)continue;let o=document.createElementNS(a,"path");o.setAttribute("class","edge"),o.setAttribute("d",`M ${t.x+200} ${t.y+10} C ${t.x+240} ${t.y+10}, ${r.x-40} ${r.y+10}, ${r.x} ${r.y+10}`),s.prepend(o)}let d=uZ("div",{},[s]);return t.truncated&&d.append(uX(`Graph bounded to depth ${t.max_depth}; more relations exist than shown.`,"neutral","graph-truncated")),d})(n,e)),c=uZ("div",{class:"actions"}),null!==(u=t.evidence)&&c.append(uF("Send evidence to chat",()=>void uT.sendMessage({role:"user",content:[{type:"text",text:u.markdown}]}),{class:"primary","data-testid":"send-evidence"}),uF("Re-share with model",()=>void uH(t),{"data-testid":"share-context"})),[u9(t.project,""),u0("Symbol","symbol-panel",[i,uX("Symbol and impact reads do not report a generation; their snapshot identity is unverified.","neutral","symbol-generation"),c]),u0("Callers","callers-panel",[a(t.callers,"callers")]),u0("Callees","callees-panel",[a(t.callees,"callees")]),u0("Impact","impact-panel",[s]),u0("Graph","graph-panel",[l]),u5(t.provenance,"ready"===t.symbol.state?{kind:"symbol",project_id:n,node_id:t.symbol.data.node_id}:{kind:"project",project_id:n})];case"failure":let d;return d=[uY((r=uP.view).failure,"page-failure")],"denied"===r.failure.kind&&d.push(uX("Only projects registered in this TraceDecay profile can be explored. Run `tracedecay init` in the repository and reopen the workspace.","neutral","denied-help")),"disconnected"===r.failure.kind&&d.push(uX("Start the daemon with `tracedecay daemon run` (or any tracedecay command) and retry.","neutral","disconnected-help")),d.push(uZ("div",{class:"actions"},[uF("Back to projects",uL,{class:"primary","data-testid":"back-to-projects"})])),null!==r.project&&d.unshift(u9(r.project,"")),[u0("Request failed","failure-panel",d)]}}}()),ux.dataset.phase=uP.phase,ux.dataset.page=uq()?.page??"",void 0!==t){let e=ux.querySelector(`[data-testid="${t}"]`);e?.focus({preventScroll:!0}),e instanceof HTMLInputElement&&null!==o&&e.setSelectionRange(...o)}}uT.ontoolinput=e=>{u$(),uz({phase:"loading",label:`Loading ${e.arguments?.query!==void 0?`search for "${String(e.arguments.query)}"`:"from TraceDecay"}…`,previous:uq()})},uT.ontoolresult=e=>{u$();let t=uN(e);null===t?uz({phase:"host_error",message:"The tool result carried no TraceDecay view payload."}):uj(t)},uT.onhostcontextchanged=e=>{uK(e),u6()},uT.onteardown=async()=>(u$(),{}),window.addEventListener("pagehide",u$),u6(),uT.connect().then(()=>{let e=uT.getHostContext();void 0!==e&&uK(e),"connecting"===uP.phase&&uz({phase:"loading",label:"Waiting for TraceDecay…",previous:null})}).catch(e=>uz({phase:"host_error",message:e instanceof Error?e.message:String(e)}))})()})();