@oh-my-pi/pi-coding-agent 14.1.1 → 14.2.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 (123) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/package.json +8 -8
  3. package/scripts/build-binary.ts +61 -0
  4. package/src/autoresearch/helpers.ts +10 -0
  5. package/src/autoresearch/index.ts +1 -11
  6. package/src/autoresearch/tools/init-experiment.ts +1 -10
  7. package/src/autoresearch/tools/log-experiment.ts +1 -11
  8. package/src/autoresearch/tools/run-experiment.ts +1 -10
  9. package/src/bun-imports.d.ts +6 -0
  10. package/src/cli/plugin-cli.ts +23 -45
  11. package/src/commit/agentic/tools/propose-commit.ts +1 -14
  12. package/src/commit/agentic/tools/split-commit.ts +1 -15
  13. package/src/commit/utils.ts +15 -1
  14. package/src/config/model-registry.ts +3 -3
  15. package/src/config/prompt-templates.ts +4 -12
  16. package/src/config/settings-schema.ts +27 -2
  17. package/src/config/settings.ts +1 -1
  18. package/src/discovery/claude-plugins.ts +61 -6
  19. package/src/discovery/codex.ts +2 -15
  20. package/src/discovery/gemini.ts +2 -15
  21. package/src/discovery/helpers.ts +40 -1
  22. package/src/discovery/opencode.ts +2 -15
  23. package/src/edit/apply-patch/index.ts +87 -0
  24. package/src/edit/apply-patch/parser.ts +174 -0
  25. package/src/edit/diff.ts +3 -14
  26. package/src/edit/index.ts +65 -2
  27. package/src/edit/modes/apply-patch.lark +19 -0
  28. package/src/edit/modes/apply-patch.ts +63 -0
  29. package/src/edit/modes/hashline.ts +3 -3
  30. package/src/edit/modes/replace.ts +2 -13
  31. package/src/edit/read-file.ts +18 -0
  32. package/src/edit/renderer.ts +61 -33
  33. package/src/extensibility/extensions/compact-handler.ts +40 -0
  34. package/src/extensibility/extensions/runner.ts +11 -29
  35. package/src/extensibility/utils.ts +7 -1
  36. package/src/internal-urls/docs-index.generated.ts +9 -2
  37. package/src/lsp/render.ts +14 -2
  38. package/src/main.ts +1 -0
  39. package/src/mcp/manager.ts +29 -48
  40. package/src/memories/index.ts +7 -1
  41. package/src/modes/acp/acp-agent.ts +3 -16
  42. package/src/modes/components/model-selector.ts +15 -24
  43. package/src/modes/components/plugin-settings.ts +16 -5
  44. package/src/modes/components/read-tool-group.ts +92 -9
  45. package/src/modes/components/settings-defs.ts +18 -0
  46. package/src/modes/components/settings-selector.ts +2 -6
  47. package/src/modes/components/tool-execution.ts +61 -28
  48. package/src/modes/controllers/event-controller.ts +3 -1
  49. package/src/modes/controllers/extension-ui-controller.ts +99 -150
  50. package/src/modes/controllers/selector-controller.ts +3 -12
  51. package/src/modes/interactive-mode.ts +4 -2
  52. package/src/modes/print-mode.ts +4 -22
  53. package/src/modes/rpc/rpc-mode.ts +18 -38
  54. package/src/modes/shared.ts +10 -1
  55. package/src/modes/utils/ui-helpers.ts +6 -2
  56. package/src/plan-mode/approved-plan.ts +5 -4
  57. package/src/prompts/system/subagent-system-prompt.md +4 -4
  58. package/src/prompts/system/subagent-user-prompt.md +2 -2
  59. package/src/prompts/system/system-prompt.md +208 -243
  60. package/src/prompts/tools/apply-patch.md +67 -0
  61. package/src/prompts/tools/ast-edit.md +18 -23
  62. package/src/prompts/tools/ast-grep.md +24 -32
  63. package/src/prompts/tools/bash.md +11 -23
  64. package/src/prompts/tools/debug.md +8 -22
  65. package/src/prompts/tools/find.md +0 -4
  66. package/src/prompts/tools/grep.md +3 -5
  67. package/src/prompts/tools/hashline.md +16 -10
  68. package/src/prompts/tools/python.md +10 -14
  69. package/src/prompts/tools/read.md +17 -24
  70. package/src/prompts/tools/task.md +57 -21
  71. package/src/prompts/tools/todo-write.md +45 -67
  72. package/src/session/agent-session.ts +4 -4
  73. package/src/session/session-manager.ts +15 -7
  74. package/src/session/streaming-output.ts +24 -0
  75. package/src/slash-commands/builtin-registry.ts +3 -14
  76. package/src/task/executor.ts +13 -34
  77. package/src/task/index.ts +82 -18
  78. package/src/task/simple-mode.ts +27 -0
  79. package/src/task/template.ts +17 -3
  80. package/src/task/types.ts +77 -30
  81. package/src/tools/ask.ts +2 -4
  82. package/src/tools/ast-edit.ts +4 -15
  83. package/src/tools/ast-grep.ts +8 -27
  84. package/src/tools/bash-skill-urls.ts +9 -7
  85. package/src/tools/bash.ts +4 -12
  86. package/src/tools/browser.ts +1 -1
  87. package/src/tools/fetch.ts +1 -14
  88. package/src/tools/file-recorder.ts +35 -0
  89. package/src/tools/find.ts +6 -3
  90. package/src/tools/gh-format.ts +12 -0
  91. package/src/tools/gh-renderer.ts +1 -8
  92. package/src/tools/gh.ts +6 -13
  93. package/src/tools/grep.ts +9 -22
  94. package/src/tools/jtd-to-json-schema.ts +16 -0
  95. package/src/tools/match-line-format.ts +20 -0
  96. package/src/tools/path-utils.ts +30 -2
  97. package/src/tools/plan-mode-guard.ts +6 -5
  98. package/src/tools/python.ts +1 -1
  99. package/src/tools/read.ts +1 -1
  100. package/src/tools/render-utils.ts +38 -6
  101. package/src/tools/renderers.ts +1 -0
  102. package/src/tools/ssh.ts +3 -11
  103. package/src/tools/submit-result.ts +1 -13
  104. package/src/tools/todo-write.ts +137 -103
  105. package/src/tools/write.ts +2 -23
  106. package/src/tui/code-cell.ts +12 -7
  107. package/src/utils/edit-mode.ts +3 -2
  108. package/src/utils/git.ts +1 -1
  109. package/src/vim/engine.ts +41 -58
  110. package/src/web/scrapers/crates-io.ts +1 -14
  111. package/src/web/scrapers/types.ts +13 -0
  112. package/src/web/search/providers/base.ts +13 -0
  113. package/src/web/search/providers/brave.ts +2 -5
  114. package/src/web/search/providers/codex.ts +20 -24
  115. package/src/web/search/providers/gemini.ts +39 -1
  116. package/src/web/search/providers/jina.ts +2 -5
  117. package/src/web/search/providers/kagi.ts +3 -8
  118. package/src/web/search/providers/kimi.ts +3 -7
  119. package/src/web/search/providers/parallel.ts +3 -8
  120. package/src/web/search/providers/synthetic.ts +3 -7
  121. package/src/web/search/providers/tavily.ts +15 -11
  122. package/src/web/search/providers/utils.ts +36 -0
  123. package/src/web/search/providers/zai.ts +3 -7
