@oh-my-pi/pi-coding-agent 3.34.0 → 3.36.0

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.
@@ -11,7 +11,7 @@
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { getConfigDirPaths } from "../../../config";
14
- import type { AnthropicAuthConfig, AuthJson, ModelsJson } from "./types";
14
+ import type { AnthropicAuthConfig, AnthropicOAuthCredential, AuthJson, ModelsJson } from "./types";
15
15
 
16
16
  const DEFAULT_BASE_URL = "https://api.anthropic.com";
17
17
 
@@ -76,6 +76,11 @@ export function isOAuthToken(apiKey: string): boolean {
76
76
  return apiKey.includes("sk-ant-oat");
77
77
  }
78
78
 
79
+ function normalizeAnthropicOAuthCredentials(entry: AuthJson["anthropic"] | undefined): AnthropicOAuthCredential[] {
80
+ if (!entry) return [];
81
+ return Array.isArray(entry) ? entry : [entry];
82
+ }
83
+
79
84
  /**
80
85
  * Find Anthropic auth config using 4-tier priority:
81
86
  * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
@@ -126,13 +131,16 @@ export async function findAnthropicAuth(): Promise<AnthropicAuthConfig | null> {
126
131
  }
127
132
 
128
133
  // 3. OAuth credentials in auth.json (with 5-minute expiry buffer, check all config dirs)
134
+ const expiryBuffer = 5 * 60 * 1000; // 5 minutes
135
+ const now = Date.now();
129
136
  for (const configDir of configDirs) {
130
137
  const authJson = await readJson<AuthJson>(path.join(configDir, "auth.json"));
131
- if (authJson?.anthropic?.type === "oauth" && authJson.anthropic.access) {
132
- const expiryBuffer = 5 * 60 * 1000; // 5 minutes
133
- if (authJson.anthropic.expires > Date.now() + expiryBuffer) {
138
+ const credentials = normalizeAnthropicOAuthCredentials(authJson?.anthropic);
139
+ for (const credential of credentials) {
140
+ if (credential.type !== "oauth" || !credential.access) continue;
141
+ if (credential.expires > now + expiryBuffer) {
134
142
  return {
135
- apiKey: authJson.anthropic.access,
143
+ apiKey: credential.access,
136
144
  baseUrl: DEFAULT_BASE_URL,
137
145
  isOAuth: true,
138
146
  };
@@ -90,14 +90,18 @@ export interface ModelsJson {
90
90
  }
91
91
 
92
92
  /** auth.json structure for OAuth credentials */
93
+ export interface AnthropicOAuthCredential {
94
+ type: "oauth";
95
+ access: string;
96
+ refresh?: string;
97
+ /** Expiry timestamp in milliseconds */
98
+ expires: number;
99
+ }
100
+
101
+ export type AnthropicAuthJsonEntry = AnthropicOAuthCredential | AnthropicOAuthCredential[];
102
+
93
103
  export interface AuthJson {
94
- anthropic?: {
95
- type: "oauth";
96
- access: string;
97
- refresh?: string;
98
- /** Expiry timestamp in milliseconds */
99
- expires: number;
100
- };
104
+ anthropic?: AnthropicAuthJsonEntry;
101
105
  }
102
106
 
103
107
  /** Anthropic API response types */
package/src/main.ts CHANGED
@@ -289,6 +289,9 @@ async function buildSessionOptions(
289
289
  process.exit(1);
290
290
  }
291
291
  options.model = model;
292
+ settingsManager.applyOverrides({
293
+ modelRoles: { default: `${model.provider}/${model.id}` },
294
+ });
292
295
  } else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
293
296
  options.model = scopedModels[0].model;
294
297
  }
