@shanepadgett/tau-agent 0.42.3 → 0.44.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 (70) hide show
  1. package/README.md +0 -1
  2. package/docs/context.md +1 -1
  3. package/docs/extending-tau-agent.md +1 -2
  4. package/extensions/attention/README.md +1 -1
  5. package/extensions/cache-diagnostics/index.ts +4 -0
  6. package/extensions/context/README.md +1 -24
  7. package/extensions/context/definitions.ts +1 -52
  8. package/extensions/context/index.ts +1 -150
  9. package/extensions/context/panel.ts +0 -72
  10. package/extensions/explore/guidance.ts +9 -1
  11. package/extensions/footer/README.md +1 -1
  12. package/extensions/footer/index.ts +3 -25
  13. package/extensions/qna/choice-question-body.ts +7 -2
  14. package/extensions/run-summary/README.md +1 -1
  15. package/extensions/run-summary/index.ts +18 -25
  16. package/extensions/runtime-context/README.md +1 -1
  17. package/extensions/runtime-context/context.ts +1 -1
  18. package/extensions/runtime-context/index.ts +10 -16
  19. package/extensions/script-runner/index.ts +21 -29
  20. package/extensions/silent-command-runner/README.md +0 -2
  21. package/extensions/silent-command-runner/index.ts +12 -41
  22. package/extensions/soul/README.md +5 -8
  23. package/extensions/soul/context.ts +115 -0
  24. package/extensions/soul/index.ts +141 -7
  25. package/extensions/soul/prompt.ts +47 -102
  26. package/extensions/soul/state.ts +114 -0
  27. package/extensions/soul/tools.ts +31 -0
  28. package/extensions/tau-help/help.md +6 -18
  29. package/extensions/tau-help/index.ts +10 -4
  30. package/extensions/tool-approval/README.md +4 -2
  31. package/extensions/tool-approval/index.ts +141 -53
  32. package/extensions/tool-approval/panel.ts +153 -0
  33. package/extensions/tool-loader/README.md +1 -1
  34. package/extensions/tool-loader/index.ts +91 -22
  35. package/package.json +2 -2
  36. package/schemas/tau.schema.json +0 -104
  37. package/shared/bounded-text-result.ts +0 -1
  38. package/shared/events.ts +19 -15
  39. package/shared/isolated-session.ts +1 -2
  40. package/shared/model-effort.ts +15 -21
  41. package/shared/prompt-contributions.ts +24 -0
  42. package/src/index.ts +1 -1
  43. package/docs/subagents.md +0 -92
  44. package/extensions/auto-compact/README.md +0 -9
  45. package/extensions/auto-compact/index.ts +0 -122
  46. package/extensions/auto-compact/settings.ts +0 -30
  47. package/extensions/context/settings.ts +0 -62
  48. package/extensions/context/sync.ts +0 -276
  49. package/extensions/context/validation.ts +0 -88
  50. package/extensions/effort/README.md +0 -7
  51. package/extensions/effort/index.ts +0 -134
  52. package/extensions/effort/state.ts +0 -18
  53. package/extensions/qna/inline-editor-row.ts +0 -56
  54. package/extensions/soul/overseer.ts +0 -273
  55. package/extensions/soul/settings.ts +0 -34
  56. package/extensions/subagent/README.md +0 -67
  57. package/extensions/subagent/agents/context-sync.md +0 -244
  58. package/extensions/subagent/agents/dormant/generalist.md +0 -33
  59. package/extensions/subagent/agents/scout.md +0 -104
  60. package/extensions/subagent/agents/web-research.md +0 -101
  61. package/extensions/subagent/agents.ts +0 -261
  62. package/extensions/subagent/cmux-dashboard.ts +0 -495
  63. package/extensions/subagent/index.ts +0 -425
  64. package/extensions/subagent/panel.ts +0 -124
  65. package/extensions/subagent/render.ts +0 -74
  66. package/extensions/subagent/resume.ts +0 -78
  67. package/extensions/subagent/run.ts +0 -641
  68. package/extensions/subagent/runtime.ts +0 -1296
  69. package/extensions/subagent/session-resource.ts +0 -61
  70. package/extensions/subagent/settings.ts +0 -18