package/src/lsp/render.ts CHANGED
@@ -335,7 +335,9 @@ function renderDiagnostics(
335
335
  const parsedDiagnostics = diagLines
336
336
  .map(line => parseDiagnosticLine(line))
337
337
  .filter((diag): diag is ParsedDiagnostic => diag !== null);
338
- const fallbackDiagnostics: RawDiagnostic[] = diagLines.map(line => ({ raw: line.trim() }));
338
+ const fallbackDiagnostics: RawDiagnostic[] = diagLines.map(line => ({
339
+ raw: sanitizeDiagnosticDisplayText(line.trim()),
340
+ }));
339
341
 
340
342
  if (expanded) {
341
343
  let output = `${icon} ${theme.fg("dim", meta.join(theme.sep.dot))}`;
@@ -651,11 +653,21 @@ interface RawDiagnostic {
651
653
 
652
654
  type DiagnosticItem = ParsedDiagnostic | RawDiagnostic;
653
655
 
656
+ function sanitizeDiagnosticDisplayText(text: string): string {
657
+ return replaceTabs(text);
658
+ }
659
+
654
660
  function parseDiagnosticLine(line: string): ParsedDiagnostic | null {
655
661
  const match = line.trim().match(/^(.*):(\d+):(\d+)\s+\[(\w+)\]\s*(.*)$/);
656
662
  if (!match) return null;
657
663
  const [, file, lineNum, colNum, severity, message] = match;
658
- return { file, line: lineNum, col: colNum, severity: severity.toLowerCase(), message };
664
+ return {
665
+ file: sanitizeDiagnosticDisplayText(file),
666
+ line: lineNum,
667
+ col: colNum,
668
+ severity: severity.toLowerCase(),
669
+ message: sanitizeDiagnosticDisplayText(message),
670
+ };
659
671
  }
660
672
 
661
673
  function severityToColor(severity: string): "error" | "warning" | "accent" | "dim" {
package/src/main.ts CHANGED
@@ -85,6 +85,7 @@ const RPC_DEFAULTED_SETTING_PATHS: SettingPath[] = [
85
85
  "task.isolation.merge",
86
86
  "task.isolation.commits",
87
87
  "task.eager",
88
+ "task.simple",
88
89
  "task.maxConcurrency",
89
90
  "task.maxRecursionDepth",
90
91
  "task.disabledAgents",
@@ -176,6 +176,33 @@ export class MCPManager {
176
176
  }
177
177
  }
178
178
 
179
+ #subscribeAndTrack(name: string, connection: MCPServerConnection, uris: string[], notificationEpoch: number): void {
180
+ void subscribeToResources(connection, uris)
181
+ .then(() => {
182
+ const action = resolveSubscriptionPostAction(
183
+ this.#notificationsEnabled,
184
+ this.#notificationsEpoch,
185
+ notificationEpoch,
186
+ );
187
+ if (action === "rollback") {
188
+ void unsubscribeFromResources(connection, uris).catch(error => {
189
+ logger.debug("Failed to rollback stale MCP resource subscription", {
190
+ path: `mcp:${name}`,
191
+ error,
192
+ });
193
+ });
194
+ return;
195
+ }
196
+ if (action === "ignore") {
197
+ return;
198
+ }
199
+ this.#subscribedResources.set(name, new Set(uris));
200
+ })
201
+ .catch(error => {
202
+ logger.debug("Failed to subscribe to MCP resources", { path: `mcp:${name}`, error });
203
+ });
204
+ }
205
+
179
206
  setNotificationsEnabled(enabled: boolean): void {
180
207
  const wasEnabled = this.#notificationsEnabled;
181
208
  this.#notificationsEnabled = enabled;
@@ -189,30 +216,7 @@ export class MCPManager {
189
216
  for (const [name, connection] of this.#connections) {
190
217
  if (connection.capabilities.resources?.subscribe && connection.resources) {
191
218
  const uris = connection.resources.map(r => r.uri);
192
- void subscribeToResources(connection, uris)
193
- .then(() => {
194
- const action = resolveSubscriptionPostAction(
195
- this.#notificationsEnabled,
196
- this.#notificationsEpoch,
197
- notificationEpoch,
198
- );
199
- if (action === "rollback") {
200
- void unsubscribeFromResources(connection, uris).catch(error => {
201
- logger.debug("Failed to rollback stale MCP resource subscription", {
202
- path: `mcp:${name}`,
203
- error,
204
- });
205
- });
206
- return;
207
- }
208
- if (action === "ignore") {
209
- return;
210
- }
211
- this.#subscribedResources.set(name, new Set(uris));
212
- })
213
- .catch(error => {
214
- logger.debug("Failed to subscribe to MCP resources", { path: `mcp:${name}`, error });
215
- });
219
+ this.#subscribeAndTrack(name, connection, uris, notificationEpoch);
216
220
  }
217
221
  }
218
222
  return;
@@ -830,30 +834,7 @@ export class MCPManager {
830
834
  if (this.#notificationsEnabled && connection.capabilities.resources?.subscribe) {
831
835
  const uris = resources.map(r => r.uri);
832
836
  const notificationEpoch = this.#notificationsEpoch;
833
- void subscribeToResources(connection, uris)
834
- .then(() => {
835
- const action = resolveSubscriptionPostAction(
836
- this.#notificationsEnabled,
837
- this.#notificationsEpoch,
838
- notificationEpoch,
839
- );
840
- if (action === "rollback") {
841
- void unsubscribeFromResources(connection, uris).catch(error => {
842
- logger.debug("Failed to rollback stale MCP resource subscription", {
843
- path: `mcp:${name}`,
844
- error,
845
- });
846
- });
847
- return;
848
- }
849
- if (action === "ignore") {
850
- return;
851
- }
852
- this.#subscribedResources.set(name, new Set(uris));
853
- })
854
- .catch(error => {
855
- logger.debug("Failed to subscribe to MCP resources", { path: `mcp:${name}`, error });
856
- });
837
+ this.#subscribeAndTrack(name, connection, uris, notificationEpoch);
857
838
  }
858
839
  } catch (error) {
859
840
  logger.debug("Failed to load MCP resources", { path: `mcp:${name}`, error });
@@ -48,6 +48,7 @@ interface MemoryRuntimeConfig {
48
48
  phase2RetryDelaySeconds: number;
49
49
  phase2HeartbeatSeconds: number;
50
50
  rolloutPayloadPercent: number;
51
+ phase1InputTokenLimit: number;
51
52
  fallbackTokenLimit: number;
52
53
  summaryInjectionTokenLimit: number;
53
54
  }
@@ -66,6 +67,7 @@ const DEFAULTS: MemoryRuntimeConfig = {
66
67
  phase2RetryDelaySeconds: 180,
67
68
  phase2HeartbeatSeconds: 30,
68
69
  rolloutPayloadPercent: 0.7,
70
+ phase1InputTokenLimit: 4_000,
69
71
  fallbackTokenLimit: 16_000,
70
72
  summaryInjectionTokenLimit: 5_000,
71
73
  };
@@ -582,7 +584,10 @@ async function runStage1Job(options: {
582
584
  const rolloutRaw = await Bun.file(claim.rolloutPath).text();
583
585
  const persisted = extractPersistableMessages(rolloutRaw);
584
586
  const serializedItems = JSON.stringify(persisted);
585
- const budgetTokens = Math.floor(modelMaxTokens * config.rolloutPayloadPercent);
587
+ const budgetTokens = Math.min(
588
+ config.phase1InputTokenLimit,
589
+ Math.floor(modelMaxTokens * config.rolloutPayloadPercent),
590
+ );
586
591
  const truncatedItems = truncateByApproxTokens(serializedItems, budgetTokens);
587
592
  const inputPrompt = prompt.render(stageOneInputTemplate, {
588
593
  thread_id: claim.threadId,
@@ -1080,6 +1085,7 @@ function loadMemoryConfig(settings: Settings): MemoryRuntimeConfig {
1080
1085
  phase2RetryDelaySeconds: settings.get("memories.phase2RetryDelaySeconds") ?? DEFAULTS.phase2RetryDelaySeconds,
1081
1086
  phase2HeartbeatSeconds: settings.get("memories.phase2HeartbeatSeconds") ?? DEFAULTS.phase2HeartbeatSeconds,
1082
1087
  rolloutPayloadPercent: settings.get("memories.rolloutPayloadPercent") ?? DEFAULTS.rolloutPayloadPercent,
1088
+ phase1InputTokenLimit: settings.get("memories.phase1InputTokenLimit") ?? DEFAULTS.phase1InputTokenLimit,
1083
1089
  fallbackTokenLimit: settings.get("memories.fallbackTokenLimit") ?? DEFAULTS.fallbackTokenLimit,
1084
1090
  summaryInjectionTokenLimit:
1085
1091
  settings.get("memories.summaryInjectionTokenLimit") ?? DEFAULTS.summaryInjectionTokenLimit,
@@ -40,6 +40,7 @@ import {
40
40
  import type { Model } from "@oh-my-pi/pi-ai";
41
41
  import { logger, VERSION } from "@oh-my-pi/pi-utils";
42
42
  import type { ExtensionUIContext } from "../../extensibility/extensions";
43
+ import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
43
44
  import { loadSlashCommands } from "../../extensibility/slash-commands";
44
45
  import { MCPManager } from "../../mcp/manager";
45
46
  import type { MCPServerConfig } from "../../mcp/types";
@@ -1163,14 +1164,7 @@ export class AcpAgent implements Agent {
1163
1164
  shutdown: () => {},
1164
1165
  getContextUsage: () => record.session.getContextUsage(),
1165
1166
  getSystemPrompt: () => record.session.systemPrompt,
1166
- compact: async instructionsOrOptions => {
1167
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
1168
- const options =
1169
- instructionsOrOptions && typeof instructionsOrOptions === "object"
1170
- ? instructionsOrOptions
1171
- : undefined;
1172
- await record.session.compact(instructions, options);
1173
- },
1167
+ compact: instructionsOrOptions => runExtensionCompact(record.session, instructionsOrOptions),
1174
1168
  },
1175
1169
  {
1176
1170
  getContextUsage: () => record.session.getContextUsage(),
@@ -1197,14 +1191,7 @@ export class AcpAgent implements Agent {
1197
1191
  reload: async () => {
1198
1192
  await record.session.reload();
1199
1193
  },
1200
- compact: async instructionsOrOptions => {
1201
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
1202
- const options =
1203
- instructionsOrOptions && typeof instructionsOrOptions === "object"
1204
- ? instructionsOrOptions
1205
- : undefined;
1206
- await record.session.compact(instructions, options);
1207
- },
1194
+ compact: instructionsOrOptions => runExtensionCompact(record.session, instructionsOrOptions),
1208
1195
  },
1209
1196
  acpExtensionUiContext,
1210
1197
  );
@@ -43,6 +43,19 @@ function getAlphaSearchTokens(query: string): string[] {
43
43
  return [...normalizeSearchText(query).matchAll(/[a-z]+/g)].map(match => match[0]).filter(token => token.length > 0);
44
44
  }
45
45
 
46
+ function computeModelRank(model: Model, roles: Record<string, RoleAssignment | undefined>): number {
47
+ let i = 0;
48
+ while (i < MODEL_ROLE_IDS.length) {
49
+ const role = MODEL_ROLE_IDS[i];
50
+ const assigned = roles[role];
51
+ if (assigned && modelsAreEqual(assigned.model, model)) {
52
+ break;
53
+ }
54
+ i++;
55
+ }
56
+ return i;
57
+ }
58
+
46
59
  interface ModelItem {
47
60
  kind: "provider";
48
61
  provider: string;
@@ -252,18 +265,7 @@ export class ModelSelectorComponent extends Container {
252
265
  const mruOrder = this.#settings.getStorage()?.getModelUsageOrder() ?? [];
253
266
  const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
254
267
 
255
- const modelRank = (model: ModelItem) => {
256
- let i = 0;
257
- while (i < MODEL_ROLE_IDS.length) {
258
- const role = MODEL_ROLE_IDS[i];
259
- const assigned = this.#roles[role];
260
- if (assigned && modelsAreEqual(assigned.model, model.model)) {
261
- break;
262
- }
263
- i++;
264
- }
265
- return i;
266
- };
268
+ const modelRank = (item: ModelItem) => computeModelRank(item.model, this.#roles);
267
269
 
268
270
  const dateRe = /-(\d{8})$/;
269
271
  const latestRe = /-latest$/;
@@ -325,18 +327,7 @@ export class ModelSelectorComponent extends Container {
325
327
  const mruOrder = this.#settings.getStorage()?.getModelUsageOrder() ?? [];
326
328
  const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
327
329
 
328
- const modelRank = (model: CanonicalModelItem) => {
329
- let i = 0;
330
- while (i < MODEL_ROLE_IDS.length) {
331
- const role = MODEL_ROLE_IDS[i];
332
- const assigned = this.#roles[role];
333
- if (assigned && modelsAreEqual(assigned.model, model.model)) {
334
- break;
335
- }
336
- i++;
337
- }
338
- return i;
339
- };
330
+ const modelRank = (item: CanonicalModelItem) => computeModelRank(item.model, this.#roles);
340
331
 
341
332
  models.sort((a, b) => {
342
333
  const aRank = modelRank(a);
@@ -22,6 +22,21 @@ import type { InstalledPlugin, PluginSettingSchema } from "../../extensibility/p
22
22
  import { getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
23
23
  import { DynamicBorder } from "./dynamic-border";
24
24
 
25
+ /**
26
+ * Forwards a keystroke to `input`, but cancels via `onCancel` when the user presses Escape.
27
+ */
28
+ export function handleInputOrEscape(
29
+ data: string,
30
+ input: { handleInput(data: string): void },
31
+ onCancel: () => void,
32
+ ): void {
33
+ if (data === "\x1b" || data === "\x1b\x1b") {
34
+ onCancel();
35
+ return;
36
+ }
37
+ input.handleInput(data);
38
+ }
39
+
25
40
  // =============================================================================
26
41
  // Plugin List Component
27
42
  // =============================================================================
@@ -383,11 +398,7 @@ class ConfigInputSubmenu extends Container {
383
398
  }
384
399
 
385
400
  handleInput(data: string): void {
386
- if (data === "\x1b" || data === "\x1b\x1b") {
387
- this.onCancel();
388
- return;
389
- }
390
- this.#input.handleInput(data);
401
+ handleInputOrEscape(data, this.#input, this.onCancel);
391
402
  }
392
403
  }
393
404
 
@@ -1,7 +1,8 @@
1
1
  import type { Component } from "@oh-my-pi/pi-tui";
2
2
  import { Container, Text } from "@oh-my-pi/pi-tui";
3
- import { theme } from "../../modes/theme/theme";
4
- import { shortenPath } from "../../tools/render-utils";
3
+ import { getLanguageFromPath, theme } from "../../modes/theme/theme";
4
+ import { PREVIEW_LIMITS, shortenPath } from "../../tools/render-utils";
5
+ import { renderCodeCell } from "../../tui";
5
6
  import type { ToolExecutionHandle } from "./tool-execution";
6
7
 
7
8
  type ReadRenderArgs = {
@@ -22,6 +23,10 @@ type ReadToolResultDetails = {
22
23
  };
23
24
  };
24
25
 
26
+ type ReadToolGroupOptions = {
27
+ showContentPreview?: boolean;
28
+ };
29
+
25
30
  function getSuffixResolution(details: ReadToolResultDetails | undefined): ReadToolSuffixResolution | undefined {
26
31
  if (typeof details?.suffixResolution?.from !== "string" || typeof details.suffixResolution.to !== "string") {
27
32
  return undefined;
@@ -35,14 +40,21 @@ type ReadEntry = {
35
40
  sel?: string;
36
41
  status: "pending" | "success" | "warning" | "error";
37
42
  correctedFrom?: string;
43
+ contentText?: string;
38
44
  };
39
45
 
46
+ /** Number of code lines to show in collapsed preview mode */
47
+ const COLLAPSED_PREVIEW_LINES = PREVIEW_LIMITS.OUTPUT_COLLAPSED;
48
+
40
49
  export class ReadToolGroupComponent extends Container implements ToolExecutionHandle {
41
50
  #entries = new Map<string, ReadEntry>();
42
51
  #text: Text;
52
+ #expanded = false;
53
+ #showContentPreview: boolean;
43
54
 
44
- constructor() {
55
+ constructor(options: ReadToolGroupOptions = {}) {
45
56
  super();
57
+ this.#showContentPreview = options.showContentPreview ?? false;
46
58
  this.#text = new Text("", 0, 0);
47
59
  this.addChild(this.#text);
48
60
  this.#updateDisplay();
@@ -81,6 +93,11 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
81
93
  entry.correctedFrom = undefined;
82
94
  }
83
95
  entry.status = result.isError ? "error" : suffixResolution ? "warning" : "success";
96
+ // Store the text content for preview/expanded display
97
+ const textContent = result.content?.find(c => c.type === "text")?.text;
98
+ if (textContent !== undefined) {
99
+ entry.contentText = textContent;
100
+ }
84
101
  this.#updateDisplay();
85
102
  }
86
103
 
@@ -88,7 +105,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
88
105
  this.#updateDisplay();
89
106
  }
90
107
 
91
- setExpanded(_expanded: boolean): void {
108
+ setExpanded(expanded: boolean): void {
109
+ this.#expanded = expanded;
92
110
  this.#updateDisplay();
93
111
  }
94
112
 
@@ -99,23 +117,37 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
99
117
  #updateDisplay(): void {
100
118
  const entries = [...this.#entries.values()];
101
119
 
120
+ // Clear previous children and rebuild the summary and preview blocks.
121
+ this.clear();
122
+ this.#text = new Text("", 0, 0);
123
+
102
124
  if (entries.length === 0) {
103
125
  this.#text.setText(` ${theme.format.bullet} ${theme.fg("toolTitle", theme.bold("Read"))}`);
126
+ this.addChild(this.#text);
104
127
  return;
105
128
  }
106
129
 
107
130
  if (entries.length === 1) {
108
131
  const entry = entries[0];
109
- const statusSymbol = this.#formatStatus(entry.status);
110
- const pathDisplay = this.#formatPath(entry);
111
- this.#text.setText(` ${statusSymbol} ${theme.fg("toolTitle", theme.bold("Read"))} ${pathDisplay}`.trimEnd());
132
+ if (!this.#shouldRenderPreview(entry)) {
133
+ const statusSymbol = this.#formatStatus(entry.status);
134
+ const pathDisplay = this.#formatPath(entry);
135
+ this.#text.setText(
136
+ ` ${statusSymbol} ${theme.fg("toolTitle", theme.bold("Read"))} ${pathDisplay}`.trimEnd(),
137
+ );
138
+ this.addChild(this.#text);
139
+ }
140
+ if (this.#shouldRenderPreview(entry)) {
141
+ this.#addContentPreview(entry);
142
+ }
112
143
  return;
113
144
  }
114
145
 
115
146
  const header = `${theme.fg("toolTitle", theme.bold("Read"))}${theme.fg("dim", ` (${entries.length})`)}`;
116
147
  const lines = [` ${theme.format.bullet} ${header}`];
117
- const total = entries.length;
118
- for (const [index, entry] of entries.entries()) {
148
+ const entriesWithoutPreview = entries.filter(entry => !this.#shouldRenderPreview(entry));
149
+ const total = entriesWithoutPreview.length;
150
+ for (const [index, entry] of entriesWithoutPreview.entries()) {
119
151
  const connector = index === total - 1 ? theme.tree.last : theme.tree.branch;
120
152
  const statusSymbol = this.#formatStatus(entry.status);
121
153
  const pathDisplay = this.#formatPath(entry);
@@ -123,6 +155,57 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
123
155
  }
124
156
 
125
157
  this.#text.setText(lines.join("\n"));
158
+ this.addChild(this.#text);
159
+
160
+ for (const entry of entries) {
161
+ if (this.#shouldRenderPreview(entry)) {
162
+ this.#addContentPreview(entry);
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Add a code-cell content preview below the entry summary.
169
+ * When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with "… N more lines (Ctrl+O for more)" hint.
170
+ * When expanded: shows full content.
171
+ */
172
+ #addContentPreview(entry: ReadEntry): void {
173
+ const lang = getLanguageFromPath(entry.path);
174
+ const filePath = shortenPath(entry.path);
175
+ const selectionSuffix = entry.sel ? `:${entry.sel}` : "";
176
+ const correctionSuffix = entry.correctedFrom ? ` (corrected from ${shortenPath(entry.correctedFrom)})` : "";
177
+ const title = filePath ? `Read ${filePath}${selectionSuffix}${correctionSuffix}` : `Read${selectionSuffix}`;
178
+ let cachedWidth: number | undefined;
179
+ let cachedLines: string[] | undefined;
180
+ const expanded = this.#expanded;
181
+ const component: Component = {
182
+ render: (width: number) => {
183
+ if (cachedLines && cachedWidth === width) return cachedLines;
184
+ cachedLines = renderCodeCell(
185
+ {
186
+ code: entry.contentText ?? "",
187
+ language: lang,
188
+ title,
189
+ status: entry.status === "success" ? "complete" : entry.status,
190
+ expanded,
191
+ codeMaxLines: expanded ? undefined : COLLAPSED_PREVIEW_LINES,
192
+ width,
193
+ },
194
+ theme,
195
+ );
196
+ cachedWidth = width;
197
+ return cachedLines;
198
+ },
199
+ invalidate: () => {
200
+ cachedWidth = undefined;
201
+ cachedLines = undefined;
202
+ },
203
+ };
204
+ this.addChild(component);
205
+ }
206
+
207
+ #shouldRenderPreview(entry: ReadEntry): boolean {
208
+ return this.#showContentPreview && entry.contentText !== undefined;
126
209
  }
127
210
 
128
211
  #formatPath(entry: ReadEntry): string {
@@ -145,6 +145,24 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
145
145
  },
146
146
  { value: "never", label: "Never", description: "Stay on the fallback model until manually changed" },
147
147
  ],
148
+ // Task input mode
149
+ "task.simple": [
150
+ {
151
+ value: "default",
152
+ label: "Default",
153
+ description: "Shared context and custom task schema are available",
154
+ },
155
+ {
156
+ value: "schema-free",
157
+ label: "Schema-free",
158
+ description: "Shared context stays available, but custom task schema is disabled",
159
+ },
160
+ {
161
+ value: "independent",
162
+ label: "Independent",
163
+ description: "No shared context or custom task schema; each task must stand alone",
164
+ },
165
+ ],
148
166
  // Task max concurrency
149
167
  "task.maxConcurrency": [
150
168
  { value: "0", label: "Unlimited" },
@@ -25,7 +25,7 @@ import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme }
25
25
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
26
26
  import { getTabBarTheme } from "../shared";
27
27
  import { DynamicBorder } from "./dynamic-border";
28
- import { PluginSettingsComponent } from "./plugin-settings";
28
+ import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
29
29
  import { getSettingsForTab, type SettingDef } from "./settings-defs";
30
30
  import { getPreset } from "./status-line/presets";
31
31
 
@@ -70,11 +70,7 @@ class TextInputSubmenu extends Container {
70
70
  }
71
71
 
72
72
  handleInput(data: string): void {
73
- if (data === "\x1b" || data === "\x1b\x1b") {
74
- this.onCancel();
75
- return;
76
- }
77
- this.#input.handleInput(data);
73
+ handleInputOrEscape(data, this.#input, this.onCancel);
78
74
  }
79
75
  }
80
76