tau-coding-agent 0.1.2 → 0.1.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.
Files changed (35) hide show
  1. package/README.md +22 -21
  2. package/extensions/answer.ts +4 -4
  3. package/extensions/branch-term.ts +3 -3
  4. package/extensions/btw.ts +4 -4
  5. package/extensions/ghostty.ts +1 -1
  6. package/extensions/git-diff-stats.ts +1 -1
  7. package/extensions/git-pr-status.ts +1 -1
  8. package/extensions/insights.ts +3 -3
  9. package/extensions/interlude.ts +1 -1
  10. package/extensions/loop.ts +3 -3
  11. package/extensions/memory.ts +8 -2
  12. package/extensions/notify.ts +1 -1
  13. package/extensions/openai-fast.ts +1 -1
  14. package/extensions/openai-verbosity.ts +1 -1
  15. package/extensions/review.ts +111 -38
  16. package/extensions/sandbox/index.ts +84 -8
  17. package/extensions/tool-display-mode.ts +544 -0
  18. package/extensions/usage/anthropic.ts +1 -1
  19. package/extensions/usage/github-copilot.ts +1 -1
  20. package/extensions/usage/index.ts +3 -3
  21. package/extensions/usage/minimax.ts +1 -1
  22. package/extensions/usage/openai-codex.ts +1 -1
  23. package/extensions/usage/openrouter.ts +1 -1
  24. package/extensions/usage/providers.ts +0 -2
  25. package/extensions/usage/types.ts +1 -2
  26. package/extensions/usage/zai.ts +1 -1
  27. package/extensions/websearch/index.ts +3 -3
  28. package/extensions/websearch/providers/anthropic.pi.ts +1 -1
  29. package/extensions/websearch/providers/gemini.pi.ts +1 -1
  30. package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
  31. package/extensions/websearch/providers/pi-model.shared.ts +2 -2
  32. package/extensions/worktree.ts +3 -3
  33. package/package.json +20 -7
  34. package/skills/oracle/scripts/oracle +0 -1
  35. package/extensions/usage/google-gemini-cli.ts +0 -232
@@ -25,8 +25,9 @@
25
25
  * },
26
26
  * "filesystem": {
27
27
  * "denyRead": ["~/.ssh", "~/.aws"],
28
- * "allowWrite": [".", "/tmp"],
28
+ * "allowWrite": ["."],
29
29
  * "denyWrite": [".env"],
30
+ * "allowTempDirs": true,
30
31
  * "allowGitCommonDir": true
31
32
  * }
32
33
  * }
@@ -47,7 +48,7 @@
47
48
 
48
49
  import { spawn, spawnSync } from "node:child_process";
49
50
  import { existsSync, readFileSync } from "node:fs";
50
- import { homedir } from "node:os";
51
+ import { homedir, tmpdir } from "node:os";
51
52
  import { join, resolve } from "node:path";