@@ -1,276 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { readFile, readdir } from "node:fs/promises";
3
- import { extname, join, relative, sep } from "node:path";
4
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
- import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
6
- import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
7
- import { discoverAgents } from "../subagent/agents.ts";
8
- import {
9
- createSubagentThread,
10
- disposeSubagentThread,
11
- extensionPathsForTools,
12
- runSubagentTurn,
13
- } from "../subagent/run.ts";
14
- import { pathExists } from "./definitions.ts";
15
- import contextSettings from "./settings.ts";
16
- import { formatContextValidationFailure, listEligibleDirtyPaths, validateContextCatalog } from "./validation.ts";
17
-
18
- const CONTEXT_SYNC_AGENT = "context-sync";
19
-
20
- export interface ContextSyncDetails {
21
- outcome: "applied" | "no-change" | "failed" | "cancelled";
22
- summary: string;
23
- reason: string;
24
- changedContextFiles: string[];
25
- agentResponse?: string;
26
- validationFailure?: string;
27
- }
28
-
29
- type SyncStatus = (status: string) => void | Promise<void>;
30
- type GitRunner = ReturnType<typeof createGitRunner>;
31
-
32
- let syncQueue = Promise.resolve();
33
-
34
- export async function runContextSync(
35
- pi: ExtensionAPI,
36
- ctx: ExtensionContext,
37
- options: { nudge?: string; signal?: AbortSignal; onStatus?: SyncStatus } = {},
38
- ): Promise<ContextSyncDetails> {
39
- return withSyncLock(() => runContextSyncLocked(pi, ctx, options));
40
- }
41
-
42
- function noChangeResult(reason: string, agentResponse?: string): ContextSyncDetails {
43
- return {
44
- outcome: "no-change",
45
- summary: "Existing context mappings already fit the changed scope.",
46
- reason,
47
- changedContextFiles: [],
48
- agentResponse,
49
- };
50
- }
51
-
52
- async function inspectSyncNeed(
53
- pi: ExtensionAPI,
54
- ctx: ExtensionContext,
55
- nudge: string | undefined,
56
- onStatus?: SyncStatus,
57
- ): Promise<
58
- | { kind: "done"; result: ContextSyncDetails }
59
- | { kind: "run"; git: GitRunner; root: string; ignoreGlobs: readonly string[] }
60
- > {
61
- await onStatus?.("Inspecting repository context");
62
- const git = createGitRunner(pi, ctx);
63
- const status = await loadRepoStatus(git);
64
- if (!status) throw new Error("No Git repository found");
65
- const settings = await loadTauExtensionSettings(ctx, contextSettings);
66
- const ignoreGlobs = settings.validation.ignoreGlobs;
67
- const validation = await validateContextCatalog(git, status.root, ignoreGlobs);
68
- const dirtyEligible = await listEligibleDirtyPaths(git, status.root, ignoreGlobs);
69
- const force = Boolean(nudge?.trim());
70
- if (!force && validation.stale.length === 0 && validation.uncovered.length === 0 && dirtyEligible.length === 0) {
71
- return {
72
- kind: "done",
73
- result: noChangeResult("No eligible dirty files, stale catalog paths, or sync nudge."),
74
- };
75
- }
76
- return { kind: "run", git, root: status.root, ignoreGlobs };
77
- }
78
-
79
- async function resolveContextSyncDefinition(ctx: ExtensionContext) {
80
- const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
81
- const definition = discovery.agents.get(CONTEXT_SYNC_AGENT);
82
- if (definition) return definition;
83
- const reason =
84
- discovery.invalid
85
- .get(CONTEXT_SYNC_AGENT)
86
- ?.map((item) => item.reason)
87
- .join("; ") ?? "unknown agent";
88
- throw new Error(`Context sync agent unavailable: ${reason}`);
89
- }
90
-
91
- async function runContextSyncAgent(
92
- pi: ExtensionAPI,
93
- ctx: ExtensionContext,
94
- root: string,
95
- nudge: string | undefined,
96
- signal: AbortSignal,
97
- onStatus: SyncStatus | undefined,
98
- beforeCatalog: Map<string, string>,
99
- ): Promise<ContextSyncDetails | { agentResponse: string }> {
100
- const definition = await resolveContextSyncDefinition(ctx);
101
- const task = buildContextSyncTask(root, nudge);
102
- await onStatus?.("Running context-sync subagent");
103
- const thread = await createSubagentThread({
104
- id: `context-sync-${Date.now()}`,
105
- displayName: definition.names[0] ?? definition.name,
106
- definition,
107
- extensionPaths: extensionPathsForTools(pi, definition.tools),
108
- initialTask: task,
109
- ctx,
110
- thinkingLevel: pi.getThinkingLevel(),
111
- signal,
112
- onWarning: (warning) => {
113
- ctx.ui.notify(`Context sync agent: ${warning}`, "warning");
114
- },
115
- });
116
- try {
117
- const result = await runSubagentTurn({
118
- thread,
119
- task,
120
- initial: true,
121
- signal,
122
- onUpdate: async (details) => {
123
- const activity = details.currentActivity ?? details.response;
124
- if (activity) await onStatus?.(activity.slice(0, 160));
125
- },
126
- });
127
- if (signal.aborted || result.details.status === "aborted") return cancelledResult(result.content);
128
- if (result.details.status !== "completed") {
129
- return {
130
- outcome: "failed",
131
- summary: "Context sync subagent failed.",
132
- reason: result.details.error ?? result.content,
133
- changedContextFiles: changedCatalogPaths(beforeCatalog, await catalogFileSnapshot(root)),
134
- agentResponse: result.content,
135
- };
136
- }
137
- return { agentResponse: result.content };
138
- } finally {
139
- await disposeSubagentThread(thread);
140
- }
141
- }
142
-
143
- async function finalizeContextSync(
144
- git: GitRunner,
145
- root: string,
146
- ignoreGlobs: readonly string[],
147
- beforeCatalog: Map<string, string>,
148
- agentResponse: string,
149
- onStatus?: SyncStatus,
150
- ): Promise<ContextSyncDetails> {
151
- const changedContextFiles = changedCatalogPaths(beforeCatalog, await catalogFileSnapshot(root));
152
- await onStatus?.("Verifying context catalog");
153
- const validationFailure = formatContextValidationFailure(await validateContextCatalog(git, root, ignoreGlobs));
154
- if (validationFailure) {
155
- return {
156
- outcome: "failed",
157
- summary: "Context sync finished but catalog invariants still fail.",
158
- reason: validationFailure,
159
- changedContextFiles,
160
- agentResponse,
161
- validationFailure,
162
- };
163
- }
164
- if (changedContextFiles.length === 0) {
165
- return noChangeResult(agentResponse.trim() || "Context-sync subagent made no catalog edits.", agentResponse);
166
- }
167
- return {
168
- outcome: "applied",
169
- summary: `Updated context catalog (${changedContextFiles.length} file${changedContextFiles.length === 1 ? "" : "s"}).`,
170
- reason: agentResponse.trim() || "Context-sync subagent updated the catalog.",
171
- changedContextFiles,
172
- agentResponse,
173
- };
174
- }
175
-
176
- async function runContextSyncLocked(
177
- pi: ExtensionAPI,
178
- ctx: ExtensionContext,
179
- options: { nudge?: string; signal?: AbortSignal; onStatus?: SyncStatus },
180
- ): Promise<ContextSyncDetails> {
181
- if (!ctx.isProjectTrusted()) throw new Error("Context sync requires a trusted project");
182
- const signal = options.signal ?? ctx.signal ?? new AbortController().signal;
183
- if (signal.aborted) return cancelledResult();
184
-
185
- const need = await inspectSyncNeed(pi, ctx, options.nudge, options.onStatus);
186
- if (need.kind === "done") return need.result;
187
- if (signal.aborted) return cancelledResult();
188
-
189
- const beforeCatalog = await catalogFileSnapshot(need.root);
190
- const agentResult = await runContextSyncAgent(
191
- pi,
192
- ctx,
193
- need.root,
194
- options.nudge,
195
- signal,
196
- options.onStatus,
197
- beforeCatalog,
198
- );
199
- if ("outcome" in agentResult) return agentResult;
200
- if (signal.aborted) return cancelledResult(agentResult.agentResponse);
201
- return finalizeContextSync(
202
- need.git,
203
- need.root,
204
- need.ignoreGlobs,
205
- beforeCatalog,
206
- agentResult.agentResponse,
207
- options.onStatus,
208
- );
209
- }
210
-
211
- function cancelledResult(agentResponse?: string): ContextSyncDetails {
212
- return {
213
- outcome: "cancelled",
214
- summary: "Context sync cancelled",
215
- reason: agentResponse?.trim() || "Cancelled by user.",
216
- changedContextFiles: [],
217
- agentResponse,
218
- };
219
- }
220
-
221
- function buildContextSyncTask(root: string, nudge?: string): string {
222
- const trimmed = nudge?.trim();
223
- return [
224
- `Keep the repository context catalog at ${root} honest.`,
225
- "Job: update `.pi/contexts` so work packs match the codebase.",
226
- "Use normal repo tools. Prefer patch for catalog edits under `.pi/contexts`.",
227
- "Walk domain → concept → job entry before placing paths. Harness verifies catalog coverage after you finish.",
228
- trimmed ? `Human nudge (soft steer, does not skip the ladder or coverage):\n${trimmed}` : undefined,
229
- ]
230
- .filter((line): line is string => Boolean(line))
231
- .join("\n\n");
232
- }
233
-
234
- async function withSyncLock<T>(task: () => Promise<T>): Promise<T> {
235
- const previous = syncQueue;
236
- let release = () => {};
237
- syncQueue = new Promise<void>((resolve) => {
238
- release = resolve;
239
- });
240
- await previous;
241
- try {
242
- return await task();
243
- } finally {
244
- release();
245
- }
246
- }
247
-
248
- async function catalogFileSnapshot(root: string): Promise<Map<string, string>> {
249
- const snapshot = new Map<string, string>();
250
- const base = join(root, ".pi", "contexts");
251
- if (!(await pathExists(base))) return snapshot;
252
- const stack = [base];
253
- while (stack.length > 0) {
254
- const directory = stack.pop();
255
- if (!directory) continue;
256
- for (const entry of await readdir(directory, { withFileTypes: true })) {
257
- const fullPath = join(directory, entry.name);
258
- if (entry.isDirectory()) {
259
- stack.push(fullPath);
260
- continue;
261
- }
262
- if (!entry.isFile() || extname(entry.name) !== ".toml") continue;
263
- const relativePath = relative(root, fullPath).split(sep).join("/");
264
- const content = await readFile(fullPath, "utf8");
265
- snapshot.set(relativePath, createHash("sha256").update(content).digest("hex"));
266
- }
267
- }
268
- return snapshot;
269
- }
270
-
271
- function changedCatalogPaths(before: Map<string, string>, after: Map<string, string>): string[] {
272
- const paths = new Set([...before.keys(), ...after.keys()]);
273
- return [...paths]
274
- .filter((path) => before.get(path) !== after.get(path))
275
- .sort((left, right) => left.localeCompare(right));
276
- }
@@ -1,88 +0,0 @@
1
- import { stat } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import type { GitRunner } from "../../shared/git.ts";
4
- import { contextEntryPaths, isContextEligiblePath, isSensitiveContextPath, loadContextEntries } from "./definitions.ts";
5
-
6
- export interface ContextValidationResult {
7
- stale: Array<{ path: string; ids: string[] }>;
8
- uncovered: string[];
9
- }
10
-
11
- export async function listEligibleDirtyPaths(
12
- git: GitRunner,
13
- root: string,
14
- ignoreGlobs: readonly string[],
15
- ): Promise<string[]> {
16
- const status = await git.run(["status", "--porcelain=v2", "-z", "--untracked-files=all"], { cwd: root });
17
- return [...parseDirtyPaths(status)]
18
- .filter((path) => !isSensitiveContextPath(path) && isContextEligiblePath(path, ignoreGlobs))
19
- .sort((left, right) => left.localeCompare(right));
20
- }
21
-
22
- export async function validateContextCatalog(
23
- git: GitRunner,
24
- root: string,
25
- ignoreGlobs: readonly string[],
26
- ): Promise<ContextValidationResult> {
27
- const entries = await loadContextEntries(root);
28
- const memberships = new Map<string, string[]>();
29
- for (const entry of entries)
30
- for (const file of contextEntryPaths(entry)) memberships.set(file, [...(memberships.get(file) ?? []), entry.id]);
31
-
32
- const stale: Array<{ path: string; ids: string[] }> = [];
33
- for (const [path, ids] of memberships) {
34
- if (!isContextEligiblePath(path, ignoreGlobs)) continue;
35
- try {
36
- if (!(await stat(join(root, path))).isFile()) stale.push({ path, ids });
37
- } catch {
38
- stale.push({ path, ids });
39
- }
40
- }
41
-
42
- const uncovered = (await listEligibleDirtyPaths(git, root, ignoreGlobs))
43
- .filter((path) => !memberships.has(path))
44
- .sort((left, right) => left.localeCompare(right));
45
- return { stale: stale.sort((left, right) => left.path.localeCompare(right.path)), uncovered };
46
- }
47
-
48
- export function formatContextValidationFailure(result: ContextValidationResult): string | undefined {
49
- if (result.stale.length === 0 && result.uncovered.length === 0) return undefined;
50
- const output = ["Context catalog validation failed."];
51
- if (result.stale.length)
52
- output.push(
53
- "",
54
- "Stale context file references:",
55
- ...result.stale.map((item) => `- ${item.path} (${item.ids.sort().join(", ")})`),
56
- );
57
- if (result.uncovered.length)
58
- output.push("", "Changed files with no context membership:", ...result.uncovered.map((path) => `- ${path}`));
59
- output.push(
60
- "",
61
- "With validation enabled, Tau runs context-sync automatically after agent turns. Manual: /context-sync or /context-sync <nudge>.",
62
- );
63
- return output.join("\n");
64
- }
65
-
66
- function parseDirtyPaths(status: string): Set<string> {
67
- const records = status.split("\0");
68
- const dirty = new Set<string>();
69
- for (let index = 0; index < records.length; index++) {
70
- const record = records[index];
71
- if (!record) continue;
72
- if (record.startsWith("? ")) {
73
- dirty.add(record.slice(2).replaceAll("\\", "/"));
74
- continue;
75
- }
76
- const fields = record.split(" ");
77
- const state = fields[1] ?? "..";
78
- if (record.startsWith("1 ") && !state.includes("D")) {
79
- dirty.add(fields.slice(8).join(" ").replaceAll("\\", "/"));
80
- } else if (record.startsWith("2 ")) {
81
- if (!state.includes("D")) dirty.add(fields.slice(9).join(" ").replaceAll("\\", "/"));
82
- index += 1;
83
- } else if (record.startsWith("u ")) {
84
- dirty.add(fields.slice(10).join(" ").replaceAll("\\", "/"));
85
- }
86
- }
87
- return dirty;
88
- }
@@ -1,7 +0,0 @@
1
- # Effort Extension
2
-
3
- Switches active chat model and thinking level as one effort tier. Available providers come from current logins, and each provider can fall back through its ranked models.
4
-
5
- Run `/effort` to choose a tier and provider. `/effort quick`, `/effort standard`, and `/effort deep` skip the tier prompt. Press `Ctrl+Shift+E` to cycle tiers on current provider.
6
-
7
- If selected provider has no usable model for tier, current model stays active. Footer shows an effort label whenever current provider, model, and thinking level match a configured tier; otherwise it shows no label.
@@ -1,134 +0,0 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { Key, type AutocompleteItem } from "@earendil-works/pi-tui";
3
- import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
4
- import {
5
- effortForSelection,
6
- type EffortProviderCandidates,
7
- type ModelEffort,
8
- resolveEffortProviders,
9
- } from "../../shared/model-effort.ts";
10
- import { EFFORT_STATE_TYPE, effortState, nextEffort } from "./state.ts";
11
-
12
- const EFFORTS: readonly ModelEffort[] = ["quick", "standard", "deep"];
13
-
14
- export default function effortExtension(pi: ExtensionAPI): void {
15
- let activeEffort: ModelEffort | undefined;
16
- let applying = false;
17
-
18
- function publish(): void {
19
- emitTauEvent(pi, "tau:model-effort.changed", { effort: activeEffort });
20
- }
21
-
22
- function setEffort(effort: ModelEffort | undefined): void {
23
- if (activeEffort === effort) return;
24
- activeEffort = effort;
25
- pi.appendEntry(EFFORT_STATE_TYPE, effortState(effort));
26
- publish();
27
- }
28
-
29
- async function apply(
30
- ctx: ExtensionContext,
31
- effort: ModelEffort,
32
- provider: EffortProviderCandidates,
33
- ): Promise<boolean> {
34
- applying = true;
35
- try {
36
- for (const candidate of provider.candidates) {
37
- if (!(await pi.setModel(candidate.model))) continue;
38
- pi.setThinkingLevel(candidate.reasoning);
39
- setEffort(effortForSelection(provider.provider, candidate.model.id, pi.getThinkingLevel()));
40
- ctx.ui.notify(`Effort: ${effort} · ${provider.label}/${candidate.model.id}`, "info");
41
- return true;
42
- }
43
- return false;
44
- } finally {
45
- applying = false;
46
- }
47
- }
48
-
49
- async function chooseProvider(ctx: ExtensionContext, effort: ModelEffort): Promise<void> {
50
- const providers = resolveEffortProviders(ctx, effort);
51
- if (providers.length === 0) {
52
- ctx.ui.notify(`No logged-in provider has a ${effort} effort model. Current model kept.`, "warning");
53
- return;
54
- }
55
- const labels = providers.map((provider) => `${provider.label} · ${provider.provider}`);
56
- const selected = await ctx.ui.select(`Provider for ${effort} effort`, labels);
57
- if (!selected) return;
58
- const provider = providers[labels.indexOf(selected)];
59
- if (!provider) return;
60
- if (!(await apply(ctx, effort, provider))) {
61
- ctx.ui.notify(`Could not select a ${effort} model from ${provider.label}. Current model kept.`, "warning");
62
- }
63
- }
64
-
65
- pi.registerCommand("effort", {
66
- description: "Select effort tier and logged-in provider",
67
- getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
68
- const value = prefix.trimStart();
69
- if (/\s/.test(value)) return null;
70
- const items = EFFORTS.filter((effort) => effort.startsWith(value)).map((effort) => ({
71
- value: effort,
72
- label: effort,
73
- }));
74
- return items.length ? items : null;
75
- },
76
- handler: async (args, ctx) => {
77
- await ctx.waitForIdle();
78
- if (!ctx.hasUI) {
79
- ctx.ui.notify("/effort requires interactive UI.", "error");
80
- return;
81
- }
82
- const arg = args.trim().toLowerCase();
83
- if (arg && !isModelEffort(arg)) {
84
- ctx.ui.notify("Usage: /effort [quick|standard|deep]", "error");
85
- return;
86
- }
87
- let effort: ModelEffort | undefined = isModelEffort(arg) ? arg : undefined;
88
- if (!arg) {
89
- const selected = await ctx.ui.select("Effort", [...EFFORTS]);
90
- if (selected && isModelEffort(selected)) effort = selected;
91
- }
92
- if (!effort) return;
93
- await chooseProvider(ctx, effort);
94
- },
95
- });
96
-
97
- pi.registerShortcut(Key.ctrlShift("e"), {
98
- description: "Cycle effort for current provider",
99
- handler: async (ctx) => {
100
- if (!ctx.isIdle()) {
101
- ctx.ui.notify("Wait for the current run before changing effort.", "warning");
102
- return;
103
- }
104
- const effort = nextEffort(activeEffort);
105
- const provider = resolveEffortProviders(ctx, effort).find((item) => item.provider === ctx.model?.provider);
106
- if (!provider || !(await apply(ctx, effort, provider))) {
107
- ctx.ui.notify(`Current provider has no available ${effort} effort model. Current model kept.`, "warning");
108
- }
109
- },
110
- });
111
-
112
- onTauEvent(pi, "model-effort.snapshot", "tau:model-effort.snapshot.requested", publish);
113
-
114
- pi.on("session_start", (_event, ctx) => {
115
- activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
116
- publish();
117
- });
118
- pi.on("session_tree", (_event, ctx) => {
119
- activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
120
- publish();
121
- });
122
- pi.on("model_select", (event) => {
123
- if (!applying) setEffort(effortForSelection(event.model.provider, event.model.id, pi.getThinkingLevel()));
124
- });
125
- pi.on("thinking_level_select", (event, ctx) => {
126
- if (!applying) {
127
- setEffort(effortForSelection(ctx.model?.provider, ctx.model?.id, event.level));
128
- }
129
- });
130
- }
131
-
132
- function isModelEffort(value: string): value is ModelEffort {
133
- return value === "quick" || value === "standard" || value === "deep";
134
- }
@@ -1,18 +0,0 @@
1
- import type { ModelEffort } from "../../shared/model-effort.ts";
2
-
3
- export const EFFORT_STATE_TYPE = "tau.model-effort.state";
4
-
5
- export interface EffortStateV1 {
6
- v: 1;
7
- effort: ModelEffort | null;
8
- }
9
-
10
- export function effortState(effort: ModelEffort | undefined): EffortStateV1 {
11
- return { v: 1, effort: effort ?? null };
12
- }
13
-
14
- export function nextEffort(effort: ModelEffort | undefined): ModelEffort {
15
- if (effort === "quick") return "standard";
16
- if (effort === "standard") return "deep";
17
- return "quick";
18
- }
@@ -1,56 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { type Editor, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
-
4
- export function renderInlineEditor(prefix: string, editor: Editor, width: number): string[] {
5
- const available = Math.max(1, width - visibleWidth(prefix));
6
- const lines = editorContentLines(editor.render(available));
7
- const contentLines = lines.length ? lines : [""];
8
- const continuation = " ".repeat(visibleWidth(prefix));
9
- return contentLines.map((line, index) =>
10
- truncateToWidth(`${index === 0 ? prefix : continuation}${line}`, width, ""),
11
- );
12
- }
13
-
14
- export function renderNoteEditor(lines: string[], editor: Editor, width: number, theme: Theme, indent: string): void {
15
- const prefix = `${indent}${theme.fg("muted", "└─ ")}`;
16
- const available = Math.max(1, width - visibleWidth(prefix));
17
- const editorLines = editorContentLines(editor.render(available));
18
- const contentLines = editorLines.length ? editorLines : [""];
19
- lines.push(truncateToWidth(`${prefix}${contentLines[0] ?? ""}`, width, ""));
20
- const continuation = " ".repeat(visibleWidth(prefix));
21
- for (const line of contentLines.slice(1)) lines.push(truncateToWidth(`${continuation}${line}`, width, ""));
22
- }
23
-
24
- export function pushSavedNote(lines: string[], note: string, width: number, theme: Theme, indent: string): void {
25
- for (const line of wrapWithPrefix(`${indent}${theme.fg("muted", "└─ ")}`, theme.fg("muted", note), width)) {
26
- lines.push(line);
27
- }
28
- }
29
-
30
- export function wrapWithPrefix(prefix: string, text: string, width: number): string[] {
31
- const restWidth = Math.max(1, width - visibleWidth(prefix));
32
- return wrapTextWithAnsi(text, restWidth).map(
33
- (line, index) => `${index === 0 ? prefix : " ".repeat(visibleWidth(prefix))}${line}`,
34
- );
35
- }
36
-
37
- function editorContentLines(lines: string[]): string[] {
38
- if (lines.length <= 2) return lines;
39
- return lines.slice(1).filter((line) => !isEditorBorderLine(line));
40
- }
41
-
42
- function isEditorBorderLine(line: string): boolean {
43
- const text = stripAnsi(line).trim();
44
- return /^[┌┐└┘─]+$/.test(text) || /^─── [↑↓] \d+ more ─*$/.test(text);
45
- }
46
-
47
- function stripAnsi(text: string): string {
48
- let result = text;
49
- while (true) {
50
- const start = result.indexOf("\u001b[");
51
- if (start === -1) return result;
52
- const end = result.indexOf("m", start + 2);
53
- if (end === -1) return result;
54
- result = result.slice(0, start) + result.slice(end + 1);
55
- }
56
- }