Investigated extension command registration and UI notification.
UnderstandBound UI contexts on startup for reload support.
BuildUpdated changelog with the fix.
EditRan tests and pushed fix to main branch.
Verifyi had you write a trivial extension in another session to .pi/extensions/test.ts (no longer there). it registered a command /test that would use ui.notify().
when i started the session, test.ts didn'T exist yet, you created it. then i did /reload. the /test command got registered, but running it did not show the ui notification. can you figure out why that is?
The user is describing an issue with a pi extension. They created a test extension that registered a /test command using ui.notify(), but when they ran the command after /reload, the notification didn't show.
Let me investigate how extensions work in pi, particularly around UI notifications and command registration. I should look at:
ui.notify() worksLet me start by reading the extension docs and related code.
Let me investigate how extensions and UI notifications work in pi.
Let me look at the examples directory and understand how commands and notifications work.
>ls -la /Users/badlogic/workspaces/pi-mono/packages/coding-agent/examples/extensions/Let me look at the notify.ts example to see how notifications work. Also let me look at how command registration works.
These examples show tools and OSC notifications. Let me look at how commands are registered and how ctx.ui.notify works. Let me check the command registration code and the UI types.
Let me look at how commands work and how ctx.ui.notify is implemented:
>rg -l "registerCommand" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src --type ts>rg -l "ui\.notify" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src --type tsui.notify is not found directly. Let me look at how the UI is provided in the extension context and where notify is implemented.
>rg "notify" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src --type ts -lLet me look at the extension runner to see how the context is passed to commands and how ui.notify is implemented.
I see the noOpUIContext which is used when there's no UI. Now let me understand the flow better. The ExtensionRunner has a setUIContext method that sets the UI context. Let me look at how extensions are loaded and when the UI context is set vs when commands are registered.
Let me look at the extension loader to understand the order of operations.
Now I understand the loading process. Let me look at how the interactive mode sets up the UI context and when commands are run. The key issue is:
setUIContext is called later by the mode (interactive mode) to provide the actual UI implementationctx.ui - but at registration time, this might be the noOpUIContextLet me look at how commands are executed to see if there's a closure issue.
>rg "handler.*ctx" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/extensions/types.ts -A 5 -B 5I see that commands receive ctx as a parameter in their handler. Let me look at how the command is executed, specifically when the ctx is created and passed to the handler. Let me look at how interactive mode handles commands.
>rg "getCommand|executeCommand" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src --type ts -lLet me check how commands are executed in agent-session.ts, since that seems to be where command execution happens.
>rg "getCommand" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts -A 20 -B 5I see! When a command is executed, ctx is created via this._extensionRunner.createCommandContext(). Let me look at createCommandContext() in runner.ts more closely.
>rg "createCommandContext|createContext" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/extensions/runner.ts -A 20 -B 2I see that createContext() is called at command execution time and it uses this.uiContext. The issue is:
createContext(): ExtensionContext { const getModel = this.getModel; return { ui: this.uiContext, // <-- This captures the value at call time ... };}The context captures this.uiContext at the time createContext() is called. So when the command runs, it gets the current UI context.
Let me check when setUIContext is called in the interactive mode. The issue could be:
/reload loads extensions but doesn't re-set the UI contextLet me look at the /reload implementation:
>rg "/reload" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src --type ts -lLet me look at how /reload is handled in interactive-mode.ts.
>rg "reload" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 30 -B 5Let me look at the session.reload() method to see what happens during reload:
>rg "async reload" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts -A 50Let me check the _buildRuntime method to see if it sets up the UI context properly after reload:
>rg "_buildRuntime" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts -A 80Now let me look at _applyExtensionBindings to understand what happens:
>rg "_applyExtensionBindings" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts -A 20 -B 5Now I see the issue! The _applyExtensionBindings function calls runner.setUIContext(this._extensionUIContext). This sets the UI context on the extension runner.
But let me check what this._extensionUIContext is set to. It should be set by bindExtensions():
>rg "_extensionUIContext" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts -A 5 -B 5I see the flow now. Let me trace it:
session.bindExtensions({ uiContext: ... }) which stores the UI context in _extensionUIContext and applies it to the runner/reload, session.reload() is called which:
_buildRuntime() which creates a NEW ExtensionRunner_applyExtensionBindings(runner) which sets runner.setUIContext(this._extensionUIContext)So the flow seems correct. Let me look more closely at the interactive mode's handleReloadCommand to see if maybe the issue is with how the UI context is created.
Actually, wait. Let me look at what _extensionUIContext contains. This is set by bindExtensions({ uiContext: ... }). Let me see what the interactive mode passes:
>rg "bindExtensions" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 100 -B 5I see! Let me look at createExtensionUIContext():
>rg "createExtensionUIContext" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 80 -B 2Now I see the issue more clearly. Let me look at showExtensionNotify:
>rg "showExtensionNotify" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 20 -B 2I see! The showExtensionNotify just calls this.showStatus(message) for info notifications. Let me look at showStatus:
>rg "showStatus" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 15 -B 2I see. showStatus adds a Text component to the chatContainer. This seems fine. Let me trace through the code once more.
When a command is executed:
_extensionRunner.getCommand(commandName) gets the command_extensionRunner.createCommandContext() creates the context with this.uiContext (current UI context)command.handler(args, ctx) is calledSo when /reload is executed:
handleReloadCommand() is called which calls this.session.reload()reload() creates a NEW ExtensionRunner via _buildRuntime()_buildRuntime() then calls _applyExtensionBindings(runner) which calls runner.setUIContext(this._extensionUIContext)So the UI context IS set on the new runner. But wait, let me check the noOpUIContext usage again:
const noOpUIContext: ExtensionUIContext = { select: async () => undefined, confirm: async () => false, input: async () => undefined, notify: () => {}, // <-- This is a no-op! ...};And in ExtensionRunner:
constructor(...) { this.uiContext = noOpUIContext; // Default to no-op ...}Then setUIContext is called to set the real UI context. This seems fine.
Let me check if maybe the issue is with how runner.setUIContext(this._extensionUIContext) works when _extensionUIContext is undefined:
>rg "setUIContext" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/extensions/runner.ts -A 5 -B 2I see it now!
setUIContext(uiContext?: ExtensionUIContext): void { this.uiContext = uiContext ?? noOpUIContext;}If uiContext is undefined, it falls back to noOpUIContext. And in _applyExtensionBindings:
private _applyExtensionBindings(runner: ExtensionRunner): void { runner.setUIContext(this._extensionUIContext); // Could be undefined! ...}The _extensionUIContext is only set when bindExtensions() is called. Let me check what happens on startup vs reload. The issue might be that _extensionUIContext is not set properly.
Actually wait, let me re-read bindExtensions:
async bindExtensions(bindings: ExtensionBindings): Promise<void> { if (bindings.uiContext !== undefined) { this._extensionUIContext = bindings.uiContext; } ...}So _extensionUIContext is set once by interactive mode during initialization. And when /reload happens, _applyExtensionBindings passes the stored _extensionUIContext to the new runner. This should work.
Let me check if maybe there's an issue with the interactive mode's createExtensionUIContext:
>rg "createExtensionUIContext" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -B 5 -A 50 | head -100I notice something interesting. The createExtensionUIContext() creates a NEW object each time it's called. In the setupExtensionShortcuts function, for shortcuts, it creates a fresh context with this.createExtensionUIContext(). But for the bindExtensions call, it only happens once during initialization.
Let me trace through what happens when:1. pi starts2. user creates test.ts extension
3. user does /reload
4. the extension is loaded
5. user runs /testAt step 4, the extension is loaded via _buildRuntime(). The _applyExtensionBindings() is called which sets the UI context on the runner.At step 5, when the /test command runs:
_extensionRunner.getCommand("test") returns the command_extensionRunner.createCommandContext() creates the context with this.uiContext- The handler is called with the contextThe createCommandContext() returns { ...this.createContext(), ... } which has ui: this.uiContext. And this.uiContext is set to the value from setUIContext().
Hmm, this should work. Let me check if maybe the issue is that the extension was created before /reload was called, and the extension stores a reference to the UI context somewhere.
Actually, wait! Looking back at the user's description: "test.ts didn't exist yet, you created it. then i did /reload." So:1. Pi started (no test.ts extension)
2. I (the agent) created test.ts
3. User did /reload
4. Extension was loaded
5. User ran /test
Let me verify this by looking at what happens during extension loading. The createExtensionAPI function creates the API that the extension uses. When the extension calls pi.registerCommand(), it stores the command. But the command's handler is a function that the extension provides, and it takes ctx as a parameter.Let me look again:
Let me look at how the command handler receives its context. The key issue is understanding if the ctx.ui that the command receives is properly bound to the real UI context after /reload.Let me create a test to see what's happening:
Let me check the exact flow by looking at how the command gets executed after reload:
>rg "tryExecuteExtensionCommand" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src --type ts -A 30I see the flow:1. _tryExecuteExtensionCommand is called2. It gets the command from this._extensionRunner.getCommand(commandName)3. It creates context via this._extensionRunner.createCommandContext()4. The context includes ui: this.uiContext from the runnerSo when /reload happens:1. A new ExtensionRunner is created in _buildRuntime2. _applyExtensionBindings is called which sets runner.setUIContext(this._extensionUIContext)3. Commands are now available on the new runner4. When /test runs, the context is created with the runner's uiContext
This should work. Let me check if _extensionUIContext itself might be the problem. Maybe it's getting garbage collected or something? Or maybe there's a race condition.
Wait, I need to check something. Let me look at how the command execution flows from the interactive mode:
>rg "tryExecuteExtensionCommand\|/test\|isExtensionCommand" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 10 -B 3Let me search for how commands are handled in the interactive mode:
>rg "handleInput\|handleSubmit\|session\.prompt" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 10 -B 3 | head -100Let me look for where input is submitted in the interactive mode:
>rg "onSubmit\|sendPrompt\|prompt\(" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 10 -B 3 | head -150Let me search in a different way:
>rg "isExtensionCommand" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src --type ts -A 10 -B 3I see that extension commands are executed via this.session.prompt(text). Let me look at how prompt handles commands:
>rg "async prompt\(" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/agent-session.ts -A 50 | head -80I see the flow now:1. prompt() is called with /test
2. _tryExecuteExtensionCommand() is called which:
createCommandContext()command.handler(args, ctx)Let me add some debug logging to understand what's happening. Actually, wait, I think I found the issue!
Let me look at createContext() again:
createContext(): ExtensionContext { const getModel = this.getModel; return { ui: this.uiContext, // <-- Captures the value at call time hasUI: this.hasUI(), // <-- Also called at call time ... };}If the issue is that hasUI() returns false, then... wait, that shouldn't affect ctx.ui.notify() working. Let me check what showExtensionNotify does again:
>rg "showExtensionNotify\|showStatus" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -A 15 | head -50Let me search more broadly:
>grep -n "showExtensionNotify\|showStatus" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -30Let me look at line 1328 more closely (the notify handler) and line 2270 (showStatus):
I see that showStatus adds a Text component to the chat container and calls this.ui.requestRender(). This should work.Let me think about this differently. The user says "the /test command got registered, but running it did not show the ui notification." This means the command is found (registered correctly), but ctx.ui.notify() didn't show anything.If I look at the flow:1. /test is entered2. Interactive mode sends it to session.prompt("/test")
3. _tryExecuteExtensionCommand is called4. The command is found (yes, because the user said it got registered)5. createCommandContext() is called which includes ui: this.uiContext
6. The command handler is called with this context7. The handler calls ctx.ui.notify(...)If this.uiContext is the no-op context, then notify would be a no-op. Let me check the noOpUIContext.notify:
const noOpUIContext: ExtensionUIContext = { ... notify: () => {}, // <-- This is a no-op! ...};Yes! If the no-op context is being used, notify() does nothing.
So the question is: why is this.uiContext still the noOpUIContext after /reload?
Let me trace through /reload:1. handleReloadCommand() is called
2. It calls this.session.reload()3. reload() calls _buildRuntime() which creates a NEW ExtensionRunner4. _buildRuntime calls _applyExtensionBindings(runner)5. _applyExtensionBindings calls runner.setUIContext(this._extensionUIContext)
The key is: is this._extensionUIContext set?It should have been set during initialization in bindExtensions().
I need to check whether bindExtensions actually runs during startup and properly initializes this._extensionUIContext with the real UI context instead of leaving it as the no-op version.
>rg "bindExtensions" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -B 5 -A 5I see that bindExtensions is called once. But I need to understand where this is called and whether it happens before or after /reload.
Let me search for where this binding code is located:
>rg -n "Create extension UI context" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.tsLet me look at the context around line 985 to understand when this is called:
I see! initExtensions() is a method that initializes the extension system. Let me find where this is called:
>rg "initExtensions\(\)" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts -B 5 -A 5initExtensions() is called during the run() method (initialization). But the key point is that bindExtensions is only called if extensionRunner exists.
Now let me understand the issue: when pi starts initially and no test.ts exists, initExtensions() is called. At that point, this.session.extensionRunner might be undefined (no extensions loaded yet). So bindExtensions is NOT called and _extensionUIContext remains undefined.Then when /reload happens:1. test.ts now exists
2. session.reload() creates a NEW ExtensionRunner (with the test.ts extension)
3. _applyExtensionBindings(runner) is called4. It calls runner.setUIContext(this._extensionUIContext)5. But _extensionUIContext is still undefined!6. So setUIContext(undefined) sets this.uiContext = noOpUIContextThat's the bug! When /reload happens, the UI context is not re-created if it wasn't created initially.Let me verify my understanding by looking at the reload handler to see if it re-initializes the UI context:
Let me look at the handleReloadCommand specifically:
>grep -n "handleReloadCommand" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.tsLet me see the full handleReloadCommand:
I found the bug. In handleReloadCommand():1.
actually, codex, please take over
Acknowledging takeover and summarizing analysis
Ok, I'll take over. The UI context issue likely happens because if no extensions exist at startup, the initialization skips binding the UI context after reload. Should I reproduce the issue, inspect files further, or proceed with a fix?
Explaining UI context reload issue and proposing fix options
After reload, the extension loads but _extensionUIContext is undefined, so the runner uses a no-op UI context, causing notifications not to appear during tests. To fix this, reload could call bindExtensions or create the UI context anew—possibly in the reload handler or session code. Should I implement this fix? If so, where do you want it applied?
Taking over. The likely cause is that when pi started there were no extensions, so InteractiveMode.initExtensions() returned early and never called session.bindExtensions(). That means _extensionUIContext stayed undefined. On /reload, AgentSession.reload() creates a new ExtensionRunner and calls _applyExtensionBindings, which passes the undefined UI context. ExtensionRunner.setUIContext(undefined) falls back to the no-op UI context, so ctx.ui.notify() does nothing. That matches what you saw.
If you want, I can prepare a fix. One option is to re-run initExtensions() after session.reload() or call session.bindExtensions() inside handleReloadCommand() whenever a new runner exists.
ohhhh that explains it! i was running it in ../shittycodingagent.ai/ and i think i have no user extensions loaded? yeah, i don't. howe can we fix that?
Explaining extension UI context reload fix
The fix involves ensuring the UI context binds in interactive mode even if no extensions start initially. This means calling session.bindExtensions at startup without exiting early when no extensionRunner exists, so _extensionUIContext is set for later reloads. I can implement this if requested.
Recommending reload UI context fix
The best approach is to call session.bindExtensions after reload in handleReloadCommand to ensure the UI context updates when a new runner appears. Since initExtensions exits early without a runner, bindExtensions offers a safer way to rebind without redundant setup calls. I can implement this if wanted and explain options for handling no extensions at startup.
It happens when you start with zero extensions. initExtensions() returns early, so session.bindExtensions() never runs and _extensionUIContext stays undefined. On /reload, the new runner gets noOpUIContext, so ctx.ui.notify() does nothing.
Fix options:
InteractiveMode.initExtensions(), call session.bindExtensions() even when there is no runner, so the UI context is stored for future reloads.handleReloadCommand(), after session.reload(), call session.bindExtensions() with createExtensionUIContext() if a runner exists.initExtensions() after reload instead of only rebuilding shortcuts.Tell me which approach you want, or I can pick one and implement it.
i suppose we need to do the same in prc and print(json mode?
Planning analysis of related code paths
I'll inspect the rpc and print/json mode code to check usage of bindExtensions before suggesting a fix, focusing on analysis rather than implementation since it wasn't requested yet.
>rg -n "bindExtensions|createExtensionUIContext" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modesalso, i would think agent-session.ts reload() would handle the rewiring of extensions and contexts. it's very weird for modes to having to do that on reload. what concise solution would make it so agent-session.ts handles all of that?
Proposing persistent binding storage in AgentSession
The key fix is to have AgentSession store extension bindings independently of runner existence by always calling bindExtensions; then AgentSession.reload can use these stored bindings to rewire even if mode didn’t bind initially.
also because if we registered the ui context after session.reload(), extensions might already have been run, specifically session_start events could have been emitted, wherein extensions can do ui things, no?
Analyzing extension binding persistence
The issue is that extension bindings aren’t consistently stored or reapplied across agent-session reloads because bindExtensions isn’t always called when no extensionRunner exists initially. To fix this, either the mode should always call bindExtensions to store bindings regardless of runner presence, or agent-session should manage storing and reapplying bindings independently before emitting session_start on reload.
Confirming mode behavior on extension bindings
The core problem is that modes like interactive skip calling bindExtensions if no extensionRunner, leaving UI context unset. To fix, modes (including rpc and print) should always call bindExtensions to set UI context regardless of runner presence, ensuring session reloads can reapply bindings. Next, I'll review rpc-mode and print-mode to verify if they also skip binding when no extensions.
Noticing conditional RPC binding
RPC mode only binds if extensionRunner exists, mirroring the earlier issue; considering whether print-mode faces the same condition.
Proposing session-level extension binding
Considering moving extension binding from modes to AgentSession to ensure context is set regardless of runner presence, either by calling bindExtensions unconditionally at startup or managing default bindings inside AgentSession for consistent context during reloads.
Clarifying extension binding fix
To ensure UI context is set before reload, all modes should call session.bindExtensions unconditionally at startup—even if no extension runner exists—so AgentSession stores bindings early. On reload, AgentSession already reapplies stored bindings before emitting session_start, preserving correct context without modes handling rewiring. This applies to interactive, RPC, and print modes.
Yes, same issue in RPC and print or json. They only call session.bindExtensions() when session.extensionRunner exists, so if you start with zero extensions the UI context never gets stored. On /reload, the new runner gets noOpUIContext, so ctx.ui.notify() does nothing.
AgentSession.reload() already rebinds and emits session_start if bindings are present. The missing piece is that the bindings never get stored when there were no extensions on startup. The concise fix is to call session.bindExtensions() unconditionally in each mode during initialization, even when there is no extension runner. That stores the UI context and command actions up front. Then reload reuses them and session_start runs with the real UI context.
This also answers your last point. If we only bind after reload(), session_start can fire before the UI is set. So the binding must be stored before reload runs.
If you want me to implement it, I will update:
packages/coding-agent/src/modes/interactive/interactive-mode.tspackages/coding-agent/src/modes/rpc/rpc-mode.tspackages/coding-agent/src/modes/print-mode.tsTell me to proceed.
please implement concisely
Clarifying file reading approach
The instructions require reading full files before editing without using offsets or limits, but the read tool truncates large files; this conflicts with the mandate. The next step is to resolve how to handle large files given this constraint.
Resolving reading large files strategy
I clarified that reading a file "in full" means initially calling read without offset/limit even if truncated, then following up with offset reads to complete it; this respects instructions while ensuring full content. The plan is to start with smaller files for edits, then handle the large interactive-mode.ts similarly.
Refactoring extension binding to always call
Planning to call session.bindExtensions unconditionally in both RPC and interactive modes, removing extensionRunner presence checks; this ensures UI context is always created and bound, while handling runner-dependent features like shortcuts and resource display conditionally.
Finalizing extension binding uniformity and reading large file
Decided to always create UI context and call bindExtensions unconditionally across modes, adjusting handling based on runner presence; preparing to read the full interactive-mode.ts despite its size to ensure safe comprehensive edits.
Resolving full file read despite truncation
To fully read large files, I'll use offset and limit in multiple reads after the initial no-offset read, balancing system instructions with practical file size limits.
Continuing full file read in chunks
Proceeding to read the remaining lines using offsets in multiple chunks to ensure the entire file is fully retrieved before editing.
Pausing analysis
Reading core print-mode file
Refactoring extension initialization
Planning to always call bindExtensions by moving UI context creation and bindExtensions call before checking for the extension runner, then conditionally run setupShortcuts and show resources afterward for clearer flow and to ensure UI context setup even without a runner.
Preparing precise code replacements
I'm crafting exact old and new code blocks for initExtensions and updating rpc-mode and print-mode to always call bindExtensions without checking extensionRunner first, ensuring no extra blanks for exact text match using the edit tool.
Updating RPC mode with edit
Locating block for precise editing
I'm planning to read a snippet around the target comment to capture the exact text of the block that needs editing before copying it from the file.
>rg -n "Set up extensions with RPC-based UI context" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/rpc/rpc-mode.tsConsidering partial file reading with offset
I'm exploring using a read-with-offset approach around line 245 to handle snippets, though the requirement states reading the full file before editing; this could optimize showing code blocks.
Extending block capture to include closing braces
I'm checking for additional lines to ensure the entire block, including its closing braces, is captured correctly.
Applying exact old text to snippet
Editing print-mode.ts to replace block
Confirming extension binding ensures UI context on reload
The bindExtensions call now always runs, setting UI context upfront even if no extension runner existed before reload, ensuring session_start emits with proper context. Next, I'll verify interactive mode calls bindExtensions unconditionally to avoid race conditions.
Validating unconditional binding won't break logic
Calling bindExtensions always is safe since it stores handlers and only emits session_start if extensionRunner exists, preserving prior behavior even in print mode without UI context. Next, I'll run the required full npm run check to catch any errors or warnings after these code changes.
>cd /Users/badlogic/workspaces/pi-mono && npm run checkSummarizing extension binding changes
Extensions now bind in all modes irrespective of the extension runner, ensuring UI context persists across reloads with session info saved. The initialization was adjusted to bind extensions before checking the runner, and RPC and print modes also bind unconditionally. Tests ran successfully with npm run check. Key files updated include those managing interactive, RPC, and print modes.
Summary:
packages/coding-agent/src/modes/interactive/interactive-mode.ts to bind extension UI context before checking for a runner, so reload has the context even when starting with zero extensions.packages/coding-agent/src/modes/rpc/rpc-mode.ts and packages/coding-agent/src/modes/print-mode.ts to bind extensions unconditionally for the same reason.Tests:
npm run checklgtm, commit and push, add a changelog entry
Preparing to update changelog
I'm reviewing the full changelog file to identify the appropriate section under Unreleased for adding a new Fixed entry and will read the full file to ensure proper insertion before making edits and committing.
Adding unreleased fix entry
I'm adding a third bullet under Unreleased Fixed: "Fixed extension UI contexts not being bound when no extensions were loaded at startup, so /reload could enable UI in new extensions," formatted as a simple sentence without an em dash or issue link.
createContext(): ExtensionContext { const getModel = this.getModel; return { ui: this.uiContext, // <-- This captures the value at call time ... };}const noOpUIContext: ExtensionUIContext = { select: async () => undefined, confirm: async () => false, input: async () => undefined, notify: () => {}, // <-- This is a no-op! ...};constructor(...) { this.uiContext = noOpUIContext; // Default to no-op ...}setUIContext(uiContext?: ExtensionUIContext): void { this.uiContext = uiContext ?? noOpUIContext;}private _applyExtensionBindings(runner: ExtensionRunner): void { runner.setUIContext(this._extensionUIContext); // Could be undefined! ...}async bindExtensions(bindings: ExtensionBindings): Promise<void> { if (bindings.uiContext !== undefined) { this._extensionUIContext = bindings.uiContext; } ...}createContext(): ExtensionContext { const getModel = this.getModel; return { ui: this.uiContext, // <-- Captures the value at call time hasUI: this.hasUI(), // <-- Also called at call time ... };}const noOpUIContext: ExtensionUIContext = { ... notify: () => {}, // <-- This is a no-op! ...};