pi-ui-extend 0.1.13 → 0.1.15

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 (92) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +5 -0
  3. package/dist/app/app.js +82 -12
  4. package/dist/app/commands/command-controller.js +1 -0
  5. package/dist/app/commands/command-host.d.ts +3 -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.js +3 -0
  9. package/dist/app/commands/command-registry.d.ts +1 -0
  10. package/dist/app/commands/command-registry.js +8 -0
  11. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  12. package/dist/app/extensions/extension-ui-controller.js +99 -61
  13. package/dist/app/input/input-action-controller.d.ts +1 -0
  14. package/dist/app/input/input-action-controller.js +8 -2
  15. package/dist/app/logger.d.ts +25 -0
  16. package/dist/app/logger.js +90 -0
  17. package/dist/app/model/model-usage-status.js +30 -15
  18. package/dist/app/popup/menu-items-controller.d.ts +2 -0
  19. package/dist/app/popup/menu-items-controller.js +45 -6
  20. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  21. package/dist/app/popup/popup-action-controller.js +7 -4
  22. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  23. package/dist/app/popup/popup-menu-controller.js +68 -322
  24. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  25. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  26. package/dist/app/rendering/conversation-viewport.js +157 -16
  27. package/dist/app/rendering/editor-panels.js +4 -2
  28. package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
  29. package/dist/app/rendering/popup-menu-renderer.js +307 -0
  30. package/dist/app/rendering/render-controller.js +5 -13
  31. package/dist/app/rendering/status-line-renderer.d.ts +1 -1
  32. package/dist/app/rendering/status-line-renderer.js +27 -24
  33. package/dist/app/rendering/toast-controller.d.ts +11 -3
  34. package/dist/app/rendering/toast-controller.js +53 -12
  35. package/dist/app/runtime.d.ts +2 -1
  36. package/dist/app/runtime.js +20 -10
  37. package/dist/app/screen/mouse-controller.d.ts +2 -2
  38. package/dist/app/screen/mouse-controller.js +27 -48
  39. package/dist/app/screen/screen-styler.d.ts +1 -1
  40. package/dist/app/screen/screen-styler.js +9 -7
  41. package/dist/app/screen/scroll-controller.d.ts +11 -9
  42. package/dist/app/screen/scroll-controller.js +50 -45
  43. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  44. package/dist/app/session/lazy-session-manager.js +539 -0
  45. package/dist/app/session/pix-system-message.d.ts +16 -0
  46. package/dist/app/session/pix-system-message.js +64 -0
  47. package/dist/app/session/session-event-controller.d.ts +11 -0
  48. package/dist/app/session/session-event-controller.js +58 -2
  49. package/dist/app/session/session-history.d.ts +18 -0
  50. package/dist/app/session/session-history.js +72 -3
  51. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  52. package/dist/app/session/session-lifecycle-controller.js +7 -2
  53. package/dist/app/session/tabs-controller.d.ts +13 -1
  54. package/dist/app/session/tabs-controller.js +248 -27
  55. package/dist/app/todo/todo-model.d.ts +3 -1
  56. package/dist/app/todo/todo-model.js +14 -2
  57. package/dist/app/types.d.ts +5 -2
  58. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  59. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  60. package/dist/config.d.ts +5 -1
  61. package/dist/config.js +73 -25
  62. package/dist/default-pix-config.js +2 -0
  63. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  64. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  65. package/dist/schemas/pix-schema.d.ts +2 -1
  66. package/dist/schemas/pix-schema.js +5 -4
  67. package/dist/terminal-width.d.ts +2 -0
  68. package/dist/terminal-width.js +64 -3
  69. package/external/pi-tools-suite/README.md +1 -0
  70. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
  71. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
  72. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
  73. package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
  74. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
  75. package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
  76. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
  77. package/external/pi-tools-suite/src/config.ts +8 -0
  78. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  79. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  80. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  81. package/external/pi-tools-suite/src/todo/index.ts +181 -11
  82. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
  83. package/external/pi-tools-suite/src/todo/todo.ts +10 -5
  84. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
  85. package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
  86. package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
  87. package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
  88. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  89. package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
  90. package/package.json +1 -1
  91. package/schemas/pi-tools-suite.json +4 -0
  92. package/schemas/pix.json +6 -2
