Read issue and related code files.
UnderstandTraced code path for root cause.
UnderstandUpdated handler to queue pending messages.
EditRan type checks and validations.
VerifyUpdated changelog with fix details.
EditAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/935
For each issue:
Read the issue in full, including all comments and linked issues/PRs.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
>gh issue view 935 --json title,body,comments,labels,state --repo badlogic/pi-mono{"body":"### What happened?\n\nhey Mario,\n\nusing 0.49.3 I tried out @hjanuschka 's extensions, specifically the `funny-working-message.ts` but it didn't work. After a bit of try-but-still-not-working, I decided that maybe the problem is not with the extension but in Pi. \nThe extension works if I initiate a conversation with the agent and while it's displaying `Working...` I can execute `fun-working` and it will swap them out. \n\nSo I used Pi/Opus in pi-mono and let it investigate and it came up with the following.\nIf this all sounds reasoanble, I've also a fix for it and 3 tests (that fail unless fix is implemented).\n\n## Opus' analysis\n\nExtensions receive `agent_start` events before the UI creates the `loadingAnimation` component. This causes `setWorkingMessage()` calls in `agent_start` handlers to silently fail.\n\n### Root Cause\n\n In `agent-session.ts`, events are emitted to extensions first, then to UI listeners:\n\n```typescript\nawait this._emitExtensionEvent(event);\n\nthis._emit(event);\n```\n\nThe setWorkingMessage() implementation checks if loadingAnimation exists:\n\n ```typescript\n if (this.loadingAnimation) { // undefined at this point\n this.loadingAnimation.setMessage(message);\n }\n ```\n\nSince the UI's agent_start handler creates loadingAnimation, extensions calling setWorkingMessage() in their agent_start handler find it undefined.\n\n\n### Steps to reproduce\n\nload `https://github.com/hjanuschka/shitty-extensions/blob/main/extensions/funny-working-message.ts` and enable it with `/fun-working`\n\nIf this is done before the agent got any other normal conversation, it will continue to display `Working...` instead of showing the funny messages.\n\n### Expected behavior\n\nfunny messages all the time\n\n### Version\n\n0.49.3","comments":[],"labels":[{"id":"LA_kwDOPbFNk88AAAACHWTBxw","name":"bug","description":"Something isn't working","color":"d73a4a"}],"state":"OPEN","title":"Extension setWorkingMessage() has no effect on agent_start event"}Now let me read the relevant code files to understand the issue and verify the root cause analysis.
Let me continue reading the rest of the agent-session.ts file:
Now let me read the interactive mode code to see how the UI creates the loadingAnimation and handles events:
>cd /Users/badlogic/workspaces/pi-mono && rg -l "loadingAnimation" packages/coding-agent/packages/coding-agent/test/fixtures/large-session.jsonl
packages/coding-agent/test/fixtures/before-compaction.jsonl
packages/coding-agent/src/modes/interactive/interactive-mode.tsLet me search for where loadingAnimation is created and where agent_start events are handled:
>cd /Users/badlogic/workspaces/pi-mono && rg -n "agent_start|loadingAnimation" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -60165: private loadingAnimation: Loader | undefined = undefined;
991: if (this.loadingAnimation) {
992: this.loadingAnimation.stop();
993: this.loadingAnimation = undefined;
1211: if (this.loadingAnimation) {
1212: this.loadingAnimation.setMessage(
1332: if (this.loadingAnimation) {
1334: this.loadingAnimation.setMessage(message);
1336: this.loadingAnimation.setMessage(
1714: if (this.loadingAnimation) {
1971: case "agent_start":
1982: if (this.loadingAnimation) {
1983: this.loadingAnimation.stop();
1986: this.loadingAnimation = new Loader(
1992: this.statusContainer.addChild(this.loadingAnimation);
2128: if (this.loadingAnimation) {
2129: this.loadingAnimation.stop();
2130: this.loadingAnimation = undefined;
3460: if (this.loadingAnimation) {
3461: this.loadingAnimation.stop();
3462: this.loadingAnimation = undefined;
4008: if (this.loadingAnimation) {
4009: this.loadingAnimation.stop();
4010: this.loadingAnimation = undefined;
4172: if (this.loadingAnimation) {
4173: this.loadingAnimation.stop();
4174: this.loadingAnimation = undefined;
4227: if (this.loadingAnimation) {
4228: this.loadingAnimation.stop();
4229: this.loadingAnimation = undefined;Now I can see the exact issue. Let me look at where _emitExtensionEvent is called versus where the UI handler runs:
Now I have a full understanding. Let me look at the extension runner to understand how emit works and where setWorkingMessage is called:
>cd /Users/badlogic/workspaces/pi-mono && rg -n "setWorkingMessage" packages/coding-agent/src/packages/coding-agent/src/modes/rpc/rpc-mode.ts:157: setWorkingMessage(_message?: string): void {
packages/coding-agent/src/core/extensions/runner.ts:129: setWorkingMessage: () => {},
packages/coding-agent/src/core/extensions/types.ts:112: setWorkingMessage(message?: string): void;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1331: setWorkingMessage: (message) => {Now let me look at the extension runner's setUIContext and how it's invoked:
>cd /Users/badlogic/workspaces/pi-mono && rg -n "setUIContext|_uiContext" packages/coding-agent/src/core/extensions/runner.ts229: setUIContext(uiContext?: ExtensionUIContext): void {Now I understand the complete picture. Let me look at how extension events are emitted to see the order of operations:
Now I fully understand the issue. Let me summarize my analysis.
Extensions receive agent_start events before the UI creates the loadingAnimation component, causing setWorkingMessage() calls in agent_start handlers to silently fail (since it checks if (this.loadingAnimation) which is undefined at that point).
The issue report's analysis is correct. In agent-session.ts, the _handleAgentEvent method processes events in this order:
// Line ~385-390 in agent-session.tsawait this._emitExtensionEvent(event); // Extensions receive event FIRST
this._emit(event); // UI listeners receive event AFTERThe _emit(event) triggers the UI's handleEvent method in interactive-mode.ts, which creates loadingAnimation on agent_start:
// Lines ~1986-1992 in interactive-mode.tscase "agent_start": this.loadingAnimation = new Loader(...) this.statusContainer.addChild(this.loadingAnimation);But by the time the UI creates loadingAnimation, the extension's agent_start handler has already run and called setWorkingMessage(), which was a no-op because loadingAnimation was still undefined:
// Lines ~1331-1337 in interactive-mode.tssetWorkingMessage: (message) => { if (this.loadingAnimation) { // undefined at extension event time! if (message) { this.loadingAnimation.setMessage(message); } else { this.loadingAnimation.setMessage(...default message...); } }}There are two clean approaches:
Option A: Queue the message if loadingAnimation doesn't exist yet
In interactive-mode.ts, store a pending working message and apply it when loadingAnimation is created:
// Add propertyprivate pendingWorkingMessage: string | undefined = undefined;
// In setWorkingMessage:setWorkingMessage: (message) => { if (this.loadingAnimation) { // Apply immediately if (message) { this.loadingAnimation.setMessage(message); } else { this.loadingAnimation.setMessage(`${this.defaultWorkingMessage}...`); } } else { // Queue for when loadingAnimation is created this.pendingWorkingMessage = message; }}
// In handleEvent agent_start case, after creating loadingAnimation:if (this.pendingWorkingMessage !== undefined) { if (this.pendingWorkingMessage) { this.loadingAnimation.setMessage(this.pendingWorkingMessage); } this.pendingWorkingMessage = undefined;}Option B: Emit UI event before extension event for agent_start
In agent-session.ts, swap the order specifically for agent_start events so UI creates the loader before extensions run:
// In _handleAgentEvent:if (event.type === "agent_start") { // For agent_start, notify UI FIRST so loadingAnimation exists this._emit(event); await this._emitExtensionEvent(event);} else { // For other events, extensions first (existing behavior) await this._emitExtensionEvent(event); this._emit(event);}Option A is preferred because:
setWorkingMessage calls before the loader existspackages/coding-agent/src/modes/interactive/interactive-mode.tsimplement
Now let me update the setWorkingMessage implementation:
Now let me update the agent_start handler to apply the pending message after creating the loader:
Now let me run the check to make sure there are no type errors:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 431 files in 274ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.All checks pass. Now let me update the changelog:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 431 files in 272ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 39ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.Done. Here's a summary of the changes:
Fixed the issue where setWorkingMessage() calls in extension agent_start handlers were silently ignored.
packages/coding-agent/src/modes/interactive/interactive-mode.ts:Added pendingWorkingMessage property to queue messages when loadingAnimation doesn't exist yet
Updated setWorkingMessage in createExtensionUIContext() to queue the message if loadingAnimation is undefined
Updated the agent_start handler to apply any pending working message after creating the loader
Extensions receive agent_start events before the UI listener creates the loadingAnimation component. The fix queues any setWorkingMessage() calls made before the loader exists and applies them once the loader is created.
commit and push just the files you touched
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/src/modes/interactive/interactive-mode.ts packages/coding-agent/CHANGELOG.md && git commit -m "fix(coding-agent): setWorkingMessage() now works in agent_start handlers
Extensions calling setWorkingMessage() in agent_start handlers previously
had no effect because the loading animation didn't exist yet. Now the
message is queued and applied once the loader is created.
Fixes #935" && git pushRunning formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 431 files in 290ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 38ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.
✅ All pre-commit checks passed!
[main 349c6420] fix(coding-agent): setWorkingMessage() now works in agent_start handlers
2 files changed, 14 insertions(+)
To https://github.com/badlogic/pi-mono.git
9dc2b9b1..349c6420 main -> mainDone. Committed and pushed the fix for issue #935.
// Line ~385-390 in agent-session.tsawait this._emitExtensionEvent(event); // Extensions receive event FIRST
this._emit(event); // UI listeners receive event AFTER// Lines ~1986-1992 in interactive-mode.tscase "agent_start": this.loadingAnimation = new Loader(...) this.statusContainer.addChild(this.loadingAnimation);// Lines ~1331-1337 in interactive-mode.tssetWorkingMessage: (message) => { if (this.loadingAnimation) { // undefined at extension event time! if (message) { this.loadingAnimation.setMessage(message); } else { this.loadingAnimation.setMessage(...default message...); } }}// Add propertyprivate pendingWorkingMessage: string | undefined = undefined;
// In setWorkingMessage:setWorkingMessage: (message) => { if (this.loadingAnimation) { // Apply immediately if (message) { this.loadingAnimation.setMessage(message); } else { this.loadingAnimation.setMessage(`${this.defaultWorkingMessage}...`); } } else { // Queue for when loadingAnimation is created this.pendingWorkingMessage = message; }}
// In handleEvent agent_start case, after creating loadingAnimation:if (this.pendingWorkingMessage !== undefined) { if (this.pendingWorkingMessage) { this.loadingAnimation.setMessage(this.pendingWorkingMessage); } this.pendingWorkingMessage = undefined;}// In _handleAgentEvent:if (event.type === "agent_start") { // For agent_start, notify UI FIRST so loadingAnimation exists this._emit(event); await this._emitExtensionEvent(event);} else { // For other events, extensions first (existing behavior) await this._emitExtensionEvent(event); this._emit(event);}