pum-agent 0.2.31-beta.1 → 0.2.32-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app.tsx +44 -10
- package/src/commands.ts +4 -0
- package/src/help-popup.tsx +1 -0
- package/src/login-controller.ts +2 -0
- package/src/model-settings.ts +15 -0
- package/src/settings-autocomplete.ts +13 -0
- package/src/settings-command.ts +1 -1
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -152,6 +152,7 @@ import {
|
|
|
152
152
|
import { setBashOutputSettingsIfPresent } from "./bash-output";
|
|
153
153
|
import { settingsCompletions } from "./settings-autocomplete";
|
|
154
154
|
import { modelCommandCompletions, modelReference, parseModelSelection, validateEffort } from "./model-command";
|
|
155
|
+
import { saveMainModelDefaults } from "./model-settings";
|
|
155
156
|
import {
|
|
156
157
|
applySettingChange,
|
|
157
158
|
listSettingsMessage,
|
|
@@ -1010,6 +1011,7 @@ export function App({
|
|
|
1010
1011
|
const [commandInput, setCommandInput] = useState("");
|
|
1011
1012
|
const [inputCursorOffset, setInputCursorOffset] = useState(0);
|
|
1012
1013
|
const [commandCursor, setCommandCursor] = useState(0);
|
|
1014
|
+
const [hoverSuggestionStart, setHoverSuggestionStart] = useState<number | null>(null);
|
|
1013
1015
|
const [commandSuggestionsDismissed, setCommandSuggestionsDismissed] = useState(false);
|
|
1014
1016
|
const [editingStashIndexState, setEditingStashIndexState] = useState<number | null>(null);
|
|
1015
1017
|
const [inputRows, setInputRows] = useState(1);
|
|
@@ -1063,7 +1065,16 @@ export function App({
|
|
|
1063
1065
|
const newsCursorRef = useRef(0);
|
|
1064
1066
|
const statsOpenRef = useRef(false);
|
|
1065
1067
|
|
|
1066
|
-
|
|
1068
|
+
// Preview only the visible autocomplete selection. The committed setting and
|
|
1069
|
+
// its session/global persistence never participate in this temporary state.
|
|
1070
|
+
const themeSuggestions = shellMode || stashOpen || commandSuggestionsDismissed || activeAgentId
|
|
1071
|
+
|| transcriptFocused || settingsOpen || helpOpen || historyOpen || statsOpen || agentSelectorOpen
|
|
1072
|
+
|| triggersOpen || loginOpen || providersOpen || newsOpen || todoOpen
|
|
1073
|
+
|| questionnaireManager?.current() || spawnPreviewManager?.current()
|
|
1074
|
+
? [] : settingsCompletions(commandInput, inputCursorOffset, cwd);
|
|
1075
|
+
const previewTheme = themeSuggestions[Math.min(commandCursor, Math.max(0, themeSuggestions.length - 1))]?.previewTheme;
|
|
1076
|
+
const displayedTheme = previewTheme ?? settings.theme;
|
|
1077
|
+
const theme = useMemo(() => loadTheme(displayedTheme), [displayedTheme]);
|
|
1067
1078
|
const { width, height } = useTerminalDimensions();
|
|
1068
1079
|
const syntaxStyle = useMemo(() => buildSyntaxStyle(theme), [theme]);
|
|
1069
1080
|
const newsReadById = useMemo(
|
|
@@ -1318,7 +1329,10 @@ export function App({
|
|
|
1318
1329
|
// Every match stays selectable. Only SUGGESTION_ROWS of them are on screen,
|
|
1319
1330
|
// and the window scrolls with the selection.
|
|
1320
1331
|
const suggestionIndex = Math.min(commandCursor, Math.max(0, suggestionCount - 1));
|
|
1321
|
-
|
|
1332
|
+
// Hover must not scroll its own row out from under a stationary pointer.
|
|
1333
|
+
const suggestionStart = hoverSuggestionStart === null
|
|
1334
|
+
? suggestionWindowStart(suggestionIndex, suggestionCount)
|
|
1335
|
+
: Math.min(hoverSuggestionStart, Math.max(0, suggestionCount - SUGGESTION_ROWS));
|
|
1322
1336
|
const visibleSettingRows = filterSettingsRows(settingsQuery);
|
|
1323
1337
|
const visibleModels = useMemo(() => filterModels(
|
|
1324
1338
|
modelRuntime.getAvailableSnapshot(),
|
|
@@ -1620,6 +1634,7 @@ export function App({
|
|
|
1620
1634
|
setCommandSuggestionsClosed(false);
|
|
1621
1635
|
commandCursorRef.current = 0;
|
|
1622
1636
|
setCommandCursor(0);
|
|
1637
|
+
setHoverSuggestionStart(null);
|
|
1623
1638
|
setCommandInput(value);
|
|
1624
1639
|
scheduleInputMetrics();
|
|
1625
1640
|
};
|
|
@@ -1671,6 +1686,7 @@ export function App({
|
|
|
1671
1686
|
setCommandSuggestionsClosed(false);
|
|
1672
1687
|
commandCursorRef.current = 0;
|
|
1673
1688
|
setCommandCursor(0);
|
|
1689
|
+
setHoverSuggestionStart(null);
|
|
1674
1690
|
setCommandInput(value);
|
|
1675
1691
|
scheduleInputMetrics();
|
|
1676
1692
|
if (removedImages > 0 || removedPastedTexts > 0) {
|
|
@@ -2727,13 +2743,20 @@ export function App({
|
|
|
2727
2743
|
/** Explicit global promotion shared by popup `s`, /s, and /store. */
|
|
2728
2744
|
const promoteSessionSettings = () => {
|
|
2729
2745
|
const next = settingsRef.current;
|
|
2746
|
+
// Always promote main, even when a child transcript is selected. Do not
|
|
2747
|
+
// call setModel here: it can reset effort and recheck authentication.
|
|
2748
|
+
const savedModel = saveMainModelDefaults(sessionRef.current);
|
|
2730
2749
|
globalSettingsRef.current = next;
|
|
2731
2750
|
saveSettings(next);
|
|
2732
2751
|
// Nothing differs from global any more, so the session owns no overrides.
|
|
2733
2752
|
sessionOverridesRef.current = {};
|
|
2734
2753
|
setSessionOverrides({});
|
|
2735
2754
|
saveSessionSettings(session.sessionFile, {});
|
|
2736
|
-
|
|
2755
|
+
void savedModel.then(() => {
|
|
2756
|
+
appendMainLine({ kind: "text", role: "system", text: "Saved these settings as the global defaults." });
|
|
2757
|
+
}).catch((error) => {
|
|
2758
|
+
appendMainLine({ kind: "text", role: "error", text: `Could not save model defaults: ${String(error)}` });
|
|
2759
|
+
});
|
|
2737
2760
|
};
|
|
2738
2761
|
|
|
2739
2762
|
const stepThinking = (step: number) => {
|
|
@@ -2742,7 +2765,7 @@ export function App({
|
|
|
2742
2765
|
// step from the committed value instead of from a stale render closure.
|
|
2743
2766
|
const i = levels.indexOf(session.agent.state.thinkingLevel as ThinkingLevel);
|
|
2744
2767
|
const target = levels[Math.max(0, Math.min(levels.length - 1, i + step))]!;
|
|
2745
|
-
session.setThinkingLevel(target);
|
|
2768
|
+
session.setThinkingLevel(target, { persist: true });
|
|
2746
2769
|
// setThinkingLevel clamps to what the model supports — show the real value.
|
|
2747
2770
|
setThinkingLevel(session.agent.state.thinkingLevel as ThinkingLevel);
|
|
2748
2771
|
};
|
|
@@ -2823,6 +2846,7 @@ export function App({
|
|
|
2823
2846
|
await deleteProvider(modelRuntime as any, entry);
|
|
2824
2847
|
// The active model may have belonged to the provider just removed.
|
|
2825
2848
|
// Move to another one rather than keep a model PUM cannot authenticate.
|
|
2849
|
+
// This automatic fallback must not replace the saved model preference.
|
|
2826
2850
|
const runtime = modelRuntime as any;
|
|
2827
2851
|
const replacement = modelAfterRemoval(
|
|
2828
2852
|
sessionRef.current.agent.state.model,
|
|
@@ -2886,7 +2910,7 @@ export function App({
|
|
|
2886
2910
|
setModelQuery("");
|
|
2887
2911
|
setModelSearchFocused(false);
|
|
2888
2912
|
session
|
|
2889
|
-
.setModel(model)
|
|
2913
|
+
.setModel(model, { persist: true })
|
|
2890
2914
|
.then(() => setModelId(session.agent.state.model.id))
|
|
2891
2915
|
.catch((err) => append({ kind: "text", role: "error", text: String(err) }));
|
|
2892
2916
|
};
|
|
@@ -3593,7 +3617,7 @@ export function App({
|
|
|
3593
3617
|
const loginCommand = trimmed === "/login";
|
|
3594
3618
|
const providersCommand = parseProvidersCommand(trimmed);
|
|
3595
3619
|
const checkPathCommand = /^\/check-path(?:\s|$)/.test(trimmed);
|
|
3596
|
-
const settingsCommand = /^\/settings(?:\s|$)/.test(trimmed);
|
|
3620
|
+
const settingsCommand = /^\/(?:settings|theme)(?:\s|$)/.test(trimmed);
|
|
3597
3621
|
const modelCommand = /^\/model(?:\s|$)/.test(trimmed);
|
|
3598
3622
|
const effortCommand = /^\/effort(?:\s|$)/.test(trimmed);
|
|
3599
3623
|
const triggersCommand = trimmed === "/triggers";
|
|
@@ -3697,11 +3721,11 @@ export function App({
|
|
|
3697
3721
|
const selection = parseModelSelection(trimmed.slice("/model".length), modelRuntime.getAvailableSnapshot());
|
|
3698
3722
|
// Validate before setModel: an invalid effort must not change the model.
|
|
3699
3723
|
// setModel owns authentication, persistence, and model-change events.
|
|
3700
|
-
await targetSession.setModel(selection.model);
|
|
3701
|
-
if (selection.effort !== undefined) targetSession.setThinkingLevel(selection.effort);
|
|
3724
|
+
await targetSession.setModel(selection.model, { persist: true });
|
|
3725
|
+
if (selection.effort !== undefined) targetSession.setThinkingLevel(selection.effort, { persist: true });
|
|
3702
3726
|
} else {
|
|
3703
3727
|
const value = trimmed.slice("/effort".length).trim();
|
|
3704
|
-
if (value) targetSession.setThinkingLevel(validateEffort(value, targetSession.agent.state.model));
|
|
3728
|
+
if (value) targetSession.setThinkingLevel(validateEffort(value, targetSession.agent.state.model), { persist: true });
|
|
3705
3729
|
}
|
|
3706
3730
|
setModelId(targetSession.agent.state.model!.id);
|
|
3707
3731
|
setThinkingLevel(targetSession.agent.state.thinkingLevel as ThinkingLevel);
|
|
@@ -5720,6 +5744,7 @@ export function App({
|
|
|
5720
5744
|
);
|
|
5721
5745
|
commandCursorRef.current = next;
|
|
5722
5746
|
setCommandCursor(next);
|
|
5747
|
+
setHoverSuggestionStart(null);
|
|
5723
5748
|
return;
|
|
5724
5749
|
}
|
|
5725
5750
|
// Keep arrow navigation inside multiline or visually wrapped prompts.
|
|
@@ -6026,7 +6051,16 @@ export function App({
|
|
|
6026
6051
|
.map((suggestion, index) => {
|
|
6027
6052
|
const highlighted = suggestionStart + index === suggestionIndex;
|
|
6028
6053
|
return (
|
|
6029
|
-
<box
|
|
6054
|
+
<box
|
|
6055
|
+
key={suggestion.key}
|
|
6056
|
+
onMouseMove={() => {
|
|
6057
|
+
const next = suggestionStart + index;
|
|
6058
|
+
commandCursorRef.current = next;
|
|
6059
|
+
setHoverSuggestionStart(suggestionStart);
|
|
6060
|
+
setCommandCursor(next);
|
|
6061
|
+
}}
|
|
6062
|
+
style={{ height: 1, flexShrink: 0, flexDirection: "row" }}
|
|
6063
|
+
>
|
|
6030
6064
|
<box style={{ width: 2, flexShrink: 0 }}>
|
|
6031
6065
|
{highlighted ? <text content="❯ " fg={theme.accent} /> : null}
|
|
6032
6066
|
</box>
|
package/src/commands.ts
CHANGED
|
@@ -81,6 +81,10 @@ export const COMMANDS: Command[] = [
|
|
|
81
81
|
name: "/settings",
|
|
82
82
|
description: "Show or change a setting: /settings <name> <value> [--global]",
|
|
83
83
|
},
|
|
84
|
+
{
|
|
85
|
+
name: "/theme",
|
|
86
|
+
description: "Show or change the theme: /theme <theme> [--global]",
|
|
87
|
+
},
|
|
84
88
|
{
|
|
85
89
|
name: "/check-path",
|
|
86
90
|
description: "Manage additional Check mode directory roots",
|
package/src/help-popup.tsx
CHANGED
|
@@ -66,6 +66,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
66
66
|
["/todo", "Show the agent's todo list"],
|
|
67
67
|
["/stats", "Show session statistics"],
|
|
68
68
|
["/settings", "Show or change a setting"],
|
|
69
|
+
["/theme <theme>", "Preview and change the theme"],
|
|
69
70
|
["/model", "Choose model [effort]; no name opens picker"],
|
|
70
71
|
["/effort [level]", "Show or change main-agent reasoning effort"],
|
|
71
72
|
["/store · /s", "Save current settings as global defaults"],
|
package/src/login-controller.ts
CHANGED
|
@@ -147,6 +147,8 @@ export class LoginController {
|
|
|
147
147
|
const selected = await refreshAndSelectModel(
|
|
148
148
|
this.runtime,
|
|
149
149
|
providerId,
|
|
150
|
+
// Authentication can select a usable model, but it is not an explicit
|
|
151
|
+
// model preference. Keep the saved main defaults unchanged.
|
|
150
152
|
(model) => session.setModel(model),
|
|
151
153
|
AbortSignal.timeout(15_000),
|
|
152
154
|
session.model,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/** Promote main-session defaults without switching models or resetting effort.
|
|
4
|
+
* Call only for explicit global promotion, never for startup or child setup.
|
|
5
|
+
*/
|
|
6
|
+
export async function saveMainModelDefaults(session: Pick<AgentSession, "agent" | "settingsManager">): Promise<void> {
|
|
7
|
+
const { model, thinkingLevel } = session.agent.state;
|
|
8
|
+
if (model) session.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
9
|
+
session.settingsManager.setDefaultThinkingLevel(thinkingLevel);
|
|
10
|
+
await session.settingsManager.flush();
|
|
11
|
+
// pi records write failures instead of rejecting flush(). Do not report a
|
|
12
|
+
// successful promotion when those writes failed.
|
|
13
|
+
const errors = session.settingsManager.drainErrors();
|
|
14
|
+
if (errors.length > 0) throw new Error(errors.map(({ error }) => error.message).join("; "));
|
|
15
|
+
}
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
export type SettingsCompletion = PathCompletion & {
|
|
22
22
|
/** Trailing hint shown after the value in the suggestion row. */
|
|
23
23
|
description?: string;
|
|
24
|
+
/** Render-only theme preview. Never update settings until command execution. */
|
|
25
|
+
previewTheme?: string;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
const CHECK_PATH_ACTIONS = ["list", "add", "remove", "clear"];
|
|
@@ -50,6 +52,16 @@ export function settingsCompletions(
|
|
|
50
52
|
cursorOffset: number,
|
|
51
53
|
cwd: string,
|
|
52
54
|
): SettingsCompletion[] {
|
|
55
|
+
const alias = /^\s*\/theme(?=\s|$)/.exec(input);
|
|
56
|
+
if (alias) {
|
|
57
|
+
const commandEnd = alias[0].length;
|
|
58
|
+
if (cursorOffset <= commandEnd) return [];
|
|
59
|
+
const expanded = input.slice(0, commandEnd).replace(/\/theme$/, "/settings theme")
|
|
60
|
+
+ input.slice(commandEnd);
|
|
61
|
+
const added = expanded.length - input.length;
|
|
62
|
+
return settingsCompletions(expanded, cursorOffset + added, cwd)
|
|
63
|
+
.map((completion) => ({ ...completion, start: completion.start - added, end: completion.end - added }));
|
|
64
|
+
}
|
|
53
65
|
if (!/^\s*\/settings(?:\s|$)/.test(input)) return [];
|
|
54
66
|
const cursor = Math.max(0, Math.min(cursorOffset, input.length));
|
|
55
67
|
// Completing behind a trailing argument would rewrite text the cursor has
|
|
@@ -111,6 +123,7 @@ export function settingsCompletions(
|
|
|
111
123
|
start,
|
|
112
124
|
end,
|
|
113
125
|
replacement: candidate.value,
|
|
126
|
+
...(spec.key === "theme" ? { previewTheme: candidate.value } : {}),
|
|
114
127
|
...(candidate.description ? { description: candidate.description } : {}),
|
|
115
128
|
})),
|
|
116
129
|
...flag,
|
package/src/settings-command.ts
CHANGED
|
@@ -367,7 +367,7 @@ export type SettingsCommand =
|
|
|
367
367
|
|
|
368
368
|
/** Returns undefined when the input is not a /settings command at all. */
|
|
369
369
|
export function parseSettingsCommand(input: string): SettingsCommand | undefined {
|
|
370
|
-
const trimmed = input.trim();
|
|
370
|
+
const trimmed = input.trim().replace(/^\/theme(?=\s|$)/, "/settings theme");
|
|
371
371
|
if (!/^\/settings(?:\s|$)/.test(trimmed)) return undefined;
|
|
372
372
|
|
|
373
373
|
const tokens = tokenizeSettingsInput(trimmed).slice(1);
|