Commit a348932af882
Changed files (2)
dots
config
emacs
pi
agent
extensions
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;