pum-agent 0.2.28-beta.1 → 0.2.30-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/README.md CHANGED
@@ -23,6 +23,10 @@ pum
23
23
  PUM opens the login panel automatically on the first start. Press `?` on an
24
24
  empty prompt to see every control.
25
25
 
26
+ Select a model in `Ctrl+P`. GPT-6 Astra is listed for OpenAI and Codex;
27
+ using it requires access on your provider account. Press `r` in the model
28
+ list to refresh providers that support catalog discovery.
29
+
26
30
  > [!WARNING]
27
31
  > PUM can read, write, and delete files. Check mode adds deterministic policy
28
32
  > checks, and supported hosts can enforce native Bash isolation, but the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.28-beta.1",
3
+ "version": "0.2.30-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -50,8 +50,9 @@
50
50
  "pack:check": "bun run scripts/validate-pack.ts"
51
51
  },
52
52
  "dependencies": {
53
- "@earendil-works/pi-ai": "^0.84.1",
54
- "@earendil-works/pi-coding-agent": "^0.84.1",
53
+ "@earendil-works/pi-ai": "^0.85.0",
54
+ "@earendil-works/pi-coding-agent": "^0.85.0",
55
+ "@earendil-works/pi-server": "^0.85.0",
55
56
  "@opentui/core": "^0.5.1",
56
57
  "@opentui/react": "^0.5.1",
57
58
  "react": "^19.2.8",
package/src/app.tsx CHANGED
@@ -1041,6 +1041,8 @@ export function App({
1041
1041
  }));
1042
1042
  const [modelQuery, setModelQuery] = useState("");
1043
1043
  const [modelSearchFocused, setModelSearchFocused] = useState(false);
1044
+ const [modelRefreshing, setModelRefreshing] = useState(false);
1045
+ const [modelRevision, setModelRevision] = useState(0);
1044
1046
  const [, setAgentRevision] = useState(0);
1045
1047
  const [triggersOpen, setTriggersOpen] = useState(false);
1046
1048
  const [processTab, setProcessTab] = useState<ProcessTab>("triggers");
@@ -1321,7 +1323,7 @@ export function App({
1321
1323
  modelRuntime.getAvailableSnapshot(),
1322
1324
  modelQuery,
1323
1325
  (providerId) => (modelRuntime as any).getProvider?.(providerId)?.name ?? "",
1324
- ), [modelRuntime, modelId, modelQuery, loginPage]);
1326
+ ), [modelRuntime, modelId, modelQuery, loginPage, modelRevision]);
1325
1327
 
1326
1328
  const inputRef = useRef<TextareaRenderable>(null);
1327
1329
  const inputModeRef = useRef(false);
