tau-coding-agent 0.1.1 → 0.1.3
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/README.md +54 -37
- package/extensions/sandbox/index.ts +50 -2
- package/extensions/tool-display-mode.ts +387 -0
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# tau-coding-agent
|
|
2
2
|
|
|
3
|
-
Coding
|
|
3
|
+
Coding agent built on top of [Pi](https://pi.dev).
|
|
4
|
+
|
|
5
|
+
Tau turns Pi's minimal core into an opinionated, polished coding-agent setup with web search, code review workflows, sandboxing, local memory, usage reporting, and a curated skill set for day-to-day software work.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -8,39 +10,54 @@ Coding-focused Tau package for [Pi](https://pi.dev).
|
|
|
8
10
|
pi install npm:tau-coding-agent
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- `
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
13
|
+
Project-local install lets a repository pin Tau for everyone working on it:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install -l npm:tau-coding-agent
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Extensions
|
|
20
|
+
|
|
21
|
+
| Extension | Command | Description |
|
|
22
|
+
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `answer` | `/answer` | Extract and interactively answer agent questions. |
|
|
24
|
+
| `branch-term` | `/branch` | Open a new terminal on the current session's git branch. |
|
|
25
|
+
| `btw` | `/btw` | Run a one-off side request with read-only tools and no context persistence. |
|
|
26
|
+
| `ghostty` | — | Ghostty tab title enhancements while the agent is working, waiting, or idle. |
|
|
27
|
+
| `git-diff-stats` | — | Status bar diff stats for local changes in the current repo. |
|
|
28
|
+
| `git-pr-status` | — | Status bar PR number and link for the current branch. |
|
|
29
|
+
| `insights` | `/insights` | Analyze Pi sessions and suggest reusable instructions, templates, skills, and extensions. |
|
|
30
|
+
| `interlude` | `alt+x` | Stash the current message draft, send one interlude message, then restore the draft. |
|
|
31
|
+
| `loop` | `/loop` | Repeat a prompt until the agent signals success. |
|
|
32
|
+
| `memory` | `/memory` | Opt-in project-local memory for learning and continuity across sessions. |
|
|
33
|
+
| `notify` | — | Terminal notification when the agent is waiting for input. |
|
|
34
|
+
| `openai-fast` | `/fast` | Toggle priority service tier for supported OpenAI models. |
|
|
35
|
+
| `openai-verbosity` | `/verbosity` | Set verbosity for supported OpenAI models. |
|
|
36
|
+
| `review` | `/review`, `/triage` | Multi-focus review and PR feedback triage for PRs, branches, commits, and local changes, with integrated follow-up fixes. |
|
|
37
|
+
| `sandbox` | `/sandbox` | OS-level sandboxing for bash commands with runtime overrides. |
|
|
38
|
+
| `tool-display-mode` | `ctrl+o` | Cycle tool output between Pi's default rendering, expanded output, and compact summaries. |
|
|
39
|
+
| `usage` | `/usage` | Historical provider usage breakdown with all-provider history and live quota snapshots. |
|
|
40
|
+
| `websearch` | — | Web search via Gemini, OpenAI, or Claude, leveraging Pi or browser session credentials. |
|
|
41
|
+
| `worktree` | `/worktree` | Create, list, and archive git worktrees, optionally opening them in a new terminal or tmux pane. |
|
|
42
|
+
|
|
43
|
+
## Skills
|
|
44
|
+
|
|
45
|
+
| Skill | Description |
|
|
46
|
+
| ------------------- | ---------------------------------------------------------------------------------- |
|
|
47
|
+
| `browser-tools` | Interactive browser automation via Chrome DevTools Protocol. |
|
|
48
|
+
| `git-clean-history` | Reimplement a branch on a fresh branch off `main` with a clean commit history. |
|
|
49
|
+
| `git-commit` | Tidy, focused commits with clear rationale in messages. |
|
|
50
|
+
| `oracle` | Second opinion from another LLM for debugging, refactors, design, or code reviews. |
|
|
51
|
+
| `sentry` | Fetch and analyze Sentry issues, events, transactions, and logs. |
|
|
52
|
+
| `update-changelog` | Update CHANGELOG.md following Keep a Changelog. |
|
|
53
|
+
| `web-design` | Distinctive, production-ready web interfaces. |
|
|
54
|
+
|
|
55
|
+
## Themes
|
|
56
|
+
|
|
57
|
+
| Theme | Description |
|
|
58
|
+
| ---------- | ------------------------------------------------------------- |
|
|
59
|
+
| `tau-dark` | Pi's official dark theme with a calmer, more cohesive polish. |
|
|
60
|
+
|
|
61
|
+
## Agent configuration
|
|
62
|
+
|
|
63
|
+
Tau does not include agent configuration; those files are highly personal. Configure Pi with your own `AGENTS.md`, `settings.json`, sandbox config, and model preferences.
|
|
@@ -74,24 +74,68 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
74
74
|
mode: DEFAULT_PROMPT_MODE,
|
|
75
75
|
network: {
|
|
76
76
|
allowedDomains: [
|
|
77
|
+
"localhost",
|
|
78
|
+
"127.0.0.1",
|
|
77
79
|
"npmjs.org",
|
|
78
80
|
"*.npmjs.org",
|
|
79
81
|
"registry.npmjs.org",
|
|
80
82
|
"registry.yarnpkg.com",
|
|
81
83
|
"pypi.org",
|
|
82
84
|
"*.pypi.org",
|
|
85
|
+
"pythonhosted.org",
|
|
86
|
+
"*.pythonhosted.org",
|
|
87
|
+
"crates.io",
|
|
88
|
+
"*.crates.io",
|
|
89
|
+
"proxy.golang.org",
|
|
90
|
+
"sum.golang.org",
|
|
91
|
+
"go.dev",
|
|
92
|
+
"golang.org",
|
|
93
|
+
"rubygems.org",
|
|
94
|
+
"*.rubygems.org",
|
|
83
95
|
"github.com",
|
|
84
96
|
"*.github.com",
|
|
97
|
+
"githubusercontent.com",
|
|
98
|
+
"*.githubusercontent.com",
|
|
85
99
|
"api.github.com",
|
|
86
100
|
"raw.githubusercontent.com",
|
|
101
|
+
"ghcr.io",
|
|
102
|
+
"docker.io",
|
|
103
|
+
"*.docker.io",
|
|
104
|
+
"production.cloudflare.docker.com",
|
|
105
|
+
"sentry.io",
|
|
106
|
+
"*.sentry.io",
|
|
107
|
+
"datadoghq.com",
|
|
108
|
+
"*.datadoghq.com",
|
|
109
|
+
"datadoghq.eu",
|
|
110
|
+
"*.datadoghq.eu",
|
|
111
|
+
"anthropic.com",
|
|
112
|
+
"*.anthropic.com",
|
|
113
|
+
"claude.ai",
|
|
114
|
+
"*.claude.ai",
|
|
115
|
+
"openai.com",
|
|
116
|
+
"*.openai.com",
|
|
117
|
+
"chatgpt.com",
|
|
118
|
+
"*.chatgpt.com",
|
|
119
|
+
"google.com",
|
|
120
|
+
"*.google.com",
|
|
121
|
+
"googleapis.com",
|
|
122
|
+
"*.googleapis.com",
|
|
123
|
+
"todoist.com",
|
|
124
|
+
"*.todoist.com",
|
|
125
|
+
"twist.com",
|
|
126
|
+
"*.twist.com",
|
|
127
|
+
"doist.com",
|
|
128
|
+
"*.doist.com",
|
|
87
129
|
],
|
|
88
130
|
deniedDomains: [],
|
|
131
|
+
allowLocalBinding: true,
|
|
89
132
|
},
|
|
90
133
|
filesystem: {
|
|
91
134
|
denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
|
|
92
|
-
allowWrite: [".", "/tmp"],
|
|
135
|
+
allowWrite: [".", "/tmp", "/private/tmp", "~/.cache", "~/.npm"],
|
|
93
136
|
denyWrite: [".env", ".env.*", "*.pem", "*.key"],
|
|
94
|
-
|
|
137
|
+
allowGitConfig: true,
|
|
138
|
+
allowGitCommonDir: true,
|
|
95
139
|
},
|
|
96
140
|
};
|
|
97
141
|
|
|
@@ -2455,11 +2499,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
2455
2499
|
" Network:",
|
|
2456
2500
|
` Allowed: ${runtimeConfig.network.allowedDomains.join(", ") || "(none)"}`,
|
|
2457
2501
|
` Denied: ${runtimeConfig.network.deniedDomains.join(", ") || "(none)"}`,
|
|
2502
|
+
` allowLocalBinding: ${runtimeConfig.network.allowLocalBinding ? "true" : "false"}`,
|
|
2503
|
+
` allowAllUnixSockets: ${runtimeConfig.network.allowAllUnixSockets ? "true" : "false"}`,
|
|
2504
|
+
` allowUnixSockets: ${runtimeConfig.network.allowUnixSockets?.join(", ") || "(none)"}`,
|
|
2458
2505
|
"",
|
|
2459
2506
|
" Filesystem:",
|
|
2460
2507
|
` Deny Read: ${runtimeConfig.filesystem.denyRead.join(", ") || "(none)"}`,
|
|
2461
2508
|
` Allow Write: ${runtimeConfig.filesystem.allowWrite.join(", ") || "(none)"}`,
|
|
2462
2509
|
` Deny Write: ${runtimeConfig.filesystem.denyWrite.join(", ") || "(none)"}`,
|
|
2510
|
+
` allowGitConfig: ${runtimeConfig.filesystem.allowGitConfig ? "true" : "false"}`,
|
|
2463
2511
|
` allowGitCommonDir: ${sandboxConfig?.filesystem.allowGitCommonDir ? "true" : "false"}`,
|
|
2464
2512
|
"",
|
|
2465
2513
|
" Advanced:",
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
CustomEditor,
|
|
6
|
+
createBashToolDefinition,
|
|
7
|
+
createFindToolDefinition,
|
|
8
|
+
createGrepToolDefinition,
|
|
9
|
+
createLsToolDefinition,
|
|
10
|
+
createReadToolDefinition,
|
|
11
|
+
type AgentToolResult,
|
|
12
|
+
type ExtensionAPI,
|
|
13
|
+
type ExtensionContext,
|
|
14
|
+
type KeybindingsManager,
|
|
15
|
+
type Theme,
|
|
16
|
+
type ToolDefinition,
|
|
17
|
+
type ToolInfo,
|
|
18
|
+
type ToolRenderResultOptions,
|
|
19
|
+
} from "@mariozechner/pi-coding-agent";
|
|
20
|
+
import { Container, Text, type Component, type EditorTheme, type TUI } from "@mariozechner/pi-tui";
|
|
21
|
+
|
|
22
|
+
// --- Constants ---
|
|
23
|
+
|
|
24
|
+
const CONFIG_FILE = "tool-display-mode.json";
|
|
25
|
+
const MODES = ["default", "expanded", "minimal"] as const;
|
|
26
|
+
const DEFAULT_MODE = "default";
|
|
27
|
+
|
|
28
|
+
// These are current Pi built-in tool output messages, used only because grep/find/ls
|
|
29
|
+
// do not expose structured zero-result details yet.
|
|
30
|
+
const GREP_NO_MATCHES_OUTPUT = "No matches found"; // core/tools/grep.ts
|
|
31
|
+
const FIND_NO_MATCHES_OUTPUT = "No files found matching pattern"; // core/tools/find.ts
|
|
32
|
+
const LS_EMPTY_DIRECTORY_OUTPUT = "(empty directory)"; // core/tools/ls.ts
|
|
33
|
+
|
|
34
|
+
const TOOL_FACTORIES = {
|
|
35
|
+
read: createReadToolDefinition,
|
|
36
|
+
bash: createBashToolDefinition,
|
|
37
|
+
grep: createGrepToolDefinition,
|
|
38
|
+
find: createFindToolDefinition,
|
|
39
|
+
ls: createLsToolDefinition,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// --- Types ---
|
|
43
|
+
|
|
44
|
+
type Mode = (typeof MODES)[number];
|
|
45
|
+
type ToolName = keyof typeof TOOL_FACTORIES;
|
|
46
|
+
type AnyToolDefinition = ToolDefinition<any, any, any>;
|
|
47
|
+
type AnyToolRenderContext = Parameters<NonNullable<AnyToolDefinition["renderResult"]>>[3];
|
|
48
|
+
type JsonObject = Record<string, unknown>;
|
|
49
|
+
|
|
50
|
+
type ToolDisplayModeConfig = {
|
|
51
|
+
mode: Mode;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// --- Config ---
|
|
55
|
+
|
|
56
|
+
function emptyConfig(): ToolDisplayModeConfig {
|
|
57
|
+
return { mode: DEFAULT_MODE };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getConfigPath(): string {
|
|
61
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
|
|
62
|
+
return path.join(agentDir, CONFIG_FILE);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isObject(value: unknown): value is JsonObject {
|
|
66
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isMode(value: string): value is Mode {
|
|
70
|
+
return (MODES as readonly string[]).includes(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseMode(value: unknown): Mode | undefined {
|
|
74
|
+
if (typeof value !== "string") return undefined;
|
|
75
|
+
|
|
76
|
+
const normalized = value.trim().toLowerCase();
|
|
77
|
+
return isMode(normalized) ? normalized : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseConfig(value: unknown): ToolDisplayModeConfig {
|
|
81
|
+
if (!isObject(value)) return emptyConfig();
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
mode: parseMode(value.mode) ?? DEFAULT_MODE,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function loadConfig(): Promise<ToolDisplayModeConfig> {
|
|
89
|
+
try {
|
|
90
|
+
const raw = await readFile(getConfigPath(), "utf8");
|
|
91
|
+
return parseConfig(JSON.parse(raw) as unknown);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
94
|
+
if (code !== "ENOENT") {
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
console.warn(`[tool-display-mode] Failed to load config: ${message}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return emptyConfig();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function saveConfig(config: ToolDisplayModeConfig): Promise<void> {
|
|
104
|
+
const configPath = getConfigPath();
|
|
105
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
106
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Tool definitions ---
|
|
110
|
+
|
|
111
|
+
function getToolDefinition(
|
|
112
|
+
cache: Map<string, Partial<Record<ToolName, AnyToolDefinition>>>,
|
|
113
|
+
name: ToolName,
|
|
114
|
+
cwd: string,
|
|
115
|
+
): AnyToolDefinition {
|
|
116
|
+
let tools = cache.get(cwd);
|
|
117
|
+
if (!tools) {
|
|
118
|
+
tools = {};
|
|
119
|
+
cache.set(cwd, tools);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (tools[name] ??= TOOL_FACTORIES[name](cwd));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function shouldRegisterToolRenderer(tools: ToolInfo[], name: ToolName): boolean {
|
|
126
|
+
const existingTool = tools.find((tool) => tool.name === name);
|
|
127
|
+
return existingTool?.sourceInfo.source === "builtin";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createToolDisplayDefinition(options: {
|
|
131
|
+
cache: Map<string, Partial<Record<ToolName, AnyToolDefinition>>>;
|
|
132
|
+
name: ToolName;
|
|
133
|
+
getMode: () => Mode;
|
|
134
|
+
}): AnyToolDefinition {
|
|
135
|
+
const { cache, name, getMode } = options;
|
|
136
|
+
const base = getToolDefinition(cache, name, process.cwd());
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...base,
|
|
140
|
+
|
|
141
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
142
|
+
return getToolDefinition(cache, name, ctx.cwd).execute(
|
|
143
|
+
toolCallId,
|
|
144
|
+
params,
|
|
145
|
+
signal,
|
|
146
|
+
onUpdate,
|
|
147
|
+
ctx,
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
renderCall(args, theme, context) {
|
|
152
|
+
const renderer = getToolDefinition(cache, name, context.cwd).renderCall;
|
|
153
|
+
return renderer?.(args, theme, { ...context, lastComponent: undefined }) ?? emptyComponent();
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
renderResult(result, options, theme, context) {
|
|
157
|
+
const mode = getMode();
|
|
158
|
+
const renderer = getToolDefinition(cache, name, context.cwd).renderResult;
|
|
159
|
+
|
|
160
|
+
if (mode === "minimal") {
|
|
161
|
+
if (name === "bash") {
|
|
162
|
+
renderer?.(result, options, theme, { ...context, lastComponent: undefined });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return renderMinimalResult(name, result, options, theme, context);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const expanded = mode === "expanded";
|
|
169
|
+
return (
|
|
170
|
+
renderer?.(result, { ...options, expanded }, theme, {
|
|
171
|
+
...context,
|
|
172
|
+
expanded,
|
|
173
|
+
lastComponent: undefined,
|
|
174
|
+
}) ?? emptyComponent()
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Display mode ---
|
|
181
|
+
|
|
182
|
+
function nextMode(currentMode: Mode): Mode {
|
|
183
|
+
const index = MODES.indexOf(currentMode);
|
|
184
|
+
return MODES[(index + 1) % MODES.length] ?? DEFAULT_MODE;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function applyMode(ctx: ExtensionContext, mode: Mode): void {
|
|
188
|
+
if (!ctx.hasUI) return;
|
|
189
|
+
|
|
190
|
+
ctx.ui.setToolsExpanded(mode === "expanded");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function showModeChange(ctx: ExtensionContext, mode: Mode): void {
|
|
194
|
+
if (!ctx.hasUI) return;
|
|
195
|
+
|
|
196
|
+
ctx.ui.notify(`Tool output: ${mode}`, "info");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function reportSaveError(ctx: ExtensionContext, error: unknown): void {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
if (ctx.hasUI) {
|
|
202
|
+
ctx.ui.notify(`Failed to save ${getConfigPath()}: ${message}`, "error");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.warn(`[tool-display-mode] Failed to save config: ${message}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Minimal rendering ---
|
|
210
|
+
|
|
211
|
+
function renderMinimalResult(
|
|
212
|
+
name: ToolName,
|
|
213
|
+
result: AgentToolResult<any>,
|
|
214
|
+
options: ToolRenderResultOptions,
|
|
215
|
+
theme: Theme,
|
|
216
|
+
context: AnyToolRenderContext,
|
|
217
|
+
): Component {
|
|
218
|
+
if (context.isError) {
|
|
219
|
+
return renderMinimalText(theme, "error", lastNonEmptyLine(textOutput(result)) ?? "error");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (options.isPartial) {
|
|
223
|
+
return renderMinimalText(theme, "muted", "running...");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let summary: string;
|
|
227
|
+
switch (name) {
|
|
228
|
+
case "bash":
|
|
229
|
+
summary = bashSummary(result);
|
|
230
|
+
break;
|
|
231
|
+
case "read":
|
|
232
|
+
summary = readSummary(result);
|
|
233
|
+
break;
|
|
234
|
+
case "grep":
|
|
235
|
+
summary = grepSummary(result);
|
|
236
|
+
break;
|
|
237
|
+
case "find":
|
|
238
|
+
summary = resultsSummary(result, "path");
|
|
239
|
+
break;
|
|
240
|
+
case "ls":
|
|
241
|
+
summary = resultsSummary(result, "entry");
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return renderMinimalText(theme, "muted", `→ ${summary}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function renderMinimalText(theme: Theme, color: "error" | "muted", text: string): Text {
|
|
249
|
+
return new Text(theme.fg(color, `\n${text}`), 0, 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function bashSummary(result: AgentToolResult<any>): string {
|
|
253
|
+
const text = stripTrailingNotice(textOutput(result)).trim();
|
|
254
|
+
if (!text || text === "(no output)") return "no output";
|
|
255
|
+
|
|
256
|
+
const lines = countLines(text);
|
|
257
|
+
return `${lines} ${plural(lines, "line")}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readSummary(result: AgentToolResult<any>): string {
|
|
261
|
+
if (result.content.some((content) => content.type === "image")) {
|
|
262
|
+
return "image";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const details = result.details as { truncation?: { outputLines?: number } } | undefined;
|
|
266
|
+
const lines =
|
|
267
|
+
details?.truncation?.outputLines ?? countLines(stripTrailingNotice(textOutput(result)));
|
|
268
|
+
|
|
269
|
+
return `${lines} ${plural(lines, "line")}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function grepSummary(result: AgentToolResult<any>): string {
|
|
273
|
+
const text = stripTrailingNotice(textOutput(result)).trim();
|
|
274
|
+
if (!text || text === GREP_NO_MATCHES_OUTPUT) {
|
|
275
|
+
return "0 lines";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const lines = text.split("\n").filter(Boolean);
|
|
279
|
+
const matchLines = lines.filter((line) => /:\d+: /.test(line));
|
|
280
|
+
const count = matchLines.length > 0 ? matchLines.length : lines.length;
|
|
281
|
+
return `${count} ${plural(count, "line")}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function resultsSummary(result: AgentToolResult<any>, noun: string): string {
|
|
285
|
+
const text = stripTrailingNotice(textOutput(result)).trim();
|
|
286
|
+
if (!text || text === FIND_NO_MATCHES_OUTPUT || text === LS_EMPTY_DIRECTORY_OUTPUT) {
|
|
287
|
+
return `0 ${plural(0, noun)}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const count = text.split("\n").filter(Boolean).length;
|
|
291
|
+
return `${count} ${plural(count, noun)}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function textOutput(result: AgentToolResult<any>): string {
|
|
295
|
+
return result.content
|
|
296
|
+
.filter((content) => content.type === "text")
|
|
297
|
+
.map((content) => content.text ?? "")
|
|
298
|
+
.join("\n");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function stripTrailingNotice(text: string): string {
|
|
302
|
+
return text.replace(/\n\n\[[\s\S]*\]$/, "");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function countLines(text: string): number {
|
|
306
|
+
if (text.length === 0) return 0;
|
|
307
|
+
|
|
308
|
+
const lines = text.split("\n");
|
|
309
|
+
if (lines.at(-1) === "") lines.pop();
|
|
310
|
+
return lines.length;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function lastNonEmptyLine(text: string): string | undefined {
|
|
314
|
+
return text
|
|
315
|
+
.split("\n")
|
|
316
|
+
.map((line) => line.trim())
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.at(-1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function plural(count: number, singular: string): string {
|
|
322
|
+
return count === 1 ? singular : `${singular}s`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function emptyComponent(): Container {
|
|
326
|
+
return new Container();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// --- Editor ---
|
|
330
|
+
|
|
331
|
+
class ToolDisplayEditor extends CustomEditor {
|
|
332
|
+
constructor(
|
|
333
|
+
tui: TUI,
|
|
334
|
+
theme: EditorTheme,
|
|
335
|
+
private readonly appKeybindings: KeybindingsManager,
|
|
336
|
+
private readonly cycleMode: () => void,
|
|
337
|
+
) {
|
|
338
|
+
super(tui, theme, appKeybindings);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
override handleInput(data: string): void {
|
|
342
|
+
if (this.appKeybindings.matches(data, "app.tools.expand")) {
|
|
343
|
+
this.cycleMode();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
super.handleInput(data);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
|
|
352
|
+
let mode: Mode = DEFAULT_MODE;
|
|
353
|
+
let registeredToolRenderers = false;
|
|
354
|
+
const toolCache = new Map<string, Partial<Record<ToolName, AnyToolDefinition>>>();
|
|
355
|
+
|
|
356
|
+
const setMode = (ctx: ExtensionContext, next: Mode): void => {
|
|
357
|
+
mode = next;
|
|
358
|
+
applyMode(ctx, mode);
|
|
359
|
+
showModeChange(ctx, mode);
|
|
360
|
+
|
|
361
|
+
void saveConfig({ mode }).catch((error) => reportSaveError(ctx, error));
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
365
|
+
mode = (await loadConfig()).mode;
|
|
366
|
+
|
|
367
|
+
if (!registeredToolRenderers) {
|
|
368
|
+
const tools = pi.getAllTools();
|
|
369
|
+
for (const name of Object.keys(TOOL_FACTORIES) as ToolName[]) {
|
|
370
|
+
if (!shouldRegisterToolRenderer(tools, name)) continue;
|
|
371
|
+
|
|
372
|
+
pi.registerTool(
|
|
373
|
+
createToolDisplayDefinition({ cache: toolCache, name, getMode: () => mode }),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
registeredToolRenderers = true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
applyMode(ctx, mode);
|
|
380
|
+
if (!ctx.hasUI) return;
|
|
381
|
+
|
|
382
|
+
ctx.ui.setEditorComponent(
|
|
383
|
+
(tui, theme, keybindings) =>
|
|
384
|
+
new ToolDisplayEditor(tui, theme, keybindings, () => setMode(ctx, nextMode(mode))),
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tau-coding-agent",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Opinionated Pi package with web search, review workflows, sandboxing, memory, usage reporting, skills, and a theme",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"agent-tools",
|
|
7
|
+
"code-review",
|
|
8
|
+
"coding-agent",
|
|
9
|
+
"memory",
|
|
10
|
+
"pi",
|
|
6
11
|
"pi-extension",
|
|
7
12
|
"pi-package",
|
|
8
|
-
"pi-skill"
|
|
13
|
+
"pi-skill",
|
|
14
|
+
"pi-theme",
|
|
15
|
+
"sandbox",
|
|
16
|
+
"websearch"
|
|
9
17
|
],
|
|
18
|
+
"homepage": "https://github.com/goncalossilva/tau#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/goncalossilva/tau/issues"
|
|
21
|
+
},
|
|
10
22
|
"license": "MIT",
|
|
11
23
|
"repository": {
|
|
12
24
|
"type": "git",
|
|
@@ -49,6 +61,7 @@
|
|
|
49
61
|
"extensions/openai-verbosity.ts",
|
|
50
62
|
"extensions/review.ts",
|
|
51
63
|
"extensions/sandbox/index.ts",
|
|
64
|
+
"extensions/tool-display-mode.ts",
|
|
52
65
|
"extensions/usage/index.ts",
|
|
53
66
|
"extensions/websearch/index.ts",
|
|
54
67
|
"extensions/worktree.ts"
|