@oh-my-pi/pi-coding-agent 14.0.2 → 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,32 @@
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
+
25
+ ## [14.0.3] - 2026-04-09
26
+
27
+ ### Fixed
28
+
29
+ - Fixed cached Ollama discovery rows so upgraded installs switch to the OpenAI Responses transport instead of staying on the old completions transport
30
+
5
31
  ## [14.0.2] - 2026-04-09
6
32
  ### Added
7
33
 
@@ -258,6 +284,10 @@
258
284
  - `/autoresearch` toggles like `/plan` when empty; slash completion no longer suggests `off`/`clear` on an empty prefix after the command
259
285
  - Chunk-mode read/edit edge cases (zero-width gap replaces, stale batch diagnostics, grouped Go receivers, line-count headers, parse error locations)
260
286
 
287
+ ### Added
288
+
289
+ - `/review` command now accepts inline args as custom instructions appended to the generated prompt for all structured review modes (PR-style, uncommitted, specific commit). When inline args are provided, option 4 (editor) is suppressed from the menu. The no-UI (Task tool) path forwards args as a focus hint.
290
+
261
291
  ## [13.19.0] - 2026-04-05
262
292
 
263
293
  ### Added
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.2",
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.2",
50
- "@oh-my-pi/pi-agent-core": "14.0.2",
51
- "@oh-my-pi/pi-ai": "14.0.2",
52
- "@oh-my-pi/pi-natives": "14.0.2",
53
- "@oh-my-pi/pi-tui": "14.0.2",
54
- "@oh-my-pi/pi-utils": "14.0.2",
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",
@@ -947,7 +947,10 @@ export class ModelRegistry {
947
947
  }
948
948
  const models = this.#applyProviderModelOverrides(
949
949
  providerConfig.provider,
950
- this.#applyProviderCompat(providerConfig.compat, cache.models),
950
+ this.#normalizeDiscoverableModels(
951
+ providerConfig,
952
+ this.#applyProviderCompat(providerConfig.compat, cache.models),
953
+ ),
951
954
  );
952
955
  cachedModels.push(...models);
953
956
  this.#providerDiscoveryStates.set(providerConfig.provider, {
@@ -967,11 +970,19 @@ export class ModelRegistry {
967
970
  return models.map(model => ({ ...model, compat: mergeCompat(model.compat, compat) }));
968
971
  }
969
972
 
973
+ #normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
974
+ if (providerConfig.provider !== "ollama" || providerConfig.api !== "openai-responses") {
975
+ return models;
976
+ }
977
+
978
+ return models.map(model => (model.api === "openai-completions" ? { ...model, api: "openai-responses" } : model));
979
+ }
980
+
970
981
  #addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
971
982
  if (!configuredProviders.has("ollama")) {
972
983
  this.#discoverableProviders.push({
973
984
  provider: "ollama",
974
- api: "openai-completions",
985
+ api: "openai-responses",
975
986
  baseUrl: Bun.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434",
976
987
  discovery: { type: "ollama" },
977
988
  optional: true,
@@ -1203,7 +1214,10 @@ export class ModelRegistry {
1203
1214
  }
1204
1215
  return this.#applyProviderModelOverrides(
1205
1216
  providerId,
1206
- this.#applyProviderCompat(providerConfig.compat, result.models),
1217
+ this.#normalizeDiscoverableModels(
1218
+ providerConfig,
1219
+ this.#applyProviderCompat(providerConfig.compat, result.models),
1220
+ ),
1207
1221
  );
1208
1222
  }
1209
1223
 
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 one leading space per indent level; 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");
@@ -197,7 +197,7 @@ const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than t
197
197
  /**
198
198
  * Build the full review prompt with diff stats and distribution guidance.
199
199
  */
200
- function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): string {
200
+ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string, additionalInstructions?: string): string {
201
201
  const agentCount = getRecommendedAgentCount(stats);
202
202
  const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
203
203
  const totalLines = stats.totalAdded + stats.totalRemoved;
@@ -221,6 +221,7 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): str
221
221
  skipDiff,
222
222
  rawDiff: rawDiff.trim(),
223
223
  linesPerFile,
224
+ additionalInstructions,
224
225
  });
225
226
  }
226
227
 