@@ -1334,6 +1336,7 @@ export function App({
1334
1336
  const processTabRef = useRef<ProcessTab>("triggers");
1335
1337
  const settingsPageRef = useRef(page);
1336
1338
  const settingsSearchFocusedRef = useRef(settingsSearchFocused);
1339
+ const modelRefreshInFlightRef = useRef(false);
1337
1340
  const focusInputAfterSwitch = useRef(false);
1338
1341
  const activeAgentIdRef = useRef<string | null>(null);
1339
1342
  const agentSelectorCursorRef = useRef(0);
@@ -2870,6 +2873,25 @@ export function App({
2870
2873
  void loadManagedProviders();
2871
2874
  }, [commandInput, managedProviders.length]);
2872
2875
 
2876
+ const refreshModels = async () => {
2877
+ if (modelRefreshInFlightRef.current) return;
2878
+ modelRefreshInFlightRef.current = true;
2879
+ setModelRefreshing(true);
2880
+ try {
2881
+ const result = await modelRuntime.refresh({ allowNetwork: true, force: true });
2882
+ if (result.errors.size > 0) {
2883
+ const details = [...result.errors].map(([provider, error]) => `${provider}: ${error.message}`).join("; ");
2884
+ append({ kind: "text", role: "error", text: `model refresh completed with errors: ${details}` });
2885
+ }
2886
+ } catch (error) {
2887
+ append({ kind: "text", role: "error", text: `model refresh failed: ${String(error)}` });
2888
+ } finally {
2889
+ modelRefreshInFlightRef.current = false;
2890
+ setModelRefreshing(false);
2891
+ setModelRevision((revision) => revision + 1);
2892
+ }
2893
+ };
2894
+
2873
2895
  const selectModel = (model: Model<any>) => {
2874
2896
  settingsPageRef.current = "main";
2875
2897
  setPage("main");
@@ -4428,7 +4450,7 @@ export function App({
4428
4450
  const call: ToolCall = {
4429
4451
  id,
4430
4452
  name: "bash",
4431
- args: [command.split("\n")[0]!.trim()],
4453
+ args: toolArgs("bash", { command }, cwd),
4432
4454
  state: "running",
4433
4455
  startedAt: Date.now(),
4434
4456
  input: { command },
@@ -5284,6 +5306,11 @@ export function App({
5284
5306
  }
5285
5307
  return;
5286
5308
  }
5309
+ if (!key.ctrl && !key.meta && !key.option && printableKey === "r") {
5310
+ key.stopPropagation();
5311
+ void refreshModels();
5312
+ return;
5313
+ }
5287
5314
  if (isModelSearchShortcut(key, modelSearchFocused)) {
5288
5315
  key.stopPropagation();
5289
5316
  setModelSearchFocused(true);
@@ -6170,6 +6197,7 @@ export function App({
6170
6197
  models={visibleModels}
6171
6198
  modelQuery={modelQuery}
6172
6199
  modelSearchFocused={modelSearchFocused}
6200
+ modelRefreshing={modelRefreshing}
6173
6201
  onSearchChange={updateSettingsQuery}
6174
6202
  onModelSearchChange={setModelQuery}
6175
6203
  onSelectModel={selectModel}
package/src/headless.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  SessionManager,
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { mkdirSync } from "node:fs";
9
+ import { installModelCatalogFallbacks } from "./model-catalog";
9
10
  import { AGENT_DIR, AUTH_PATH, MODELS_PATH, sessionDir } from "./config";
10
11
  import { createMemoryExtension, MEMORY_EDIT_TOOL_NAME, MEMORY_READ_TOOL_NAME } from "./memory";
11
12
  import { checkPathsForProject, loadSettings } from "./settings";
@@ -148,6 +149,7 @@ async function runPromptSession(
148
149
  authPath: AUTH_PATH,
149
150
  modelsPath: MODELS_PATH,
150
151
  });
152
+ installModelCatalogFallbacks(modelRuntime);
151
153
  if ((await modelRuntime.getAvailable()).length === 0) {
152
154
  process.stderr.write("pum: no provider is available. Run 'pum login' first.\n");
153
155
  return 1;
package/src/main.tsx CHANGED
@@ -15,6 +15,7 @@ import { createMemoryExtension } from "./memory";
15
15
  import { checkPathsForProject, loadSettings } from "./settings";
16
16
  import { setBashOutputSettingsIfPresent } from "./bash-output";
17
17
  import { installWebSearch, webSearch } from "./web-search";
18
+ import { installModelCatalogFallbacks } from "./model-catalog";
18
19
  import { identityExtension } from "./identity";
19
20
  import { setWritingStyle, writingStyleExtension } from "./writing-style";
20
21
  import { checkModePromptExtension, setSandboxModeSource } from "./check-mode-prompt";
@@ -97,6 +98,7 @@ export async function start(
97
98
  authPath: AUTH_PATH,
98
99
  modelsPath: MODELS_PATH,
99
100
  });
101
+ installModelCatalogFallbacks(modelRuntime);
100
102
  const loginRequired = options.login || (await modelRuntime.getAvailable()).length === 0;
101
103
 
102
104
  const settings = loadSettings();
@@ -0,0 +1,47 @@
1
+ import type { Model, Provider } from "@earendil-works/pi-ai";
2
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
3
+
4
+ const ASTRA_ID = "gpt-6-astra";
5
+
6
+ /** Temporary catalog fallback until pi includes Astra. Never replace upstream metadata. */
7
+ export function withAstraModel(base: Provider): Provider {
8
+ if (base.id !== "openai" && base.id !== "openai-codex") return base;
9
+ if (base.getModels().some((model) => model.id === ASTRA_ID)) return base;
10
+ const codex = base.id === "openai-codex";
11
+ // https://developers.openai.com/api/docs/models/gpt-6-astra
12
+ const astra: Model<any> = {
13
+ id: ASTRA_ID,
14
+ name: "GPT-6 Astra",
15
+ provider: base.id,
16
+ api: codex ? "openai-codex-responses" : "openai-responses",
17
+ baseUrl: base.baseUrl ?? (codex ? "https://chatgpt.com/backend-api" : "https://api.openai.com/v1"),
18
+ reasoning: true,
19
+ input: ["text", "image"],
20
+ // Keep pi's conservative Codex context budget until that transport publishes one.
21
+ contextWindow: codex ? 272000 : 1050000,
22
+ maxTokens: 128000,
23
+ cost: {
24
+ input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5,
25
+ tiers: [{ inputTokensAbove: 272000, input: 20, output: 75, cacheRead: 2, cacheWrite: 25 }],
26
+ },
27
+ thinkingLevelMap: {
28
+ off: null, minimal: null, low: "low", medium: "medium",
29
+ high: "high", xhigh: "xhigh", max: "max",
30
+ },
31
+ };
32
+ const wrapped: Provider = Object.create(base);
33
+ wrapped.getModels = () => {
34
+ const models = base.getModels();
35
+ return models.some((model) => model.id === ASTRA_ID) ? models : [...models, astra];
36
+ };
37
+ return wrapped;
38
+ }
39
+
40
+ export function installModelCatalogFallbacks(runtime: ModelRuntime): void {
41
+ for (const id of ["openai", "openai-codex"]) {
42
+ const base = runtime.getProvider(id);
43
+ if (!base) continue;
44
+ const provider = withAstraModel(base);
45
+ if (provider !== base) runtime.registerNativeProvider(provider);
46
+ }
47
+ }
package/src/replay.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AgentMessageKind, Line } from "./transcript";
2
- import { type ToolCall } from "./tool-line";
2
+ import { toolArgs, type ToolCall } from "./tool-line";
3
3
  import { interruptedToolCall, settledToolCall, startedToolCall } from "./tool-row";
4
4
  import {
5
5
  WEB_SEARCH_CUSTOM_TYPE,
@@ -274,7 +274,7 @@ export function replayEntries(
274
274
  const call: ToolCall = {
275
275
  id: `user-bash:${entry?.id ?? lines.length}`,
276
276
  name: "bash",
277
- args: [message.command.split("\n")[0]!.trim()],
277
+ args: toolArgs("bash", { command: message.command }, cwd),
278
278
  state: "running",
279
279
  input: { command: message.command },
280
280
  userInitiated: true,
@@ -203,6 +203,7 @@ export type PopupProps = {
203
203
  models: readonly Model<any>[];
204
204
  modelQuery?: string;
205
205
  modelSearchFocused?: boolean;
206
+ modelRefreshing?: boolean;
206
207
  onSearchChange: (value: string) => void;
207
208
  onModelSearchChange?: (value: string) => void;
208
209
  onSelectModel: (model: Model<any>) => void;
@@ -226,6 +227,7 @@ export function SettingsPopup({
226
227
  models,
227
228
  modelQuery = "",
228
229
  modelSearchFocused = false,
230
+ modelRefreshing = false,
229
231
  onSearchChange,
230
232
  onModelSearchChange = () => {},
231
233
  onSelectModel,
@@ -378,7 +380,11 @@ export function SettingsPopup({
378
380
  />}
379
381
  </box> : null}
380
382
  {layout.footerHeight ? <text
381
- content="/ search ↑↓ move enter select esc back"
383
+ content={modelRefreshing
384
+ ? layout.narrow ? "Refreshing models… esc" : "Refreshing models… esc back"
385
+ : layout.narrow
386
+ ? "/ search ↑↓ enter select r refresh esc"
387
+ : "/ search ↑↓ move enter select r refresh esc back"}
382
388
  fg={theme.dim}
383
389
  bg={theme.popupBg}
384
390
  wrapMode="none"
@@ -2376,7 +2376,7 @@ export class SubagentManager {
2376
2376
  const call: ToolCall = {
2377
2377
  id: callId,
2378
2378
  name: "bash",
2379
- args: [command.split("\n")[0]!.trim()],
2379
+ args: toolArgs("bash", { command }, record.snapshot.worktree.path),
2380
2380
  state: "running",
2381
2381
  startedAt: Date.now(),
2382
2382
  input: { command },
package/src/tool-line.ts CHANGED
@@ -99,7 +99,7 @@ export function toolArgs(name: string, args: any, cwd: string): string[] {
99
99
  if (!args || typeof args !== "object") return [];
100
100
 
101
101
  if (name === "bash" && typeof args.command === "string") {
102
- return [args.command.split("\n")[0]!.trim()];
102
+ return [args.command.replaceAll("\r\n", "\n").replaceAll("\r", "\n")];
103
103
  }
104
104
  if (name === "web_search") {
105
105
  // web-search.ts owns which fields are safe to show and joins them; this
@@ -177,7 +177,9 @@ export function toolArgs(name: string, args: any, cwd: string): string[] {
177
177
 
178
178
  /** One flat line for logs, persistence, and anywhere without styled chunks. */
179
179
  export function toolArgText(name: string, args: any, cwd: string): string {
180
- return toolArgs(name, args, cwd).join(", ");
180
+ return toolArgs(name, args, cwd)
181
+ .map((arg) => arg.replaceAll("\n", "\\n"))
182
+ .join(", ");
181
183
  }
182
184
 
183
185
  /** Count changed lines from a tool result's unified patch details. */