@oh-my-pi/pi-coding-agent 14.6.6 → 14.7.0

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 (56) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/examples/hooks/handoff.ts +1 -1
  3. package/examples/hooks/qna.ts +1 -1
  4. package/examples/sdk/03-custom-prompt.ts +7 -4
  5. package/examples/sdk/README.md +1 -1
  6. package/package.json +7 -7
  7. package/src/autoresearch/index.ts +48 -44
  8. package/src/cli/read-cli.ts +58 -0
  9. package/src/cli.ts +1 -0
  10. package/src/commands/read.ts +40 -0
  11. package/src/commit/agentic/agent.ts +1 -1
  12. package/src/commit/analysis/conventional.ts +1 -1
  13. package/src/commit/analysis/summary.ts +1 -1
  14. package/src/commit/changelog/generate.ts +1 -1
  15. package/src/commit/map-reduce/map-phase.ts +1 -1
  16. package/src/commit/map-reduce/reduce-phase.ts +1 -1
  17. package/src/config/settings-schema.ts +39 -0
  18. package/src/edit/line-hash.ts +34 -4
  19. package/src/edit/modes/hashline.ts +201 -6
  20. package/src/edit/streaming.ts +4 -1
  21. package/src/export/html/index.ts +1 -1
  22. package/src/extensibility/extensions/runner.ts +3 -3
  23. package/src/extensibility/extensions/types.ts +4 -4
  24. package/src/main.ts +3 -3
  25. package/src/memories/index.ts +1 -1
  26. package/src/modes/components/agent-dashboard.ts +1 -1
  27. package/src/modes/components/read-tool-group.ts +4 -9
  28. package/src/modes/components/tool-execution.ts +4 -0
  29. package/src/modes/controllers/event-controller.ts +2 -0
  30. package/src/modes/rpc/rpc-types.ts +1 -1
  31. package/src/modes/utils/context-usage.ts +12 -5
  32. package/src/modes/utils/ui-helpers.ts +1 -0
  33. package/src/prompts/system/project-prompt.md +36 -0
  34. package/src/prompts/system/system-prompt.md +0 -29
  35. package/src/prompts/tools/github.md +1 -0
  36. package/src/prompts/tools/read.md +15 -14
  37. package/src/sdk.ts +29 -28
  38. package/src/session/agent-session.ts +20 -12
  39. package/src/session/compaction/branch-summarization.ts +1 -1
  40. package/src/session/compaction/compaction.ts +3 -3
  41. package/src/session/session-dump-format.ts +10 -5
  42. package/src/session/streaming-output.ts +1 -1
  43. package/src/system-prompt.ts +35 -3
  44. package/src/task/executor.ts +4 -3
  45. package/src/tools/fetch.ts +4 -4
  46. package/src/tools/gh.ts +187 -0
  47. package/src/tools/inspect-image.ts +1 -1
  48. package/src/tools/output-meta.ts +1 -1
  49. package/src/tools/path-utils.ts +11 -0
  50. package/src/tools/read.ts +388 -204
  51. package/src/tools/search.ts +1 -1
  52. package/src/tools/sqlite-reader.ts +1 -1
  53. package/src/utils/commit-message-generator.ts +1 -1
  54. package/src/utils/title-generator.ts +1 -1
  55. package/src/web/search/providers/anthropic.ts +1 -1
  56. package/src/workspace-tree.ts +396 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,47 @@
1
1
  # Changelog
2
2
 
3
3
  ## [Unreleased]