52
53
  import {
53
54
  SandboxManager,
@@ -62,8 +63,8 @@ import {
62
63
  globToRegex,
63
64
  normalizePathForSandbox,
64
65
  } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
65
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
66
- import { type BashOperations, createBashTool } from "@mariozechner/pi-coding-agent";
66
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
67
+ import { type BashOperations, createBashTool } from "@earendil-works/pi-coding-agent";
67
68
 
68
69
  // --- Constants ---
69
70
 
@@ -74,24 +75,69 @@ const DEFAULT_CONFIG: SandboxConfig = {
74
75
  mode: DEFAULT_PROMPT_MODE,
75
76
  network: {
76
77
  allowedDomains: [
78
+ "localhost",
79
+ "127.0.0.1",
77
80
  "npmjs.org",
78
81
  "*.npmjs.org",
79
82
  "registry.npmjs.org",
80
83
  "registry.yarnpkg.com",
81
84
  "pypi.org",
82
85
  "*.pypi.org",
86
+ "pythonhosted.org",
87
+ "*.pythonhosted.org",
88
+ "crates.io",
89
+ "*.crates.io",
90
+ "proxy.golang.org",
91
+ "sum.golang.org",
92
+ "go.dev",
93
+ "golang.org",
94
+ "rubygems.org",
95
+ "*.rubygems.org",
83
96
  "github.com",
84
97
  "*.github.com",
98
+ "githubusercontent.com",
99
+ "*.githubusercontent.com",
85
100
  "api.github.com",
86
101
  "raw.githubusercontent.com",
102
+ "ghcr.io",
103
+ "docker.io",
104
+ "*.docker.io",
105
+ "production.cloudflare.docker.com",
106
+ "sentry.io",
107
+ "*.sentry.io",
108
+ "datadoghq.com",
109
+ "*.datadoghq.com",
110
+ "datadoghq.eu",
111
+ "*.datadoghq.eu",
112
+ "anthropic.com",
113
+ "*.anthropic.com",
114
+ "claude.ai",
115
+ "*.claude.ai",
116
+ "openai.com",
117
+ "*.openai.com",
118
+ "chatgpt.com",
119
+ "*.chatgpt.com",
120
+ "google.com",
121
+ "*.google.com",
122
+ "googleapis.com",
123
+ "*.googleapis.com",
124
+ "todoist.com",
125
+ "*.todoist.com",
126
+ "twist.com",
127
+ "*.twist.com",
128
+ "doist.com",
129
+ "*.doist.com",
87
130
  ],
88
131
  deniedDomains: [],
132
+ allowLocalBinding: true,
89
133
  },
90
134
  filesystem: {
91
135
  denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
92
- allowWrite: [".", "/tmp"],
136
+ allowWrite: [".", "~/.cache", "~/.npm"],
93
137
  denyWrite: [".env", ".env.*", "*.pem", "*.key"],
94
- allowGitCommonDir: false,
138
+ allowTempDirs: true,
139
+ allowGitConfig: true,
140
+ allowGitCommonDir: true,
95
141
  },
96
142
  };
97
143
 
@@ -120,6 +166,7 @@ type SandboxConfig = Omit<SandboxRuntimeConfig, "filesystem"> & {
120
166
  enabled?: boolean;
121
167
  mode?: PromptMode;
122
168
  filesystem: SandboxRuntimeConfig["filesystem"] & {
169
+ allowTempDirs?: boolean;
123
170
  allowGitCommonDir?: boolean;
124
171
  };
125
172
  };
@@ -582,6 +629,10 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
582
629
  DEFAULT_CONFIG.filesystem.denyWrite,
583
630
  "filesystem.denyWrite",
584
631
  ),
632
+ allowTempDirs:
633
+ typeof config.filesystem?.allowTempDirs === "boolean"
634
+ ? config.filesystem.allowTempDirs
635
+ : DEFAULT_CONFIG.filesystem.allowTempDirs,
585
636
  allowGitCommonDir:
586
637
  typeof config.filesystem?.allowGitCommonDir === "boolean"
587
638
  ? config.filesystem.allowGitCommonDir
@@ -705,12 +756,32 @@ function deepMerge(base: SandboxConfig, overrides: Partial<SandboxConfig>): Sand
705
756
  return result;
706
757
  }
707
758
 
