Commit a348932af882

Vincent Demeester <vincent@sbr.pm>
2026-06-23 22:47:52
fix: make pi /mode command work in RPC/Emacs
Made the prompt-editor extension RPC-friendly so /mode works in the Emacs pi-coding-agent frontend: - Guard TUI-only custom() picker with ctx.mode check, fall back to ctx.ui.select() in RPC mode for bare /mode - Added success notification on mode switch so RPC clients get visible feedback (model change was silent before) - Added Emacs advice to refresh header after extension slash commands via get_state (pi emits no model-change event) - Fixed pi-coding-agent-dwim to require pi-coding-agent-ui before calling pi-coding-agent-project-buffers
1 parent b5e37ea
Changed files (2)
dots
config
emacs
pi
agent
dots/config/emacs/init.el
@@ -3242,6 +3242,7 @@ No prefix ARG:
 With \\[universal-argument]: prompt for a session name (new named session).
 With \\[universal-argument] \\[universal-argument]: always prompt with completion."
     (interactive "P")
+    (require 'pi-coding-agent-ui nil t)
     (cond
      ((equal arg '(16))
       (let* ((buffers (or (pi-coding-agent-project-buffers)
@@ -3313,7 +3314,30 @@ With \\[universal-argument] \\[universal-argument]: always prompt with completio
                                    (when (buffer-live-p chat-buf)
                                      (with-current-buffer chat-buf
                                        (pi-coding-agent--update-state-from-response r)))))))))
-                        nil t))))
+                        nil t)))
+  ;; Refresh header after extension slash commands (e.g. /mode) so the model
+  ;; name stays in sync.  Extension-driven pi.setModel() calls emit no RPC
+  ;; model-change event, so we re-query get_state after the prompt response.
+  (define-advice pi-coding-agent--send-prompt (:around (orig text &rest args) refresh-after-ext-cmd)
+    "After non-builtin slash commands, refresh state so the header updates."
+    (if (and (string-prefix-p "/" text)
+             (not (assoc (car (split-string (substring text 1)))
+                         pi-coding-agent--builtin-commands)))
+        (let ((proc (pi-coding-agent--get-process))
+              (chat-buf (pi-coding-agent--get-chat-buffer)))
+          (apply orig text args)
+          (when (and proc (process-live-p proc) (buffer-live-p chat-buf))
+            ;; Delay: let the extension handler finish its async work
+            (run-at-time 2.0 nil
+              (lambda ()
+                (when (and (process-live-p proc) (buffer-live-p chat-buf))
+                  (pi-coding-agent--rpc-async proc '(:type "get_state")
+                    (lambda (r)
+                      (when (and (plist-get r :success) (buffer-live-p chat-buf))
+                        (with-current-buffer chat-buf
+                          (pi-coding-agent--update-state-from-response r)
+                          (pi-coding-agent--refresh-header))))))))))
+      (apply orig text args))))
 
 (use-package devdocs
   :commands (devdocs-lookup devdocs-install vde/install-devdocs)
dots/pi/agent/extensions/prompt-editor.ts
@@ -644,6 +644,15 @@ async function applyMode(pi: ExtensionAPI, ctx: ExtensionContext, mode: string):
 	if (ctx.hasUI) {
 		updateModeStatus(ctx);
 		requestEditorRender?.();
+		// Confirm the switch explicitly. The TUI reflects the model change in its
+		// header, but RPC frontends (e.g. the Emacs client) get no model-change
+		// event, so without this notify a successful `/mode <name>` looks like a
+		// no-op there.
+		if (modelAppliedOk && mode !== CUSTOM_MODE_NAME) {
+			const label =
+				spec.provider && spec.modelId ? `${spec.provider}/${spec.modelId}` : "unchanged model";
+			ctx.ui.notify(`Mode "${mode}" → ${label}`, "info");
+		}
 	}
 }
 
@@ -723,60 +732,63 @@ async function selectModeUI(pi: ExtensionAPI, ctx: ExtensionContext): Promise<vo
 		});
 		items.push({ value: MODE_UI_CONFIGURE, label: MODE_UI_CONFIGURE });
 