4
+ ### Added
5
+
6
+ - Added `pr_create` operation to the GitHub tool to create pull requests with title/body (or `fill`), base/head branch, draft, reviewer, assignee, and label options and return a summarized result including the new PR URL
7
+
8
+ ## [14.7.0] - 2026-05-04
9
+ ### Breaking Changes
10
+
11
+ - Changed session system-prompt APIs to use ordered string block arrays by requiring `buildSystemPrompt`, `CreateAgentSessionOptions.systemPrompt`, `Session.rebuildSystemPrompt`, and extension `before_agent_start`/`getSystemPrompt` hooks to accept and return `systemPrompt: string[]` instead of a plain system-prompt string or separate `projectPrompt` field
12
+ - Changed `buildSystemPrompt` and session `rebuildSystemPrompt` APIs to return `{ systemPrompt, projectPrompt }`, requiring callers expecting a plain system prompt string to update to the new shape
13
+ - Removed the top-level `sel` parameter from the `read` tool schema, requiring callers to migrate to `path`-embedded selectors (for example `path:50-100`, `path:raw`, or `https://...:L1-L40`)
14
+
15
+ ### Added
16
+
17
+ - Added a separate `projectPrompt` artifact containing per-session project context (workstation, context files, AGENTS.md rules, workspace tree, and append prompt) so dynamic context is decoupled from the static system prompt
18
+ - Added `Project prompt` token accounting to context-usage breakdowns and charts
19
+ - Added `tools.elideFileMutationInputs` setting to optionally elide large `write`, `edit`, and `apply_patch` payloads in history after successful mutations
20
+ - Added hashline-style return data for elided `write` calls so tools can include the resulting file content without leaking full input text
21
+ - Added `buildDirectoryTree` and `DirectoryTree` exports to generate configurable directory trees with options for depth, entry limits, hidden-file handling, and truncation caps
22
+ - Added `buildWorkspaceTree` and `WorkspaceTree` exports so callers can precompute and pass a workspace context to prompt generation
23
+ - Added `workspaceTree` support to `buildSystemPrompt` options to reuse a prebuilt directory snapshot
24
+ - Added `read.summarize.enabled`, `read.summarize.minBodyLines`, and `read.summarize.minCommentLines` settings to control whether `read` returns structural summaries and how many multiline body/comment lines are collapsed
25
+ - Added `edit.hashlineAutoDropPureInsertDuplicates` setting to opt into dropping 2+ pure-insert hashline payload lines that duplicate adjacent file context; default is `false`.
26
+
27
+ ### Changed
28
+
29
+ - Updated session dump and HTML export output to serialize ordered system-prompt blocks (including project context) and removed the dedicated project-prompt dump section
30
+ - Renamed context-usage system-prompt accounting from a separate `projectPrompt` bucket to `systemContext` to match the new multi-block prompt structure
31
+ - Changed prompt delivery to inject non-empty `projectPrompt` as a leading `developer` message before conversation messages instead of merging it into the base system prompt
32
+ - Added `projectPrompt` to session dumps to expose the injected per-session project context separately
33
+ - Changed write success output and preview rendering to display hashline-formatted written content from captured file text when mutation inputs are elided
34
+ - Changed `read` directory rendering to return a two-level recency-sorted directory tree (including nested folders) instead of a flat alphabetical entry list, while still applying configurable truncation
35
+ - Changed generated system prompts to include a working-directory tree block after directory context, showing recent files/directories (depth ≤ 3) and truncation notices when entries are elided
36
+ - Changed `read` summary rendering to merge opening- and closing-brace boundaries around elided sections into a single `..` line (including closers like `};` or `})`), reducing those segments to one concise anchored summary line
37
+ - Changed default `read` output for parseable code files without an explicit selector to return a structural summary instead of full verbatim lines, while still supporting full output for `:raw` and explicit ranges
38
+ - Changed truncation/pagination hints in read, archive, and SQLite outputs to use colon syntax (`Use :<offset>`) when continuing reads
39
+ - Changed the read tool UI preview title to include summary elision counts when a summary is returned
40
+ - Changed hashline pure-insert duplicate auto-drop to be opt-in through `edit.hashlineAutoDropPureInsertDuplicates` instead of always enabled.
41
+
42
+ ### Fixed
43
+
44
+ - Fixed selector parsing for colon-containing paths by only splitting `:<sel>` when the suffix matches a valid line-range or `raw` pattern, preventing paths like `db.sqlite:users:42` from being misread as selectors
4
45
 
5
46
  ## [14.6.6] - 2026-05-04
6
47
 