@@ -230,17 +231,30 @@ export class ReviewCommand implements CustomCommand {
230
231
 
231
232
  constructor(private api: CustomCommandAPI) {}
232
233
 
233
- async execute(_args: string[], ctx: HookCommandContext): Promise<string | undefined> {
234
+ async execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> {
234
235
  if (!ctx.hasUI) {
235
- return "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
236
+ const base = "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
237
+ return args.length > 0 ? `${base} Focus: ${args.join(" ")}` : base;
236
238
  }
237
239
 
238
- const mode = await ctx.ui.select("Review Mode", [
239
- "1. Review against a base branch (PR Style)",
240
- "2. Review uncommitted changes",
241
- "3. Review a specific commit",
242
- "4. Custom review instructions",
243
- ]);
240
+ // Inline args act as additional instructions appended to the generated prompt.
241
+ // When present, skip option 4 (editor) — the args already provide the instructions.
242
+ const extraInstructions = args.length > 0 ? args.join(" ") : undefined;
243
+
244
+ const menuItems = extraInstructions
245
+ ? [
246
+ "1. Review against a base branch (PR Style)",
247
+ "2. Review uncommitted changes",
248
+ "3. Review a specific commit",
249
+ ]
250
+ : [
251
+ "1. Review against a base branch (PR Style)",
252
+ "2. Review uncommitted changes",
253
+ "3. Review a specific commit",
254
+ "4. Custom review instructions",
255
+ ];
256
+
257
+ const mode = await ctx.ui.select("Review Mode", menuItems);
244
258
 
245
259
  if (!mode) return undefined;
246
260
 
@@ -282,6 +296,7 @@ export class ReviewCommand implements CustomCommand {
282
296
  `Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
283
297
  stats,
284
298
  diffText,
299
+ extraInstructions,
285
300
  );
286
301
  }
287
302
 
@@ -318,7 +333,12 @@ export class ReviewCommand implements CustomCommand {
318
333
  return undefined;
319
334
  }
320
335
 
321
- return buildReviewPrompt("Reviewing uncommitted changes (staged + unstaged)", stats, combinedDiff);
336
+ return buildReviewPrompt(
337
+ "Reviewing uncommitted changes (staged + unstaged)",
338
+ stats,
339
+ combinedDiff,
340
+ extraInstructions,
341
+ );
322
342
  }
323
343
 
324
344
  case 3: {
@@ -354,7 +374,7 @@ export class ReviewCommand implements CustomCommand {
354
374
  return undefined;
355
375
  }
356
376
 
357
- return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText);
377
+ return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText, extraInstructions);
358
378
  }
359
379
 
360
380
  case 4: {
@@ -374,11 +394,12 @@ export class ReviewCommand implements CustomCommand {
374
394
  if (reviewDiff) {
375
395
  const stats = parseDiff(reviewDiff);
376
396
  // Even if all files filtered, include the custom instructions
377
- return `${buildReviewPrompt(
397
+ return buildReviewPrompt(
378
398
  `Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
379
399
  stats,
380
400
  reviewDiff,
381
- )}\n\n### Additional Instructions\n\n${instructions}`;
401
+ instructions,
402
+ );
382
403
  }
383
404
 
384
405
  // No diff available, just pass instructions
@@ -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/config.ts CHANGED
@@ -197,6 +197,21 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
197
197
  { markers: ["go.mod", "go.sum"], binDir: "bin" },
198
198
  ];
199
199
 
200
+ const WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS = [".exe", ".cmd", ".bat"] as const;
201
+
202
+ function resolveLocalCommand(basePath: string): string | null {
203
+ if (fs.existsSync(basePath)) return basePath;
204
+ if (process.platform !== "win32") return null;
205
+
206
+ // Package managers write Windows launchers with executable suffixes in node_modules/.bin.
207
+ for (const extension of WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS) {
208
+ const candidate = `${basePath}${extension}`;
209
+ if (fs.existsSync(candidate)) return candidate;
210
+ }
211
+
212
+ return null;
213
+ }
214
+
200
215
  /**
201
216
  * Resolve a command to an executable path.
202
217
  * Checks project-local bin directories first, then falls back to $PATH.
@@ -210,8 +225,9 @@ export function resolveCommand(command: string, cwd: string): string | null {
210
225
  for (const { markers, binDir } of LOCAL_BIN_PATHS) {
211
226
  if (hasRootMarkers(cwd, markers)) {
212
227
  const localPath = path.join(cwd, binDir, command);
213
- if (fs.existsSync(localPath)) {
214
- return localPath;
228
+ const resolvedLocalPath = resolveLocalCommand(localPath);
229
+ if (resolvedLocalPath) {
230
+ return resolvedLocalPath;
215
231
  }
216
232
  }
217
233
  }
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)