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
@@ -5,7 +5,7 @@ import type { ThemeName } from "../theme.js";
5
5
  import type { ToastKind, ToastNotifier } from "../ui.js";
6
6
  import type { RenderedLink } from "./screen/file-links.js";
7
7
  import type { WorkspaceMutation } from "./workspace/workspace-undo.js";
8
- import type { SUBAGENT_ACTIVE_STATUSES, SUBAGENT_RENDER_MODES, SUBAGENT_STATUSES, SUBAGENT_TERMINAL_STATUSES, THINKING_LEVELS, TODO_ACTIONS, TODO_PRIORITIES, TODO_STATUSES } from "./constants.js";
8
+ import type { SUBAGENT_ACTIVE_STATUSES, SUBAGENT_RENDER_MODES, SUBAGENT_STATUSES, SUBAGENT_TERMINAL_STATUSES, THINKING_LEVELS, TODO_ACTIONS, TODO_STATUSES } from "./constants.js";
9
9
  export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
10
10
  export type NativeModifierKey = "shift" | "command" | "control" | "option";
11
11
  export type SessionActivity = "idle" | "running" | "thinking";
@@ -92,23 +92,22 @@ export type Entry = {
92
92
  };
93
93
  export type TodoAction = (typeof TODO_ACTIONS)[number];
94
94
  export type TodoStatus = (typeof TODO_STATUSES)[number];
95
- export type TodoPriority = (typeof TODO_PRIORITIES)[number];
96
95
  export type TodoTask = {
97
96
  id: number;
98
97
  subject: string;
99
98
  status: TodoStatus;
100
99
  description?: string;
101
100
  activeForm?: string;
102
- priority?: TodoPriority;
101
+ thinking?: ThinkingLevel;
103
102
  parentId?: number;
104
103
  blockedBy?: number[];
105
- tags?: string[];
106
104
  owner?: string;
107
105
  metadata?: Record<string, unknown>;
108
106
  };
109
107
  export type TodoTaskLinePart = {
110
108
  text: string;
111
109
  muted?: boolean;
110
+ thinking?: ThinkingLevel;
112
111
  };