@@ -94,7 +94,7 @@ export default function (pi: HookAPI) {
94
94
 
95
95
  const response = await complete(
96
96
  ctx.model!,
97
- { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
97
+ { systemPrompt: [SYSTEM_PROMPT], messages: [userMessage] },
98
98
  { apiKey, signal: loader.signal },
99
99
  );
100
100
 
@@ -85,7 +85,7 @@ export default function (pi: HookAPI) {
85
85
 
86
86
  const response = await complete(
87
87
  ctx.model!,
88
- { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
88
+ { systemPrompt: [SYSTEM_PROMPT], messages: [userMessage] },
89
89
  { apiKey, signal: loader.signal },
90
90
  );
91
91
 
@@ -7,8 +7,10 @@ import { createAgentSession, SessionManager } from "@oh-my-pi/pi-coding-agent";
7
7
 
8
8
  // Option 1: Replace prompt entirely
9
9
  const { session: session1 } = await createAgentSession({
10
- systemPrompt: `You are a helpful assistant that speaks like a pirate.
10
+ systemPrompt: [
11
+ `You are a helpful assistant that speaks like a pirate.
11
12
  Always end responses with "Arrr!"`,
13
+ ],
12
14
  sessionManager: SessionManager.inMemory(),
13
15
  });
14
16
 
@@ -24,11 +26,12 @@ console.log("\n");
24
26
 
25
27
  // Option 2: Modify default prompt (receives default, returns modified)
26
28
  const { session: session2 } = await createAgentSession({
27
- systemPrompt: defaultPrompt => `${defaultPrompt}
28
-
29
- ## Additional Instructions
29
+ systemPrompt: defaultPrompt => [
30
+ ...defaultPrompt,
31
+ `## Additional Instructions
30
32
  - Always be concise
31
33
  - Use bullet points when listing things`,
34
+ ],
32
35
  sessionManager: SessionManager.inMemory(),
33
36
  });
34
37
 
@@ -87,7 +87,7 @@ const { session } = await createAgentSession({
87
87
  model,
88
88
  authStorage: customAuth,
89
89
  modelRegistry: customRegistry,
90
- systemPrompt: "You are helpful.",
90
+ systemPrompt: ["You are helpful."],
91
91
  toolNames: ["read", "bash"],
92
92
  customTools: [{ tool: myTool }],
93
93
  hooks: [{ factory: myHook }],
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.6.6",
4
+ "version": "14.7.0",
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.20.0",
48
48
  "@mozilla/readability": "^0.6.0",
49
- "@oh-my-pi/omp-stats": "14.6.6",
50
- "@oh-my-pi/pi-agent-core": "14.6.6",
51
- "@oh-my-pi/pi-ai": "14.6.6",
52
- "@oh-my-pi/pi-natives": "14.6.6",
53
- "@oh-my-pi/pi-tui": "14.6.6",
54
- "@oh-my-pi/pi-utils": "14.6.6",
49
+ "@oh-my-pi/omp-stats": "14.7.0",
50
+ "@oh-my-pi/pi-agent-core": "14.7.0",
51
+ "@oh-my-pi/pi-ai": "14.7.0",
52
+ "@oh-my-pi/pi-natives": "14.7.0",
53
+ "@oh-my-pi/pi-tui": "14.7.0",
54
+ "@oh-my-pi/pi-utils": "14.7.0",
55
55
  "@puppeteer/browsers": "^2.13.0",
56
56
  "@sinclair/typebox": "^0.34.49",
57
57
  "@xterm/headless": "^6.0.0",
@@ -356,54 +356,58 @@ export const createAutoresearchExtension: ExtensionFactory = api => {
356
356
  ? null
357
357
  : "Heads up: you are not on a dedicated `autoresearch/*` branch. `log_experiment discard` will only revert run-modified files, not reset to baseline — so harness files written before `init_experiment` may not survive a discard. Clean the worktree and re-run `/autoresearch` if you want full revert safety.";
358
358
  return {
359
- systemPrompt: prompt.render(setupPromptTemplate, {
360
- base_system_prompt: event.systemPrompt,
359
+ systemPrompt: [
360
+ prompt.render(setupPromptTemplate, {
361
+ base_system_prompt: event.systemPrompt.join("\n\n"),
362
+ has_goal: goal.trim().length > 0,
363
+ goal,
364
+ working_dir: ctx.cwd,
365
+ has_branch: Boolean(currentBranch),
366
+ branch: currentBranch ?? "",
367
+ has_baseline_warning: baselineWarning !== null,
368
+ baseline_warning: baselineWarning ?? "",
369
+ }),
370
+ ],
371
+ };
372
+ }
373
+ return {
374
+ systemPrompt: [
375
+ prompt.render(promptTemplate, {
376
+ base_system_prompt: event.systemPrompt.join("\n\n"),
361
377
  has_goal: goal.trim().length > 0,
362
378
  goal,
363
379
  working_dir: ctx.cwd,
364
- has_branch: Boolean(currentBranch),
365
- branch: currentBranch ?? "",
366
- has_baseline_warning: baselineWarning !== null,
367
- baseline_warning: baselineWarning ?? "",
380
+ default_metric_name: state.metricName,
381
+ metric_name: state.metricName,
382
+ has_branch: Boolean(state.branch),
383
+ branch: state.branch,
384
+ has_baseline_commit: Boolean(state.baselineCommit),
385
+ baseline_commit: state.baselineCommit ? state.baselineCommit.slice(0, 12) : "",
386
+ has_notes: state.notes.trim().length > 0,
387
+ notes: state.notes,
388
+ current_segment: state.currentSegment + 1,
389
+ current_segment_run_count: currentSegmentResults.length,
390
+ has_baseline_metric: baselineMetric !== null,
391
+ baseline_metric_display: formatNum(baselineMetric, state.metricUnit),
392
+ baseline_run_number: baselineRunNumber,
393
+ has_best_result: bestResult !== null && bestMetric !== null,
394
+ best_metric_display: bestMetric !== null ? formatNum(bestMetric, state.metricUnit) : "-",
395
+ best_run_number: bestResult ? (bestResult.runNumber ?? state.results.indexOf(bestResult) + 1) : null,
396
+ has_recent_results: recentResults.length > 0,
397
+ recent_results: recentResults,
398
+ has_unjustified_runs: unjustifiedRuns.length > 0,
399
+ unjustified_runs: unjustifiedRuns,
400
+ has_pending_run: Boolean(pendingRun),
401
+ pending_run_number: pendingRun?.runNumber,
402
+ pending_run_command: pendingRun?.command,
403
+ pending_run_passed: pendingRun?.passed ?? false,
404
+ has_pending_run_metric: pendingRun?.parsedPrimary !== null && pendingRun?.parsedPrimary !== undefined,
405
+ pending_run_metric_display:
406
+ pendingRun?.parsedPrimary !== null && pendingRun?.parsedPrimary !== undefined
407
+ ? formatNum(pendingRun.parsedPrimary, state.metricUnit)
408
+ : null,
368
409
  }),
369
- };
370
- }
371
- return {
372
- systemPrompt: prompt.render(promptTemplate, {
373
- base_system_prompt: event.systemPrompt,
374
- has_goal: goal.trim().length > 0,
375
- goal,
376
- working_dir: ctx.cwd,
377
- default_metric_name: state.metricName,
378
- metric_name: state.metricName,
379
- has_branch: Boolean(state.branch),
380
- branch: state.branch,
381
- has_baseline_commit: Boolean(state.baselineCommit),
382
- baseline_commit: state.baselineCommit ? state.baselineCommit.slice(0, 12) : "",
383
- has_notes: state.notes.trim().length > 0,
384
- notes: state.notes,
385
- current_segment: state.currentSegment + 1,
386
- current_segment_run_count: currentSegmentResults.length,
387
- has_baseline_metric: baselineMetric !== null,
388
- baseline_metric_display: formatNum(baselineMetric, state.metricUnit),
389
- baseline_run_number: baselineRunNumber,
390
- has_best_result: bestResult !== null && bestMetric !== null,
391
- best_metric_display: bestMetric !== null ? formatNum(bestMetric, state.metricUnit) : "-",
392
- best_run_number: bestResult ? (bestResult.runNumber ?? state.results.indexOf(bestResult) + 1) : null,
393
- has_recent_results: recentResults.length > 0,
394
- recent_results: recentResults,
395
- has_unjustified_runs: unjustifiedRuns.length > 0,
396
- unjustified_runs: unjustifiedRuns,
397
- has_pending_run: Boolean(pendingRun),
398
- pending_run_number: pendingRun?.runNumber,
399
- pending_run_command: pendingRun?.command,
400
- pending_run_passed: pendingRun?.passed ?? false,
401
- has_pending_run_metric: pendingRun?.parsedPrimary !== null && pendingRun?.parsedPrimary !== undefined,
402
- pending_run_metric_display:
403
- pendingRun?.parsedPrimary !== null && pendingRun?.parsedPrimary !== undefined
404
- ? formatNum(pendingRun.parsedPrimary, state.metricUnit)
405
- : null,
406
- }),
410
+ ],
407
411
  };
408
412
  });
409
413
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Read CLI command handler.
3
+ *
4
+ * Handles `omp read` — invokes the `read` agent tool against a path/URL and
5
+ * prints the resulting content blocks exactly as the model would receive them
6
+ * (including truncation/limit notices appended by the meta-notice wrapper).
7
+ */
8
+ import { getProjectDir } from "@oh-my-pi/pi-utils";
9
+ import chalk from "chalk";
10
+ import { Settings } from "../config/settings";
11
+ import type { ToolSession } from "../tools";
12
+ import { wrapToolWithMetaNotice } from "../tools/output-meta";
13
+ import { ReadTool } from "../tools/read";
14
+ import { renderError } from "../tools/tool-errors";
15
+
16
+ export interface ReadCommandArgs {
17
+ path: string;
18
+ timeout?: number;
19
+ }
20
+
21
+ export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
22
+ if (!cmd.path) {
23
+ process.stderr.write(chalk.red("error: path is required\n"));
24
+ process.exit(1);
25
+ }
26
+
27
+ const cwd = getProjectDir();
28
+ const settings = await Settings.init({ cwd });
29
+
30
+ const session: ToolSession = {
31
+ cwd,
32
+ hasUI: false,
33
+ settings,
34
+ getSessionFile: () => null,
35
+ getSessionSpawns: () => "*",
36
+ };
37
+
38
+ const tool = wrapToolWithMetaNotice(new ReadTool(session));
39
+
40
+ try {
41
+ const result = await tool.execute("omp-read", { path: cmd.path, timeout: cmd.timeout });
42
+
43
+ for (const block of result.content) {
44
+ if (block.type === "text") {
45
+ process.stdout.write(block.text);
46
+ if (!block.text.endsWith("\n")) process.stdout.write("\n");
47
+ } else if (block.type === "image") {
48
+ const decodedBytes = Buffer.from(block.data, "base64").byteLength;
49
+ process.stdout.write(
50
+ chalk.dim(`[image content: ${block.mimeType}, ${decodedBytes} bytes base64-decoded]\n`),
51
+ );
52
+ }
53
+ }
54
+ } catch (err) {
55
+ process.stderr.write(`${chalk.red(renderError(err))}\n`);
56
+ process.exit(1);
57
+ }
58
+ }
package/src/cli.ts CHANGED
@@ -53,6 +53,7 @@ const commands: CommandEntry[] = [
53
53
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
54
54
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
55
55
  { name: "shell", load: () => import("./commands/shell").then(m => m.default) },
56
+ { name: "read", load: () => import("./commands/read").then(m => m.default) },
56
57
  { name: "ssh", load: () => import("./commands/ssh").then(m => m.default) },
57
58
  { name: "stats", load: () => import("./commands/stats").then(m => m.default) },
58
59
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Show what the read tool will return for a given path.
3
+ */
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
+ import { type ReadCommandArgs, runReadCommand } from "../cli/read-cli";
6
+ import { initTheme } from "../modes/theme/theme";
7
+
8
+ export default class Read extends Command {
9
+ static description = "Show what the read tool will return for a path or URL";
10
+
11
+ static args = {
12
+ path: Args.string({
13
+ description: "Path or URL to read (append :sel for line ranges or raw mode, e.g. src/foo.ts:50-100)",
14
+ required: true,
15
+ }),
16
+ };
17
+
18
+ static flags = {
19
+ timeout: Flags.integer({ description: "Request timeout in seconds (URLs only)" }),
20
+ };
21
+
22
+ static examples = [
23
+ "omp read src/foo.ts",
24
+ "omp read src/foo.ts:50-100",
25
+ "omp read src/foo.ts:raw",
26
+ "omp read https://example.com",
27
+ "omp read path/to/archive.zip:dir/file.ts",
28
+ "omp read path/to/db.sqlite:users:42",
29
+ ];
30
+
31
+ async run(): Promise<void> {
32
+ const { args, flags } = await this.parse(Read);
33
+ const cmd: ReadCommandArgs = {
34
+ path: args.path ?? "",
35
+ timeout: flags.timeout,
36
+ };
37
+ await initTheme();
38
+ await runReadCommand(cmd);
39
+ }
40
+ }
@@ -60,7 +60,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
60
60
  settings: input.settings,
61
61
  model: input.model,
62
62
  thinkingLevel: input.thinkingLevel,
63
- systemPrompt,
63
+ systemPrompt: [systemPrompt],
64
64
  customTools: tools,
65
65
  enableLsp: false,
66
66
  enableMCP: false,
@@ -89,7 +89,7 @@ export async function generateConventionalAnalysis({
89
89
  const response = await completeSimple(
90
90
  model,
91
91
  {
92
- systemPrompt: prompt.render(analysisSystemPrompt),
92
+ systemPrompt: [prompt.render(analysisSystemPrompt)],
93
93
  messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
94
94
  tools: [ConventionalAnalysisTool],
95
95
  },
@@ -53,7 +53,7 @@ export async function generateSummary({
53
53
  const response = await completeSimple(
54
54
  model,
55
55
  {
56
- systemPrompt,
56
+ systemPrompt: [systemPrompt],
57
57
  messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }],
58
58
  tools: [SummaryTool],
59
59
  },
@@ -58,7 +58,7 @@ export async function generateChangelogEntries({
58
58
  const response = await completeSimple(
59
59
  model,
60
60
  {
61
- systemPrompt: prompt.render(changelogSystemPrompt),
61
+ systemPrompt: [prompt.render(changelogSystemPrompt)],
62
62
  messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
63
63
  tools: [changelogTool],
64
64
  },
@@ -62,7 +62,7 @@ export async function runMapPhase({
62
62
  context_header: contextHeader,
63
63
  });
64
64
  const request = {
65
- systemPrompt,
65
+ systemPrompt: [systemPrompt],
66
66
  messages: [{ role: "user", content: userContent, timestamp: Date.now() }] as Message[],
67
67
  };
68
68
 
@@ -76,7 +76,7 @@ export async function runReducePhase({
76
76
  const response = await completeSimple(
77
77
  model,
78
78
  {
79
- systemPrompt: prompt.render(reduceSystemPrompt),
79
+ systemPrompt: [prompt.render(reduceSystemPrompt)],
80
80
  messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
81
81
  tools: [ReduceTool],
82
82
  },
@@ -1419,6 +1419,15 @@ export const SETTINGS_SCHEMA = {
1419
1419
  },
1420
1420
  },
1421
1421
 
1422
+ "edit.hashlineAutoDropPureInsertDuplicates": {
1423
+ type: "boolean",
1424
+ default: false,
1425
+ ui: {
1426
+ tab: "editing",
1427
+ label: "Hashline Duplicate Insert Drop",
1428
+ description: "Drop 2+ pure-insert payload lines that duplicate adjacent file context",
1429
+ },
1430
+ },
1422
1431
  "edit.blockAutoGenerated": {
1423
1432
  type: "boolean",
1424
1433
  default: true,
@@ -1466,6 +1475,36 @@ export const SETTINGS_SCHEMA = {
1466
1475
  },
1467
1476
  },
1468
1477
 
1478
+ "read.summarize.enabled": {
1479
+ type: "boolean",
1480
+ default: true,
1481
+ ui: {
1482
+ tab: "editing",
1483
+ label: "Read Summaries",
1484
+ description: "Return structural code summaries when read is called without an explicit selector",
1485
+ },
1486
+ },
1487
+
1488
+ "read.summarize.minBodyLines": {
1489
+ type: "number",
1490
+ default: 4,
1491
+ ui: {
1492
+ tab: "editing",
1493
+ label: "Read Summary Body Lines",
1494
+ description: "Minimum multiline body or literal length before read summaries collapse it",
1495
+ },
1496
+ },
1497
+
1498
+ "read.summarize.minCommentLines": {
1499
+ type: "number",
1500
+ default: 6,
1501
+ ui: {
1502
+ tab: "editing",
1503
+ label: "Read Summary Comment Lines",
1504
+ description: "Minimum multiline block comment length before read summaries collapse it",
1505
+ },
1506
+ },
1507
+
1469
1508
  "read.toolResultPreview": {
1470
1509
  type: "boolean",
1471
1510
  default: false,
@@ -783,18 +783,48 @@ export const HL_BODY_SEP = "|";
783
783
  export const HL_BODY_SEP_RE_RAW = regexEscape(HL_BODY_SEP);
784
784
 
785
785
  const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
786
+ const RE_STRUCTURAL_STRIP = /[\s{}]/g;
787
+
788
+ /**
789
+ * Bigram returned for lines that contain only whitespace and `{`/`}`.
790
+ * Picks the English ordinal suffix for the line number (`1` → `st`,
791
+ * `2` → `nd`, `3` → `rd`, `11`/`12`/`13` → `th`, else `th`) so the
792
+ * line digits + bigram BPE-merge into a single ordinal token (`1st`, `42nd`,
793
+ * `100th`, …). Brace-only lines therefore cost one token for the whole
794
+ * `LINE+ID` anchor instead of two.
795
+ */
796
+ function structuralBigram(line: number): string {
797
+ const mod100 = line % 100;
798
+ if (mod100 >= 11 && mod100 <= 13) return "th";
799
+ switch (line % 10) {
800
+ case 1:
801
+ return "st";
802
+ case 2:
803
+ return "nd";
804
+ case 3:
805
+ return "rd";
806
+ default:
807
+ return "th";
808
+ }
809
+ }
786
810
 
787
811
  /**
788
812
  * Compute a 2-character hash of a single line via xxHash32 mod 647 over
789
- * {@link HL_BIGRAMS}. Lines with no letter or digit (e.g. bare `}`,
790
- * bare `{`) mix the line number into the seed so adjacent identical
791
- * brace-only lines get distinct hashes; lines with significant content stay
792
- * line-number-independent so a line is identifiable across small shifts.
813
+ * {@link HL_BIGRAMS}. Lines that contain only whitespace and `{`/`}` collapse
814
+ * to an ordinal-suffix bigram (see {@link structuralBigram}) so brace-only
815
+ * structure shares one merged ordinal token (`1st`, `42nd`, `100th`, …).
816
+ * Other lines with no letter or digit mix the line number into the seed so
817
+ * adjacent identical punctuation-only lines get distinct hashes; lines with
818
+ * significant content stay line-number-independent so a line is identifiable
819
+ * across small shifts.
793
820
  *
794
821
  * The line input should not include a trailing newline.
795
822
  */
796
823
  export function computeLineHash(idx: number, line: string): string {
797
824
  line = line.replace(/\r/g, "").trimEnd();
825
+ if (line.replace(RE_STRUCTURAL_STRIP, "").length === 0) {
826
+ return structuralBigram(idx);
827
+ }
798
828
  const seed = RE_SIGNIFICANT.test(line) ? 0 : idx;
799
829
  return HL_BIGRAMS[Bun.hash.xxHash32(line, seed) % HL_BIGRAMS_COUNT];
800
830
  }