package/dist/config.js CHANGED
@@ -4,9 +4,13 @@ import { homedir } from "node:os";
4
4
  import { applyEdits, modify, parse } from "jsonc-parser";
5
5
  import { appIconThemeFromFallbackFlag, appIconThemeOverrideFromEnv, parseAppIconThemeName, resolveAppIconThemeNameFromEnv, } from "./app/icons.js";
6
6
  import { DEFAULT_PIX_CONFIG_JSONC } from "./default-pix-config.js";
7
+ const PIX_SCHEMA_URL = "https://unpkg.com/pi-ui-extend/schemas/pix.json";
7
8
  export function getPixConfigPath(homeDir = homedir()) {
8
9
  return join(homeDir, ".config", "pi", "pix.jsonc");
9
10
  }
11
+ export function getProjectPixConfigPath(cwd) {
12
+ return join(cwd, ".pi", "pix.jsonc");
13
+ }
10
14
  const PIX_CONFIG_PATH = getPixConfigPath();
11
15
  const DEFAULT_TOOL_RENDERER = {
12
16
  default: {
@@ -68,9 +72,6 @@ const DEFAULT_MODEL_COLORS = {
68
72
  "antigravity/antigravity-claude-*": "error",
69
73
  },
70
74
  };
71
- const DEFAULT_ICON_THEME = {
72
- name: "nerdFont",
73
- };
74
75
  const DEFAULT_DICTATION = {
75
76
  language: "en",
76
77
  languages: {
@@ -164,17 +165,21 @@ function extractDefaultModelConfig(raw) {
164
165
  return undefined;
165
166
  const configured = raw.defaultModel ?? raw.modelDefault;
166
167
  if (typeof configured === "string") {
167
- const modelRef = configured.trim();
168
- return modelRef ? { modelRef } : undefined;
168
+ return normalizeDefaultModelRef(configured);
169
169
  }
170
170
  if (!isPlainObject(configured))
171
171
  return undefined;
172
172
  const modelRef = nonEmptyString(configured.modelRef) ?? nonEmptyString(configured.model);
173
173
  if (!modelRef)
174
174
  return undefined;
175
- const thinking = normalizeThinkingLevel(configured.thinking) ?? normalizeThinkingLevel(configured.thinkingLevel);
175
+ const normalizedModel = normalizeDefaultModelRef(modelRef);
176
+ if (!normalizedModel)
177
+ return undefined;
178
+ const thinking = normalizeDefaultThinking(configured.thinking)
179
+ ?? normalizeDefaultThinking(configured.thinkingLevel)
180
+ ?? normalizedModel.thinking;
176
181
  return {
177
- modelRef,
182
+ modelRef: normalizedModel.modelRef,
178
183
  ...(thinking === undefined ? {} : { thinking }),
179
184
  };
180
185
  }
@@ -241,6 +246,11 @@ function extractDictationConfig(raw) {
241
246
  languages,
242
247
  } : undefined;
243
248
  }
249
+ function extractIgnoreContextFiles(raw) {
250
+ if (!isPlainObject(raw))
251
+ return undefined;
252
+ return typeof raw.ignoreContextFiles === "boolean" ? raw.ignoreContextFiles : undefined;
253
+ }
244
254
  function normalizeDictationLanguage(value) {
245
255
  if (typeof value !== "string")
246
256
  return undefined;
@@ -253,6 +263,9 @@ function normalizeThinkingLevel(value) {
253
263
  const normalized = value.trim().toLowerCase();
254
264
  return THINKING_LEVELS.includes(normalized) ? normalized : undefined;
255
265
  }
266
+ function normalizeDefaultThinking(value) {
267
+ return normalizeThinkingLevel(value);
268
+ }
256
269
  function nonEmptyString(value) {
257
270
  if (typeof value !== "string")
258
271
  return undefined;
@@ -274,15 +287,29 @@ function defaultPixConfig() {
274
287
  modelColors: DEFAULT_MODEL_COLORS,
275
288
  iconTheme: { name: resolveAppIconThemeNameFromEnv() },
276
289
  dictation: DEFAULT_DICTATION,
290
+ ignoreContextFiles: false,
277
291
  };
278
292
  }
293
+ function pixConfigFromParsed(parsed, fallback = defaultPixConfig()) {
294
+ const toolRenderer = extractToolRendererConfig(parsed) ?? fallback.toolRenderer;
295
+ const outputFilters = extractOutputFiltersConfig(parsed) ?? fallback.outputFilters;
296
+ const defaultModel = extractDefaultModelConfig(parsed) ?? fallback.defaultModel;
297
+ const promptEnhancer = extractPromptEnhancerConfig(parsed) ?? fallback.promptEnhancer;
298
+ const autocomplete = extractAutocompleteConfig(parsed) ?? fallback.autocomplete;
299
+ const modelColors = extractModelColorsConfig(parsed) ?? fallback.modelColors;
300
+ const configuredIconTheme = extractIconThemeConfig(parsed) ?? fallback.iconTheme;
301
+ const iconTheme = { name: appIconThemeOverrideFromEnv() ?? configuredIconTheme.name };
302
+ const dictation = extractDictationConfig(parsed) ?? fallback.dictation;
303
+ const ignoreContextFiles = extractIgnoreContextFiles(parsed) ?? fallback.ignoreContextFiles;
304
+ return { toolRenderer, outputFilters, ...(defaultModel === undefined ? {} : { defaultModel }), promptEnhancer, autocomplete, modelColors, iconTheme, dictation, ignoreContextFiles };
305
+ }
279
306
  export function resolveDefaultModelRef(config) {
280
307
  const modelRef = config.defaultModel?.modelRef.trim();
281
308
  if (!modelRef)
282
309
  return undefined;
283
310
  const thinking = config.defaultModel?.thinking;
284
311
  if (!thinking)
285
- return modelRef;
312
+ return stripThinkingSuffix(modelRef);
286
313
  return `${stripThinkingSuffix(modelRef)}:${thinking}`;
287
314
  }
288
315
  export function savePixDefaultModel(modelRef) {
@@ -297,7 +324,7 @@ export function savePixDefaultModel(modelRef) {
297
324
  return extractDefaultModelConfig(parseJsonc(updated));
298
325
  }
299
326
  export function savePixDefaultThinking(thinking, fallbackModelRef) {
300
- const normalizedThinking = normalizeThinkingLevel(thinking);
327
+ const normalizedThinking = normalizeDefaultThinking(thinking);
301
328
  if (!normalizedThinking)
302
329
  return undefined;
303
330
  const configPath = PIX_CONFIG_PATH;
@@ -318,6 +345,21 @@ export function savePixAutocompleteModel(modelRef) {
318
345
  writeFileSync(configPath, updated);
319
346
  return extractAutocompleteConfig(parseJsonc(updated)) ?? { ...DEFAULT_AUTOCOMPLETE, modelRef: modelRef.trim() };
320
347
  }
348
+ export function saveProjectPixIgnoreContextFiles(cwd, ignoreContextFiles) {
349
+ const configPath = getProjectPixConfigPath(cwd);
350
+ const source = existsSync(configPath) ? readFileSync(configPath, "utf8") : `{
351
+ "$schema": "${PIX_SCHEMA_URL}"
352
+ }
353
+ `;
354
+ const updated = upsertPixIgnoreContextFilesInJsonc(source, ignoreContextFiles);
355
+ mkdirSync(dirname(configPath), { recursive: true });
356
+ writeFileSync(configPath, updated);
357
+ return extractIgnoreContextFiles(parseJsonc(updated)) ?? ignoreContextFiles;
358
+ }
359
+ export function upsertPixIgnoreContextFilesInJsonc(source, ignoreContextFiles) {
360
+ const formattingOptions = { insertSpaces: true, tabSize: 2 };
361
+ return applyEdits(source, modify(source, ["ignoreContextFiles"], ignoreContextFiles, { formattingOptions }));
362
+ }
321
363
  export function upsertPixDefaultModelInJsonc(source, modelRef) {
322
364
  const normalized = normalizeDefaultModelRef(modelRef);
323
365
  if (!normalized)
@@ -332,7 +374,7 @@ export function upsertPixDefaultModelInJsonc(source, modelRef) {
332
374
  return upsertPixDefaultModelObjectInJsonc(source, parsed, next);
333
375
  }
334
376
  export function upsertPixDefaultThinkingInJsonc(source, thinking, fallbackModelRef) {
335
- const normalizedThinking = normalizeThinkingLevel(thinking);
377
+ const normalizedThinking = normalizeDefaultThinking(thinking);
336
378
  if (!normalizedThinking)
337
379
  return source;
338
380
  const parsed = parseJsonc(source);
@@ -370,7 +412,7 @@ function normalizeDefaultModelRef(modelRef) {
370
412
  if (colonIndex <= 0)
371
413
  return { modelRef: trimmed };
372
414
  const suffix = trimmed.slice(colonIndex + 1);
373
- const thinking = normalizeThinkingLevel(suffix);
415
+ const thinking = normalizeDefaultThinking(suffix);
374
416
  return thinking ? { modelRef: trimmed.slice(0, colonIndex), thinking } : { modelRef: trimmed };
375
417
  }
376
418
  function stripThinkingSuffix(modelRef) {
@@ -378,7 +420,7 @@ function stripThinkingSuffix(modelRef) {
378
420
  if (colonIndex <= 0)
379
421
  return modelRef;
380
422
  const suffix = modelRef.slice(colonIndex + 1);
381
- return normalizeThinkingLevel(suffix) ? modelRef.slice(0, colonIndex) : modelRef;
423
+ return normalizeDefaultThinking(suffix) ? modelRef.slice(0, colonIndex) : modelRef;
382
424
  }
383
425
  function extractToolRendererRule(value) {
384
426
  if (!isPlainObject(value))
@@ -415,32 +457,38 @@ function ensurePixConfigExists(configPath) {
415
457
  throw error;
416
458
  }
417
459
  }
418
- export function loadPixConfig() {
460
+ export function loadPixConfig(cwd) {
419
461
  const configPath = PIX_CONFIG_PATH;
420
462
  try {
421
463
  ensurePixConfigExists(configPath);
422
464
  }
423
465
  catch (error) {
424
466
  process.stderr.write(`[pix] Failed to create ${configPath}: ${error instanceof Error ? error.message : String(error)}\n`);
425
- return defaultPixConfig();
467
+ return loadProjectPixConfig(cwd, defaultPixConfig());
426
468
  }
427
469
  try {
428
470
  const raw = readFileSync(configPath, "utf8");
429
471
  const parsed = parseJsonc(raw);
430
- const toolRenderer = extractToolRendererConfig(parsed) ?? DEFAULT_TOOL_RENDERER;
431
- const outputFilters = extractOutputFiltersConfig(parsed) ?? DEFAULT_OUTPUT_FILTERS;
432
- const defaultModel = extractDefaultModelConfig(parsed);
433
- const promptEnhancer = extractPromptEnhancerConfig(parsed) ?? DEFAULT_PROMPT_ENHANCER;
434
- const autocomplete = extractAutocompleteConfig(parsed) ?? DEFAULT_AUTOCOMPLETE;
435
- const modelColors = extractModelColorsConfig(parsed) ?? DEFAULT_MODEL_COLORS;
436
- const configuredIconTheme = extractIconThemeConfig(parsed) ?? DEFAULT_ICON_THEME;
437
- const iconTheme = { name: appIconThemeOverrideFromEnv() ?? configuredIconTheme.name };
438
- const dictation = extractDictationConfig(parsed) ?? DEFAULT_DICTATION;
439
- return { toolRenderer, outputFilters, ...(defaultModel === undefined ? {} : { defaultModel }), promptEnhancer, autocomplete, modelColors, iconTheme, dictation };
472
+ return loadProjectPixConfig(cwd, pixConfigFromParsed(parsed));
473
+ }
474
+ catch (error) {
475
+ process.stderr.write(`[pix] Failed to load ${configPath}: ${error instanceof Error ? error.message : String(error)}\n`);
476
+ return loadProjectPixConfig(cwd, defaultPixConfig());
477
+ }
478
+ }
479
+ function loadProjectPixConfig(cwd, fallback) {
480
+ if (!cwd)
481
+ return fallback;
482
+ const configPath = getProjectPixConfigPath(cwd);
483
+ if (!existsSync(configPath))
484
+ return fallback;
485
+ try {
486
+ const raw = readFileSync(configPath, "utf8");
487
+ return pixConfigFromParsed(parseJsonc(raw), fallback);
440
488
  }
441
489
  catch (error) {
442
490
  process.stderr.write(`[pix] Failed to load ${configPath}: ${error instanceof Error ? error.message : String(error)}\n`);
443
- return defaultPixConfig();
491
+ return fallback;
444
492
  }
445
493
  }
446
494
  export function savePixDictationLanguage(language) {
@@ -2,6 +2,8 @@ export const DEFAULT_PIX_CONFIG_JSONC = String.raw `{
2
2
  "$schema": "https://unpkg.com/pi-ui-extend/schemas/pix.json",
3
3
  // pix renderer configuration
4
4
  "defaultModel": { "modelRef": "openai-codex/gpt-5.5", "thinking": "medium" },
5
+ // Disable AGENTS.md / CLAUDE.md discovery for this project when set in <cwd>/.pi/pix.jsonc.
6
+ "ignoreContextFiles": false,
5
7
 
6
8
  "toolRenderer": {
7
9
  "default": { "previewLines": 0, "direction": "head", "color": "toolTitle" },
@@ -9,6 +9,7 @@ export declare const PiToolsSuiteConfigSchema: Type.TObject<{
9
9
  $schema: Type.TOptional<Type.TString>;
10
10
  enabled: Type.TOptional<Type.TBoolean>;
11
11
  disabledModules: Type.TOptional<Type.TArray<Type.TString>>;
12
+ todoThinking: Type.TOptional<Type.TBoolean>;
12
13
  terminalBell: Type.TOptional<Type.TObject<{
13
14
  sound: Type.TOptional<Type.TBoolean>;
14
15
  }>>;
@@ -203,6 +203,7 @@ export const PiToolsSuiteConfigSchema = Type.Object({
203
203
  $schema: Type.Optional(Type.String({ description: "JSON Schema URL used by editors for validation and autocomplete." })),
204
204
  enabled: Type.Optional(Type.Boolean({ description: "Enable or disable the entire pi-tools-suite extension." })),
205
205
  disabledModules: Type.Optional(Type.Array(Type.String(), { description: "List of disabled module names (e.g. ['lsp', 'prompt-commands'])." })),
206
+ todoThinking: Type.Optional(Type.Boolean({ description: "Enable per-todo thinking levels and automatic thinking switch/restore when tasks become in-progress/completed." })),
206
207
  terminalBell: Type.Optional(TerminalBellConfig),
207
208
  dcp: Type.Optional(DcpConfig),
208
209
  asyncSubagents: Type.Optional(AsyncSubagentsConfig),
@@ -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
@@ -8,6 +8,7 @@
8
8
  import { Type, Static } from "typebox";
9
9
  export declare const PixConfigSchema: Type.TObject<{
10
10
  $schema: Type.TOptional<Type.TString>;
11
+ ignoreContextFiles: Type.TOptional<Type.TBoolean>;
11
12
  defaultModel: Type.TOptional<Type.TObject<{
12
13
  modelRef: Type.TOptional<Type.TString>;
13
14
  thinking: Type.TOptional<Type.TUnion<Type.TLiteral<string>[]>>;
@@ -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,7 @@ 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." })),
76
77
  defaultModel: Type.Optional(DefaultModelConfig),
77
78
  toolRenderer: Type.Optional(ToolRendererConfig),
78
79
  outputFilters: Type.Optional(OutputFiltersConfig),
@@ -86,6 +87,6 @@ export const PixConfigSchema = Type.Object({
86
87
  $id: "https://unpkg.com/pi-ui-extend/schemas/pix.json",
87
88
  $schema: "https://json-schema.org/draft-07/schema#",
88
89
  title: "Pix Configuration",
89
- description: "Configuration for the pix terminal renderer (~/.config/pi/pix.jsonc).",
90
+ description: "Configuration for the pix terminal renderer (~/.config/pi/pix.jsonc, with project overrides in <cwd>/.pi/pix.jsonc).",
90
91
  additionalProperties: true,
91
92
  });
@@ -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
  }
@@ -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,10 +1,15 @@
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
5
  import type { OpencodeAntigravityAccount, OpencodeAntigravityImportResult, OpencodeAntigravityStorage, PiAuthCredential, PiAuthData } from "./types";
7
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
+ }
12
+
8
13
  export function splitRefresh(refresh: string): { refreshToken: string; projectId?: string; managedProjectId?: string } {
9
14
  const [refreshToken = "", projectId = "", managedProjectId = ""] = refresh.split("|");
10
15
  return {
@@ -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> {
@@ -40,17 +40,15 @@ export function formatImportResult(result: OpencodeAntigravityImportResult): str
40
40
  return `Could not import Antigravity auth from ${result.sourcePath}: ${result.reason ?? "unknown error"}.`;
41
41
  }
42
42
 
43
- export function parseAddAccountCommandArgs(args: string): { activate?: boolean; email?: string; authPath?: string } {
43
+ export function parseAddAccountCommandArgs(args: string): { activate?: boolean; email?: string } {
44
44
  const tokens = tokenizeArgs(args);
45
- const parsed: { activate?: boolean; email?: string; authPath?: string } = {};
45
+ const parsed: { activate?: boolean; email?: string } = {};
46
46
  for (let i = 0; i < tokens.length; i += 1) {
47
47
  const token = tokens[i];
48
48
  if (token === "--activate" || token === "-a") {
49
49
  parsed.activate = true;
50
50
  } else if (token === "--email" && tokens[i + 1]) {
51
51
  parsed.email = tokens[++i];
52
- } else if (token === "--auth-path" && tokens[i + 1]) {
53
- parsed.authPath = tokens[++i];
54
52
  }
55
53
  }
56
54
  return parsed;
@@ -4,8 +4,8 @@ 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 ?? "";
7
+ export const CLIENT_ID = "";
8
+ export const CLIENT_SECRET = "";
9
9
  export const REDIRECT_URI = "http://localhost:51121/oauth-callback";
10
10
  export const SCOPES = [
11
11
  "https://www.googleapis.com/auth/cloud-platform",
@@ -4,7 +4,7 @@ import { formatAddAccountResult, formatImportResult, parseAddAccountCommandArgs,
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
10
  export { importOpencodeAntigravityAccount } from "./auth-store";
@@ -22,6 +22,12 @@ export default async function antigravityAuth(pi: ExtensionAPI): Promise<void> {
22
22
  pi.on("before_provider_request", (_event, ctx) => {
23
23
  rememberAntigravityUi(ctx.ui);
24
24
  });
25
+ pi.on("message_end", (event, ctx) => {
26
+ rememberAntigravityUi(ctx.ui);
27
+ const message = event.message;
28
+ if (message.role !== "assistant" || message.provider !== PROVIDER_ID || message.stopReason !== "error" || !message.errorMessage) return;
29
+ notifyAntigravityProviderFailure(message.errorMessage, { ui: ctx.ui, model: message.model });
30
+ });
25
31
  }
26
32
 
27
33
  pi.registerCommand("antigravity-import", {
@@ -63,7 +69,7 @@ export default async function antigravityAuth(pi: ExtensionAPI): Promise<void> {
63
69
  ctx.ui?.notify?.(formatAddAccountResult(result), "info");
64
70
  emitAntigravityStatus(await getCurrentAntigravityStatus());
65
71
  } catch (error) {
66
- ctx.ui?.notify?.(error instanceof Error ? error.message : String(error), "error");
72
+ notifyAntigravityLoginFailure(error);
67
73
  }
68
74
  },
69
75
  });