759
+ let cachedTemporaryWritePaths: string[] | undefined;
760
+
761
+ function getTemporaryWritePaths(): string[] {
762
+ if (cachedTemporaryWritePaths) return cachedTemporaryWritePaths;
763
+
764
+ // allowTempDirs always includes the conventional shared temp path, even when
765
+ // os.tmpdir() points at a platform-specific per-user directory.
766
+ const currentTmpDir = tmpdir();
767
+ const paths = ["/tmp", currentTmpDir, normalizePathForSandbox(currentTmpDir)];
768
+ if (process.platform === "darwin") paths.push("/private/tmp");
769
+
770
+ cachedTemporaryWritePaths = Array.from(
771
+ new Set(paths.map((path) => path.replace(/\/+$/, "") || "/")),
772
+ );
773
+ return cachedTemporaryWritePaths;
774
+ }
775
+
708
776
  function toRuntimeConfig(config: SandboxConfig): SandboxRuntimeConfig {
709
- const { allowGitCommonDir: _allowGitCommonDir, ...filesystem } = config.filesystem;
777
+ const { allowGitCommonDir: _allowGitCommonDir, allowTempDirs, ...filesystem } = config.filesystem;
778
+ const allowWrite = allowTempDirs
779
+ ? Array.from(new Set([...filesystem.allowWrite, ...getTemporaryWritePaths()]))
780
+ : filesystem.allowWrite;
710
781
 
711
782
  return {
712
783
  network: config.network,
713
- filesystem,
784
+ filesystem: { ...filesystem, allowWrite },
714
785
  ignoreViolations: config.ignoreViolations,
715
786
  enableWeakerNestedSandbox: config.enableWeakerNestedSandbox,
716
787
  enableWeakerNetworkIsolation: config.enableWeakerNetworkIsolation,
@@ -2455,11 +2526,16 @@ export default function (pi: ExtensionAPI) {
2455
2526
  " Network:",
2456
2527
  ` Allowed: ${runtimeConfig.network.allowedDomains.join(", ") || "(none)"}`,
2457
2528
  ` Denied: ${runtimeConfig.network.deniedDomains.join(", ") || "(none)"}`,
2529
+ ` allowLocalBinding: ${runtimeConfig.network.allowLocalBinding ? "true" : "false"}`,
2530
+ ` allowAllUnixSockets: ${runtimeConfig.network.allowAllUnixSockets ? "true" : "false"}`,
2531
+ ` allowUnixSockets: ${runtimeConfig.network.allowUnixSockets?.join(", ") || "(none)"}`,
2458
2532
  "",
2459
2533
  " Filesystem:",
2460
2534
  ` Deny Read: ${runtimeConfig.filesystem.denyRead.join(", ") || "(none)"}`,
2461
2535
  ` Allow Write: ${runtimeConfig.filesystem.allowWrite.join(", ") || "(none)"}`,
2462
2536
  ` Deny Write: ${runtimeConfig.filesystem.denyWrite.join(", ") || "(none)"}`,
2537
+ ` allowTempDirs: ${sandboxConfig?.filesystem.allowTempDirs ? "true" : "false"}`,
2538
+ ` allowGitConfig: ${runtimeConfig.filesystem.allowGitConfig ? "true" : "false"}`,
2463
2539
  ` allowGitCommonDir: ${sandboxConfig?.filesystem.allowGitCommonDir ? "true" : "false"}`,
2464
2540
  "",
2465
2541
  " Advanced:",
@@ -0,0 +1,544 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ type AppKeybinding,
6
+ CustomEditor,
7
+ createBashToolDefinition,
8
+ createFindToolDefinition,
9
+ createGrepToolDefinition,
10
+ createLsToolDefinition,
11
+ createReadToolDefinition,
12
+ type AgentToolResult,
13
+ type ExtensionAPI,
14
+ type ExtensionContext,
15
+ type KeybindingsManager,
16
+ type Theme,
17
+ type ToolDefinition,
18
+ type ToolInfo,
19
+ type ToolRenderResultOptions,
20
+ } from "@earendil-works/pi-coding-agent";
21
+ import {
22
+ Container,
23
+ Text,
24
+ type AutocompleteProvider,
25
+ type Component,
26
+ type EditorComponent,
27
+ type Focusable,
28
+ } from "@earendil-works/pi-tui";
29
+
30
+ // --- Constants ---
31
+
32
+ const CONFIG_FILE = "tool-display-mode.json";
33
+ const MODES = ["default", "expanded", "minimal"] as const;
34
+ const DEFAULT_MODE = "default";
35
+
36
+ // These are current Pi built-in tool output messages, used only because grep/find/ls
37
+ // do not expose structured zero-result details yet.
38
+ const GREP_NO_MATCHES_OUTPUT = "No matches found"; // core/tools/grep.ts
39
+ const FIND_NO_MATCHES_OUTPUT = "No files found matching pattern"; // core/tools/find.ts
40
+ const LS_EMPTY_DIRECTORY_OUTPUT = "(empty directory)"; // core/tools/ls.ts
41
+
42
+ const TOOL_FACTORIES = {
43
+ read: createReadToolDefinition,
44
+ bash: createBashToolDefinition,
45
+ grep: createGrepToolDefinition,
46
+ find: createFindToolDefinition,
47
+ ls: createLsToolDefinition,
48
+ };
49
+
50
+ // --- Types ---
51
+
52
+ type Mode = (typeof MODES)[number];
53
+ type ToolName = keyof typeof TOOL_FACTORIES;
54
+ type AnyToolDefinition = ToolDefinition<any, any, any>;
55
+ type AnyToolRenderContext = Parameters<NonNullable<AnyToolDefinition["renderResult"]>>[3];
56
+ type JsonObject = Record<string, unknown>;
57
+ type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
58
+
59
+ type ToolDisplayModeConfig = {
60
+ mode: Mode;
61
+ };
62
+
63
+ type CustomEditorLike = EditorComponent &
64
+ Partial<Focusable> & {
65
+ actionHandlers?: Map<AppKeybinding, () => void>;
66
+ onEscape?: () => void;
67
+ onCtrlD?: () => void;
68
+ onPasteImage?: () => void;
69
+ onExtensionShortcut?: (data: string) => boolean;
70
+ isShowingAutocomplete?: () => boolean;
71
+ };
72
+
73
+ // --- Config ---
74
+
75
+ function emptyConfig(): ToolDisplayModeConfig {
76
+ return { mode: DEFAULT_MODE };
77
+ }
78
+
79
+ function getConfigPath(): string {
80
+ const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
81
+ return path.join(agentDir, CONFIG_FILE);
82
+ }
83
+
84
+ function isObject(value: unknown): value is JsonObject {
85
+ return typeof value === "object" && value !== null && !Array.isArray(value);
86
+ }
87
+
88
+ function isMode(value: string): value is Mode {
89
+ return (MODES as readonly string[]).includes(value);
90
+ }
91
+
92
+ function parseMode(value: unknown): Mode | undefined {
93
+ if (typeof value !== "string") return undefined;
94
+
95
+ const normalized = value.trim().toLowerCase();
96
+ return isMode(normalized) ? normalized : undefined;
97
+ }
98
+
99
+ function parseConfig(value: unknown): ToolDisplayModeConfig {
100
+ if (!isObject(value)) return emptyConfig();
101
+
102
+ return {
103
+ mode: parseMode(value.mode) ?? DEFAULT_MODE,
104
+ };
105
+ }
106
+
107
+ async function loadConfig(): Promise<ToolDisplayModeConfig> {
108
+ try {
109
+ const raw = await readFile(getConfigPath(), "utf8");
110
+ return parseConfig(JSON.parse(raw) as unknown);
111
+ } catch (error) {
112
+ const code = (error as NodeJS.ErrnoException).code;
113
+ if (code !== "ENOENT") {
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ console.warn(`[tool-display-mode] Failed to load config: ${message}`);
116
+ }
117
+
118
+ return emptyConfig();
119
+ }
120
+ }
121
+
122
+ async function saveConfig(config: ToolDisplayModeConfig): Promise<void> {
123
+ const configPath = getConfigPath();
124
+ await mkdir(path.dirname(configPath), { recursive: true });
125
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
126
+ }
127
+
128
+ // --- Tool definitions ---
129
+
130
+ function getToolDefinition(
131
+ cache: Map<string, Partial<Record<ToolName, AnyToolDefinition>>>,
132
+ name: ToolName,
133
+ cwd: string,
134
+ ): AnyToolDefinition {
135
+ let tools = cache.get(cwd);
136
+ if (!tools) {
137
+ tools = {};
138
+ cache.set(cwd, tools);
139
+ }
140
+
141
+ return (tools[name] ??= TOOL_FACTORIES[name](cwd));
142
+ }
143
+
144
+ function shouldRegisterToolRenderer(tools: ToolInfo[], name: ToolName): boolean {
145
+ const existingTool = tools.find((tool) => tool.name === name);
146
+ return existingTool?.sourceInfo.source === "builtin";
147
+ }
148
+
149
+ function createToolDisplayDefinition(options: {
150
+ cache: Map<string, Partial<Record<ToolName, AnyToolDefinition>>>;
151
+ name: ToolName;
152
+ getMode: () => Mode;
153
+ }): AnyToolDefinition {
154
+ const { cache, name, getMode } = options;
155
+ const base = getToolDefinition(cache, name, process.cwd());
156
+
157
+ return {
158
+ ...base,
159
+
160
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
161
+ return getToolDefinition(cache, name, ctx.cwd).execute(
162
+ toolCallId,
163
+ params,
164
+ signal,
165
+ onUpdate,
166
+ ctx,
167
+ );
168
+ },
169
+
170
+ renderCall(args, theme, context) {
171
+ const renderer = getToolDefinition(cache, name, context.cwd).renderCall;
172
+ return renderer?.(args, theme, { ...context, lastComponent: undefined }) ?? emptyComponent();
173
+ },
174
+
175
+ renderResult(result, options, theme, context) {
176
+ const mode = getMode();
177
+ const renderer = getToolDefinition(cache, name, context.cwd).renderResult;
178
+
179
+ if (mode === "minimal") {
180
+ if (name === "bash") {
181
+ renderer?.(result, options, theme, { ...context, lastComponent: undefined });
182
+ }
183
+
184
+ return renderMinimalResult(name, result, options, theme, context);
185
+ }
186
+
187
+ const expanded = mode === "expanded";
188
+ return (
189
+ renderer?.(result, { ...options, expanded }, theme, {
190
+ ...context,
191
+ expanded,
192
+ lastComponent: undefined,
193
+ }) ?? emptyComponent()
194
+ );
195
+ },
196
+ };
197
+ }
198
+
199
+ // --- Display mode ---
200
+
201
+ function nextMode(currentMode: Mode): Mode {
202
+ const index = MODES.indexOf(currentMode);
203
+ return MODES[(index + 1) % MODES.length] ?? DEFAULT_MODE;
204
+ }
205
+
206
+ function applyMode(ctx: ExtensionContext, mode: Mode): void {
207
+ if (!ctx.hasUI) return;
208
+
209
+ ctx.ui.setToolsExpanded(mode === "expanded");
210
+ }
211
+
212
+ function showModeChange(ctx: ExtensionContext, mode: Mode): void {
213
+ if (!ctx.hasUI) return;
214
+
215
+ ctx.ui.notify(`Tool output: ${mode}`, "info");
216
+ }
217
+
218
+ function reportSaveError(ctx: ExtensionContext, error: unknown): void {
219
+ const message = error instanceof Error ? error.message : String(error);
220
+ if (ctx.hasUI) {
221
+ ctx.ui.notify(`Failed to save ${getConfigPath()}: ${message}`, "error");
222
+ return;
223
+ }
224
+
225
+ console.warn(`[tool-display-mode] Failed to save config: ${message}`);
226
+ }
227
+
228
+ // --- Minimal rendering ---
229
+
230
+ function renderMinimalResult(
231
+ name: ToolName,
232
+ result: AgentToolResult<any>,
233
+ options: ToolRenderResultOptions,
234
+ theme: Theme,
235
+ context: AnyToolRenderContext,
236
+ ): Component {
237
+ if (context.isError) {
238
+ return renderMinimalText(theme, "error", lastNonEmptyLine(textOutput(result)) ?? "error");
239
+ }
240
+
241
+ if (options.isPartial) {
242
+ return renderMinimalText(theme, "muted", "running...");
243
+ }
244
+
245
+ let summary: string;
246
+ switch (name) {
247
+ case "bash":
248
+ summary = bashSummary(result);
249
+ break;
250
+ case "read":
251
+ summary = readSummary(result);
252
+ break;
253
+ case "grep":
254
+ summary = grepSummary(result);
255
+ break;
256
+ case "find":
257
+ summary = resultsSummary(result, "path");
258
+ break;
259
+ case "ls":
260
+ summary = resultsSummary(result, "entry");
261
+ break;
262
+ }
263
+
264
+ return renderMinimalText(theme, "muted", `↳ ${summary}`);
265
+ }
266
+
267
+ function renderMinimalText(theme: Theme, color: "error" | "muted", text: string): Text {
268
+ return new Text(theme.fg(color, text), 0, 0);
269
+ }
270
+
271
+ function bashSummary(result: AgentToolResult<any>): string {
272
+ const text = stripTrailingNotice(textOutput(result)).trim();
273
+ if (!text || text === "(no output)") return "no output";
274
+
275
+ const lines = countLines(text);
276
+ return `${lines} ${plural(lines, "line")}`;
277
+ }
278
+
279
+ function readSummary(result: AgentToolResult<any>): string {
280
+ if (result.content.some((content) => content.type === "image")) {
281
+ return "image";
282
+ }
283
+
284
+ const details = result.details as { truncation?: { outputLines?: number } } | undefined;
285
+ const lines =
286
+ details?.truncation?.outputLines ?? countLines(stripTrailingNotice(textOutput(result)));
287
+
288
+ return `${lines} ${plural(lines, "line")}`;
289
+ }
290
+
291
+ function grepSummary(result: AgentToolResult<any>): string {
292
+ const text = stripTrailingNotice(textOutput(result)).trim();
293
+ if (!text || text === GREP_NO_MATCHES_OUTPUT) {
294
+ return "0 lines";
295
+ }
296
+
297
+ const lines = text.split("\n").filter(Boolean);
298
+ const matchLines = lines.filter((line) => /:\d+: /.test(line));
299
+ const count = matchLines.length > 0 ? matchLines.length : lines.length;
300
+ return `${count} ${plural(count, "line")}`;
301
+ }
302
+
303
+ function resultsSummary(result: AgentToolResult<any>, noun: string): string {
304
+ const text = stripTrailingNotice(textOutput(result)).trim();
305
+ if (!text || text === FIND_NO_MATCHES_OUTPUT || text === LS_EMPTY_DIRECTORY_OUTPUT) {
306
+ return `0 ${plural(0, noun)}`;
307
+ }
308
+
309
+ const count = text.split("\n").filter(Boolean).length;
310
+ return `${count} ${plural(count, noun)}`;
311
+ }
312
+
313
+ function textOutput(result: AgentToolResult<any>): string {
314
+ return result.content
315
+ .filter((content) => content.type === "text")
316
+ .map((content) => content.text ?? "")
317
+ .join("\n");
318
+ }
319
+
320
+ function stripTrailingNotice(text: string): string {
321
+ return text.replace(/\n\n\[[\s\S]*\]$/, "");
322
+ }
323
+
324
+ function countLines(text: string): number {
325
+ if (text.length === 0) return 0;
326
+
327
+ const lines = text.split("\n");
328
+ if (lines.at(-1) === "") lines.pop();
329
+ return lines.length;
330
+ }
331
+
332
+ function lastNonEmptyLine(text: string): string | undefined {
333
+ return text
334
+ .split("\n")
335
+ .map((line) => line.trim())
336
+ .filter(Boolean)
337
+ .at(-1);
338
+ }
339
+
340
+ function plural(count: number, singular: string): string {
341
+ return count === 1 ? singular : `${singular}s`;
342
+ }
343
+
344
+ function emptyComponent(): Container {
345
+ return new Container();
346
+ }
347
+
348
+ // --- Editor ---
349
+
350
+ class ToolDisplayEditor implements EditorComponent, Focusable {
351
+ readonly actionHandlers: Map<AppKeybinding, () => void>;
352
+ private readonly customBase: CustomEditorLike;
353
+ private fallbackFocused = false;
354
+ private fallbackOnEscape?: () => void;
355
+ private fallbackOnCtrlD?: () => void;
356
+ private fallbackOnPasteImage?: () => void;
357
+ private fallbackOnExtensionShortcut?: (data: string) => boolean;
358
+
359
+ constructor(
360
+ private readonly base: EditorComponent,
361
+ private readonly appKeybindings: KeybindingsManager,
362
+ private readonly cycleMode: () => void,
363
+ ) {
364
+ this.customBase = base as CustomEditorLike;
365
+ this.actionHandlers = this.customBase.actionHandlers ?? new Map();
366
+ }
367
+
368
+ get focused(): boolean {
369
+ return this.customBase.focused ?? this.fallbackFocused;
370
+ }
371
+
372
+ set focused(value: boolean) {
373
+ this.fallbackFocused = value;
374
+ if ("focused" in this.customBase) this.customBase.focused = value;
375
+ }
376
+
377
+ get onSubmit(): ((text: string) => void) | undefined {
378
+ return this.base.onSubmit;
379
+ }
380
+
381
+ set onSubmit(handler: ((text: string) => void) | undefined) {
382
+ this.base.onSubmit = handler;
383
+ }
384
+
385
+ get onChange(): ((text: string) => void) | undefined {
386
+ return this.base.onChange;
387
+ }
388
+
389
+ set onChange(handler: ((text: string) => void) | undefined) {
390
+ this.base.onChange = handler;
391
+ }
392
+
393
+ get wantsKeyRelease(): boolean | undefined {
394
+ return this.base.wantsKeyRelease;
395
+ }
396
+
397
+ get borderColor(): ((str: string) => string) | undefined {
398
+ return this.base.borderColor;
399
+ }
400
+
401
+ set borderColor(handler: ((str: string) => string) | undefined) {
402
+ this.base.borderColor = handler;
403
+ }
404
+
405
+ get onEscape(): (() => void) | undefined {
406
+ return this.customBase.onEscape ?? this.fallbackOnEscape;
407
+ }
408
+
409
+ set onEscape(handler: (() => void) | undefined) {
410
+ this.fallbackOnEscape = handler;
411
+ if ("onEscape" in this.customBase) this.customBase.onEscape = handler;
412
+ }
413
+
414
+ get onCtrlD(): (() => void) | undefined {
415
+ return this.customBase.onCtrlD ?? this.fallbackOnCtrlD;
416
+ }
417
+
418
+ set onCtrlD(handler: (() => void) | undefined) {
419
+ this.fallbackOnCtrlD = handler;
420
+ if ("onCtrlD" in this.customBase) this.customBase.onCtrlD = handler;
421
+ }
422
+
423
+ get onPasteImage(): (() => void) | undefined {
424
+ return this.customBase.onPasteImage ?? this.fallbackOnPasteImage;
425
+ }
426
+
427
+ set onPasteImage(handler: (() => void) | undefined) {
428
+ this.fallbackOnPasteImage = handler;
429
+ if ("onPasteImage" in this.customBase) this.customBase.onPasteImage = handler;
430
+ }
431
+
432
+ get onExtensionShortcut(): ((data: string) => boolean) | undefined {
433
+ return this.customBase.onExtensionShortcut ?? this.fallbackOnExtensionShortcut;
434
+ }
435
+
436
+ set onExtensionShortcut(handler: ((data: string) => boolean) | undefined) {
437
+ this.fallbackOnExtensionShortcut = handler;
438
+ if ("onExtensionShortcut" in this.customBase) this.customBase.onExtensionShortcut = handler;
439
+ }
440
+
441
+ getText(): string {
442
+ return this.base.getText();
443
+ }
444
+
445
+ setText(text: string): void {
446
+ this.base.setText(text);
447
+ }
448
+
449
+ addToHistory(text: string): void {
450
+ this.base.addToHistory?.(text);
451
+ }
452
+
453
+ insertTextAtCursor(text: string): void {
454
+ this.base.insertTextAtCursor?.(text);
455
+ }
456
+
457
+ getExpandedText(): string {
458
+ return this.base.getExpandedText?.() ?? this.base.getText();
459
+ }
460
+
461
+ setAutocompleteProvider(provider: AutocompleteProvider): void {
462
+ this.base.setAutocompleteProvider?.(provider);
463
+ }
464
+
465
+ setPaddingX(padding: number): void {
466
+ this.base.setPaddingX?.(padding);
467
+ }
468
+
469
+ setAutocompleteMaxVisible(maxVisible: number): void {
470
+ this.base.setAutocompleteMaxVisible?.(maxVisible);
471
+ }
472
+
473
+ render(width: number): string[] {
474
+ return this.base.render(width);
475
+ }
476
+
477
+ invalidate(): void {
478
+ this.base.invalidate();
479
+ }
480
+
481
+ handleInput(data: string): void {
482
+ if (this.appKeybindings.matches(data, "app.tools.expand")) {
483
+ this.cycleMode();
484
+ return;
485
+ }
486
+
487
+ this.base.handleInput(data);
488
+ }
489
+ }
490
+
491
+ export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
492
+ let mode: Mode = DEFAULT_MODE;
493
+ let registeredToolRenderers = false;
494
+ let installedEditorFactory: EditorFactory | undefined;
495
+ let previousEditorFactory: EditorFactory | undefined;
496
+ const toolCache = new Map<string, Partial<Record<ToolName, AnyToolDefinition>>>();
497
+
498
+ const setMode = (ctx: ExtensionContext, next: Mode): void => {
499
+ mode = next;
500
+ applyMode(ctx, mode);
501
+ showModeChange(ctx, mode);
502
+
503
+ void saveConfig({ mode }).catch((error) => reportSaveError(ctx, error));
504
+ };
505
+
506
+ pi.on("session_start", async (_event, ctx) => {
507
+ mode = (await loadConfig()).mode;
508
+
509
+ if (!registeredToolRenderers) {
510
+ const tools = pi.getAllTools();
511
+ for (const name of Object.keys(TOOL_FACTORIES) as ToolName[]) {
512
+ if (!shouldRegisterToolRenderer(tools, name)) continue;
513
+
514
+ pi.registerTool(
515
+ createToolDisplayDefinition({ cache: toolCache, name, getMode: () => mode }),
516
+ );
517
+ }
518
+ registeredToolRenderers = true;
519
+ }
520
+
521
+ applyMode(ctx, mode);
522
+ if (!ctx.hasUI) return;
523
+
524
+ if (ctx.ui.getEditorComponent() === installedEditorFactory) return;
525
+
526
+ previousEditorFactory = ctx.ui.getEditorComponent();
527
+ installedEditorFactory = (tui, theme, keybindings) => {
528
+ const baseEditor =
529
+ previousEditorFactory?.(tui, theme, keybindings) ??
530
+ new CustomEditor(tui, theme, keybindings);
531
+ return new ToolDisplayEditor(baseEditor, keybindings, () => setMode(ctx, nextMode(mode)));
532
+ };
533
+ ctx.ui.setEditorComponent(installedEditorFactory);
534
+ });
535
+
536
+ pi.on("session_shutdown", async (_event, ctx) => {
537
+ if (ctx.hasUI && ctx.ui.getEditorComponent() === installedEditorFactory) {
538
+ ctx.ui.setEditorComponent(previousEditorFactory);
539
+ }
540
+
541
+ installedEditorFactory = undefined;
542
+ previousEditorFactory = undefined;
543
+ });
544
+ }