pi-ui-extend 0.1.13 → 0.1.17

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.
Files changed (111) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +7 -0
  3. package/dist/app/app.js +102 -17
  4. package/dist/app/commands/command-controller.js +2 -0
  5. package/dist/app/commands/command-host.d.ts +5 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  9. package/dist/app/commands/command-navigation-actions.js +62 -0
  10. package/dist/app/commands/command-registry.d.ts +2 -0
  11. package/dist/app/commands/command-registry.js +16 -0
  12. package/dist/app/constants.d.ts +0 -1
  13. package/dist/app/constants.js +0 -1
  14. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  15. package/dist/app/extensions/extension-ui-controller.js +99 -61
  16. package/dist/app/icons.d.ts +1 -0
  17. package/dist/app/icons.js +2 -0
  18. package/dist/app/input/input-action-controller.d.ts +2 -0
  19. package/dist/app/input/input-action-controller.js +8 -1
  20. package/dist/app/logger.d.ts +25 -0
  21. package/dist/app/logger.js +90 -0
  22. package/dist/app/model/model-usage-status.js +30 -15
  23. package/dist/app/popup/menu-items-controller.d.ts +4 -0
  24. package/dist/app/popup/menu-items-controller.js +68 -6
  25. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  26. package/dist/app/popup/popup-action-controller.js +7 -4
  27. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  28. package/dist/app/popup/popup-menu-controller.js +97 -326
  29. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  30. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  31. package/dist/app/rendering/conversation-viewport.js +157 -16
  32. package/dist/app/rendering/editor-panels.js +22 -9
  33. package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
  34. package/dist/app/rendering/popup-menu-renderer.js +405 -0
  35. package/dist/app/rendering/render-controller.js +30 -28
  36. package/dist/app/rendering/render-text.js +5 -2
  37. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  38. package/dist/app/rendering/status-line-renderer.js +217 -117
  39. package/dist/app/rendering/toast-controller.d.ts +12 -3
  40. package/dist/app/rendering/toast-controller.js +70 -12
  41. package/dist/app/runtime.d.ts +2 -1
  42. package/dist/app/runtime.js +20 -10
  43. package/dist/app/screen/mouse-controller.d.ts +2 -2
  44. package/dist/app/screen/mouse-controller.js +27 -48
  45. package/dist/app/screen/screen-styler.d.ts +1 -1
  46. package/dist/app/screen/screen-styler.js +9 -7
  47. package/dist/app/screen/scroll-controller.d.ts +12 -9
  48. package/dist/app/screen/scroll-controller.js +56 -45
  49. package/dist/app/screen/status-controller.js +2 -1
  50. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  51. package/dist/app/session/lazy-session-manager.js +539 -0
  52. package/dist/app/session/pix-system-message.d.ts +16 -0
  53. package/dist/app/session/pix-system-message.js +64 -0
  54. package/dist/app/session/request-history.d.ts +4 -0
  55. package/dist/app/session/request-history.js +11 -0
  56. package/dist/app/session/session-event-controller.d.ts +11 -0
  57. package/dist/app/session/session-event-controller.js +58 -2
  58. package/dist/app/session/session-history.d.ts +18 -0
  59. package/dist/app/session/session-history.js +72 -3
  60. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  61. package/dist/app/session/session-lifecycle-controller.js +7 -2
  62. package/dist/app/session/session-search.js +10 -0
  63. package/dist/app/session/tabs-controller.d.ts +17 -5
  64. package/dist/app/session/tabs-controller.js +308 -29
  65. package/dist/app/todo/todo-model.d.ts +4 -2
  66. package/dist/app/todo/todo-model.js +23 -13
  67. package/dist/app/types.d.ts +17 -6
  68. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  69. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  70. package/dist/config.d.ts +6 -1
  71. package/dist/config.js +82 -25
  72. package/dist/default-pix-config.js +4 -0
  73. package/dist/fuzzy.d.ts +2 -0
  74. package/dist/fuzzy.js +27 -7
  75. package/dist/input-editor.d.ts +9 -0
  76. package/dist/input-editor.js +52 -0
  77. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  78. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  79. package/dist/schemas/pix-schema.d.ts +3 -1
  80. package/dist/schemas/pix-schema.js +6 -4
  81. package/dist/terminal-width.d.ts +2 -0
  82. package/dist/terminal-width.js +64 -3
  83. package/dist/theme.js +6 -6
  84. package/dist/ui.d.ts +8 -0
  85. package/external/pi-tools-suite/README.md +3 -2
  86. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
  87. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
  88. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  89. package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
  90. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
  91. package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
  92. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
  93. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  94. package/external/pi-tools-suite/src/config.ts +8 -0
  95. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  96. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  98. package/external/pi-tools-suite/src/todo/index.ts +123 -14
  99. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  100. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
  101. package/external/pi-tools-suite/src/todo/todo.ts +12 -23
  102. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
  103. package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
  104. package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
  105. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
  106. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  107. package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
  108. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  109. package/package.json +1 -1
  110. package/schemas/pi-tools-suite.json +4 -0
  111. package/schemas/pix.json +11 -2