113
112
  export type TodoTaskRow = {
114
113
  task: TodoTask;
@@ -261,6 +260,7 @@ export type StatusLineLayout = {
261
260
  text: string;
262
261
  sessionLabel: string;
263
262
  workspaceLabel: string;
263
+ inputBorderWidgetStartColumn?: number;
264
264
  modelUsageLabel?: string;
265
265
  contextBarLabel?: string;
266
266
  userJumpWidget?: StatusUserJumpWidgetLayout;
@@ -379,6 +379,14 @@ export type PixMenuItem<T = string> = {
379
379
  label: string;
380
380
  description?: string;
381
381
  keywords?: readonly string[];
382
+ labelHighlightRanges?: readonly {
383
+ start: number;
384
+ end: number;
385
+ }[];
386
+ descriptionHighlightRanges?: readonly {
387
+ start: number;
388
+ end: number;
389
+ }[];
382
390
  variant?: PixMenuVariant;
383
391
  };
384
392
  export type PixMenuOptions = {
@@ -386,6 +394,8 @@ export type PixMenuOptions = {
386
394
  placeholder?: string;
387
395
  emptyText?: string;
388
396
  searchable?: boolean;
397
+ minScorePerCharacter?: number;
398
+ preferKeyboardLayoutMatches?: boolean;
389
399
  preserveStatus?: boolean;
390
400
  };
391
401
  export type PixMenuSelectOptions = Omit<PixMenuOptions, "title">;
@@ -498,9 +508,10 @@ export type ThinkingMenuValue = {
498
508
  level: ThinkingLevel;
499
509
  current: boolean;
500
510
  };
501
- export type UserMessageMenuValue = "copy" | "fork" | "undo";
511
+ export type UserMessageMenuValue = "copy" | "fork" | "fork-new-tab" | "undo";
502
512
  export type UserMessageJumpMenuValue = {
503
- entryId: string;
513
+ entryId?: string;
514
+ sessionEntryId?: string;
504
515
  };
505
516
  export type QueueMessageMenuValue = "cancel" | "edit" | "send-now";
506
517
  export type ResumeMenuValue = {
@@ -18,6 +18,7 @@ export type AppWorkspaceActionsControllerHost = {
18
18
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
19
19
  render(): void;
20
20
  isRunning(): boolean;
21
+ forkSessionEntryInNewTab(sessionEntryId: string): Promise<boolean>;
21
22
  };
22
23
  export declare class AppWorkspaceActionsController {
23
24
  private readonly host;
@@ -30,6 +31,7 @@ export declare class AppWorkspaceActionsController {
30
31
  syncUserSessionEntryMetadata(): void;
31
32
  copyUserMessage(entryId: string): Promise<void>;
32
33
  forkFromUserMessage(entryId: string): Promise<void>;
34
+ forkFromUserMessageInNewTab(entryId: string): Promise<void>;
33
35
  undoChangesFromUserMessage(entryId: string): Promise<void>;
34
36
  private resolveUserSessionEntryId;
35
37
  private getIdleRuntimeForAction;
@@ -108,6 +108,18 @@ export class AppWorkspaceActionsController {
108
108
  this.host.setSessionStatus(runtime.session);
109
109
  this.host.showToast("Session forked", "success");
110
110
  }
111
+ async forkFromUserMessageInNewTab(entryId) {
112
+ const runtime = this.getIdleRuntimeForAction("fork in new tab");
113
+ if (!runtime)
114
+ return;
115
+ const entry = this.host.findUserEntry(entryId);
116
+ if (!entry)
117
+ throw new Error("User message is no longer available");
118
+ const sessionEntryId = this.resolveUserSessionEntryId(entry);
119
+ if (!sessionEntryId)
120
+ throw new Error("Session entry for this message is not available yet");
121
+ await this.host.forkSessionEntryInNewTab(sessionEntryId);
122
+ }
111
123
  async undoChangesFromUserMessage(entryId) {
112
124
  const runtime = this.getIdleRuntimeForAction("undo changes");
113
125
  if (!runtime)
package/dist/config.d.ts CHANGED
@@ -67,16 +67,21 @@ export type PixConfig = {
67
67
  modelColors: ModelColorsConfig;
68
68
  iconTheme: IconThemeConfig;
69
69
  dictation: DictationConfig;
70
+ ignoreContextFiles: boolean;
71
+ maxProjectSessions: number;
70
72
  };
71
73
  export declare function getPixConfigPath(homeDir?: string): string;
74
+ export declare function getProjectPixConfigPath(cwd: string): string;
72
75
  export declare function resolveDefaultModelRef(config: PixConfig): string | undefined;
73
76
  export declare function savePixDefaultModel(modelRef: string): DefaultModelConfig | undefined;
74
77
  export declare function savePixDefaultThinking(thinking: string, fallbackModelRef?: string): DefaultModelConfig | undefined;
75
78
  export declare function savePixAutocompleteModel(modelRef: string): AutocompleteConfig;
79
+ export declare function saveProjectPixIgnoreContextFiles(cwd: string, ignoreContextFiles: boolean): boolean;
80
+ export declare function upsertPixIgnoreContextFilesInJsonc(source: string, ignoreContextFiles: boolean): string;
76
81
  export declare function upsertPixDefaultModelInJsonc(source: string, modelRef: string): string;
77
82
  export declare function upsertPixDefaultThinkingInJsonc(source: string, thinking: string, fallbackModelRef?: string): string;
78
83
  export declare function upsertPixAutocompleteModelInJsonc(source: string, modelRef: string): string;
79
- export declare function loadPixConfig(): PixConfig;
84
+ export declare function loadPixConfig(cwd?: string): PixConfig;
80
85
  export declare function savePixDictationLanguage(language: string): void;
81
86
  export declare function upsertPixDictationLanguageInJsonc(source: string, language: string): string;
82
87
  export declare function resolveModelColor(modelRef: string, config: ModelColorsConfig): string | undefined;
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,18 @@ 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
+ }
254
+ function extractMaxProjectSessions(raw) {
255
+ if (!isPlainObject(raw))
256
+ return undefined;
257
+ if (typeof raw.maxProjectSessions !== "number" || !Number.isFinite(raw.maxProjectSessions))
258
+ return undefined;
259
+ return Math.max(0, Math.floor(raw.maxProjectSessions));
260
+ }
244
261
  function normalizeDictationLanguage(value) {
245
262
  if (typeof value !== "string")
246
263
  return undefined;
@@ -253,6 +270,9 @@ function normalizeThinkingLevel(value) {
253
270
  const normalized = value.trim().toLowerCase();
254
271
  return THINKING_LEVELS.includes(normalized) ? normalized : undefined;
255
272
  }
273
+ function normalizeDefaultThinking(value) {
274
+ return normalizeThinkingLevel(value);
275
+ }
256
276
  function nonEmptyString(value) {
257
277
  if (typeof value !== "string")
258
278
  return undefined;
@@ -274,15 +294,31 @@ function defaultPixConfig() {
274
294
  modelColors: DEFAULT_MODEL_COLORS,
275
295
  iconTheme: { name: resolveAppIconThemeNameFromEnv() },
276
296
  dictation: DEFAULT_DICTATION,
297
+ ignoreContextFiles: false,
298
+ maxProjectSessions: 0,
277
299
  };
278
300
  }
301
+ function pixConfigFromParsed(parsed, fallback = defaultPixConfig()) {
302
+ const toolRenderer = extractToolRendererConfig(parsed) ?? fallback.toolRenderer;
303
+ const outputFilters = extractOutputFiltersConfig(parsed) ?? fallback.outputFilters;
304
+ const defaultModel = extractDefaultModelConfig(parsed) ?? fallback.defaultModel;
305
+ const promptEnhancer = extractPromptEnhancerConfig(parsed) ?? fallback.promptEnhancer;
306
+ const autocomplete = extractAutocompleteConfig(parsed) ?? fallback.autocomplete;
307
+ const modelColors = extractModelColorsConfig(parsed) ?? fallback.modelColors;
308
+ const configuredIconTheme = extractIconThemeConfig(parsed) ?? fallback.iconTheme;
309
+ const iconTheme = { name: appIconThemeOverrideFromEnv() ?? configuredIconTheme.name };
310
+ const dictation = extractDictationConfig(parsed) ?? fallback.dictation;
311
+ const ignoreContextFiles = extractIgnoreContextFiles(parsed) ?? fallback.ignoreContextFiles;
312
+ const maxProjectSessions = extractMaxProjectSessions(parsed) ?? fallback.maxProjectSessions;
313
+ return { toolRenderer, outputFilters, ...(defaultModel === undefined ? {} : { defaultModel }), promptEnhancer, autocomplete, modelColors, iconTheme, dictation, ignoreContextFiles, maxProjectSessions };
314
+ }
279
315
  export function resolveDefaultModelRef(config) {
280
316
  const modelRef = config.defaultModel?.modelRef.trim();
281
317
  if (!modelRef)
282
318
  return undefined;
283
319
  const thinking = config.defaultModel?.thinking;
284
320
  if (!thinking)
285
- return modelRef;
321
+ return stripThinkingSuffix(modelRef);
286
322
  return `${stripThinkingSuffix(modelRef)}:${thinking}`;
287
323
  }
288
324
  export function savePixDefaultModel(modelRef) {
@@ -297,7 +333,7 @@ export function savePixDefaultModel(modelRef) {
297
333
  return extractDefaultModelConfig(parseJsonc(updated));
298
334
  }
299
335
  export function savePixDefaultThinking(thinking, fallbackModelRef) {
300
- const normalizedThinking = normalizeThinkingLevel(thinking);
336
+ const normalizedThinking = normalizeDefaultThinking(thinking);
301
337
  if (!normalizedThinking)
302
338
  return undefined;
303
339
  const configPath = PIX_CONFIG_PATH;
@@ -318,6 +354,21 @@ export function savePixAutocompleteModel(modelRef) {
318
354
  writeFileSync(configPath, updated);
319
355
  return extractAutocompleteConfig(parseJsonc(updated)) ?? { ...DEFAULT_AUTOCOMPLETE, modelRef: modelRef.trim() };
320
356
  }
357
+ export function saveProjectPixIgnoreContextFiles(cwd, ignoreContextFiles) {
358
+ const configPath = getProjectPixConfigPath(cwd);
359
+ const source = existsSync(configPath) ? readFileSync(configPath, "utf8") : `{
360
+ "$schema": "${PIX_SCHEMA_URL}"
361
+ }
362
+ `;
363
+ const updated = upsertPixIgnoreContextFilesInJsonc(source, ignoreContextFiles);
364
+ mkdirSync(dirname(configPath), { recursive: true });
365
+ writeFileSync(configPath, updated);
366
+ return extractIgnoreContextFiles(parseJsonc(updated)) ?? ignoreContextFiles;
367
+ }
368
+ export function upsertPixIgnoreContextFilesInJsonc(source, ignoreContextFiles) {
369
+ const formattingOptions = { insertSpaces: true, tabSize: 2 };
370
+ return applyEdits(source, modify(source, ["ignoreContextFiles"], ignoreContextFiles, { formattingOptions }));
371
+ }
321
372
  export function upsertPixDefaultModelInJsonc(source, modelRef) {
322
373
  const normalized = normalizeDefaultModelRef(modelRef);
323
374
  if (!normalized)
@@ -332,7 +383,7 @@ export function upsertPixDefaultModelInJsonc(source, modelRef) {
332
383
  return upsertPixDefaultModelObjectInJsonc(source, parsed, next);
333
384
  }
334
385
  export function upsertPixDefaultThinkingInJsonc(source, thinking, fallbackModelRef) {
335
- const normalizedThinking = normalizeThinkingLevel(thinking);
386
+ const normalizedThinking = normalizeDefaultThinking(thinking);
336
387
  if (!normalizedThinking)
337
388
  return source;
338
389
  const parsed = parseJsonc(source);
@@ -370,7 +421,7 @@ function normalizeDefaultModelRef(modelRef) {
370
421
  if (colonIndex <= 0)
371
422
  return { modelRef: trimmed };
372
423
  const suffix = trimmed.slice(colonIndex + 1);
373
- const thinking = normalizeThinkingLevel(suffix);
424
+ const thinking = normalizeDefaultThinking(suffix);
374
425
  return thinking ? { modelRef: trimmed.slice(0, colonIndex), thinking } : { modelRef: trimmed };
375
426
  }
376
427
  function stripThinkingSuffix(modelRef) {
@@ -378,7 +429,7 @@ function stripThinkingSuffix(modelRef) {
378
429
  if (colonIndex <= 0)
379
430
  return modelRef;
380
431
  const suffix = modelRef.slice(colonIndex + 1);
381
- return normalizeThinkingLevel(suffix) ? modelRef.slice(0, colonIndex) : modelRef;
432
+ return normalizeDefaultThinking(suffix) ? modelRef.slice(0, colonIndex) : modelRef;
382
433
  }
383
434
  function extractToolRendererRule(value) {
384
435
  if (!isPlainObject(value))
@@ -415,32 +466,38 @@ function ensurePixConfigExists(configPath) {
415
466
  throw error;
416
467
  }
417
468
  }
418
- export function loadPixConfig() {
469
+ export function loadPixConfig(cwd) {
419
470
  const configPath = PIX_CONFIG_PATH;
420
471
  try {
421
472
  ensurePixConfigExists(configPath);
422
473
  }
423
474
  catch (error) {
424
475
  process.stderr.write(`[pix] Failed to create ${configPath}: ${error instanceof Error ? error.message : String(error)}\n`);
425
- return defaultPixConfig();
476
+ return loadProjectPixConfig(cwd, defaultPixConfig());
426
477
  }
427
478
  try {
428
479
  const raw = readFileSync(configPath, "utf8");
429
480
  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 };
481
+ return loadProjectPixConfig(cwd, pixConfigFromParsed(parsed));
482
+ }
483
+ catch (error) {
484
+ process.stderr.write(`[pix] Failed to load ${configPath}: ${error instanceof Error ? error.message : String(error)}\n`);
485
+ return loadProjectPixConfig(cwd, defaultPixConfig());
486
+ }
487
+ }
488
+ function loadProjectPixConfig(cwd, fallback) {
489
+ if (!cwd)
490
+ return fallback;
491
+ const configPath = getProjectPixConfigPath(cwd);
492
+ if (!existsSync(configPath))
493
+ return fallback;
494
+ try {
495
+ const raw = readFileSync(configPath, "utf8");
496
+ return pixConfigFromParsed(parseJsonc(raw), fallback);
440
497
  }
441
498
  catch (error) {
442
499
  process.stderr.write(`[pix] Failed to load ${configPath}: ${error instanceof Error ? error.message : String(error)}\n`);
443
- return defaultPixConfig();
500
+ return fallback;
444
501
  }
445
502
  }
446
503
  export function savePixDictationLanguage(language) {
@@ -2,6 +2,10 @@ 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,
7
+ // Maximum pi session JSONL files to retain per project. 0 disables automatic deletion.
8
+ "maxProjectSessions": 0,
5
9
 
6
10
  "toolRenderer": {
7
11
  "default": { "previewLines": 0, "direction": "head", "color": "toolTitle" },
package/dist/fuzzy.d.ts CHANGED
@@ -19,5 +19,7 @@ export type FuzzyMatch<T> = {
19
19
  export type FuzzySearchOptions = {
20
20
  limit?: number;
21
21
  includeEmptyQuery?: boolean;
22
+ minScorePerCharacter?: number;
23
+ preferKeyboardLayoutMatches?: boolean;
22
24
  };
23
25
  export declare function fuzzySearch<T>(items: readonly FuzzySearchItem<T>[], query: string, options?: FuzzySearchOptions): FuzzyMatch<T>[];
package/dist/fuzzy.js CHANGED
@@ -41,7 +41,7 @@ export function fuzzySearch(items, query, options = {}) {
41
41
  const includeEmptyQuery = options.includeEmptyQuery ?? true;
42
42
  if (!includeEmptyQuery && normalizedQuery.length === 0)
43
43
  return [];
44
- const matches = [];
44
+ const candidateMatches = [];
45
45
  items.forEach((item, rank) => {
46
46
  const texts = [item.label, ...(item.aliases ?? []), ...(item.keywords ?? [])];
47
47
  let best;
@@ -51,21 +51,28 @@ export function fuzzySearch(items, query, options = {}) {
51
51
  if (!score)
52
52
  continue;
53
53
  const adjustedScore = score.score - queryVariant.penalty;
54
- if (!best || adjustedScore > best.score)
55
- best = { ...score, score: adjustedScore, text };
54
+ if (!best || adjustedScore > best.score) {
55
+ best = { ...score, score: adjustedScore, text, keyboardLayout: queryVariant.keyboardLayout };
56
+ }
56
57
  }
57
58
  }
58
59
  if (!best)
59
60
  return;
60
- matches.push({
61
+ if (isBelowMinimumScore(best.score, normalizedQuery, options.minScorePerCharacter))
62
+ return;
63
+ candidateMatches.push({
61
64
  value: item.value,
62
65
  label: item.label,
63
66
  matchedText: best.text,
64
67
  matchedRanges: best.ranges,
65
68
  score: best.score,
66
69
  rank,
70
+ keyboardLayout: best.keyboardLayout,
67
71
  });
68
72
  });
73
+ const matches = options.preferKeyboardLayoutMatches && shouldPreferKeyboardLayoutMatches(query) && candidateMatches.some((match) => match.keyboardLayout)
74
+ ? candidateMatches.filter((match) => match.keyboardLayout)
75
+ : candidateMatches;
69
76
  matches.sort((left, right) => {
70
77
  const scoreDelta = right.score - left.score;
71
78
  if (scoreDelta !== 0)
@@ -75,7 +82,12 @@ export function fuzzySearch(items, query, options = {}) {
75
82
  return rankDelta;
76
83
  return left.label.localeCompare(right.label);
77
84
  });
78
- return options.limit === undefined ? matches : matches.slice(0, options.limit);
85
+ return (options.limit === undefined ? matches : matches.slice(0, options.limit)).map(({ keyboardLayout: _keyboardLayout, ...match }) => match);
86
+ }
87
+ function isBelowMinimumScore(score, normalizedQuery, minScorePerCharacter) {
88
+ if (minScorePerCharacter === undefined || normalizedQuery.length === 0)
89
+ return false;
90
+ return score < normalizedQuery.length * minScorePerCharacter;
79
91
  }
80
92
  function scoreFuzzyMatch(normalizedQuery, text) {
81
93
  if (normalizedQuery.length === 0) {
@@ -149,7 +161,7 @@ function normalizeText(value) {
149
161
  return value.toLowerCase();
150
162
  }
151
163
  function getQueryVariants(normalizedQuery) {
152
- const variants = [{ query: normalizedQuery, penalty: 0 }];
164
+ const variants = [{ query: normalizedQuery, penalty: 0, keyboardLayout: false }];
153
165
  addKeyboardLayoutVariant(variants, normalizedQuery, RUSSIAN_TO_ENGLISH_KEYBOARD_LAYOUT);
154
166
  addKeyboardLayoutVariant(variants, normalizedQuery, ENGLISH_TO_RUSSIAN_KEYBOARD_LAYOUT);
155
167
  return variants;
@@ -158,7 +170,15 @@ function addKeyboardLayoutVariant(variants, normalizedQuery, layout) {
158
170
  const variant = remapKeyboardLayout(normalizedQuery, layout);
159
171
  if (variant === normalizedQuery || variants.some((existing) => existing.query === variant))
160
172
  return;
161
- variants.push({ query: variant, penalty: KEYBOARD_LAYOUT_VARIANT_PENALTY });
173
+ variants.push({ query: variant, penalty: KEYBOARD_LAYOUT_VARIANT_PENALTY, keyboardLayout: true });
174
+ }
175
+ function shouldPreferKeyboardLayoutMatches(query) {
176
+ const trimmed = query.trim();
177
+ if (!trimmed)
178
+ return false;
179
+ if (/^[А-ЯЁ][а-яё]+$/u.test(trimmed))
180
+ return false;
181
+ return /[А-ЯЁ]/u.test(trimmed);
162
182
  }
163
183
  function remapKeyboardLayout(value, layout) {
164
184
  return Array.from(value, (char) => layout[char] ?? char).join("");
@@ -49,6 +49,11 @@ export interface VisualLine {
49
49
  end: number;
50
50
  }>;
51
51
  }
52
+ export interface InputEditorDraftState {
53
+ text: string;
54
+ cursor: number;
55
+ attachments?: readonly Attachment[];
56
+ }
52
57
  /** Full render-ready snapshot of the editor state. */
53
58
  export interface RenderedEditor {
54
59
  visualLines: VisualLine[];
@@ -70,6 +75,7 @@ export declare class InputEditor {
70
75
  private readonly _redoStack;
71
76
  private _historyMutationDepth;
72
77
  private _restoringHistory;
78
+ private _contentVersion;
73
79
  get text(): string;
74
80
  get cursor(): number;
75
81
  get selection(): Selection | undefined;
@@ -78,6 +84,8 @@ export declare class InputEditor {
78
84
  get hasAttachments(): boolean;
79
85
  get canUndo(): boolean;
80
86
  get canRedo(): boolean;
87
+ get contentVersion(): number;
88
+ get draftState(): InputEditorDraftState;
81
89
  /** Get only image attachments. */
82
90
  get images(): ImageContent[];
83
91
  /** Get the text to submit — virtual attachment tags are expanded or removed. */
@@ -91,6 +99,7 @@ export declare class InputEditor {
91
99
  */
92
100
  get promptText(): string;
93
101
  setText(text: string, cursor?: number): void;
102
+ setDraftState(state: InputEditorDraftState): void;
94
103
  clear(): void;
95
104
  undo(): boolean;
96
105
  redo(): boolean;
@@ -27,6 +27,7 @@ export class InputEditor {
27
27
  _redoStack = [];
28
28
  _historyMutationDepth = 0;
29
29
  _restoringHistory = false;
30
+ _contentVersion = 0;
30
31
  // ── public getters ──────────────────────────────────────────────
31
32
  get text() { return this._text; }
32
33
  get cursor() { return this._cursor; }
@@ -38,6 +39,14 @@ export class InputEditor {
38
39
  get hasAttachments() { return this._attachments.length > 0; }
39
40
  get canUndo() { return this._undoStack.length > 0; }
40
41
  get canRedo() { return this._redoStack.length > 0; }
42
+ get contentVersion() { return this._contentVersion; }
43
+ get draftState() {
44
+ return {
45
+ text: this._text,
46
+ cursor: this._cursor,
47
+ ...(this._attachments.length > 0 ? { attachments: this._attachments.map(cloneAttachment) } : {}),
48
+ };
49
+ }
41
50
  /** Get only image attachments. */
42
51
  get images() {
43
52
  const images = [];
@@ -93,7 +102,20 @@ export class InputEditor {
93
102
  this.clearSelection();
94
103
  });
95
104
  }
105
+ setDraftState(state) {
106
+ this.recordEdit(() => {
107
+ this._text = state.text;
108
+ this._cursor = state.cursor;
109
+ this._attachments.length = 0;
110
+ this._attachments.push(...Array.from(state.attachments ?? [], cloneAttachment));
111
+ this._imageCounter = Math.max(this._imageCounter, maxTagCounter(this._attachments, /^\[Image (\d+)/u));
112
+ this._pasteCounter = Math.max(this._pasteCounter, maxTagCounter(this._attachments, /^\[Pasted ~?(\d+)/u));
113
+ this.clampCursor();
114
+ this.clearSelection();
115
+ });
116
+ }
96
117
  clear() {
118
+ const hadContent = this._text.length > 0 || this._attachments.length > 0;
97
119
  this._text = "";
98
120
  this._cursor = 0;
99
121
  this.clearSelection();
@@ -102,6 +124,8 @@ export class InputEditor {
102
124
  this._imageCounter = 0;
103
125
  this._pasteCounter = 0;
104
126
  this.clearHistory();
127
+ if (hadContent)
128
+ this._contentVersion += 1;
105
129
  }
106
130
  undo() {
107
131
  const previous = this._undoStack.pop();
@@ -115,6 +139,7 @@ export class InputEditor {
115
139
  this._redoStack.length = 0;
116
140
  }
117
141
  this.restoreHistorySnapshot(previous);
142
+ this._contentVersion += 1;
118
143
  this.trimHistory();
119
144
  return true;
120
145
  }
@@ -130,6 +155,7 @@ export class InputEditor {
130
155
  this._undoStack.length = 0;
131
156
  }
132
157
  this.restoreHistorySnapshot(next);
158
+ this._contentVersion += 1;
133
159
  this.trimHistory();
134
160
  return true;
135
161
  }
@@ -545,6 +571,7 @@ export class InputEditor {
545
571
  }
546
572
  if (!this.didContentChangeFrom(before))
547
573
  return;
574
+ this._contentVersion += 1;
548
575
  this.clearScrollOffset();
549
576
  if (!this.isRecordableHistorySnapshot(before)) {
550
577
  this.clearHistory();
@@ -867,6 +894,31 @@ function removeVirtualAttachmentTag(text, tag) {
867
894
  return text.replace(withTrailingSpace, "");
868
895
  return text.replace(tag, "");
869
896
  }
897
+ function cloneAttachment(attachment) {
898
+ if (attachment.kind === "image") {
899
+ return { kind: "image", tag: attachment.tag, image: { ...attachment.image } };
900
+ }
901
+ if (attachment.kind === "pasted-text") {
902
+ return { kind: "pasted-text", tag: attachment.tag, text: attachment.text, lineCount: attachment.lineCount };
903
+ }
904
+ return {
905
+ kind: "file",
906
+ tag: attachment.tag,
907
+ path: attachment.path,
908
+ ...(attachment.content === undefined ? {} : { content: attachment.content }),
909
+ ...(attachment.image === undefined ? {} : { image: { ...attachment.image } }),
910
+ };
911
+ }
912
+ function maxTagCounter(attachments, pattern) {
913
+ let max = 0;
914
+ for (const attachment of attachments) {
915
+ const match = pattern.exec(attachment.tag);
916
+ const value = match ? Number.parseInt(match[1] ?? "", 10) : NaN;
917
+ if (Number.isFinite(value))
918
+ max = Math.max(max, value);
919
+ }
920
+ return max;
921
+ }
870
922
  function suggestionSpansForChunk(chunkStart, chunkEnd, suggestionStart, suggestionEnd, prefixLength) {
871
923
  const start = Math.max(chunkStart, suggestionStart);
872
924
  const end = Math.min(chunkEnd, suggestionEnd);
@@ -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,8 @@
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>;
12
+ maxProjectSessions: Type.TOptional<Type.TNumber>;
11
13
  defaultModel: Type.TOptional<Type.TObject<{
12
14
  modelRef: Type.TOptional<Type.TString>;
13
15
  thinking: Type.TOptional<Type.TUnion<Type.TLiteral<string>[]>>;