@shanepadgett/tau-agent 0.1.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.
- package/README.md +30 -0
- package/docs/extending-tau-agent.md +119 -0
- package/docs/subagents.md +57 -0
- package/docs/tui.md +107 -0
- package/extensions/appshot/README.md +11 -0
- package/extensions/appshot/capture.swift +191 -0
- package/extensions/appshot/index.ts +245 -0
- package/extensions/appshot/native-helper.ts +102 -0
- package/extensions/attention/README.md +23 -0
- package/extensions/attention/index.ts +71 -0
- package/extensions/auto-name/README.md +26 -0
- package/extensions/auto-name/index.ts +163 -0
- package/extensions/branch/README.md +22 -0
- package/extensions/branch/index.ts +145 -0
- package/extensions/branch/panel.ts +148 -0
- package/extensions/clear-screen/README.md +29 -0
- package/extensions/clear-screen/index.ts +75 -0
- package/extensions/commit/README.md +15 -0
- package/extensions/commit/commit-plan.ts +328 -0
- package/extensions/commit/git-change-set.ts +268 -0
- package/extensions/commit/index.ts +138 -0
- package/extensions/commit/review-ui.ts +745 -0
- package/extensions/explore/README.md +7 -0
- package/extensions/explore/autoread.ts +140 -0
- package/extensions/explore/find.ts +147 -0
- package/extensions/explore/grep.ts +749 -0
- package/extensions/explore/index.ts +17 -0
- package/extensions/explore/limits.ts +22 -0
- package/extensions/explore/ls.ts +113 -0
- package/extensions/explore/path-display.ts +41 -0
- package/extensions/explore/path-tree.ts +150 -0
- package/extensions/explore/read.ts +160 -0
- package/extensions/explore/result.ts +29 -0
- package/extensions/explore/traverse.ts +226 -0
- package/extensions/footer/README.md +23 -0
- package/extensions/footer/index.ts +534 -0
- package/extensions/footer/settings.ts +13 -0
- package/extensions/ideas/README.md +10 -0
- package/extensions/ideas/browser.ts +66 -0
- package/extensions/ideas/index.ts +31 -0
- package/extensions/ideas/store.ts +32 -0
- package/extensions/image-gen/README.md +11 -0
- package/extensions/image-gen/client.ts +186 -0
- package/extensions/image-gen/index.ts +152 -0
- package/extensions/manage-sessions/README.md +12 -0
- package/extensions/manage-sessions/index.ts +58 -0
- package/extensions/manage-sessions/manager-ui.ts +323 -0
- package/extensions/manage-sessions/sessions.ts +126 -0
- package/extensions/patch/README.md +48 -0
- package/extensions/patch/executor.ts +355 -0
- package/extensions/patch/index.ts +170 -0
- package/extensions/patch/matcher.ts +236 -0
- package/extensions/patch/parser.ts +347 -0
- package/extensions/patch/render.ts +247 -0
- package/extensions/patch/summary.ts +35 -0
- package/extensions/publish/README.md +11 -0
- package/extensions/publish/index.ts +225 -0
- package/extensions/qna/README.md +45 -0
- package/extensions/qna/additional-context-body.ts +51 -0
- package/extensions/qna/body-render.ts +6 -0
- package/extensions/qna/choice-question-body.ts +322 -0
- package/extensions/qna/index.ts +245 -0
- package/extensions/qna/inline-editor-row.ts +56 -0
- package/extensions/qna/input-question-body.ts +89 -0
- package/extensions/qna/model.ts +317 -0
- package/extensions/qna/panel.ts +240 -0
- package/extensions/qna/ui.ts +16 -0
- package/extensions/reference/README.md +47 -0
- package/extensions/reference/index.ts +76 -0
- package/extensions/reference/panel.ts +874 -0
- package/extensions/reference/settings.ts +29 -0
- package/extensions/run-summary/README.md +5 -0
- package/extensions/run-summary/index.ts +98 -0
- package/extensions/silent-command-runner/README.md +25 -0
- package/extensions/silent-command-runner/index.ts +468 -0
- package/extensions/silent-command-runner/settings.ts +65 -0
- package/extensions/soul/README.md +19 -0
- package/extensions/soul/index.ts +20 -0
- package/extensions/soul/prompt.ts +254 -0
- package/extensions/soul/settings.ts +13 -0
- package/extensions/stash/README.md +48 -0
- package/extensions/stash/browser.ts +51 -0
- package/extensions/stash/index.ts +55 -0
- package/extensions/stash/store.ts +36 -0
- package/extensions/subagent/README.md +34 -0
- package/extensions/subagent/agents/scout.md +85 -0
- package/extensions/subagent/agents/web-research.md +95 -0
- package/extensions/subagent/agents.ts +196 -0
- package/extensions/subagent/index.ts +214 -0
- package/extensions/subagent/render.ts +89 -0
- package/extensions/subagent/run.ts +325 -0
- package/extensions/tau/README.md +17 -0
- package/extensions/tau/index.ts +122 -0
- package/extensions/tau-help/README.md +3 -0
- package/extensions/tau-help/help.md +125 -0
- package/extensions/tau-help/index.ts +68 -0
- package/extensions/turn-budget/README.md +14 -0
- package/extensions/turn-budget/index.ts +150 -0
- package/extensions/turn-budget/settings.ts +35 -0
- package/extensions/web/README.md +13 -0
- package/extensions/web/codesearch.ts +79 -0
- package/extensions/web/exa.ts +101 -0
- package/extensions/web/html.ts +67 -0
- package/extensions/web/index.ts +13 -0
- package/extensions/web/limits.ts +11 -0
- package/extensions/web/tool-output.ts +54 -0
- package/extensions/web/webfetch.ts +177 -0
- package/extensions/web/websearch.ts +100 -0
- package/package.json +57 -0
- package/prompts/.gitkeep +1 -0
- package/prompts/cavemanify.md +25 -0
- package/prompts/implement.md +25 -0
- package/prompts/interview.md +48 -0
- package/prompts/plan-feature.md +30 -0
- package/prompts/plan-implementation.md +50 -0
- package/schemas/tau.schema.json +205 -0
- package/shared/agent-blocked.ts +12 -0
- package/shared/description.ts +28 -0
- package/shared/events.ts +135 -0
- package/shared/git.ts +41 -0
- package/shared/injected-context.ts +69 -0
- package/shared/jsonl-store.ts +108 -0
- package/shared/model-fallback/index.ts +278 -0
- package/shared/model-fallback/settings.ts +18 -0
- package/shared/model-fallback/types.ts +8 -0
- package/shared/ranges.ts +21 -0
- package/shared/settings/define.ts +15 -0
- package/shared/settings/files.ts +20 -0
- package/shared/settings/json.ts +41 -0
- package/shared/settings/load.ts +59 -0
- package/shared/settings/merge.ts +21 -0
- package/shared/settings/paths.ts +41 -0
- package/shared/settings/schema.ts +25 -0
- package/shared/settings/specs.ts +23 -0
- package/shared/text.ts +11 -0
- package/shared/tool-row-state.ts +43 -0
- package/skills/writing-preferences/SKILL.md +136 -0
- package/themes/.gitkeep +1 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
4
|
+
import { type ApplyPatchSummary, deriveStats } from "./executor.ts";
|
|
5
|
+
|
|
6
|
+
// renderCall owns only the pre-execution streaming preview. Once execution starts, pi stacks
|
|
7
|
+
// renderResult below renderCall in the same container, so yielding here avoids a duplicated op list.
|
|
8
|
+
// renderResult receives partial summaries via result.details on each onUpdate, so the live indicator
|
|
9
|
+
// walk lives there natively — no progress stash needed.
|
|
10
|
+
|
|
11
|
+
interface PreviewOp {
|
|
12
|
+
sectionIndex: number;
|
|
13
|
+
kind: "add" | "replace" | "update" | "delete";
|
|
14
|
+
path: string;
|
|
15
|
+
moveTo?: string;
|
|
16
|
+
linesAdded: number;
|
|
17
|
+
linesRemoved: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const END_PATCH_MARKER = "*** End Patch";
|
|
21
|
+
const ADD_FILE_MARKER = "*** Add File: ";
|
|
22
|
+
const REPLACE_FILE_MARKER = "*** Replace File: ";
|
|
23
|
+
const DELETE_FILE_MARKER = "*** Delete File: ";
|
|
24
|
+
const UPDATE_FILE_MARKER = "*** Update File: ";
|
|
25
|
+
const MOVE_TO_MARKER = "*** Move to: ";
|
|
26
|
+
|
|
27
|
+
function topLevelDirective(line: string): string {
|
|
28
|
+
return line.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateDirective(line: string): string {
|
|
32
|
+
return line.trimEnd();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pathAfter(value: string, prefix: string): string | undefined {
|
|
36
|
+
if (!value.startsWith(prefix)) return undefined;
|
|
37
|
+
const path = value.slice(prefix.length).trim();
|
|
38
|
+
return path.length > 0 ? path : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function startPreviewOp(ops: PreviewOp[], kind: PreviewOp["kind"], path: string): PreviewOp {
|
|
42
|
+
const op = { sectionIndex: ops.length + 1, kind, path, linesAdded: 0, linesRemoved: 0 };
|
|
43
|
+
ops.push(op);
|
|
44
|
+
return op;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Tolerant preview scan — never throws, display-oriented (partial patches during streaming).
|
|
48
|
+
function scanPreview(input: string | undefined): PreviewOp[] {
|
|
49
|
+
if (typeof input !== "string" || !input) return [];
|
|
50
|
+
const lines = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
51
|
+
const ops: PreviewOp[] = [];
|
|
52
|
+
let current: PreviewOp | undefined;
|
|
53
|
+
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const value = current?.kind === "update" ? updateDirective(line) : topLevelDirective(line);
|
|
56
|
+
if (value === END_PATCH_MARKER) break;
|
|
57
|
+
|
|
58
|
+
const addPath = pathAfter(value, ADD_FILE_MARKER);
|
|
59
|
+
if (addPath !== undefined) {
|
|
60
|
+
current = startPreviewOp(ops, "add", addPath);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const replacePath = pathAfter(value, REPLACE_FILE_MARKER);
|
|
65
|
+
if (replacePath !== undefined) {
|
|
66
|
+
current = startPreviewOp(ops, "replace", replacePath);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const deletePath = pathAfter(value, DELETE_FILE_MARKER);
|
|
71
|
+
if (deletePath !== undefined) {
|
|
72
|
+
current = startPreviewOp(ops, "delete", deletePath);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const updatePath = pathAfter(value, UPDATE_FILE_MARKER);
|
|
77
|
+
if (updatePath !== undefined) {
|
|
78
|
+
current = startPreviewOp(ops, "update", updatePath);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!current) continue;
|
|
83
|
+
|
|
84
|
+
if (current.kind === "update") {
|
|
85
|
+
const movePath = pathAfter(updateDirective(line), MOVE_TO_MARKER);
|
|
86
|
+
if (movePath !== undefined) {
|
|
87
|
+
current.moveTo = movePath;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (line.startsWith("+")) current.linesAdded += 1;
|
|
91
|
+
else if (line.startsWith("-")) current.linesRemoved += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (current.kind === "add" || current.kind === "replace") {
|
|
96
|
+
if (line.startsWith("+")) current.linesAdded += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return ops;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function statsBadge(summary: ApplyPatchSummary | undefined, preview: PreviewOp[], theme: Theme): string {
|
|
104
|
+
let fileCount: number;
|
|
105
|
+
let linesAdded: number;
|
|
106
|
+
let linesRemoved: number;
|
|
107
|
+
let moves: number;
|
|
108
|
+
|
|
109
|
+
if (summary && summary.changes.length > 0) {
|
|
110
|
+
const s = deriveStats(summary);
|
|
111
|
+
fileCount = new Set(summary.changes.map((c) => c.move?.to ?? c.path)).size;
|
|
112
|
+
linesAdded = s.linesAdded;
|
|
113
|
+
linesRemoved = s.linesRemoved;
|
|
114
|
+
moves = s.moved.length;
|
|
115
|
+
} else {
|
|
116
|
+
fileCount = new Set(preview.map((op) => op.moveTo ?? op.path)).size;
|
|
117
|
+
linesAdded = preview.reduce((sum, op) => sum + op.linesAdded, 0);
|
|
118
|
+
linesRemoved = preview.reduce((sum, op) => sum + op.linesRemoved, 0);
|
|
119
|
+
moves = preview.filter((op) => op.moveTo).length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const stats = [
|
|
123
|
+
linesAdded > 0 ? `+${linesAdded}` : "",
|
|
124
|
+
linesRemoved > 0 ? `-${linesRemoved}` : "",
|
|
125
|
+
moves > 0 ? `>${moves}` : "",
|
|
126
|
+
]
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join(" ");
|
|
129
|
+
|
|
130
|
+
const badge = stats
|
|
131
|
+
? `${fileCount} file${fileCount === 1 ? "" : "s"} · ${stats}`
|
|
132
|
+
: `${fileCount} file${fileCount === 1 ? "" : "s"}`;
|
|
133
|
+
return theme.fg("accent", badge);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function opLabel(op: PreviewOp): string {
|
|
137
|
+
if (op.moveTo) return `Move ${op.path} → ${op.moveTo}`;
|
|
138
|
+
if (op.kind === "add") return `Add ${op.path}`;
|
|
139
|
+
if (op.kind === "replace") return `Replace ${op.path}`;
|
|
140
|
+
if (op.kind === "delete") return `Delete ${op.path}`;
|
|
141
|
+
return `Edit ${op.path}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type OpStatus = "pending" | "applied" | "failed";
|
|
145
|
+
|
|
146
|
+
function deriveStatuses(summary: ApplyPatchSummary): Map<number, OpStatus> {
|
|
147
|
+
const map = new Map<number, OpStatus>();
|
|
148
|
+
for (const change of summary.changes) map.set(change.sectionIndex, "applied");
|
|
149
|
+
for (const failure of summary.failures) {
|
|
150
|
+
if (failure.sectionIndex > 0) map.set(failure.sectionIndex, "failed");
|
|
151
|
+
}
|
|
152
|
+
return map;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderOpLine(op: PreviewOp, status: OpStatus | undefined, theme: Theme): string {
|
|
156
|
+
const label = `${theme.fg("muted", " ")}${theme.fg("text", opLabel(op))}`;
|
|
157
|
+
const ind =
|
|
158
|
+
status === "applied"
|
|
159
|
+
? theme.fg("success", "✓")
|
|
160
|
+
: status === "failed"
|
|
161
|
+
? theme.fg("error", "!")
|
|
162
|
+
: theme.fg("muted", "…");
|
|
163
|
+
return ind ? `${label} ${ind}` : label;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface RenderCallContext {
|
|
167
|
+
expanded: boolean;
|
|
168
|
+
executionStarted: boolean;
|
|
169
|
+
isPartial: boolean;
|
|
170
|
+
lastComponent?: unknown;
|
|
171
|
+
rowState: ToolRowStateStore;
|
|
172
|
+
rowId: string;
|
|
173
|
+
invalidate: () => void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function patchTitle(theme: Theme, context: Pick<RenderCallContext, "rowState" | "rowId" | "invalidate">): string {
|
|
177
|
+
context.rowState.watch(context.rowId, context.invalidate);
|
|
178
|
+
return formatToolRowTitle(context.rowState, context.rowId, "patch", theme);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function textComponent(lastComponent: unknown): Text {
|
|
182
|
+
return (lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function renderPatchCall(args: { input?: string } | undefined, theme: Theme, context: RenderCallContext): Text {
|
|
186
|
+
const text = textComponent(context.lastComponent);
|
|
187
|
+
// Once execution starts, renderResult owns the view (it gets the live summary). Yield to avoid
|
|
188
|
+
// a duplicated op list, since pi stacks both renderers in the same container.
|
|
189
|
+
if (context.executionStarted || !context.isPartial) {
|
|
190
|
+
text.setText("");
|
|
191
|
+
return text;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const input = args?.input;
|
|
195
|
+
const preview = scanPreview(input);
|
|
196
|
+
const header = `${patchTitle(theme, context)} ${statsBadge(undefined, preview, theme)}`;
|
|
197
|
+
|
|
198
|
+
if (context.expanded) {
|
|
199
|
+
const raw = typeof input === "string" ? input.replace(/\r\n/g, "\n").trim() : "";
|
|
200
|
+
text.setText(raw ? `${header}\n${theme.fg("muted", raw)}` : header);
|
|
201
|
+
return text;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (preview.length === 0) {
|
|
205
|
+
text.setText(header);
|
|
206
|
+
return text;
|
|
207
|
+
}
|
|
208
|
+
const lines = preview.map((op) => renderOpLine(op, "pending", theme));
|
|
209
|
+
text.setText([header, ...lines].join("\n"));
|
|
210
|
+
return text;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function renderPatchResult(
|
|
214
|
+
result: { details?: ApplyPatchSummary },
|
|
215
|
+
options: { expanded: boolean },
|
|
216
|
+
theme: Theme,
|
|
217
|
+
context: {
|
|
218
|
+
expanded: boolean;
|
|
219
|
+
args?: { input?: string };
|
|
220
|
+
lastComponent?: unknown;
|
|
221
|
+
rowState: ToolRowStateStore;
|
|
222
|
+
rowId: string;
|
|
223
|
+
invalidate: () => void;
|
|
224
|
+
},
|
|
225
|
+
): Text {
|
|
226
|
+
const text = textComponent(context.lastComponent);
|
|
227
|
+
const summary = result.details;
|
|
228
|
+
if (!summary) {
|
|
229
|
+
text.setText("");
|
|
230
|
+
return text;
|
|
231
|
+
}
|
|
232
|
+
const preview = scanPreview(context.args?.input);
|
|
233
|
+
const header = `${patchTitle(theme, context)} ${statsBadge(summary, preview, theme)}`;
|
|
234
|
+
const statuses = deriveStatuses(summary);
|
|
235
|
+
|
|
236
|
+
if (options.expanded) {
|
|
237
|
+
const raw = context.args?.input?.replace(/\r\n/g, "\n").trim() ?? "";
|
|
238
|
+
const sections = [header];
|
|
239
|
+
if (raw) sections.push(theme.fg("muted", raw));
|
|
240
|
+
text.setText(sections.join("\n"));
|
|
241
|
+
return text;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const lines = preview.map((op) => renderOpLine(op, statuses.get(op.sectionIndex), theme));
|
|
245
|
+
text.setText([header, ...lines].join("\n"));
|
|
246
|
+
return text;
|
|
247
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ApplyPatchSummary, deriveStats } from "./executor.ts";
|
|
2
|
+
|
|
3
|
+
export function formatPatchSummary(summary: ApplyPatchSummary): string {
|
|
4
|
+
const s = deriveStats(summary);
|
|
5
|
+
const lines: string[] = [];
|
|
6
|
+
if (summary.status === "failed") {
|
|
7
|
+
lines.push("No changes applied.");
|
|
8
|
+
} else {
|
|
9
|
+
const parts: string[] = [];
|
|
10
|
+
if (s.linesAdded > 0) parts.push(`+${s.linesAdded}`);
|
|
11
|
+
if (s.linesRemoved > 0) parts.push(`-${s.linesRemoved}`);
|
|
12
|
+
const badge = parts.length > 0 ? ` [${parts.join(" ")}]` : "";
|
|
13
|
+
lines.push(`Applied ${s.completedOperations}/${summary.totalSections} sections.${badge}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const path of s.added) lines.push(`A ${path}`);
|
|
17
|
+
for (const path of s.replaced) lines.push(`M ${path}`);
|
|
18
|
+
for (const path of s.updated) lines.push(`M ${path}`);
|
|
19
|
+
for (const path of s.deleted) lines.push(`D ${path}`);
|
|
20
|
+
for (const move of s.moved) lines.push(`R ${move.from} -> ${move.to}`);
|
|
21
|
+
|
|
22
|
+
if (summary.failures.length > 0) {
|
|
23
|
+
lines.push("Failures:");
|
|
24
|
+
for (const failure of summary.failures) {
|
|
25
|
+
const kind = failure.kind ? `${failure.kind} ` : "";
|
|
26
|
+
const path = failure.path ?? "";
|
|
27
|
+
const chunk =
|
|
28
|
+
failure.chunkIndex && failure.totalChunks ? ` chunk ${failure.chunkIndex}/${failure.totalChunks}` : "";
|
|
29
|
+
const ctx = failure.contextHint ? ` (context: "${failure.contextHint}")` : "";
|
|
30
|
+
lines.push(`- ${kind}${path}${chunk}: ${failure.message}${ctx}`.trim());
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Publish
|
|
2
|
+
|
|
3
|
+
Create a tagged Tau release and monitor its GitHub Actions npm publish run.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
/publish
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`/publish` requires a clean Git working tree and authenticated GitHub CLI. It recommends a semantic version bump from commits since the previous release tag, asks for confirmation, then pushes a release tag. GitHub Actions publishes the TUI package before the agent package through npm trusted publishing.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createGitRunner, type GitRunner, loadRepoStatus } from "../../shared/git.ts";
|
|
5
|
+
import { errorText } from "../../shared/text.ts";
|
|
6
|
+
|
|
7
|
+
const PACKAGE_PATHS = ["packages/tui/package.json", "packages/agent/package.json"] as const;
|
|
8
|
+
const LOCKFILE_PATH = "package-lock.json";
|
|
9
|
+
const WORKFLOW_NAME = "publish.yml";
|
|
10
|
+
const WORKFLOW_WAIT_MS = 10 * 60_000;
|
|
11
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
12
|
+
|
|
13
|
+
type Bump = "patch" | "minor" | "major";
|
|
14
|
+
|
|
15
|
+
interface PackageManifest {
|
|
16
|
+
name: string;
|
|
17
|
+
version: string;
|
|
18
|
+
dependencies?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface WorkflowRun {
|
|
22
|
+
databaseId: number;
|
|
23
|
+
status: string;
|
|
24
|
+
conclusion: string | null;
|
|
25
|
+
url: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function publishExtension(pi: ExtensionAPI): void {
|
|
29
|
+
pi.registerCommand("publish", {
|
|
30
|
+
description: "Create a tagged release and monitor trusted npm publishing",
|
|
31
|
+
handler: async (args, ctx) => {
|
|
32
|
+
await ctx.waitForIdle();
|
|
33
|
+
if (args.trim()) {
|
|
34
|
+
ctx.ui.notify("Usage: /publish", "error");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!ctx.hasUI) {
|
|
38
|
+
ctx.ui.notify("Publishing requires interactive confirmation.", "error");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
ctx.ui.setStatus("publish", "preparing release");
|
|
42
|
+
try {
|
|
43
|
+
await publish(pi, ctx);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
ctx.ui.notify(`Publish failed: ${errorText(error)}`, "error");
|
|
46
|
+
} finally {
|
|
47
|
+
ctx.ui.setStatus("publish", undefined);
|
|
48
|
+
ctx.ui.setWidget("publish", undefined);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function publish(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
|
55
|
+
const git = createGitRunner(pi, ctx);
|
|
56
|
+
const repo = await loadRepoStatus(git);
|
|
57
|
+
if (!repo) throw new Error("No git repository found.");
|
|
58
|
+
if (repo.fileCount > 0)
|
|
59
|
+
throw new Error("Working tree is not clean. Commit or stash every change before publishing.");
|
|
60
|
+
await run(pi, ctx, "gh", ["auth", "status"], repo.root);
|
|
61
|
+
|
|
62
|
+
const manifests = await loadManifests(repo.root);
|
|
63
|
+
const currentVersion = sharedVersion(manifests);
|
|
64
|
+
const tag = await latestReleaseTag(git, repo.root);
|
|
65
|
+
const bump = await recommendBump(git, repo.root, tag);
|
|
66
|
+
const version = tag ? incrementVersion(currentVersion, bump) : currentVersion;
|
|
67
|
+
const selectedBump = await ctx.ui.select(`Release ${version}`, [
|
|
68
|
+
`Use recommended ${bump} bump`,
|
|
69
|
+
"Choose a different bump",
|
|
70
|
+
"Cancel",
|
|
71
|
+
]);
|
|
72
|
+
if (!selectedBump || selectedBump === "Cancel") return;
|
|
73
|
+
|
|
74
|
+
const finalBump =
|
|
75
|
+
selectedBump === "Choose a different bump"
|
|
76
|
+
? await ctx.ui.select("Release type", ["patch", "minor", "major"])
|
|
77
|
+
: bump;
|
|
78
|
+
if (!finalBump) return;
|
|
79
|
+
const finalVersion = tag ? incrementVersion(currentVersion, finalBump as Bump) : currentVersion;
|
|
80
|
+
const releaseTag = `v${finalVersion}`;
|
|
81
|
+
if (await tagExists(git, repo.root, releaseTag)) throw new Error(`Tag ${releaseTag} already exists.`);
|
|
82
|
+
|
|
83
|
+
const confirmed = await ctx.ui.confirm(
|
|
84
|
+
`Publish ${releaseTag}?`,
|
|
85
|
+
`This will push ${releaseTag}. GitHub Actions will publish @shanepadgett/tau-tui and @shanepadgett/tau-agent to npm.`,
|
|
86
|
+
);
|
|
87
|
+
if (!confirmed) return;
|
|
88
|
+
|
|
89
|
+
if (tag) await writeReleaseVersion(repo.root, manifests, finalVersion);
|
|
90
|
+
ctx.ui.setStatus("publish", "packing packages");
|
|
91
|
+
await run(pi, ctx, "npm", ["pack", "--dry-run", "--workspace", "@shanepadgett/tau-tui"], repo.root);
|
|
92
|
+
await run(pi, ctx, "npm", ["pack", "--dry-run", "--workspace", "@shanepadgett/tau-agent"], repo.root);
|
|
93
|
+
|
|
94
|
+
if (tag) {
|
|
95
|
+
await git.run(["add", ...PACKAGE_PATHS, LOCKFILE_PATH], { cwd: repo.root });
|
|
96
|
+
await git.run(["commit", "-m", `chore(release): ${releaseTag}`], { cwd: repo.root });
|
|
97
|
+
}
|
|
98
|
+
await git.run(["tag", releaseTag], { cwd: repo.root });
|
|
99
|
+
await git.run(["push", "origin", "HEAD"], { cwd: repo.root, timeout: 120_000 });
|
|
100
|
+
await git.run(["push", "origin", releaseTag], { cwd: repo.root, timeout: 120_000 });
|
|
101
|
+
await monitorWorkflow(pi, ctx, repo.root, await git.run(["rev-parse", "HEAD"], { cwd: repo.root }), releaseTag);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function loadManifests(root: string): Promise<PackageManifest[]> {
|
|
105
|
+
return Promise.all(
|
|
106
|
+
PACKAGE_PATHS.map(async (path) => JSON.parse(await readFile(join(root, path), "utf8")) as PackageManifest),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sharedVersion(manifests: readonly PackageManifest[]): string {
|
|
111
|
+
const versions = new Set(manifests.map((manifest) => manifest.version));
|
|
112
|
+
if (versions.size !== 1) throw new Error("Publishable packages must use the same version.");
|
|
113
|
+
const version = manifests[0]?.version;
|
|
114
|
+
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) throw new Error("Package version must be stable semver.");
|
|
115
|
+
return version;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function latestReleaseTag(git: GitRunner, root: string): Promise<string | undefined> {
|
|
119
|
+
const tags = await git.run(["tag", "--list", "v*", "--sort=-version:refname"], { cwd: root });
|
|
120
|
+
return tags.split("\n").find((tag) => /^v\d+\.\d+\.\d+$/.test(tag));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function recommendBump(git: GitRunner, root: string, tag: string | undefined): Promise<Bump> {
|
|
124
|
+
const range = tag ? `${tag}..HEAD` : "HEAD";
|
|
125
|
+
const commits = await git.run(["log", "--format=%B%x00", range], { cwd: root });
|
|
126
|
+
if (/BREAKING CHANGE:|^[^\n]*!:/m.test(commits)) return "major";
|
|
127
|
+
if (/^feat(?:\([^\n]+\))?:/m.test(commits)) return "minor";
|
|
128
|
+
return "patch";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function incrementVersion(version: string, bump: Bump): string {
|
|
132
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
133
|
+
if (!match) throw new Error(`Invalid version: ${version}`);
|
|
134
|
+
const major = Number(match[1]);
|
|
135
|
+
const minor = Number(match[2]);
|
|
136
|
+
const patch = Number(match[3]);
|
|
137
|
+
if (bump === "major") return `${major + 1}.0.0`;
|
|
138
|
+
if (bump === "minor") return `${major}.${minor + 1}.0`;
|
|
139
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function tagExists(git: GitRunner, root: string, tag: string): Promise<boolean> {
|
|
143
|
+
return Boolean(await git.run(["tag", "--list", tag], { cwd: root }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function writeReleaseVersion(
|
|
147
|
+
root: string,
|
|
148
|
+
manifests: readonly PackageManifest[],
|
|
149
|
+
version: string,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
for (const [index, path] of PACKAGE_PATHS.entries()) {
|
|
152
|
+
const manifest = manifests[index];
|
|
153
|
+
if (!manifest) throw new Error(`Missing package manifest: ${path}`);
|
|
154
|
+
manifest.version = version;
|
|
155
|
+
if (manifest.name === "@shanepadgett/tau-agent" && manifest.dependencies)
|
|
156
|
+
manifest.dependencies["@shanepadgett/tau-tui"] = version;
|
|
157
|
+
await writeFile(join(root, path), `${JSON.stringify(manifest, null, "\t")}\n`);
|
|
158
|
+
}
|
|
159
|
+
const lockfile = JSON.parse(await readFile(join(root, LOCKFILE_PATH), "utf8")) as {
|
|
160
|
+
packages: Record<string, PackageManifest>;
|
|
161
|
+
};
|
|
162
|
+
for (const path of PACKAGE_PATHS) {
|
|
163
|
+
const manifest = lockfile.packages[path];
|
|
164
|
+
if (!manifest) throw new Error(`Missing lockfile package: ${path}`);
|
|
165
|
+
manifest.version = version;
|
|
166
|
+
if (manifest.name === "@shanepadgett/tau-agent" && manifest.dependencies)
|
|
167
|
+
manifest.dependencies["@shanepadgett/tau-tui"] = version;
|
|
168
|
+
}
|
|
169
|
+
await writeFile(join(root, LOCKFILE_PATH), `${JSON.stringify(lockfile, null, "\t")}\n`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function run(
|
|
173
|
+
pi: ExtensionAPI,
|
|
174
|
+
ctx: ExtensionCommandContext,
|
|
175
|
+
command: string,
|
|
176
|
+
args: string[],
|
|
177
|
+
cwd: string,
|
|
178
|
+
): Promise<string> {
|
|
179
|
+
const result = await pi.exec(command, args, { cwd, signal: ctx.signal, timeout: 120_000 });
|
|
180
|
+
if (result.code === 0) return result.stdout.trim();
|
|
181
|
+
throw new Error([result.stderr, result.stdout].filter(Boolean).join("\n").trim() || `${command} failed`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function monitorWorkflow(
|
|
185
|
+
pi: ExtensionAPI,
|
|
186
|
+
ctx: ExtensionCommandContext,
|
|
187
|
+
root: string,
|
|
188
|
+
sha: string,
|
|
189
|
+
tag: string,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
const deadline = Date.now() + WORKFLOW_WAIT_MS;
|
|
192
|
+
while (Date.now() < deadline) {
|
|
193
|
+
const output = await run(
|
|
194
|
+
pi,
|
|
195
|
+
ctx,
|
|
196
|
+
"gh",
|
|
197
|
+
[
|
|
198
|
+
"run",
|
|
199
|
+
"list",
|
|
200
|
+
"--workflow",
|
|
201
|
+
WORKFLOW_NAME,
|
|
202
|
+
"--commit",
|
|
203
|
+
sha,
|
|
204
|
+
"--limit",
|
|
205
|
+
"1",
|
|
206
|
+
"--json",
|
|
207
|
+
"databaseId,status,conclusion,url",
|
|
208
|
+
],
|
|
209
|
+
root,
|
|
210
|
+
);
|
|
211
|
+
const workflowRun = (JSON.parse(output) as WorkflowRun[])[0];
|
|
212
|
+
if (workflowRun) {
|
|
213
|
+
ctx.ui.setWidget("publish", [`Release ${tag}`, `GitHub Actions: ${workflowRun.status}`, workflowRun.url]);
|
|
214
|
+
if (workflowRun.status === "completed") {
|
|
215
|
+
if (workflowRun.conclusion === "success") {
|
|
216
|
+
ctx.ui.notify(`Published ${tag}: ${workflowRun.url}`, "info");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`GitHub Actions ${workflowRun.conclusion ?? "failed"}: ${workflowRun.url}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
await new Promise<void>((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`Timed out waiting for GitHub Actions for ${tag}. Check the Actions tab.`);
|
|
225
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# qna Extension
|
|
2
|
+
|
|
3
|
+
Structured question UI for blocked agent decisions.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
`/qna [context]` temporarily enables `ask_question` and sends a hidden instruction asking the agent to re-ask its last question with structured UI. Optional context is included as framing guidance for the question.
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
Registers `ask_question`, inactive by default. It is enabled for `/qna`, then disabled after `ask_question` returns or the agent turn ends.
|
|
12
|
+
|
|
13
|
+
Use only when missing user intent, preference, or constraint would materially change the next action. Supports:
|
|
14
|
+
|
|
15
|
+
- `select`: one option or custom answer
|
|
16
|
+
- `multi`: multiple options plus optional custom answer
|
|
17
|
+
- `input`: free-form typed answer
|
|
18
|
+
- `confirm`: fixed yes/no
|
|
19
|
+
|
|
20
|
+
Selectable questions require real options, recommendation values, and an honest recommendation reason. User can add notes to real options.
|
|
21
|
+
|
|
22
|
+
Every panel includes a final user-owned `Additional Context` tab. It is not part of the tool schema. Non-empty text returns as top-level `additionalContext`.
|
|
23
|
+
|
|
24
|
+
## UI
|
|
25
|
+
|
|
26
|
+
- Question tabs get a `•` suffix when answered. The final `Additional Context` tab gets `•` when it has text.
|
|
27
|
+
- The final `Additional Context` tab is always present and uses Pi's native single-line input.
|
|
28
|
+
- `select` and `confirm` render as pointer lists, with `✓` on the selected answer.
|
|
29
|
+
- `multi` renders checkboxes.
|
|
30
|
+
- `input` uses Pi's native single-line input.
|
|
31
|
+
- Custom answers edit inline: move to "Type your own answer..." and type.
|
|
32
|
+
- Notes render under option descriptions with `└─`.
|
|
33
|
+
- `tab`/`←→` moves between questions and additional context.
|
|
34
|
+
- `enter` advances through questions and submits from additional context. `alt+enter` submits anytime; unanswered questions are skipped.
|
|
35
|
+
- Results render as a Markdown-style numbered question/answer list, plus an additional context section when supplied.
|
|
36
|
+
|
|
37
|
+
## Layout
|
|
38
|
+
|
|
39
|
+
- `index.ts`: Pi registration, schema, prompt guidance, renderers
|
|
40
|
+
- `model.ts`: validation, normalized state, result building
|
|
41
|
+
- `ui.ts`: custom TUI component
|
|
42
|
+
|
|
43
|
+
## Skipped
|
|
44
|
+
|
|
45
|
+
No settings, replay, persistence, notifications, interview workflow, or preview panes. Add when usage proves need.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
type Focusable,
|
|
4
|
+
Input,
|
|
5
|
+
Key,
|
|
6
|
+
matchesKey,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
wrapTextWithAnsi,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import { type QnaState, saveAdditionalContext } from "./model.ts";
|
|
11
|
+
|
|
12
|
+
export class AdditionalContextBody implements Component, Focusable {
|
|
13
|
+
private readonly getState: () => QnaState;
|
|
14
|
+
private readonly setState: (state: QnaState) => void;
|
|
15
|
+
private readonly submit: () => void;
|
|
16
|
+
private readonly input = new Input();
|
|
17
|
+
|
|
18
|
+
constructor(getState: () => QnaState, setState: (state: QnaState) => void, submit: () => void) {
|
|
19
|
+
this.getState = getState;
|
|
20
|
+
this.setState = setState;
|
|
21
|
+
this.submit = submit;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get focused(): boolean {
|
|
25
|
+
return this.input.focused;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
set focused(value: boolean) {
|
|
29
|
+
this.input.focused = value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
handleInput(data: string): void {
|
|
33
|
+
if (matchesKey(data, Key.enter)) {
|
|
34
|
+
this.setState(saveAdditionalContext(this.getState(), this.input.getValue()));
|
|
35
|
+
this.submit();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.input.handleInput(data);
|
|
39
|
+
this.setState(saveAdditionalContext(this.getState(), this.input.getValue()));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render(width: number): string[] {
|
|
43
|
+
return [
|
|
44
|
+
...wrapTextWithAnsi("Any additional context for the agent? Optional.", width),
|
|
45
|
+
"",
|
|
46
|
+
...this.input.render(width).map((line) => truncateToWidth(line, width, "")),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
invalidate(): void {}
|
|
51
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { NormalizedQuestion } from "./model.ts";
|
|
3
|
+
|
|
4
|
+
export function renderQuestionPrompt(question: NormalizedQuestion, width: number): string[] {
|
|
5
|
+
return [...wrapTextWithAnsi(question.prompt, width), ""];
|
|
6
|
+
}
|