fix(coding-agent): persist custom working message across loader recreation

Replace transient pendingWorkingMessage with a persistent workingMessage
field on InteractiveMode, matching the workingIndicatorOptions pattern.
New loaders now use this.workingMessage || this.defaultWorkingMessage
instead of always falling back to the default.

Also add working-message-test.ts extension example.

closes #3566
This commit is contained in:
Mario Zechner
2026-04-22 22:24:32 +02:00
Unverified
parent 876a197b19
commit cc76e73c05
3 changed files with 33 additions and 13 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed `ctx.ui.setWorkingMessage()` to persist across loader recreation, matching the behavior of `ctx.ui.setWorkingIndicator()` ([#3566](https://github.com/badlogic/pi-mono/issues/3566))
## [0.69.0] - 2026-04-22
### New Features
@@ -0,0 +1,25 @@
/**
* Working Message Persistence Test
*
* Sets a custom working message and indicator on session start so you can
* verify they survive across loader recreations (e.g. between agent turns).
*
* Usage:
* pi --extension examples/extensions/working-message-test.ts
*
* Then send a few messages in interactive mode. The working message should
* stay "Working... (custom)" with a brown dot indicator every time the
* loader appears, not revert to the default gray "Working...".
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
const CUSTOM_MESSAGE = "\x1b[38;2;155;86;63mWorking... (custom)\x1b[39m";
const CUSTOM_INDICATOR = { frames: ["\x1b[38;2;155;86;63m●\x1b[39m"] };
export default function (pi: ExtensionAPI) {
pi.on("session_start", async (_event, ctx) => {
ctx.ui.setWorkingMessage(CUSTOM_MESSAGE);
ctx.ui.setWorkingIndicator(CUSTOM_INDICATOR);
});
}
@@ -205,7 +205,7 @@ export class InteractiveMode {
private isInitialized = false;
private onInputCallback?: (text: string) => void;
private loadingAnimation: Loader | undefined = undefined;
private pendingWorkingMessage: string | undefined = undefined;
private workingMessage: string | undefined = undefined;
private workingIndicatorOptions: LoaderIndicatorOptions | undefined = undefined;
private readonly defaultWorkingMessage = "Working...";
private readonly defaultHiddenThinkingLabel = "Thinking...";
@@ -1724,7 +1724,7 @@ export class InteractiveMode {
this.setupAutocompleteProvider();
this.defaultEditor.onExtensionShortcut = undefined;
this.updateTerminalTitle();
this.pendingWorkingMessage = undefined;
this.workingMessage = undefined;
this.setWorkingIndicator();
if (this.loadingAnimation) {
this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
@@ -1875,6 +1875,7 @@ export class InteractiveMode {
onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
setStatus: (key, text) => this.setExtensionStatus(key, text),
setWorkingMessage: (message) => {
this.workingMessage = message;
if (this.loadingAnimation) {
if (message) {
this.loadingAnimation.setMessage(message);
@@ -1883,9 +1884,6 @@ export class InteractiveMode {
`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`,
);
}
} else {
// Queue message for when loadingAnimation is created (handles agent_start race)
this.pendingWorkingMessage = message;
}
},
setWorkingIndicator: (options) => this.setWorkingIndicator(options),
@@ -2591,17 +2589,10 @@ export class InteractiveMode {
this.ui,
(spinner) => theme.fg("accent", spinner),
(text) => theme.fg("muted", text),
this.defaultWorkingMessage,
this.workingMessage || this.defaultWorkingMessage,
this.workingIndicatorOptions,
);
this.statusContainer.addChild(this.loadingAnimation);
// Apply any pending working message queued before loader existed
if (this.pendingWorkingMessage !== undefined) {
if (this.pendingWorkingMessage) {
this.loadingAnimation.setMessage(this.pendingWorkingMessage);
}
this.pendingWorkingMessage = undefined;
}
this.ui.requestRender();
break;