@@ -1,5 +1,5 @@
1
1
  /**
2
- * TypeBox JSON Schema definitions for pix.jsonc (~/.config/pi/pix.jsonc).
2
+ * TypeBox JSON Schema definitions for pix.jsonc (~/.config/pi/pix.jsonc or <cwd>/.pi/pix.jsonc).
3
3
  *
4
4
  * These schemas describe the _user-facing_ config shape — all fields are optional
5
5
  * because the runtime applies generous defaults. The generated JSON Schema files
@@ -9,7 +9,7 @@ import { Type } from "typebox";
9
9
  // ---------------------------------------------------------------------------
10
10
  // Shared primitives
11
11
  // ---------------------------------------------------------------------------
12
- const ThinkingLevel = Type.Union(["off", "minimal", "low", "medium", "high", "xhigh"].map((v) => Type.Literal(v)), { description: "Model thinking budget level." });
12
+ const DefaultThinkingSelection = Type.Union(["off", "minimal", "low", "medium", "high", "xhigh"].map((v) => Type.Literal(v)), { description: "Default model thinking budget level." });
13
13
  // ---------------------------------------------------------------------------
14
14
  // Tool renderer
15
15
  // ---------------------------------------------------------------------------
@@ -33,7 +33,7 @@ const OutputFiltersConfig = Type.Object({
33
33
  }, { description: "Output filter patterns." });
34
34
  const DefaultModelConfig = Type.Object({
35
35
  modelRef: Type.Optional(Type.String({ description: "Provider/model identifier, e.g. 'openai-codex/gpt-5.4'." })),
36
- thinking: Type.Optional(ThinkingLevel),
36
+ thinking: Type.Optional(DefaultThinkingSelection),
37
37
  }, { description: "Default model selection for new sessions." });
38
38
  const PromptEnhancerConfig = Type.Object({
39
39
  modelRef: Type.Optional(Type.String({ description: "Model used for prompt enhancement." })),
@@ -73,6 +73,8 @@ const DictationConfig = Type.Object({
73
73
  // ---------------------------------------------------------------------------
74
74
  export const PixConfigSchema = Type.Object({
75
75
  $schema: Type.Optional(Type.String({ description: "JSON Schema URL used by editors for validation and autocomplete." })),
76
+ ignoreContextFiles: Type.Optional(Type.Boolean({ description: "Disable AGENTS.md / CLAUDE.md discovery for sessions started in this project, equivalent to pi --no-context-files." })),
77
+ maxProjectSessions: Type.Optional(Type.Number({ description: "Maximum number of pi session JSONL files to retain per project. Set to 0 to disable automatic session deletion.", minimum: 0 })),
76
78
  defaultModel: Type.Optional(DefaultModelConfig),
77
79
  toolRenderer: Type.Optional(ToolRendererConfig),
78
80
  outputFilters: Type.Optional(OutputFiltersConfig),
@@ -86,6 +88,6 @@ export const PixConfigSchema = Type.Object({
86
88
  $id: "https://unpkg.com/pi-ui-extend/schemas/pix.json",
87
89
  $schema: "https://json-schema.org/draft-07/schema#",
88
90
  title: "Pix Configuration",
89
- description: "Configuration for the pix terminal renderer (~/.config/pi/pix.jsonc).",
91
+ description: "Configuration for the pix terminal renderer (~/.config/pi/pix.jsonc, with project overrides in <cwd>/.pi/pix.jsonc).",
90
92
  additionalProperties: true,
91
93
  });
@@ -1,6 +1,8 @@
1
1
  export declare function expandTabs(text: string, tabWidth?: number): string;
2
2
  export declare function stringDisplayWidth(text: string): number;
3
3
  export declare function sliceByDisplayWidth(text: string, width: number): string;
4
+ export declare function displayIndexForColumn(text: string, column: number): number;
5
+ export declare function sliceByDisplayColumns(text: string, startColumn: number, endColumn: number): string;
4
6
  export declare function padOrTrimDisplay(text: string, width: number): string;
5
7
  export declare function wrapDisplayLine(text: string, width: number): string[];
6
8
  export declare function wrapDisplayLineByWords(text: string, width: number): string[];
@@ -4,6 +4,8 @@ const EMOJI_PRESENTATION_REGEX = /\p{Emoji_Presentation}/u;
4
4
  const EMOJI_REGEX = /\p{Emoji}/u;
5
5
  const GRAPHEME_SEGMENTER = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : undefined;
6
6
  export function expandTabs(text, tabWidth = TAB_WIDTH) {
7
+ if (!text.includes("\t"))
8
+ return text;
7
9
  let result = "";
8
10
  let column = 0;
9
11
  for (const cluster of displayClusters(text)) {
@@ -29,6 +31,8 @@ export function expandTabs(text, tabWidth = TAB_WIDTH) {
29
31
  return result;
30
32
  }
31
33
  export function stringDisplayWidth(text) {
34
+ if (isPrintableAscii(text))
35
+ return text.length;
32
36
  let width = 0;
33
37
  for (const cluster of displayClusters(text)) {
34
38
  width += cluster.width;
@@ -37,6 +41,8 @@ export function stringDisplayWidth(text) {
37
41
  }
38
42
  export function sliceByDisplayWidth(text, width) {
39
43
  const safeWidth = Math.max(0, width);
44
+ if (isPrintableAscii(text))
45
+ return text.slice(0, safeWidth);
40
46
  let result = "";
41
47
  let used = 0;
42
48
  let sawAnsi = false;
@@ -58,13 +64,41 @@ export function sliceByDisplayWidth(text, width) {
58
64
  return `${result}${ANSI_RESET}`;
59
65
  return result;
60
66
  }
67
+ export function displayIndexForColumn(text, column) {
68
+ const targetColumn = Math.max(1, column);
69
+ let displayColumn = 1;
70
+ for (const cluster of indexedDisplayClusters(text)) {
71
+ if (targetColumn <= displayColumn)
72
+ return cluster.start;
73
+ if (cluster.ansi || cluster.width <= 0)
74
+ continue;
75
+ const nextColumn = displayColumn + cluster.width;
76
+ if (targetColumn < nextColumn)
77
+ return cluster.start;
78
+ if (targetColumn === nextColumn)
79
+ return cluster.end;
80
+ displayColumn = nextColumn;
81
+ }
82
+ return text.length;
83
+ }
84
+ export function sliceByDisplayColumns(text, startColumn, endColumn) {
85
+ const startIndex = displayIndexForColumn(text, startColumn);
86
+ const endIndex = Math.max(startIndex, displayIndexForColumn(text, endColumn));
87
+ return text.slice(startIndex, endIndex);
88
+ }
61
89
  export function padOrTrimDisplay(text, width) {
62
90
  const safeWidth = Math.max(0, width);
91
+ if (isPrintableAscii(text)) {
92
+ const trimmed = text.slice(0, safeWidth);
93
+ return `${trimmed}${" ".repeat(Math.max(0, safeWidth - trimmed.length))}`;
94
+ }
63
95
  const trimmed = sliceByDisplayWidth(text, safeWidth);
64
96
  return `${trimmed}${" ".repeat(Math.max(0, safeWidth - stringDisplayWidth(trimmed)))}`;
65
97
  }
66
98
  export function wrapDisplayLine(text, width) {
67
99
  const safeWidth = Math.max(1, width);
100
+ if (isPrintableAscii(text))
101
+ return wrapPrintableAsciiLine(text, safeWidth);
68
102
  const chunks = [];
69
103
  let chunk = "";
70
104
  let chunkWidth = 0;
@@ -152,6 +186,23 @@ function appendTokenToEmptyChunk(token, width, chunks) {
152
186
  function trimTrailingWhitespace(text) {
153
187
  return text.replace(/\s+$/u, "");
154
188
  }
189
+ function isPrintableAscii(text) {
190
+ for (let index = 0; index < text.length; index += 1) {
191
+ const code = text.charCodeAt(index);
192
+ if (code < 0x20 || code > 0x7e)
193
+ return false;
194
+ }
195
+ return true;
196
+ }
197
+ function wrapPrintableAsciiLine(text, width) {
198
+ if (text.length <= width)
199
+ return [text];
200
+ const chunks = [];
201
+ for (let start = 0; start < text.length; start += width) {
202
+ chunks.push(text.slice(start, start + width));
203
+ }
204
+ return chunks;
205
+ }
155
206
  function ansiSequenceLength(text, index) {
156
207
  if (text.charCodeAt(index) !== 0x1b)
157
208
  return 0;
@@ -179,29 +230,39 @@ function ansiSequenceLength(text, index) {
179
230
  return 2;
180
231
  }
181
232
  function* displayClusters(text) {
233
+ for (const cluster of indexedDisplayClusters(text)) {
234
+ yield { text: cluster.text, width: cluster.width, ansi: cluster.ansi };
235
+ }
236
+ }
237
+ function* indexedDisplayClusters(text) {
182
238
  for (let index = 0; index < text.length;) {
183
239
  const ansiLength = ansiSequenceLength(text, index);
184
240
  if (ansiLength > 0) {
241
+ const start = index;
185
242
  const cluster = text.slice(index, index + ansiLength);
186
- yield { text: cluster, width: 0, ansi: true };
187
243
  index += ansiLength;
244
+ yield { text: cluster, width: 0, ansi: true, start, end: index };
188
245
  continue;
189
246
  }
190
247
  const nextAnsiIndex = text.indexOf("\x1b", index + 1);
191
248
  const textEnd = nextAnsiIndex === -1 ? text.length : nextAnsiIndex;
192
249
  const segment = text.slice(index, textEnd);
193
250
  if (GRAPHEME_SEGMENTER) {
251
+ let segmentOffset = index;
194
252
  for (const { segment: cluster } of GRAPHEME_SEGMENTER.segment(segment)) {
195
- yield { text: cluster, width: graphemeDisplayWidth(cluster), ansi: false };
253
+ const start = segmentOffset;
254
+ segmentOffset += cluster.length;
255
+ yield { text: cluster, width: graphemeDisplayWidth(cluster), ansi: false, start, end: segmentOffset };
196
256
  }
197
257
  index = textEnd;
198
258
  continue;
199
259
  }
200
260
  while (index < textEnd) {
261
+ const start = index;
201
262
  const codePoint = text.codePointAt(index) ?? 0;
202
263
  const cluster = String.fromCodePoint(codePoint);
203
- yield { text: cluster, width: graphemeDisplayWidth(cluster), ansi: false };
204
264
  index += codePointLength(codePoint);
265
+ yield { text: cluster, width: graphemeDisplayWidth(cluster), ansi: false, start, end: index };
205
266
  }
206
267
  }
207
268
  }
package/dist/theme.js CHANGED
@@ -18,11 +18,11 @@ export const THEMES = {
18
18
  inputCursorBackground: "#7fb3c8",
19
19
  popupForeground: "#e6edf3",
20
20
  popupBackground: "#1e1e1e",
21
- popupHeaderBackground: "#2a2f36",
21
+ popupHeaderBackground: "#263241",
22
22
  popupBorder: "#1e1e1e",
23
23
  popupMuted: "#8a8a8a",
24
- popupSelectedForeground: "#090d13",
25
- popupSelectedBackground: "#7fb3c8",
24
+ popupSelectedForeground: "#e6edf3",
25
+ popupSelectedBackground: "#2a2f36",
26
26
  selectionForeground: "#ffffff",
27
27
  selectionBackground: "#3b82f6",
28
28
  toastForeground: "#0d1117",
@@ -60,11 +60,11 @@ export const THEMES = {
60
60
  inputCursorBackground: "#0284c7",
61
61
  popupForeground: "#0f172a",
62
62
  popupBackground: "#ffffff",
63
- popupHeaderBackground: "#f1f5f9",
63
+ popupHeaderBackground: "#dbeafe",
64
64
  popupBorder: "#ffffff",
65
65
  popupMuted: "#64748b",
66
- popupSelectedForeground: "#f8fafc",
67
- popupSelectedBackground: "#0284c7",
66
+ popupSelectedForeground: "#0f172a",
67
+ popupSelectedBackground: "#f1f5f9",
68
68
  selectionForeground: "#ffffff",
69
69
  selectionBackground: "#2563eb",
70
70
  toastForeground: "#064e3b",
package/dist/ui.d.ts CHANGED
@@ -2,6 +2,14 @@ export type PopupMenuItem<T> = {
2
2
  value: T;
3
3
  label: string;
4
4
  description?: string;
5
+ labelHighlightRanges?: readonly {
6
+ start: number;
7
+ end: number;
8
+ }[];
9
+ descriptionHighlightRanges?: readonly {
10
+ start: number;
11
+ end: number;
12
+ }[];
5
13
  };
6
14
  export type VisiblePopupMenuItem<T> = PopupMenuItem<T> & {
7
15
  index: number;
@@ -8,8 +8,8 @@ This package keeps shared Pi tools as ordinary source folders under `src/` and r
8
8
  - `src/async-subagents` — `subagents` tool and sub-agent slash commands, including oh-my-openagent-style `/ultrawork` (`/ulw`) and `/hyperplan` orchestration prompts, plus config-defined sub-agent model/thinking/args presets selected via `/subagent-preset` from `asyncSubagents` in `~/.config/pi/pi-tools-suite.jsonc`; includes the `frontend` profile for Gemini-friendly UI/UX and visual frontend work plus the `vision` profile for screenshot/image description via `openai-codex/gpt-5.4-mini`; enforces a 30-minute per-agent execution timeout, project-wide `maxConcurrent` queueing, optional retry/backoff, and `result.json` structured metadata/chaining fields next to raw `result.md`; stores project-local run files and a registry under `.pi/subagents/` so result/status collection can recover after compaction or reload while the main session remains alive
9
9
  - `src/lsp` — shared LSP diagnostics hook/library that enriches mutating tool results with diagnostics and shuts down language servers on session shutdown
10
10
  - `src/repo-discovery` — `/idx-init`, `/idx-update`, and indexed-only `repo_architecture` / `repo_structure` / `repo_ast` / `repo_search` / `repo_explain` / `repo_deps`; tools register only when the launch project has `.indexer-cli`
11
- - `src/antigravity-auth` — `antigravity` custom provider with Google Antigravity OAuth login, startup account list, `/antigravity-import` credential migration from opencode, `/antigravity-add-account` OAuth append into rotation, `/antigravity-account` status display, account rotation/failover, Antigravity plus Gemini CLI model registration, and streaming through the Cloud Code Assist unified gateway
12
- - `src/todo` — `todo` tool, `/todos`, `/todos-persist`, and `/todos-scope`; supports priorities, tags, parent/subtask hierarchy, blockers, ready-task filtering, deferred out-of-scope items, batch operations, JSON/Markdown import/export, automatic clearing when all visible todos are completed, and optional project persistence via `/todos persist on` or `/todos-persist on`; localization/i18n has been removed
11
+ - `src/antigravity-auth` — `antigravity` custom provider with Google Antigravity OAuth login, startup account list, auth.json-only runtime account loading, `/antigravity-add-account` OAuth append into rotation, `/antigravity-account` status display, account rotation/failover, Antigravity plus Gemini CLI model registration, and streaming through the Cloud Code Assist unified gateway
12
+ - `src/todo` — `todo` tool, `/todos`, `/todos-persist`, and `/todos-scope`; supports parent/subtask hierarchy, blockers, ready-task filtering, deferred out-of-scope items, batch operations, JSON/Markdown import/export, automatic clearing when all visible todos are completed, and optional project persistence via `/todos persist on` or `/todos-persist on`; localization/i18n has been removed
13
13
  - `src/model-tools` — model-specific tool aliases such as Claude/GLM-style `Read` / `Edit` / `Write` / `Bash` / `Grep` / `Glob` / `LS`, GPT/Codex-style `shell`, and model-gated `apply_patch`
14
14
  - `src/usage` — `/usage` command and startup hint for read-only AI quota checks across OpenAI, Zhipu AI, Z.ai, and Google Antigravity, including Antigravity quota by model
15
15
  - `src/web-search` — `web_search` and `web_fetch` tools migrated from `@ollama/pi-web-search`; calls the local Ollama experimental web search/fetch APIs, honors `OLLAMA_HOST`, supports request timeouts via `timeout_ms` / `PI_WEB_SEARCH_TIMEOUT_MS`, and reports targeted `ollama signin`, unsupported-endpoint, invalid-response, timeout, DNS, and Ollama-not-running errors
@@ -62,6 +62,7 @@ DCP settings are stored only under `dcp` in the user shared config file `~/.conf
62
62
  "enabled": true,
63
63
  "compress": {
64
64
  "minContextPercent": "25%",
65
+ "maxContextPercent": "65%",
65
66
  "maxContextLimit": 160000,
66
67
  "nudgeFrequency": 1,
67
68
  "iterationNudgeThreshold": 8,
@@ -1,9 +1,14 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
4
  import { DEFAULT_PROJECT_ID, PROVIDER_ID } from "./constants";
6
- import type { OpencodeAntigravityAccount, OpencodeAntigravityImportResult, OpencodeAntigravityStorage, PiAuthCredential, PiAuthData } from "./types";
5
+ import type { GoogleOAuthClientCredentials, OpencodeAntigravityAccount, OpencodeAntigravityImportResult, OpencodeAntigravityStorage, PiAuthCredential, PiAuthData } from "./types";
6
+
7
+ export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
8
+
9
+ function testPiAuthPath(): string | undefined {
10
+ return process.env.NODE_ENV === "test" ? process.env.PI_TOOLS_SUITE_TEST_AUTH_PATH : undefined;
11
+ }
7
12
 
8
13
  export function splitRefresh(refresh: string): { refreshToken: string; projectId?: string; managedProjectId?: string } {
9
14
  const [refreshToken = "", projectId = "", managedProjectId = ""] = refresh.split("|");
@@ -33,13 +38,17 @@ export function decodeApiKey(apiKey: string): { access: string; projectId?: stri
33
38
  return { access, projectId: projectId || undefined };
34
39
  }
35
40
 
36
- function getDefaultOpencodeAccountsPath(): string {
41
+ export function getDefaultOpencodeAccountsPath(): string {
37
42
  const configDir = process.env.OPENCODE_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "opencode");
38
43
  return join(configDir, "antigravity-accounts.json");
39
44
  }
40
45
 
46
+ export async function importDefaultOpencodeAntigravityAccount(options: { overwrite?: boolean } = {}): Promise<OpencodeAntigravityImportResult> {
47
+ return importOpencodeAntigravityAccount({ sourcePath: getDefaultOpencodeAccountsPath(), authPath: getPiAuthPath(), overwrite: options.overwrite });
48
+ }
49
+
41
50
  export function getPiAuthPath(): string {
42
- return join(getAgentDir(), "auth.json");
51
+ return testPiAuthPath() ?? PI_AUTH_PATH;
43
52
  }
44
53
 
45
54
  export async function readJsonFile<T>(path: string, fallback: T): Promise<T> {
@@ -58,7 +67,36 @@ export async function writeJsonFileSecure(path: string, data: unknown): Promise<
58
67
  }
59
68
 
60
69
  export function getStoredAccounts(credential?: PiAuthCredential): OpencodeAntigravityAccount[] {
61
- return Array.isArray(credential?.accounts) ? credential.accounts.filter((account) => account.enabled !== false && account.refreshToken) : [];
70
+ return Array.isArray(credential?.accounts) ? credential.accounts.filter((account) => account.enabled !== false && getAccountRefreshToken(account)) : [];
71
+ }
72
+
73
+ export function getAccountRefreshToken(account: OpencodeAntigravityAccount): string | undefined {
74
+ if (account.refreshToken) return account.refreshToken;
75
+ if (!account.refresh) return undefined;
76
+ const refresh = splitRefresh(account.refresh);
77
+ return refresh.refreshToken || undefined;
78
+ }
79
+
80
+ function stringProperty(source: unknown, keys: string[]): string | undefined {
81
+ if (!source || typeof source !== "object") return undefined;
82
+ const record = source as Record<string, unknown>;
83
+ for (const key of keys) {
84
+ const value = record[key];
85
+ if (typeof value === "string" && value) return value;
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ export function getGoogleOAuthClientCredentials(...sources: Array<unknown>): GoogleOAuthClientCredentials | undefined {
91
+ for (const source of sources) {
92
+ const nested = source && typeof source === "object" ? (source as Record<string, unknown>).oauthClient : undefined;
93
+ const nestedClientId = stringProperty(nested, ["clientId", "client_id", "id"]);
94
+ const nestedClientSecret = stringProperty(nested, ["clientSecret", "client_secret", "secret"]);
95
+ const clientId = nestedClientId ?? stringProperty(source, ["clientId", "client_id", "googleClientId", "google_client_id", "oauthClientId", "oauth_client_id"]);
96
+ const clientSecret = nestedClientSecret ?? stringProperty(source, ["clientSecret", "client_secret", "googleClientSecret", "google_client_secret", "oauthClientSecret", "oauth_client_secret"]);
97
+ if (clientId) return { clientId, ...(clientSecret ? { clientSecret } : {}) };
98
+ }
99
+ return undefined;
62
100
  }
63
101
 
64
102
  export function clampAccountIndex(index: unknown, accountCount: number): number {
@@ -67,7 +105,12 @@ export function clampAccountIndex(index: unknown, accountCount: number): number
67
105
  }
68
106
 
69
107
  export function getAccountProjectId(account: OpencodeAntigravityAccount): string {
70
- return account.projectId || account.managedProjectId || DEFAULT_PROJECT_ID;
108
+ if (account.projectId || account.managedProjectId) return account.projectId || account.managedProjectId || DEFAULT_PROJECT_ID;
109
+ if (account.refresh) {
110
+ const refresh = splitRefresh(account.refresh);
111
+ return refresh.projectId || refresh.managedProjectId || DEFAULT_PROJECT_ID;
112
+ }
113
+ return DEFAULT_PROJECT_ID;
71
114
  }
72
115
 
73
116
  export function accountFromCredential(credential?: PiAuthCredential): OpencodeAntigravityAccount | undefined {
@@ -79,6 +122,7 @@ export function accountFromCredential(credential?: PiAuthCredential): OpencodeAn
79
122
  refreshToken: refresh.refreshToken,
80
123
  projectId: refresh.projectId || refresh.managedProjectId || DEFAULT_PROJECT_ID,
81
124
  managedProjectId: refresh.managedProjectId,
125
+ ...getGoogleOAuthClientCredentials(credential),
82
126
  enabled: true,
83
127
  };
84
128
  }
@@ -99,7 +143,7 @@ function selectOpencodeAccount(
99
143
  storage: OpencodeAntigravityStorage,
100
144
  options: { accountIndex?: number; email?: string },
101
145
  ): { account: OpencodeAntigravityAccount; index: number; count: number } | undefined {
102
- const accounts = storage.accounts?.filter((account) => account && typeof account.refreshToken === "string" && account.refreshToken) ?? [];
146
+ const accounts = storage.accounts?.filter((account) => account && getAccountRefreshToken(account)) ?? [];
103
147
  if (accounts.length === 0) return undefined;
104
148
 
105
149
  if (options.email) {
@@ -177,7 +221,7 @@ export async function importOpencodeAntigravityAccount(options: {
177
221
  access: "",
178
222
  expires: 0,
179
223
  email: selected.account.email,
180
- accounts: storage.accounts.filter((account) => account.enabled !== false && account.refreshToken),
224
+ accounts: storage.accounts.filter((account) => account.enabled !== false && getAccountRefreshToken(account)),
181
225
  activeIndex: selected.index,
182
226
  };
183
227
  await writeJsonFileSecure(authPath, piAuth);
@@ -1,56 +1,18 @@
1
- import type { AntigravityAddAccountResult, AntigravityStatusDetails, OpencodeAntigravityImportResult } from "./types";
1
+ import type { AntigravityAddAccountResult, AntigravityStatusDetails } from "./types";
2
2
 
3
3
  function tokenizeArgs(args: string): string[] {
4
4
  return args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^("|')|("|')$/g, "")) ?? [];
5
5
  }
6
6
 
7
- export function parseImportCommandArgs(args: string): { sourcePath?: string; overwrite?: boolean; accountIndex?: number; email?: string } {
7
+ export function parseAddAccountCommandArgs(args: string): { activate?: boolean; email?: string } {
8
8
  const tokens = tokenizeArgs(args);
9
- const parsed: { sourcePath?: string; overwrite?: boolean; accountIndex?: number; email?: string } = {};
10
- for (let i = 0; i < tokens.length; i += 1) {
11
- const token = tokens[i];
12
- if (token === "--force" || token === "-f") {
13
- parsed.overwrite = true;
14
- } else if (token === "--path" && tokens[i + 1]) {
15
- parsed.sourcePath = tokens[++i];
16
- } else if ((token === "--index" || token === "--account-index") && tokens[i + 1]) {
17
- const index = Number(tokens[++i]);
18
- if (Number.isInteger(index)) parsed.accountIndex = index;
19
- } else if (token === "--email" && tokens[i + 1]) {
20
- parsed.email = tokens[++i];
21
- } else if (!token.startsWith("-") && !parsed.sourcePath) {
22
- parsed.sourcePath = token;
23
- }
24
- }
25
- return parsed;
26
- }
27
-
28
- export function formatImportResult(result: OpencodeAntigravityImportResult): string {
29
- const account = result.email ? `${result.email} ` : "";
30
- const position = typeof result.accountIndex === "number" && result.accountCount ? `(account ${result.accountIndex}/${result.accountCount - 1}) ` : "";
31
- if (result.imported) {
32
- return `Imported ${account}${position}from ${result.sourcePath} into ${result.authPath}. Token will refresh on first use.${result.overwroteExisting ? " Existing Antigravity auth was overwritten." : ""}`;
33
- }
34
- if (result.reason === "auth-exists-use-force") {
35
- return `Antigravity auth already exists in ${result.authPath}; run /antigravity-import --force to overwrite it with ${account}${position}from ${result.sourcePath}.`;
36
- }
37
- if (result.reason === "already-imported") {
38
- return `Antigravity auth is already imported from ${result.sourcePath} (${account}${position.trim()}).`;
39
- }
40
- return `Could not import Antigravity auth from ${result.sourcePath}: ${result.reason ?? "unknown error"}.`;
41
- }
42
-
43
- export function parseAddAccountCommandArgs(args: string): { activate?: boolean; email?: string; authPath?: string } {
44
- const tokens = tokenizeArgs(args);
45
- const parsed: { activate?: boolean; email?: string; authPath?: string } = {};
9
+ const parsed: { activate?: boolean; email?: string } = {};
46
10
  for (let i = 0; i < tokens.length; i += 1) {
47
11
  const token = tokens[i];
48
12
  if (token === "--activate" || token === "-a") {
49
13
  parsed.activate = true;
50
14
  } else if (token === "--email" && tokens[i + 1]) {
51
15
  parsed.email = tokens[++i];
52
- } else if (token === "--auth-path" && tokens[i + 1]) {
53
- parsed.authPath = tokens[++i];
54
16
  }
55
17
  }
56
18
  return parsed;
@@ -4,8 +4,6 @@ export const STATUS_KEY = "dcp:antigravity";
4
4
  export const LEGACY_STATUS_KEY = "antigravity";
5
5
  export const ALL_ACCOUNTS_EXHAUSTED_MARKER = "ANTIGRAVITY_ALL_ACCOUNTS_EXHAUSTED";
6
6
 
7
- export const CLIENT_ID = process.env.PIX_ANTIGRAVITY_GOOGLE_CLIENT_ID ?? process.env.ANTIGRAVITY_GOOGLE_CLIENT_ID ?? "";
8
- export const CLIENT_SECRET = process.env.PIX_ANTIGRAVITY_GOOGLE_CLIENT_SECRET ?? process.env.ANTIGRAVITY_GOOGLE_CLIENT_SECRET ?? "";
9
7
  export const REDIRECT_URI = "http://localhost:51121/oauth-callback";
10
8
  export const SCOPES = [
11
9
  "https://www.googleapis.com/auth/cloud-platform",
@@ -1,15 +1,14 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { decodeApiKey, getEffectiveProjectId, importOpencodeAntigravityAccount } from "./auth-store";
3
- import { formatAddAccountResult, formatImportResult, parseAddAccountCommandArgs, parseImportCommandArgs } from "./commands";
2
+ import { decodeApiKey, getEffectiveProjectId } from "./auth-store";
3
+ import { formatAddAccountResult, parseAddAccountCommandArgs } from "./commands";
4
4
  import { API_ID, DEFAULT_PROJECT_ID, ENDPOINT_DAILY, PROVIDER_ID } from "./constants";
5
5
  import { modelDefinitions } from "./models";
6
6
  import { addAntigravityAccount, loginAntigravity, refreshAntigravityToken } from "./oauth";
7
- import { emitAntigravityStatus, getCurrentAntigravityStatus, publishAntigravityAuthStartupSection, rememberAntigravityApi, rememberAntigravityUi } from "./status";
7
+ import { emitAntigravityStatus, getCurrentAntigravityStatus, notifyAntigravityLoginFailure, notifyAntigravityProviderFailure, publishAntigravityAuthStartupSection, rememberAntigravityApi, rememberAntigravityUi } from "./status";
8
8
  import { streamAntigravity } from "./stream";
9
9
 
10
- export { importOpencodeAntigravityAccount } from "./auth-store";
11
10
  export { addAntigravityAccount } from "./oauth";
12
- export type { AntigravityAddAccountResult, OpencodeAntigravityImportResult } from "./types";
11
+ export type { AntigravityAddAccountResult } from "./types";
13
12
 
14
13
  export default async function antigravityAuth(pi: ExtensionAPI): Promise<void> {
15
14
  rememberAntigravityApi(pi);
@@ -22,20 +21,14 @@ export default async function antigravityAuth(pi: ExtensionAPI): Promise<void> {
22
21
  pi.on("before_provider_request", (_event, ctx) => {
23
22
  rememberAntigravityUi(ctx.ui);
24
23
  });
24
+ pi.on("message_end", (event, ctx) => {
25
+ rememberAntigravityUi(ctx.ui);
26
+ const message = event.message;
27
+ if (message.role !== "assistant" || message.provider !== PROVIDER_ID || message.stopReason !== "error" || !message.errorMessage) return;
28
+ notifyAntigravityProviderFailure(message.errorMessage, { ui: ctx.ui, model: message.model });
29
+ });
25
30
  }
26
31
 
27
- pi.registerCommand("antigravity-import", {
28
- description: "Import Antigravity OAuth from opencode antigravity-accounts.json into Pi auth.json",
29
- handler: async (args: string, ctx: any) => {
30
- try {
31
- const result = await importOpencodeAntigravityAccount(parseImportCommandArgs(args));
32
- ctx.ui?.notify?.(formatImportResult(result), result.imported ? "info" : result.reason === "auth-exists-use-force" ? "warn" : "error");
33
- } catch (error) {
34
- ctx.ui?.notify?.(error instanceof Error ? error.message : String(error), "error");
35
- }
36
- },
37
- });
38
-
39
32
  pi.registerCommand("antigravity-add-account", {
40
33
  description: "Add a new Google Antigravity OAuth account to the Pi rotation pool",
41
34
  handler: async (args: string, ctx: any) => {
@@ -63,7 +56,7 @@ export default async function antigravityAuth(pi: ExtensionAPI): Promise<void> {
63
56
  ctx.ui?.notify?.(formatAddAccountResult(result), "info");
64
57
  emitAntigravityStatus(await getCurrentAntigravityStatus());
65
58
  } catch (error) {
66
- ctx.ui?.notify?.(error instanceof Error ? error.message : String(error), "error");
59
+ notifyAntigravityLoginFailure(error);
67
60
  }
68
61
  },
69
62
  });