-		const choice = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
-			let filterText = "";
-			const container = new Container();
-			container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
-			const titleText = new Text(theme.fg("accent", theme.bold(`Mode (current: ${runtime.currentMode})`)), 1, 0);
-			container.addChild(titleText);
+		const choice =
+			ctx.mode === "tui"
+				? await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
+						let filterText = "";
+						const container = new Container();
+						container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
+						const titleText = new Text(theme.fg("accent", theme.bold(`Mode (current: ${runtime.currentMode})`)), 1, 0);
+						container.addChild(titleText);
 
-			const filterDisplay = new Text(theme.fg("dim", "Type to filter..."), 1, 0);
-			container.addChild(filterDisplay);
+						const filterDisplay = new Text(theme.fg("dim", "Type to filter..."), 1, 0);
+						container.addChild(filterDisplay);
 
-			const selectList = new SelectList(items, Math.min(items.length, 15), {
-				selectedPrefix: (t: string) => theme.fg("accent", t),
-				selectedText: (t: string) => theme.fg("accent", t),
-				description: (t: string) => theme.fg("muted", t),
-				scrollInfo: (t: string) => theme.fg("dim", t),
-				noMatch: (t: string) => theme.fg("warning", t),
-			});
-			selectList.onSelect = (item) => done(item.value);
-			selectList.onCancel = () => done(null);
-			container.addChild(selectList);
+						const selectList = new SelectList(items, Math.min(items.length, 15), {
+							selectedPrefix: (t: string) => theme.fg("accent", t),
+							selectedText: (t: string) => theme.fg("accent", t),
+							description: (t: string) => theme.fg("muted", t),
+							scrollInfo: (t: string) => theme.fg("dim", t),
+							noMatch: (t: string) => theme.fg("warning", t),
+						});
+						selectList.onSelect = (item) => done(item.value);
+						selectList.onCancel = () => done(null);
+						container.addChild(selectList);
 
-			container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
-			container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
+						container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
+						container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
 
-			const updateFilterDisplay = () => {
-				if (filterText) {
-					filterDisplay.setText(theme.fg("accent", "Filter: ") + theme.fg("text", filterText) + theme.fg("dim", "▏"));
-				} else {
-					filterDisplay.setText(theme.fg("dim", "Type to filter..."));
-				}
-			};
+						const updateFilterDisplay = () => {
+							if (filterText) {
+								filterDisplay.setText(theme.fg("accent", "Filter: ") + theme.fg("text", filterText) + theme.fg("dim", "▏"));
+							} else {
+								filterDisplay.setText(theme.fg("dim", "Type to filter..."));
+							}
+						};
 
-			return {
-				render: (w: number) => container.render(w),
-				invalidate: () => container.invalidate(),
-				handleInput: (data: string) => {
-					if (matchesKey(data, Key.backspace)) {
-						if (filterText.length > 0) {
-							filterText = filterText.slice(0, -1);
-							selectList.setFilter(filterText);
-							updateFilterDisplay();
-						}
-					} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
-						// Printable character
-						filterText += data;
-						selectList.setFilter(filterText);
-						updateFilterDisplay();
-					} else {
-						selectList.handleInput(data);
-					}
-					tui.requestRender();
-				},
-			};
-		});
+						return {
+							render: (w: number) => container.render(w),
+							invalidate: () => container.invalidate(),
+							handleInput: (data: string) => {
+								if (matchesKey(data, Key.backspace)) {
+									if (filterText.length > 0) {
+										filterText = filterText.slice(0, -1);
+										selectList.setFilter(filterText);
+										updateFilterDisplay();
+									}
+								} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
+									// Printable character
+									filterText += data;
+									selectList.setFilter(filterText);
+									updateFilterDisplay();
+								} else {
+									selectList.handleInput(data);
+								}
+								tui.requestRender();
+							},
+						};
+				  })
+				: await ctx.ui.select(`Mode (current: ${runtime.currentMode})`, [...names, MODE_UI_CONFIGURE]);
 
 		if (!choice) return;