@oh-my-pi/pi-coding-agent 14.0.3 → 14.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.0.4] - 2026-04-10
6
+ ### Added
7
+
8
+ - Added `PI_CHUNK_AUTOINDENT` environment variable to control whether chunk read/edit tools normalize indentation to canonical tabs or preserve literal file whitespace
9
+ - Added dynamic chunk tool prompts that automatically adjust guidance based on `PI_CHUNK_AUTOINDENT` setting without exposing a tool parameter
10
+ - Added `<instruction-priority>`, `<output-contract>`, `<default-follow-through>`, `<tool-persistence>`, and `<completeness-contract>` sections to system prompt for improved long-horizon agent workflows
11
+
12
+ ### Changed
13
+
14
+ - Updated chunk edit tool to apply `normalizeIndent` setting during edit operations, enabling literal whitespace preservation when `PI_CHUNK_AUTOINDENT=0`
15
+ - Refactored environment variable parsing to use `$flag()` and `$envpos()` utilities from pi-utils for consistent boolean and integer handling across codebase
16
+ - Updated system prompt communication guidelines to emphasize conciseness and information density, and added guidance on avoiding repetition of user requests
17
+ - Enhanced system prompt with explicit rules for design integrity, verification before yielding, and handling of missing context via tool-based retrieval
18
+ - Added `PI_CHUNK_AUTOINDENT` to control whether chunk read/edit tools normalize indentation, and updated chunk prompts to switch guidance automatically based on that setting
19
+ - Refined the default system prompt with explicit instruction-priority, output-contract, tool-persistence, completeness, and verification rules for long-horizon GPT-5.4-style agent workflows
20
+
21
+ ### Fixed
22
+
23
+ - Fixed typo in system prompt: 'backwards compatibiltity' → 'backwards compatibility'
24
+
5
25
  ## [14.0.3] - 2026-04-09
6
26
 
7
27
  ### Fixed
@@ -6900,4 +6920,4 @@ Initial public release.
6900
6920
  - Git branch display in footer
6901
6921
  - Message queueing during streaming responses
6902
6922
  - OAuth integration for Gmail and Google Calendar access
6903
- - HTML export with syntax highlighting and collapsible sections
6923
+ - HTML export with syntax highlighting and collapsible sections
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "14.0.3",
4
+ "version": "14.0.4",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.16.1",
48
48
  "@mozilla/readability": "^0.6",
49
- "@oh-my-pi/omp-stats": "14.0.3",
50
- "@oh-my-pi/pi-agent-core": "14.0.3",
51
- "@oh-my-pi/pi-ai": "14.0.3",
52
- "@oh-my-pi/pi-natives": "14.0.3",
53
- "@oh-my-pi/pi-tui": "14.0.3",
54
- "@oh-my-pi/pi-utils": "14.0.3",
49
+ "@oh-my-pi/omp-stats": "14.0.4",
50
+ "@oh-my-pi/pi-agent-core": "14.0.4",
51
+ "@oh-my-pi/pi-ai": "14.0.4",
52
+ "@oh-my-pi/pi-natives": "14.0.4",
53
+ "@oh-my-pi/pi-tui": "14.0.4",
54
+ "@oh-my-pi/pi-utils": "14.0.4",
55
55
  "@sinclair/typebox": "^0.34",
56
56
  "@xterm/headless": "^6.0",
57
57
  "ajv": "^8.18",
package/src/edit/index.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  executeChunkMode,
20
20
  isChunkParams,
21
21
  resolveAnchorStyle,
22
+ resolveChunkAutoIndent,
22
23
  } from "./modes/chunk";
23
24
  import { executeHashlineMode, type HashlineParams, hashlineEditParamsSchema, isHashlineParams } from "./modes/hashline";
24
25
  import { executePatchMode, isPatchParams, type PatchParams, patchEditSchema } from "./modes/patch";