@@ -70,8 +70,7 @@ export class OAuthSelectorComponent extends Container {
70
70
  const isAvailable = provider.available;
71
71
 
72
72
  // Check if user is logged in for this provider
73
- const credentials = this.authStorage.get(provider.id);
74
- const isLoggedIn = credentials?.type === "oauth";
73
+ const isLoggedIn = this.authStorage.hasOAuth(provider.id);
75
74
  const statusIndicator = isLoggedIn ? theme.fg("success", ` ${theme.status.success} logged in`) : "";
76
75
 
77
76
  let line = "";
@@ -369,20 +369,23 @@ export class ToolExecutionComponent extends Container {
369
369
  this.contentBox.setBgFn(bgFn);
370
370
  this.contentBox.clear();
371
371
 
372
- // Render call component
373
- try {
374
- const callComponent = renderer.renderCall(this.args, theme);
375
- if (callComponent) {
376
- // Ensure component has invalidate() method for Component interface
377
- const component = callComponent as any;
378
- if (!component.invalidate) {
379
- component.invalidate = () => {};
372
+ const shouldRenderCall = !this.result || !renderer.mergeCallAndResult;
373
+ if (shouldRenderCall) {
374
+ // Render call component
375
+ try {
376
+ const callComponent = renderer.renderCall(this.args, theme);
377
+ if (callComponent) {
378
+ // Ensure component has invalidate() method for Component interface
379
+ const component = callComponent as any;
380
+ if (!component.invalidate) {
381
+ component.invalidate = () => {};
382
+ }
383
+ this.contentBox.addChild(component);
380
384
  }
381
- this.contentBox.addChild(component);
385
+ } catch {
386
+ // Fall back to default on error
387
+ this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
382
388
  }
383
- } catch {
384
- // Fall back to default on error
385
- this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
386
389
  }
387
390
 
388
391
  // Render result component if we have a result
@@ -38,6 +38,7 @@ import { VoiceSupervisor } from "../../core/voice-supervisor";
38
38
  import { disableProvider, enableProvider } from "../../discovery";
39
39
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
40
40
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
41
+ import { resizeImage } from "../../utils/image-resize";
41
42
  import { registerAsyncCleanup } from "../cleanup";
42
43
  import { ArminComponent } from "./components/armin";
43
44
  import { AssistantMessageComponent } from "./components/assistant-message";
@@ -1139,7 +1140,9 @@ export class InteractiveMode {
1139
1140
  if (this.session.isStreaming) {
1140
1141
  this.editor.addToHistory(text);
1141
1142
  this.editor.setText("");
1142
- await this.session.prompt(text, { streamingBehavior: "steer" });
1143
+ const images = this.pendingImages.length > 0 ? [...this.pendingImages] : undefined;
1144
+ this.pendingImages = [];
1145
+ await this.session.prompt(text, { streamingBehavior: "steer", images });
1143
1146
  this.updatePendingMessagesDisplay();
1144
1147
  this.ui.requestRender();
1145
1148
  return;
@@ -1154,7 +1157,7 @@ export class InteractiveMode {
1154
1157
  if (!hasUserMessages && !this.sessionManager.getSessionTitle()) {
1155
1158
  const registry = this.session.modelRegistry;
1156
1159
  const smolModel = this.settingsManager.getModelRole("smol");
1157
- generateSessionTitle(text, registry, smolModel)
1160
+ generateSessionTitle(text, registry, smolModel, this.session.sessionId)
1158
1161
  .then(async (title) => {
1159
1162
  if (title) {
1160
1163
  await this.sessionManager.setSessionTitle(title);
@@ -1481,11 +1484,14 @@ export class InteractiveMode {
1481
1484
  }
1482
1485
 
1483
1486
  private sendCompletionNotification(): void {
1487
+ if (this.isBackgrounded === false) return;
1484
1488
  if (isNotificationSuppressed()) return;
1485
1489
  const method = this.settingsManager.getNotificationOnComplete();
1486
1490
  if (method === "off") return;
1487
1491
  const protocol = method === "auto" ? detectNotificationProtocol() : method;
1488
- sendNotification(protocol, "Agent complete");
1492
+ const title = this.sessionManager.getSessionTitle();
1493
+ const message = title ? `${title}: Complete` : "Complete";
1494
+ sendNotification(protocol, message);
1489
1495
  }
1490
1496
 
1491
1497
  /** Extract text content from a user message */
@@ -1504,22 +1510,24 @@ export class InteractiveMode {
1504
1510
  * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
1505
1511
  * we update the previous status line instead of appending new ones to avoid log spam.
1506
1512
  */
1507
- private showStatus(message: string): void {
1513
+ private showStatus(message: string, options?: { dim?: boolean }): void {
1508
1514
  if (this.isBackgrounded) {
1509
1515
  return;
1510
1516
  }
1511
1517
  const children = this.chatContainer.children;
1512
1518
  const last = children.length > 0 ? children[children.length - 1] : undefined;
1513
1519
  const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
1520
+ const useDim = options?.dim ?? true;
1521
+ const rendered = useDim ? theme.fg("dim", message) : message;
1514
1522
 
1515
1523
  if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
1516
- this.lastStatusText.setText(theme.fg("dim", message));
1524
+ this.lastStatusText.setText(rendered);
1517
1525
  this.ui.requestRender();
1518
1526
  return;
1519
1527
  }
1520
1528
 
1521
1529
  const spacer = new Spacer(1);
1522
- const text = new Text(theme.fg("dim", message), 1, 0);
1530
+ const text = new Text(rendered, 1, 0);
1523
1531
  this.chatContainer.addChild(spacer);
1524
1532
  this.chatContainer.addChild(text);
1525
1533
  this.lastStatusSpacer = spacer;
@@ -1822,10 +1830,24 @@ export class InteractiveMode {
1822
1830
  try {
1823
1831
  const image = await readImageFromClipboard();
1824
1832
  if (image) {
1833
+ let imageData = image;
1834
+ if (this.settingsManager.getImageAutoResize()) {
1835
+ try {
1836
+ const resized = await resizeImage({
1837
+ type: "image",
1838
+ data: image.data,
1839
+ mimeType: image.mimeType,
1840
+ });
1841
+ imageData = { data: resized.data, mimeType: resized.mimeType };
1842
+ } catch {
1843
+ imageData = image;
1844
+ }
1845
+ }
1846
+
1825
1847
  this.pendingImages.push({
1826
1848
  type: "image",
1827
- data: image.data,
1828
- mimeType: image.mimeType,
1849
+ data: imageData.data,
1850
+ mimeType: imageData.mimeType,
1829
1851
  });
1830
1852
  // Insert styled placeholder at cursor like Claude does
1831
1853
  const imageNum = this.pendingImages.length;
@@ -1980,7 +2002,8 @@ export class InteractiveMode {
1980
2002
 
1981
2003
  private async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
1982
2004
  try {
1983
- const result = await this.session.cycleRoleModels(["slow", "default", "smol"], options);
2005
+ const roleOrder = ["slow", "default", "smol"];
2006
+ const result = await this.session.cycleRoleModels(roleOrder, options);
1984
2007
  if (!result) {
1985
2008
  this.showStatus("Only one role model available");
1986
2009
  return;
@@ -1989,10 +2012,24 @@ export class InteractiveMode {
1989
2012
  this.statusLine.invalidate();
1990
2013
  this.updateEditorBorderColor();
1991
2014
  const roleLabel = result.role === "default" ? "default" : result.role;
2015
+ const roleLabelStyled = theme.bold(theme.fg("accent", roleLabel));
1992
2016
  const thinkingStr =
1993
2017
  result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
1994
2018
  const tempLabel = options?.temporary ? " (temporary)" : "";
1995
- this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}`);
2019
+ const cycleSeparator = theme.fg("dim", " > ");
2020
+ const cycleLabel = roleOrder
2021
+ .map((role) => {
2022
+ if (role === result.role) {
2023
+ return theme.bold(theme.fg("accent", role));
2024
+ }
2025
+ return theme.fg("muted", role);
2026
+ })
2027
+ .join(cycleSeparator);
2028
+ const orderLabel = ` (cycle: ${cycleLabel})`;
2029
+ this.showStatus(
2030
+ `Switched to ${roleLabelStyled}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}${orderLabel}`,
2031
+ { dim: false },
2032
+ );
1996
2033
  } catch (error) {
1997
2034
  this.showError(error instanceof Error ? error.message : String(error));
1998
2035
  }
@@ -2576,9 +2613,7 @@ export class InteractiveMode {
2576
2613
  private async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
2577
2614
  if (mode === "logout") {
2578
2615
  const providers = this.session.modelRegistry.authStorage.list();
2579
- const loggedInProviders = providers.filter(
2580
- (p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth",
2581
- );
2616
+ const loggedInProviders = providers.filter((p) => this.session.modelRegistry.authStorage.hasOAuth(p));
2582
2617
  if (loggedInProviders.length === 0) {
2583
2618
  this.showStatus("No OAuth providers logged in. Use /login first.");
2584
2619
  return;
@@ -2599,6 +2634,7 @@ export class InteractiveMode {
2599
2634
  await this.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
2600
2635
  onAuth: (info: { url: string; instructions?: string }) => {
2601
2636
  this.chatContainer.addChild(new Spacer(1));
2637
+ this.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
2602
2638
  // Use OSC 8 hyperlink escape sequence for clickable link
2603
2639
  const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
2604
2640
  this.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
@@ -8,11 +8,6 @@ Use this tool to:
8
8
  - Request user preferences (styling, naming conventions, architecture patterns)
9
9
  - Offer meaningful choices about task direction
10
10
 
11
- Do NOT use for:
12
- - Questions resolvable by reading files or docs
13
- - Permission for normal dev tasks (just proceed)
14
- - Decisions you should make from codebase context
15
-
16
11
  Tips:
17
12
  - Place recommended option first with " (Recommended)" suffix
18
13
  - 2-5 concise, distinct options
@@ -22,3 +17,14 @@ Tips:
22
17
  question: "Which authentication method should this API use?"
23
18
  options: [{"label": "JWT (Recommended)"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
24
19
  </example>
20
+
21
+ ## Critical: Resolve before asking
22
+
23
+ **Exhaust all other options before asking.** Questions interrupt user flow.
24
+
25
+ 1. **Unknown file location?** → Search with grep/find first. Only ask if search fails.
26
+ 2. **Ambiguous syntax/format?** → Infer from context and codebase conventions. Make a reasonable choice.
27
+ 3. **Missing details?** → Check docs, related files, commit history. Fill gaps yourself.
28
+ 4. **Implementation approach?** → Choose based on codebase patterns. Ask only for genuinely novel architectural decisions.
29
+
30
+ If you can make a reasonable inference from the user's request, **do it**. Users communicate intent, not specifications—your job is to translate intent into correct implementation.
@@ -13,6 +13,7 @@ Do NOT use Bash for:
13
13
 
14
14
  ## Command structure
15
15
 
16
+ - Use `workdir` parameter to run commands in a specific directory instead of `cd dir && ...`
16
17
  - Paths with spaces must use double quotes: `cd "/path/with spaces"`
17
18
  - For sequential dependent operations, chain with `&&`: `mkdir foo && cd foo && touch bar`
18
19
  - For parallel independent operations, make multiple tool calls in one message
@@ -0,0 +1,8 @@
1
+ Basic calculations.
2
+
3
+ Input:
4
+ - calculations: array of { expression: string, prefix: string, suffix: string }
5
+
6
+ Notes:
7
+ - Supports +, -, *, /, %, ** and parentheses.
8
+ - Supports decimal, hex (0x), binary (0b), and octal (0o) literals.