@@ -197,6 +198,7 @@ export class EditTool implements AgentTool<TInput> {
197
198
  description: (session: ToolSession) =>
198
199
  prompt.render(chunkEditDescription, {
199
200
  anchorStyle: resolveAnchorStyle(session.settings),
201
+ chunkAutoIndent: resolveChunkAutoIndent(),
200
202
  }),
201
203
  parameters: chunkEditParamsSchema,
202
204
  invalidParamsMessage: "Invalid edit parameters for chunk mode.",
@@ -11,6 +11,7 @@ import {
11
11
  ChunkState,
12
12
  type EditOperation as NativeEditOperation,
13
13
  } from "@oh-my-pi/pi-natives";
14
+ import { $envpos } from "@oh-my-pi/pi-utils";
14
15
  import { type Static, Type } from "@sinclair/typebox";
15
16
  import type { BunFile } from "bun";
16
17
  import { LRUCache } from "lru-cache";
@@ -63,6 +64,34 @@ const validAnchorStyles: Record<string, ChunkAnchorStyle> = {
63
64
  bare: ChunkAnchorStyle.Bare,
64
65
  };
65
66
 
67
+ export function resolveChunkAutoIndent(rawValue = Bun.env.PI_CHUNK_AUTOINDENT): boolean {
68
+ if (!rawValue) return true;
69
+ const normalized = rawValue.trim().toLowerCase();
70
+ switch (normalized) {
71
+ case "1":
72
+ case "true":
73
+ case "yes":
74
+ case "on":
75
+ return true;
76
+ case "0":
77
+ case "false":
78
+ case "no":
79
+ case "off":
80
+ return false;
81
+ default:
82
+ throw new Error(`Invalid PI_CHUNK_AUTOINDENT: ${rawValue}`);
83
+ }
84
+ }
85
+
86
+ function getChunkRenderIndentOptions(): {
87
+ normalizeIndent: boolean;
88
+ tabReplacement: string;
89
+ } {
90
+ return resolveChunkAutoIndent()
91
+ ? { normalizeIndent: true, tabReplacement: " " }
92
+ : { normalizeIndent: false, tabReplacement: "\t" };
93
+ }
94
+
66
95
  export function resolveAnchorStyle(settings?: Settings): ChunkAnchorStyle {
67
96
  const envStyle = Bun.env.PI_ANCHOR_STYLE;
68
97
  return (
@@ -72,16 +101,8 @@ export function resolveAnchorStyle(settings?: Settings): ChunkAnchorStyle {
72
101
  );
73
102
  }
74
103
 
75
- const readEnvInt = (name: string, defaultValue: number): number => {
76
- const value = Bun.env[name];
77
- if (!value) return defaultValue;
78
- const parsed = Number.parseInt(value, 10);
79
- if (Number.isNaN(parsed) || parsed <= 0) return defaultValue;
80
- return parsed;
81
- };
82
-
83
104
  const chunkStateCache = new LRUCache<string, ChunkCacheEntry>({
84
- max: readEnvInt("PI_CHUNK_CACHE_MAX_ENTRIES", 200),
105
+ max: $envpos("PI_CHUNK_CACHE_MAX_ENTRIES", 200),
85
106
  });
86
107
 
87
108
  export function invalidateChunkCache(filePath: string): void {
@@ -215,6 +236,7 @@ export async function formatChunkedRead(params: {
215
236
  const normalizedLanguage = normalizeLanguage(language);
216
237
  const { state } = await loadChunkStateForFile(filePath, normalizedLanguage);
217
238
  const displayPath = displayPathForFile(filePath, cwd);
239
+ const renderIndentOptions = getChunkRenderIndentOptions();
218
240
  const result = state.renderRead({
219
241
  readPath,
220
242
  displayPath,
@@ -224,8 +246,8 @@ export async function formatChunkedRead(params: {
224
246
  absoluteLineRange: absoluteLineRange
225
247
  ? { startLine: absoluteLineRange.startLine, endLine: absoluteLineRange.endLine ?? absoluteLineRange.startLine }
226
248
  : undefined,
227
- tabReplacement: " ",
228
- normalizeIndent: true,
249
+ tabReplacement: renderIndentOptions.tabReplacement,
250
+ normalizeIndent: renderIndentOptions.normalizeIndent,
229
251
  });
230
252
  return { text: result.text, resolvedPath: filePath, chunk: result.chunk };
231
253
  }
@@ -280,6 +302,7 @@ export function applyChunkEdits(params: {
280
302
  const state = ChunkState.parse(normalizedSource, normalizeLanguage(params.language));
281
303
  const result = state.applyEdits({
282
304
  operations: nativeOperations,
305
+ normalizeIndent: resolveChunkAutoIndent(),
283
306
  defaultSelector: params.defaultSelector,
284
307
  defaultCrc: params.defaultCrc,
285
308
  anchorStyle: params.anchorStyle,
@@ -312,7 +335,8 @@ export const chunkToolEditSchema = Type.Object({
312
335
  "Chunk selector. Format: 'path@region' for insertions, 'path#CRC@region' for replace. Omit @region to target the full chunk. Valid regions: head, body, tail, decl.",
313
336
  }),
314
337
  content: Type.String({
315
- description: "New content. Use \\t for indentation. Do NOT include the chunk's base padding.",
338
+ description:
339
+ "New content. Write indentation relative to the targeted region as described in the tool prompt. Do NOT include the chunk's base padding.",
316
340
  }),
317
341
  });
318
342
  export const chunkEditParamsSchema = Type.Object(
@@ -387,7 +411,7 @@ async function writeChunkResult(params: {
387
411
  invalidateFsScanAfterWrite(resolvedPath);
388
412
 
389
413
  const diffResult = generateUnifiedDiffString(result.diffSourceBefore, result.diffSourceAfter);
390
- const warningsBlock = result.warnings.length > 0 ? `\n\n${result.warnings.join("\n")}` : "";
414
+ const warningsBlock = result.warnings.length > 0 ? `\n\nWarnings:\n${result.warnings.join("\n")}` : "";
391
415
  const meta = outputMeta()
392
416
  .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
393
417
  .get();
@@ -156,8 +156,8 @@ interface ExecuteHashlineModeOptions {
156
156
  beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
157
157
  }
158
158
 
159
- export function hashlineParseText(edit: string[] | string | null): string[] {
160
- if (edit === null) return [];
159
+ export function hashlineParseText(edit: string[] | string | null | undefined): string[] {
160
+ if (edit == null) return [];
161
161
  if (typeof edit === "string") {
162
162
  const normalizedEdit = edit.endsWith("\n") ? edit.slice(0, -1) : edit;
163
163
  edit = normalizedEdit.replaceAll("\r", "").split("\n");
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
2
+ import { getAgentDir, getProjectDir, isBunTestRuntime, isEnoent, logger } from "@oh-my-pi/pi-utils";
3
3
  import { OutputSink } from "../session/streaming-output";
4
4
  import { shutdownSharedGateway } from "./gateway-coordinator";
5
5
  import {
@@ -299,10 +299,6 @@ async function writePreludeCache(state: PreludeCacheState, helpers: PreludeHelpe
299
299
  }
300
300
  }
301
301
 
302
- function isPythonTestEnvironment(): boolean {
303
- return Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
304
- }
305
-
306
302
  function getPreludeIntrospectionOptions(
307
303
  options: KernelSessionExecutionOptions = {},
308
304
  ): Pick<KernelExecuteOptions, "signal" | "timeoutMs"> {
@@ -318,7 +314,7 @@ async function cachePreludeDocs(
318
314
  cacheState?: PreludeCacheState | null,
319
315
  ): Promise<PreludeHelper[]> {
320
316
  cachedPreludeDocs = docs;
321
- if (!isPythonTestEnvironment() && docs.length > 0) {
317
+ if (!isBunTestRuntime() && docs.length > 0) {
322
318
  const state = cacheState ?? (await buildPreludeCacheState(cwd));
323
319
  await writePreludeCache(state, docs);
324
320
  }
@@ -416,7 +412,7 @@ export async function warmPythonEnvironment(
416
412
  cachedPreludeDocs = [];
417
413
  return { ok: false, reason, docs: [] };
418
414
  }
419
- if (!isPythonTestEnvironment()) {
415
+ if (!isBunTestRuntime()) {
420
416
  try {
421
417
  cacheState = await buildPreludeCacheState(cwd);
422
418
  const cached = await readPreludeCache(cacheState);
package/src/ipy/kernel.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $env, logger, Snowflake } from "@oh-my-pi/pi-utils";
1
+ import { $env, $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
2
2
  import { $ } from "bun";
3
3
  import { Settings } from "../config/settings";
4
4
  import { htmlToBasicMarkdown } from "../web/scrapers/types";
@@ -10,7 +10,7 @@ import { filterEnv, resolvePythonRuntime } from "./runtime";
10
10
 
11
11
  const TEXT_ENCODER = new TextEncoder();
12
12
  const TEXT_DECODER = new TextDecoder();
13
- const TRACE_IPC = $env.PI_PYTHON_IPC_TRACE === "1";
13
+ const TRACE_IPC = $flag("PI_PYTHON_IPC_TRACE");
14
14
  const PRELUDE_INTROSPECTION_SNIPPET = "import json\nprint(json.dumps(__omp_prelude_docs__()))";
15
15
 
16
16
  class SharedGatewayCreateError extends Error {
@@ -195,7 +195,7 @@ export interface PythonKernelAvailability {
195
195
  }
196
196
 
197
197
  export async function checkPythonKernelAvailability(cwd: string): Promise<PythonKernelAvailability> {
198
- if (Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test" || $env.PI_PYTHON_SKIP_CHECK === "1") {
198
+ if (isBunTestRuntime() || $flag("PI_PYTHON_SKIP_CHECK")) {
199
199
  return { ok: true };
200
200
  }
201
201
 
package/src/lsp/lspmux.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import { $env, $which, logger } from "@oh-my-pi/pi-utils";
3
+ import { $flag, $which, logger } from "@oh-my-pi/pi-utils";
4
4
  import { TOML } from "bun";
5
5
 
6
6
  /**
@@ -135,7 +135,7 @@ export async function detectLspmux(): Promise<LspmuxState> {
135
135
  return cachedState;
136
136
  }
137
137
 
138
- if ($env.PI_DISABLE_LSPMUX === "1") {
138
+ if ($flag("PI_DISABLE_LSPMUX")) {
139
139
  cachedState = { available: false, running: false, binaryPath: null, config: null };
140
140
  cacheTimestamp = now;
141
141
  return cachedState;
package/src/lsp/utils.ts CHANGED
@@ -16,149 +16,7 @@ import type {
16
16
  WorkspaceEdit,
17
17
  } from "./types";
18
18
 
19
- // =============================================================================
20
- // Language Detection
21
- // =============================================================================
22
-
23
- const LANGUAGE_MAP: Record<string, string> = {
24
- // TypeScript/JavaScript
25
- ".ts": "typescript",
26
- ".tsx": "typescriptreact",
27
- ".js": "javascript",
28
- ".jsx": "javascriptreact",
29
- ".mjs": "javascript",
30
- ".cjs": "javascript",
31
- ".mts": "typescript",
32
- ".cts": "typescript",
33
-
34
- // Systems languages
35
- ".rs": "rust",
36
- ".go": "go",
37
- ".c": "c",
38
- ".h": "c",
39
- ".cpp": "cpp",
40
- ".cc": "cpp",
41
- ".cxx": "cpp",
42
- ".hpp": "cpp",
43
- ".hxx": "cpp",
44
- ".zig": "zig",
45
-
46
- // Scripting languages
47
- ".py": "python",
48
- ".rb": "ruby",
49
- ".lua": "lua",
50
- ".sh": "shellscript",
51
- ".bash": "shellscript",
52
- ".zsh": "shellscript",
53
- ".fish": "fish",
54
- ".pl": "perl",
55
- ".pm": "perl",
56
- ".php": "php",
57
-
58
- // JVM languages
59
- ".java": "java",
60
- ".kt": "kotlin",
61
- ".kts": "kotlin",
62
- ".scala": "scala",
63
- ".groovy": "groovy",
64
- ".clj": "clojure",
65
-
66
- // .NET languages
67
- ".cs": "csharp",
68
- ".fs": "fsharp",
69
- ".vb": "vb",
70
-
71
- // Web
72
- ".html": "html",
73
- ".htm": "html",
74
- ".css": "css",
75
- ".scss": "scss",
76
- ".sass": "sass",
77
- ".less": "less",
78
- ".vue": "vue",
79
- ".svelte": "svelte",
80
- ".astro": "astro",
81
-
82
- // Data formats
83
- ".json": "json",
84
- ".jsonc": "jsonc",
85
- ".yaml": "yaml",
86
- ".yml": "yaml",
87
- ".toml": "toml",
88
- ".xml": "xml",
89
- ".ini": "ini",
90
-
91
- // Documentation
92
- ".md": "markdown",
93
- ".markdown": "markdown",
94
- ".rst": "restructuredtext",
95
- ".adoc": "asciidoc",
96
- ".tex": "latex",
97
-
98
- // Other
99
- ".sql": "sql",
100
- ".graphql": "graphql",
101
- ".gql": "graphql",
102
- ".proto": "protobuf",
103
- ".dockerfile": "dockerfile",
104
- ".tf": "terraform",
105
- ".hcl": "hcl",
106
- ".nix": "nix",
107
- ".ex": "elixir",
108
- ".exs": "elixir",
109
- ".erl": "erlang",
110
- ".hrl": "erlang",
111
- ".hs": "haskell",
112
- ".ml": "ocaml",
113
- ".mli": "ocaml",
114
- ".swift": "swift",
115
- ".r": "r",
116
- ".R": "r",
117
- ".jl": "julia",
118
- ".dart": "dart",
119
- ".elm": "elm",
120
- ".v": "v",
121
- ".nim": "nim",
122
- ".cr": "crystal",
123
- ".d": "d",
124
- ".pas": "pascal",
125
- ".pp": "pascal",
126
- ".lisp": "lisp",
127
- ".lsp": "lisp",
128
- ".rkt": "racket",
129
- ".scm": "scheme",
130
- ".ps1": "powershell",
131
- ".psm1": "powershell",
132
- ".bat": "bat",
133
- ".cmd": "bat",
134
- ".tla": "tlaplus",
135
- ".tlaplus": "tlaplus",
136
- };
137
-
138
- /**
139
- * Detect language ID from file path.
140
- * Returns the LSP language identifier for the file type.
141
- */
142
- export function detectLanguageId(filePath: string): string {
143
- const ext = path.extname(filePath).toLowerCase();
144
- const basename = path.basename(filePath).toLowerCase();
145
-
146
- // Handle special filenames
147
- if (basename === "dockerfile" || basename.startsWith("dockerfile.") || basename === "containerfile") {
148
- return "dockerfile";
149
- }
150
- if (basename === "makefile" || basename === "gnumakefile") {
151
- return "makefile";
152
- }
153
- if (basename === "justfile") {
154
- return "just";
155
- }
156
- if (basename === "cmakelists.txt" || ext === ".cmake") {
157
- return "cmake";
158
- }
159
-
160
- return LANGUAGE_MAP[ext] ?? "plaintext";
161
- }
19
+ export { detectLanguageId } from "../utils/lang-from-path";
162
20
 
163
21
  // =============================================================================
164
22
  // URI Handling (Cross-Platform)
@@ -20,6 +20,8 @@ import { defaultThemes } from "./defaults";
20
20
  import lightThemeJson from "./light.json" with { type: "json" };
21
21
  import { getMermaidAscii } from "./mermaid-cache";
22
22
 
23
+ export { getLanguageFromPath } from "../../utils/lang-from-path";
24
+
23
25
  // ============================================================================
24
26
  // Symbol Presets
25
27
  // ============================================================================
@@ -2305,167 +2307,6 @@ export function highlightCode(code: string, lang?: string): string[] {
2305
2307
  }
2306
2308
  }
2307
2309
 
2308
- /**
2309
- * Get language identifier from file path extension.
2310
- */
2311
- export function getLanguageFromPath(filePath: string): string | undefined {
2312
- const baseName = path.basename(filePath).toLowerCase();
2313
- if (baseName === ".env" || baseName.startsWith(".env.")) return "env";
2314
- if (
2315
- baseName === ".gitignore" ||
2316
- baseName === ".gitattributes" ||
2317
- baseName === ".gitmodules" ||
2318
- baseName === ".editorconfig" ||
2319
- baseName === ".npmrc" ||
2320
- baseName === ".prettierrc" ||
2321
- baseName === ".eslintrc"
2322
- ) {
2323
- return "conf";
2324
- }
2325
- if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
2326
- return "dockerfile";
2327
- }
2328
- if (baseName === "justfile") return "just";
2329
- if (baseName === "cmakelists.txt") return "cmake";
2330
-
2331
- const ext = filePath.split(".").pop()?.toLowerCase();
2332
- if (!ext) return undefined;
2333
-
2334
- const extToLang: Record<string, string> = {
2335
- ts: "typescript",
2336
- cts: "typescript",
2337
- mts: "typescript",
2338
- tsx: "tsx",
2339
- js: "javascript",
2340
- jsx: "javascript",
2341
- mjs: "javascript",
2342
- cjs: "javascript",
2343
- py: "python",
2344
- pyi: "python",
2345
- rb: "ruby",
2346
- rbw: "ruby",
2347
- gemspec: "ruby",
2348
- rs: "rust",
2349
- go: "go",
2350
- java: "java",
2351
- kt: "kotlin",
2352
- ktm: "kotlin",
2353
- kts: "kotlin",
2354
- swift: "swift",
2355
- c: "c",
2356
- h: "c",
2357
- cpp: "cpp",
2358
- cc: "cpp",
2359
- cxx: "cpp",
2360
- hh: "cpp",
2361
- hpp: "cpp",
2362
- cu: "cpp",
2363
- ino: "cpp",
2364
- cs: "csharp",
2365
- clj: "clojure",
2366
- cljc: "clojure",
2367
- cljs: "clojure",
2368
- edn: "clojure",
2369
- php: "php",
2370
- sh: "bash",
2371
- bash: "bash",
2372
- zsh: "bash",
2373
- ksh: "bash",
2374
- bats: "bash",
2375
- tmux: "bash",
2376
- cgi: "bash",
2377
- fcgi: "bash",
2378
- command: "bash",
2379
- tool: "bash",
2380
- fish: "fish",
2381
- ps1: "powershell",
2382
- psm1: "powershell",
2383
- sql: "sql",
2384
- html: "html",
2385
- htm: "html",
2386
- xhtml: "html",
2387
- astro: "astro",
2388
- vue: "vue",
2389
- svelte: "svelte",
2390
- css: "css",
2391
- scss: "scss",
2392
- sass: "sass",
2393
- less: "less",
2394
- json: "json",
2395
- ipynb: "ipynb",
2396
- hbs: "handlebars",
2397
- hsb: "handlebars",
2398
- handlebars: "handlebars",
2399
- yaml: "yaml",
2400
- yml: "yaml",
2401
- toml: "toml",
2402
- xml: "xml",
2403
- xsl: "xml",
2404
- xslt: "xml",
2405
- svg: "xml",
2406
- plist: "xml",
2407
- md: "markdown",
2408
- markdown: "markdown",
2409
- mdx: "markdown",
2410
- diff: "diff",
2411
- patch: "diff",
2412
- dockerfile: "dockerfile",
2413
- containerfile: "dockerfile",
2414
- makefile: "make",
2415
- justfile: "just",
2416
- mk: "make",
2417
- mak: "make",
2418
- cmake: "cmake",
2419
- lua: "lua",
2420
- jl: "julia",
2421
- pl: "perl",
2422
- pm: "perl",
2423
- perl: "perl",
2424
- r: "r",
2425
- scala: "scala",
2426
- sc: "scala",
2427
- sbt: "scala",
2428
- ex: "elixir",
2429
- exs: "elixir",
2430
- erl: "erlang",
2431
- hs: "haskell",
2432
- nix: "nix",
2433
- odin: "odin",
2434
- zig: "zig",
2435
- star: "starlark",
2436
- bzl: "starlark",
2437
- sol: "solidity",
2438
- v: "verilog",
2439
- sv: "verilog",
2440
- svh: "verilog",
2441
- vh: "verilog",
2442
- m: "objc",
2443
- mm: "objc",
2444
- ml: "ocaml",
2445
- vim: "vim",
2446
- graphql: "graphql",
2447
- proto: "protobuf",
2448
- tf: "hcl",
2449
- hcl: "hcl",
2450
- tfvars: "hcl",
2451
- txt: "text",
2452
- text: "text",
2453
- log: "log",
2454
- csv: "csv",
2455
- tsv: "tsv",
2456
- ini: "ini",
2457
- cfg: "conf",
2458
- conf: "conf",
2459
- config: "conf",
2460
- properties: "conf",
2461
- tla: "tlaplus",
2462
- tlaplus: "tlaplus",
2463
- env: "env",
2464
- };
2465
-
2466
- return extToLang[ext];
2467
- }
2468
-
2469
2310
  export function getSymbolTheme(): SymbolTheme {
2470
2311
  const preset = theme.getSymbolPreset();
2471
2312
 
@@ -52,9 +52,29 @@ Push back when warranted: state the downside, propose an alternative, but **MUST
52
52
  <communication>
53
53
  - No emojis, filler, or ceremony.
54
54
  - (1) Correctness first, (2) Brevity second, (3) Politeness third.
55
- - User-supplied content **MUST** override any other guidelines.
55
+ - Prefer concise, information-dense writing.
56
+ - Avoid repeating the user's request or narrating routine tool calls.
56
57
  </communication>
57
58
 
59
+ <instruction-priority>
60
+ - User instructions override default style, tone, formatting, and initiative preferences.
61
+ - Higher-priority system constraints about safety, permissions, tool boundaries, and task completion do not yield.
62
+ - If a newer user instruction conflicts with an earlier user instruction, follow the newer one.
63
+ - Preserve earlier instructions that do not conflict.
64
+ </instruction-priority>
65
+
66
+ <output-contract>
67
+ - Brief preambles are allowed when they improve orientation, but they **MUST** stay short and **MUST NOT** be treated as completion.
68
+ - Claims about code, tools, tests, docs, or external sources **MUST** be grounded in what you actually observed. If a statement is an inference, say so.
69
+ - Apply brevity to prose, not to evidence, verification, or blocking details.
70
+ </output-contract>
71
+
72
+ <default-follow-through>
73
+ - If the user's intent is clear and the next step is reversible and low-risk, proceed without asking.
74
+ - Ask only when the next step is irreversible, has external side effects, or requires a missing choice that would materially change the outcome.
75
+ - If you proceed, state what you did, what you verified, and what remains optional.
76
+ </default-follow-through>
77
+
58
78
  <behavior>
59
79
  You **MUST** guard against the completion reflex — the urge to ship something that compiles before you've understood the problem:
60
80
  - Compiling ≠ Correctness. "It works" ≠ "Works in all cases".
@@ -248,6 +268,15 @@ Don't open a file hoping. Hope is not a strategy.
248
268
  {{#has tools "task"}}- `task` for investigate+edit in one pass — prefer this over a separate explore→task chain{{/has}}
249
269
  {{/ifAny}}
250
270
 
271
+ <tool-persistence>
272
+ - Use tools whenever they materially improve correctness, completeness, or grounding.
273
+ - Do not stop at the first plausible answer if another tool call would materially reduce uncertainty, verify a dependency, or improve coverage.
274
+ - Before taking an action, check whether prerequisite discovery, lookup, or memory retrieval is required. Resolve prerequisites first.
275
+ - If a lookup is empty, partial, or suspiciously narrow, retry with a different strategy before concluding nothing exists.
276
+ - When multiple retrieval steps are independent, parallelize them. When one result determines the next step, keep the workflow sequential.
277
+ - After parallel retrieval, pause to synthesize before making more calls.
278
+ </tool-persistence>
279
+
251
280
  {{#if (includes tools "inspect_image")}}
252
281
  ### Image inspection
253
282
  - For image understanding tasks: **MUST** use `inspect_image` over `read` to avoid overloading main session context.
@@ -262,7 +291,14 @@ These are inviolable. Violation is system failure.
262
291
  - You **MUST NOT** suppress tests to make code pass. You **MUST NOT** fabricate outputs not observed.
263
292
  - You **MUST NOT** solve the wished-for problem instead of the actual problem.
264
293
  - You **MUST NOT** ask for information obtainable from tools, repo context, or files.
265
- - You **MUST** always design a clean solution. You **MUST NOT** introduce unnecessary backwards compatibiltity layers, no shims, no gradual migration, no bridges to old code unless user explicitly asks for it. Let the errors guide you on what to include in the refactoring. **ALWAYS default to performing full CUTOVER!**
294
+ - You **MUST** always design a clean solution. You **MUST NOT** introduce unnecessary backwards compatibility layers, no shims, no gradual migration, no bridges to old code unless user explicitly asks for it. Let the errors guide you on what to include in the refactoring. **ALWAYS default to performing full CUTOVER!**
295
+
296
+ <completeness-contract>
297
+ - Treat the task as incomplete until every requested deliverable is done or explicitly marked [blocked].
298
+ - Keep an internal checklist of requested outcomes, implied cleanup, affected callsites, tests, docs, and follow-on edits.
299
+ - For lists, batches, paginated results, or multi-file migrations, determine expected scope when possible and confirm coverage before yielding.
300
+ - If something is blocked, label it [blocked], say exactly what is missing, and distinguish it from work that is complete.
301
+ </completeness-contract>
266
302
 
267
303
  # Design Integrity
268
304
 
@@ -280,6 +316,7 @@ Design integrity means the code tells the truth about what the system currently
280
316
  {{#has tools "task"}}- You **MUST** determine if the task is parallelizable via `task` tool.{{/has}}
281
317
  - If multi-file or imprecisely scoped, you **MUST** write out a step-by-step plan, phased if it warrants, before touching any file.
282
318
  - For new work, you **MUST**: (1) think about architecture, (2) search official docs/papers on best practices, (3) review existing codebase, (4) compare research with codebase, (5) implement the best fit or surface tradeoffs.
319
+ - If required context is missing, do **NOT** guess. Prefer tool-based retrieval first, ask a minimal question only when the answer cannot be recovered from tools, repo context, or files.
283
320
  ## 2. Before You Edit
284
321
  - Read the relevant section of any file before editing. Don't edit from a grep snippet alone — context above and below the match changes what the correct edit is.
285
322
  - You **MUST** grep for existing examples before implementing any pattern, utility, or abstraction. If the codebase already solves it, you **MUST** use that. Inventing a parallel convention is **PROHIBITED**.
@@ -313,6 +350,7 @@ When a tool call fails, read the full error before doing anything else. When a f
313
350
  - Test everything rigorously → Future contributor cannot break behavior without failure. Prefer unit/e2e.
314
351
  - You **MUST NOT** rely on mocks — they invent behaviors that never happen in production and hide real bugs.
315
352
  - You **SHOULD** run only tests you added/modified unless asked otherwise.
353
+ - Before yielding, verify: (1) every requirement is satisfied, (2) claims match files/tool output/source material, (3) the output format matches the ask, and (4) any high-impact action was either verified or explicitly held for permission.
316
354
  - You **MUST NOT** yield without proof when non-trivial work, self-assessment is deceptive: tests, linters, type checks, repro steps… exhaust all external verification.
317
355
 
318
356
  {{#if secretsEnabled}}
@@ -7,11 +7,20 @@ Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit
7
7
  - replacements: `chunk#CRC` or `chunk#CRC@region`
8
8
  - Without a `@region` it defaults to the entire chunk including leading trivia. Valid regions: `head`, `body`, `tail`, `decl`.
9
9
  - If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
10
+ {{#if chunkAutoIndent}}
10
11
  - Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `@body` of a method, write the body starting at column 0:
11
12
  ```
12
13
  content: "if (x) {\n\treturn true;\n}"
13
14
  ```
14
15
  The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
16
+ {{else}}
17
+ - Match the file's literal tabs/spaces in `content`. Do not convert indentation to canonical `\t`.
18
+ - Write content at indent-level 0 relative to the target region. For example, to replace `@body` of a method, write:
19
+ ```
20
+ content: "if (x) {\n return true;\n}"
21
+ ```
22
+ The tool adds the correct base indent automatically, then preserves the tabs/spaces you used inside the snippet. Never manually pad with the chunk's own indentation.
23
+ {{/if}}
15
24
  - `@region` only works on container chunks (classes, functions, impl blocks, sections). Do **not** use `@head`/`@body`/`@tail` on leaf chunks (enum variants, fields, single statements) — use the whole chunk instead.
16
25
  - `replace` requires the current CRC. Insertions do not.
17
26
  - **CRCs change after every edit.** Always use the selectors/CRCs from the most recent `read` or edit response. Never reuse a CRC from a previous edit.
@@ -113,7 +122,11 @@ Given this `read` output for `example.ts`:
113
122
 
114
123
  **Replace a whole chunk** (rename a function):
115
124
  ~~~json
125
+ {{#if chunkAutoIndent}}
116
126
  { "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n\tconst c = new Counter();\n\tc.value = start;\n\treturn c;\n}\n" }
127
+ {{else}}
128
+ { "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n const c = new Counter();\n c.value = start;\n return c;\n}\n" }
129
+ {{/if}}
117
130
  ~~~
118
131
  Result — the entire chunk is rewritten:
119
132
  ```
@@ -163,7 +176,11 @@ function createCounter(initial: number): Counter {
163
176
 
164
177
  **Insert after a chunk** (`after`):
165
178
  ~~~json
179
+ {{#if chunkAutoIndent}}
166
180
  { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n\treturn s === Status.Active;\n}\n" }
181
+ {{else}}
182
+ { "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n return s === Status.Active;\n}\n" }
183
+ {{/if}}
167
184
  ~~~
168
185
  Result — a new function appears after the enum:
169
186
  ```
@@ -194,7 +211,11 @@ class Counter {
194
211
 
195
212
  **Append inside a container** (`@body` + `append`):
196
213
  ~~~json
214
+ {{#if chunkAutoIndent}}
197
215
  { "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n\tthis.value = 0;\n}\n" }
216
+ {{else}}
217
+ { "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n this.value = 0;\n}\n" }
218
+ {{/if}}
198
219
  ~~~
199
220
  Result — a new method is added at the end of the class body, before the closing `}`:
200
221
  ```
@@ -214,10 +235,18 @@ Result — a new method is added at the end of the class body, before the closin
214
235
  ```
215
236
  Result — the method is removed from the class.
216
237
  - Indentation rules (important):
238
+ {{#if chunkAutoIndent}}
217
239
  - Use `\t` for each indent level. The tool converts tabs to the file's actual style (2-space, 4-space, etc.).
240
+ {{else}}
241
+ - Match the file's real indentation characters in your snippet. The tool preserves your literal tabs/spaces after adding the target region's base indent.
242
+ {{/if}}
218
243
  - Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
219
244
  - For `@body` of a function: write at column 0, e.g. `"return x;\n"`. The tool adds the correct base indent.
220
245
  - For `@head`: write at the chunk's own depth. A class member's head uses `"/** doc */\nstart(): void {"`.
246
+ {{#if chunkAutoIndent}}
221
247
  - For a top-level item: start at zero indent. Write `"function foo() {\n\treturn 1;\n}\n"`.
248
+ {{else}}
249
+ - For a top-level item: start at zero indent. Write `"function foo() {\n return 1;\n}\n"`.
250
+ {{/if}}
222
251
  - The tool strips common leading indentation from your content as a safety net, so accidental over-indentation is corrected.
223
252
  </examples>
@@ -9,6 +9,12 @@ Each opening anchor `[< full.chunk.path#CCCC ]` in the default output identifies
9
9
  If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with CRCs.
10
10
  Line numbers in the gutter are absolute file line numbers.
11
11
 
12
+ {{#if chunkAutoIndent}}
13
+ Chunk reads normalize leading indentation so copied content round-trips cleanly into chunk edits.
14
+ {{else}}
15
+ Chunk reads preserve literal leading tabs/spaces from the file. When editing, keep the same whitespace characters you see here.
16
+ {{/if}}
17
+
12
18
  Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
13
19
  </instruction>
14
20
 
package/src/sdk.ts CHANGED
@@ -15,6 +15,7 @@ import { SearchDb } from "@oh-my-pi/pi-natives";
15
15
  import type { Component } from "@oh-my-pi/pi-tui";
16
16
  import {
17
17
  $env,
18
+ $flag,
18
19
  getAgentDbPath,
19
20
  getAgentDir,
20
21
  getProjectDir,
@@ -1262,7 +1263,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1262
1263
 
1263
1264
  const repeatToolDescriptions = settings.get("repeatToolDescriptions");
1264
1265
  const eagerTasks = settings.get("task.eager");
1265
- const intentField = settings.get("tools.intentTracing") || $env.PI_INTENT_TRACING === "1" ? INTENT_FIELD : undefined;
1266
+ const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
1266
1267
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
1267
1268
  toolContextStore.setToolNames(toolNames);
1268
1269
  const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
@@ -3,7 +3,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
3
3
  import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
- import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
6
+ import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import { computeLineHash } from "../edit/line-hash";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -103,7 +103,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
103
103
  if (maxReplacements !== undefined && (!Number.isFinite(maxReplacements) || maxReplacements < 1)) {
104
104
  throw new ToolError("limit must be a positive number");
105
105
  }
106
- const maxFiles = parseInt(process.env.PI_MAX_AST_FILES ?? "", 10) || 1000;
106
+ const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
107
107
 
108
108
  const formatScopePath = (targetPath: string): string => {
109
109
  const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
@@ -1,7 +1,7 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
2
  import type { ToolChoice } from "@oh-my-pi/pi-ai";
3
3
  import type { SearchDb } from "@oh-my-pi/pi-natives";
4
- import { $env, logger } from "@oh-my-pi/pi-utils";
4
+ import { $env, $flag, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils";
5
5
  import type { AsyncJobManager } from "../async";
6
6
  import type { PromptTemplate } from "../config/prompt-templates";
7
7
  import type { Settings } from "../config/settings";
@@ -297,8 +297,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
297
297
  !skipPythonPreflight &&
298
298
  pythonMode !== "bash-only" &&
299
299
  (requestedTools === undefined || requestedTools.includes("python"));
300
- const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
301
- const skipPythonWarm = isTestEnv || $env.PI_PYTHON_SKIP_CHECK === "1";
300
+ const skipPythonWarm = isBunTestRuntime() || $flag("PI_PYTHON_SKIP_CHECK");
302
301
  if (shouldCheckPython) {
303
302
  const availability = await logger.time("createTools:pythonCheck", checkPythonKernelAvailability, session.cwd);
304
303
  pythonAvailable = availability.ok;
package/src/tools/read.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  parseChunkReadPath,
15
15
  parseChunkSelector,
16
16
  resolveAnchorStyle,
17
+ resolveChunkAutoIndent,
17
18
  } from "../edit/modes/chunk";
18
19
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
19
20
  import { parseInternalUrl } from "../internal-urls/parse";
@@ -449,6 +450,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
449
450
  resolveEditMode(session) === "chunk"
450
451
  ? prompt.render(readChunkDescription, {
451
452
  anchorStyle: resolveAnchorStyle(session.settings),
453
+ chunkAutoIndent: resolveChunkAutoIndent(),
452
454
  })
453
455
  : prompt.render(readDescription, {
454
456
  DEFAULT_LIMIT: String(this.#defaultLimit),
@@ -8,7 +8,7 @@
8
8
  import { Database } from "bun:sqlite";
9
9
  import path from "node:path";
10
10
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
11
- import { $env, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
11
+ import { $flag, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
12
12
  import { Type } from "@sinclair/typebox";
13
13
  import type { Settings } from "..";
14
14
  import type { ToolSession } from "./index";
@@ -19,7 +19,7 @@ const ReportToolIssueParams = Type.Object({
19
19
  });
20
20
 
21
21
  export function isAutoQaEnabled(settings?: Settings): boolean {
22
- return $env.PI_AUTO_QA === "1" || !!settings?.get("dev.autoqa");
22
+ return $flag("PI_AUTO_QA") || !!settings?.get("dev.autoqa");
23
23
  }
24
24
 
25
25
  export function getAutoQaDbPath(): string {
@@ -1,4 +1,4 @@
1
- import { $env } from "@oh-my-pi/pi-utils";
1
+ import { $env, $flag } from "@oh-my-pi/pi-utils";
2
2
 
3
3
  export type EditMode = "replace" | "patch" | "hashline" | "chunk";
4
4
 
@@ -36,7 +36,7 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
36
36
  const envMode = normalizeEditMode($env.PI_EDIT_VARIANT);
37
37
  if (envMode) return envMode;
38
38
 
39
- if ($env.PI_STRICT_EDIT_MODE === "1") {
39
+ if (!$flag("PI_STRICT_EDIT_MODE")) {
40
40
  if (activeModel?.includes("spark")) return "replace";
41
41
  if (activeModel?.includes("nano")) return "replace";
42
42
  if (activeModel?.includes("mini")) return "replace";
@@ -0,0 +1,239 @@
1
+ import * as path from "node:path";
2
+
3
+ /**
4
+ * Extension segment → [highlight language id, LSP language id].
5
+ * Highlight ids match tree-sitter / native highlighter; LSP ids match Language Server Protocol.
6
+ */
7
+ const EXTENSION_LANG: Record<string, readonly [string, string]> = {
8
+ // TypeScript / JavaScript
9
+ ts: ["typescript", "typescript"],
10
+ cts: ["typescript", "typescript"],
11
+ mts: ["typescript", "typescript"],
12
+ tsx: ["tsx", "typescriptreact"],
13
+ js: ["javascript", "javascript"],
14
+ jsx: ["javascript", "javascriptreact"],
15
+ mjs: ["javascript", "javascript"],
16
+ cjs: ["javascript", "javascript"],
17
+
18
+ // Systems
19
+ rs: ["rust", "rust"],
20
+ go: ["go", "go"],
21
+ c: ["c", "c"],
22
+ h: ["c", "c"],
23
+ cpp: ["cpp", "cpp"],
24
+ cc: ["cpp", "cpp"],
25
+ cxx: ["cpp", "cpp"],
26
+ hh: ["cpp", "cpp"],
27
+ hpp: ["cpp", "cpp"],
28
+ hxx: ["cpp", "cpp"],
29
+ cu: ["cpp", "cpp"],
30
+ ino: ["cpp", "cpp"],
31
+ zig: ["zig", "zig"],
32
+
33
+ // Scripting
34
+ py: ["python", "python"],
35
+ pyi: ["python", "python"],
36
+ rb: ["ruby", "ruby"],
37
+ rbw: ["ruby", "ruby"],
38
+ gemspec: ["ruby", "ruby"],
39
+ lua: ["lua", "lua"],
40
+ sh: ["bash", "shellscript"],
41
+ bash: ["bash", "shellscript"],
42
+ zsh: ["bash", "shellscript"],
43
+ ksh: ["bash", "shellscript"],
44
+ bats: ["bash", "shellscript"],
45
+ tmux: ["bash", "shellscript"],
46
+ cgi: ["bash", "shellscript"],
47
+ fcgi: ["bash", "shellscript"],
48
+ command: ["bash", "shellscript"],
49
+ tool: ["bash", "shellscript"],
50
+ fish: ["fish", "fish"],
51
+ pl: ["perl", "perl"],
52
+ pm: ["perl", "perl"],
53
+ perl: ["perl", "perl"],
54
+ php: ["php", "php"],
55
+
56
+ // JVM
57
+ java: ["java", "java"],
58
+ kt: ["kotlin", "kotlin"],
59
+ ktm: ["kotlin", "kotlin"],
60
+ kts: ["kotlin", "kotlin"],
61
+ scala: ["scala", "scala"],
62
+ sc: ["scala", "scala"],
63
+ sbt: ["scala", "scala"],
64
+ groovy: ["groovy", "groovy"],
65
+ clj: ["clojure", "clojure"],
66
+ cljc: ["clojure", "clojure"],
67
+ cljs: ["clojure", "clojure"],
68
+ edn: ["clojure", "clojure"],
69
+
70
+ // .NET
71
+ cs: ["csharp", "csharp"],
72
+ fs: ["fsharp", "fsharp"],
73
+ vb: ["vb", "vb"],
74
+
75
+ // Web
76
+ html: ["html", "html"],
77
+ htm: ["html", "html"],
78
+ xhtml: ["html", "html"],
79
+ css: ["css", "css"],
80
+ scss: ["scss", "scss"],
81
+ sass: ["sass", "sass"],
82
+ less: ["less", "less"],
83
+ vue: ["vue", "vue"],
84
+ svelte: ["svelte", "svelte"],
85
+ astro: ["astro", "astro"],
86
+
87
+ // Data
88
+ json: ["json", "json"],
89
+ jsonc: ["jsonc", "jsonc"],
90
+ yaml: ["yaml", "yaml"],
91
+ yml: ["yaml", "yaml"],
92
+ toml: ["toml", "toml"],
93
+ xml: ["xml", "xml"],
94
+ xsl: ["xml", "xml"],
95
+ xslt: ["xml", "xml"],
96
+ svg: ["xml", "xml"],
97
+ plist: ["xml", "xml"],
98
+ ini: ["ini", "ini"],
99
+
100
+ // Docs
101
+ md: ["markdown", "markdown"],
102
+ markdown: ["markdown", "markdown"],
103
+ mdx: ["markdown", "markdown"],
104
+ rst: ["restructuredtext", "restructuredtext"],
105
+ adoc: ["asciidoc", "asciidoc"],
106
+ tex: ["latex", "latex"],
107
+
108
+ // Other languages
109
+ sql: ["sql", "sql"],
110
+ graphql: ["graphql", "graphql"],
111
+ gql: ["graphql", "graphql"],
112
+ proto: ["protobuf", "protobuf"],
113
+ dockerfile: ["dockerfile", "dockerfile"],
114
+ containerfile: ["dockerfile", "dockerfile"],
115
+ tf: ["hcl", "terraform"],
116
+ hcl: ["hcl", "hcl"],
117
+ tfvars: ["hcl", "hcl"],
118
+ nix: ["nix", "nix"],
119
+ ex: ["elixir", "elixir"],
120
+ exs: ["elixir", "elixir"],
121
+ erl: ["erlang", "erlang"],
122
+ hrl: ["erlang", "erlang"],
123
+ hs: ["haskell", "haskell"],
124
+ ml: ["ocaml", "ocaml"],
125
+ mli: ["ocaml", "ocaml"],
126
+ swift: ["swift", "swift"],
127
+ r: ["r", "r"],
128
+ jl: ["julia", "julia"],
129
+ dart: ["dart", "dart"],
130
+ elm: ["elm", "elm"],
131
+ v: ["verilog", "v"],
132
+ nim: ["nim", "nim"],
133
+ cr: ["crystal", "crystal"],
134
+ d: ["d", "d"],
135
+ pas: ["pascal", "pascal"],
136
+ pp: ["pascal", "pascal"],
137
+ lisp: ["lisp", "lisp"],
138
+ lsp: ["lisp", "lisp"],
139
+ rkt: ["racket", "racket"],
140
+ scm: ["scheme", "scheme"],
141
+ ps1: ["powershell", "powershell"],
142
+ psm1: ["powershell", "powershell"],
143
+ bat: ["bat", "bat"],
144
+ cmd: ["bat", "bat"],
145
+ tla: ["tlaplus", "tlaplus"],
146
+ tlaplus: ["tlaplus", "tlaplus"],
147
+ m: ["objc", "plaintext"],
148
+ mm: ["objc", "plaintext"],
149
+ sol: ["solidity", "plaintext"],
150
+ odin: ["odin", "plaintext"],
151
+ star: ["starlark", "plaintext"],
152
+ bzl: ["starlark", "plaintext"],
153
+ sv: ["verilog", "plaintext"],
154
+ svh: ["verilog", "plaintext"],
155
+ vh: ["verilog", "plaintext"],
156
+ vim: ["vim", "plaintext"],
157
+ ipynb: ["ipynb", "plaintext"],
158
+ hbs: ["handlebars", "plaintext"],
159
+ hsb: ["handlebars", "plaintext"],
160
+ handlebars: ["handlebars", "plaintext"],
161
+ diff: ["diff", "plaintext"],
162
+ patch: ["diff", "plaintext"],
163
+ makefile: ["make", "plaintext"],
164
+ mk: ["make", "plaintext"],
165
+ mak: ["make", "plaintext"],
166
+ cmake: ["cmake", "cmake"],
167
+ justfile: ["just", "plaintext"],
168
+ txt: ["text", "plaintext"],
169
+ text: ["text", "plaintext"],
170
+ log: ["log", "plaintext"],
171
+ csv: ["csv", "plaintext"],
172
+ tsv: ["tsv", "plaintext"],
173
+ cfg: ["conf", "plaintext"],
174
+ conf: ["conf", "plaintext"],
175
+ config: ["conf", "plaintext"],
176
+ properties: ["conf", "plaintext"],
177
+ env: ["env", "plaintext"],
178
+ gitignore: ["conf", "plaintext"],
179
+ gitattributes: ["conf", "plaintext"],
180
+ gitmodules: ["conf", "plaintext"],
181
+ editorconfig: ["conf", "plaintext"],
182
+ npmrc: ["conf", "plaintext"],
183
+ prettierrc: ["conf", "plaintext"],
184
+ eslintrc: ["conf", "plaintext"],
185
+ prettierignore: ["conf", "plaintext"],
186
+ eslintignore: ["conf", "plaintext"],
187
+ };
188
+
189
+ /** Final segment after the last `.` in the full path (prior theme behavior). */
190
+ function themeExtensionKey(filePath: string): string {
191
+ const extBeg = filePath.lastIndexOf(".");
192
+ return extBeg !== -1 ? filePath.slice(extBeg + 1).toLowerCase() : filePath.toLowerCase();
193
+ }
194
+
195
+ function lspExtensionKey(filePath: string): string {
196
+ const ext = path.extname(filePath).toLowerCase();
197
+ return ext.startsWith(".") ? ext.slice(1) : "";
198
+ }
199
+
200
+ /**
201
+ * Language id for syntax highlighting and UI (icons, read tool), or undefined if unknown.
202
+ */
203
+ export function getLanguageFromPath(filePath: string): string | undefined {
204
+ const pair = EXTENSION_LANG[themeExtensionKey(filePath)];
205
+ if (pair) return pair[0];
206
+
207
+ const baseName = path.basename(filePath).toLowerCase();
208
+ if (baseName.startsWith(".env.")) return "env";
209
+ if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
210
+ return "dockerfile";
211
+ }
212
+ if (baseName === "justfile") return "just";
213
+ if (baseName === "cmakelists.txt") return "cmake";
214
+
215
+ return undefined;
216
+ }
217
+
218
+ /**
219
+ * LSP language identifier; falls back to `plaintext`.
220
+ */
221
+ export function detectLanguageId(filePath: string): string {
222
+ const baseName = path.basename(filePath).toLowerCase();
223
+ if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
224
+ return "dockerfile";
225
+ }
226
+ if (baseName === "makefile" || baseName === "gnumakefile") {
227
+ return "makefile";
228
+ }
229
+ if (baseName === "justfile") {
230
+ return "just";
231
+ }
232
+
233
+ const lspExt = lspExtensionKey(filePath);
234
+ if (baseName === "cmakelists.txt" || lspExt === "cmake") {
235
+ return "cmake";
236
+ }
237
+
238
+ return EXTENSION_LANG[lspExt]?.[1] ?? "plaintext";
239
+ }
@@ -1,4 +1,4 @@
1
- import { $env } from "@oh-my-pi/pi-utils";
1
+ import { $env, $flag } from "@oh-my-pi/pi-utils";
2
2
 
3
3
  const SIXEL_START_REGEX = /\x1bP(?:[0-9;]*)q/u;
4
4
  const SIXEL_END_SEQUENCE = "\x1b\\";
@@ -15,7 +15,7 @@ const SIXEL_PLACEHOLDER_PREFIX = "__OMP_SIXEL_SEQUENCE_";
15
15
  */
16
16
  export function isSixelPassthroughEnabled(): boolean {
17
17
  const forcedProtocol = $env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase();
18
- return forcedProtocol === "sixel" && $env.PI_ALLOW_SIXEL_PASSTHROUGH === "1";
18
+ return forcedProtocol === "sixel" && $flag("PI_ALLOW_SIXEL_PASSTHROUGH");
19
19
  }
20
20
  /** Returns true when the text contains a SIXEL start sequence. */
21
21
  export function containsSixelSequence(text: string): boolean {