@shanepadgett/tau-agent 0.31.1 → 0.33.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/extensions/commit/commit-effort.ts +14 -0
- package/extensions/commit/commit-plan.ts +6 -7
- package/extensions/commit/git-change-set.ts +36 -4
- package/extensions/context/index.ts +1 -1
- package/extensions/effort/README.md +7 -0
- package/extensions/effort/index.ts +134 -0
- package/extensions/effort/state.ts +33 -0
- package/extensions/footer/README.md +1 -1
- package/extensions/footer/index.ts +36 -6
- package/extensions/reference/README.md +1 -0
- package/extensions/reference/panel.ts +34 -4
- package/extensions/tau-help/help.md +5 -5
- package/extensions/working-memory/README.md +9 -1
- package/extensions/working-memory/checkpoint.ts +84 -16
- package/extensions/working-memory/index.ts +8 -5
- package/extensions/working-memory/memory.ts +1 -1
- package/extensions/working-memory/render.ts +9 -2
- package/extensions/working-memory/state.ts +11 -5
- package/package.json +2 -2
- package/shared/events.ts +5 -0
- package/shared/model-effort.ts +116 -0
- package/extensions/publish/README.md +0 -13
- package/extensions/publish/index.ts +0 -301
- package/extensions/publish/publish-ui.ts +0 -73
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ModelEffort } from "../../shared/model-effort.ts";
|
|
2
|
+
import type { DirtyFile } from "./git-change-set.ts";
|
|
3
|
+
|
|
4
|
+
const LOW_EFFORT_MAX_CHANGED_LINES = 500;
|
|
5
|
+
|
|
6
|
+
export function commitEffort(files: readonly Pick<DirtyFile, "changeSize">[]): ModelEffort {
|
|
7
|
+
let changedLines = 0;
|
|
8
|
+
for (const file of files) {
|
|
9
|
+
if (file.changeSize === "binary") return "medium";
|
|
10
|
+
changedLines += file.changeSize;
|
|
11
|
+
if (changedLines > LOW_EFFORT_MAX_CHANGED_LINES) return "medium";
|
|
12
|
+
}
|
|
13
|
+
return "low";
|
|
14
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { type
|
|
2
|
+
import { type Tool, Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveEffortCandidates } from "../../shared/model-effort.ts";
|
|
5
|
+
import { generateToolValidated, generateValidated } from "../../shared/model-fallback/index.ts";
|
|
5
6
|
import { truncAt } from "../../shared/text.ts";
|
|
7
|
+
import { commitEffort } from "./commit-effort.ts";
|
|
6
8
|
import type { CommitEvidence, DirtyFile } from "./git-change-set.ts";
|
|
7
9
|
|
|
8
10
|
const MAX_PLAN_EVIDENCE_CHARS = 48_000;
|
|
9
11
|
const CONVENTIONAL_COMMIT_TYPES = ["feat", "fix", "docs", "refactor", "test", "chore", "perf", "ci", "build", "revert"];
|
|
10
|
-
const COMMIT_MODELS: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel }> = [
|
|
11
|
-
{ provider: "openai-codex", model: "gpt-5.6-terra", reasoning: "low" },
|
|
12
|
-
];
|
|
13
12
|
const COMMIT_PLAN_TOOL = {
|
|
14
13
|
name: "create_commit_plan",
|
|
15
14
|
description: "Submit the commit plan for the dirty repository files.",
|
|
@@ -55,7 +54,7 @@ export async function generatePlan(
|
|
|
55
54
|
const prompt = buildPlanPrompt(evidence, previousPlan, regenerationNote);
|
|
56
55
|
return generateToolValidated(
|
|
57
56
|
ctx,
|
|
58
|
-
await
|
|
57
|
+
await resolveEffortCandidates(ctx, commitEffort(evidence.files), true),
|
|
59
58
|
prompt,
|
|
60
59
|
COMMIT_PLAN_TOOL,
|
|
61
60
|
(input) => commitGroupsFromToolInput(input, evidence.files),
|
|
@@ -85,7 +84,7 @@ export async function regenerateMessage(
|
|
|
85
84
|
const prompt = buildMessagePrompt(evidence, selected, previousPlan, selectedGroupId, regenerationNote);
|
|
86
85
|
return generateValidated(
|
|
87
86
|
ctx,
|
|
88
|
-
await
|
|
87
|
+
await resolveEffortCandidates(ctx, commitEffort(selected), true),
|
|
89
88
|
prompt,
|
|
90
89
|
requireCommitMessage,
|
|
91
90
|
undefined,
|
|
@@ -26,6 +26,7 @@ export interface DirtyFile {
|
|
|
26
26
|
kind: "modified" | "added" | "deleted" | "renamed" | "untracked";
|
|
27
27
|
untracked: boolean;
|
|
28
28
|
evidence: string;
|
|
29
|
+
changeSize: number | "binary";
|
|
29
30
|
renamedFrom?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -115,9 +116,11 @@ async function collectFileEvidence(
|
|
|
115
116
|
for (let index = 0; index < files.length; index += EVIDENCE_CONCURRENCY) {
|
|
116
117
|
withEvidence.push(
|
|
117
118
|
...(await Promise.all(
|
|
118
|
-
files
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
files.slice(index, index + EVIDENCE_CONCURRENCY).map(async (file) => ({
|
|
120
|
+
...file,
|
|
121
|
+
evidence: await loadEvidenceForFile(git, root, file, hasHead),
|
|
122
|
+
changeSize: await loadChangeSize(git, root, file, hasHead),
|
|
123
|
+
})),
|
|
121
124
|
)),
|
|
122
125
|
);
|
|
123
126
|
}
|
|
@@ -155,7 +158,7 @@ function parsePorcelainV2Z(raw: string): DirtyFile[] {
|
|
|
155
158
|
|
|
156
159
|
return parsed
|
|
157
160
|
.sort((left, right) => left.path.localeCompare(right.path))
|
|
158
|
-
.map((file, index) => ({ ...file, id: index + 1, kind: changeKind(file), evidence: "" }));
|
|
161
|
+
.map((file, index) => ({ ...file, id: index + 1, kind: changeKind(file), evidence: "", changeSize: 0 }));
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
function statusText(fields: readonly string[]): string {
|
|
@@ -176,6 +179,35 @@ async function loadEvidenceForFile(git: GitRunner, root: string, file: DirtyFile
|
|
|
176
179
|
return truncAt(diff || "metadata-only change", MAX_FILE_EVIDENCE_CHARS);
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
async function loadChangeSize(
|
|
183
|
+
git: GitRunner,
|
|
184
|
+
root: string,
|
|
185
|
+
file: DirtyFile,
|
|
186
|
+
hasHead: boolean,
|
|
187
|
+
): Promise<number | "binary"> {
|
|
188
|
+
if (file.renamedFrom && !/[MAD]/.test(file.status)) return 0;
|
|
189
|
+
const paths = file.renamedFrom ? [file.renamedFrom, file.path] : [file.path];
|
|
190
|
+
const output = file.untracked
|
|
191
|
+
? await git.run(["diff", "--no-index", "--numstat", "--", "/dev/null", file.path], { cwd: root, optional: true })
|
|
192
|
+
: hasHead
|
|
193
|
+
? await git.run(["diff", "--numstat", "HEAD", "--", ...paths], { cwd: root, optional: true })
|
|
194
|
+
: [
|
|
195
|
+
await git.run(["diff", "--cached", "--numstat", "--", ...paths], { cwd: root, optional: true }),
|
|
196
|
+
await git.run(["diff", "--numstat", "--", ...paths], { cwd: root, optional: true }),
|
|
197
|
+
].join("\n");
|
|
198
|
+
|
|
199
|
+
let total = 0;
|
|
200
|
+
for (const line of output.split("\n")) {
|
|
201
|
+
if (!line) continue;
|
|
202
|
+
const [added, deleted] = line.split("\t");
|
|
203
|
+
if (added === "-" || deleted === "-") return "binary";
|
|
204
|
+
const additions = Number(added);
|
|
205
|
+
const deletions = Number(deleted);
|
|
206
|
+
if (Number.isFinite(additions) && Number.isFinite(deletions)) total += additions + deletions;
|
|
207
|
+
}
|
|
208
|
+
return total;
|
|
209
|
+
}
|
|
210
|
+
|
|
179
211
|
async function diffFromIndexAndWorktree(git: GitRunner, root: string, paths: readonly string[]): Promise<string> {
|
|
180
212
|
const [staged, unstaged] = await Promise.all([
|
|
181
213
|
git.run(["diff", "--cached", "--stat", "--patch", "--no-color", "--no-ext-diff", "--", ...paths], {
|
|
@@ -147,7 +147,7 @@ export default function contextExtension(pi: ExtensionAPI): void {
|
|
|
147
147
|
),
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
|
-
return { messages: [contextProjectionMessage(projectionCache.content)
|
|
150
|
+
return { messages: [...messages, contextProjectionMessage(projectionCache.content)] };
|
|
151
151
|
});
|
|
152
152
|
pi.on("agent_start", async (_event, ctx) => {
|
|
153
153
|
await refreshSettings(ctx);
|
|
@@ -0,0 +1,7 @@
|
|
|
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 low`, `/effort medium`, and `/effort high` 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.
|
|
@@ -0,0 +1,134 @@
|
|
|
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[] = ["low", "medium", "high"];
|
|
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 [low|medium|high]", "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 === "low" || value === "medium" || value === "high";
|
|
134
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ModelEffort } from "../../shared/model-effort.ts";
|
|
3
|
+
|
|
4
|
+
export const EFFORT_STATE_TYPE = "tau.model-effort.state";
|
|
5
|
+
|
|
6
|
+
export interface EffortStateV1 {
|
|
7
|
+
v: 1;
|
|
8
|
+
effort: ModelEffort | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function effortState(effort: ModelEffort | undefined): EffortStateV1 {
|
|
12
|
+
return { v: 1, effort: effort ?? null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function replayEffortState(branch: readonly SessionEntry[]): ModelEffort | undefined {
|
|
16
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
17
|
+
const entry = branch[index];
|
|
18
|
+
if (entry?.type !== "custom" || entry.customType !== EFFORT_STATE_TYPE) continue;
|
|
19
|
+
const data = entry.data;
|
|
20
|
+
if (!data || typeof data !== "object") continue;
|
|
21
|
+
const value = data as Record<string, unknown>;
|
|
22
|
+
if (value.v !== 1) continue;
|
|
23
|
+
if (value.effort === null) return undefined;
|
|
24
|
+
if (value.effort === "low" || value.effort === "medium" || value.effort === "high") return value.effort;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function nextEffort(effort: ModelEffort | undefined): ModelEffort {
|
|
30
|
+
if (effort === "low") return "medium";
|
|
31
|
+
if (effort === "medium") return "high";
|
|
32
|
+
return "low";
|
|
33
|
+
}
|
|
@@ -6,7 +6,8 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { join, relative } from "node:path";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
import { onTauEvent } from "../../shared/events.ts";
|
|
9
|
+
import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
|
|
10
|
+
import { effortForSelection, type ModelEffort } from "../../shared/model-effort.ts";
|
|
10
11
|
import { loadTauExtensionSettings, updateTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
11
12
|
import footerSettings from "./settings.ts";
|
|
12
13
|
|
|
@@ -39,6 +40,8 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
39
40
|
let footerInstalled = false;
|
|
40
41
|
let requestRender: (() => void) | undefined;
|
|
41
42
|
let unsubscribeFooterItems: (() => void) | undefined;
|
|
43
|
+
let unsubscribeModelEffort: (() => void) | undefined;
|
|
44
|
+
let activeEffort: ModelEffort | undefined;
|
|
42
45
|
const gitByCwd = new Map<string, GitSummary | undefined>();
|
|
43
46
|
let gitRefresh: Promise<void> | undefined;
|
|
44
47
|
let dailyCost: number | undefined;
|
|
@@ -76,7 +79,11 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
76
79
|
const git = gitByCwd.get(currentCtx.cwd);
|
|
77
80
|
const model = currentCtx.model ? `${currentCtx.model.provider}/${currentCtx.model.id}` : "no-model";
|
|
78
81
|
const thinking = pi.getThinkingLevel();
|
|
79
|
-
const
|
|
82
|
+
const separator = theme.fg("dim", " • ");
|
|
83
|
+
const effort = effortText(theme, activeEffort);
|
|
84
|
+
const topLeft = [theme.fg("dim", gitText(git)), effort, theme.fg("dim", `${model} (${thinking})`)]
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join(separator);
|
|
80
87
|
const sessionUsage = sessionCost(currentCtx);
|
|
81
88
|
const session = formatCost(sessionUsage.cost);
|
|
82
89
|
const daily = dailyCost === undefined ? "$?" : formatCost(dailyCost);
|
|
@@ -87,7 +94,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
87
94
|
.join(" • ");
|
|
88
95
|
|
|
89
96
|
return [
|
|
90
|
-
renderSplit(width,
|
|
97
|
+
renderSplit(width, topLeft, topRight),
|
|
91
98
|
renderSplit(width, theme.fg("dim", bottomLeft), theme.fg("dim", bottomRight)),
|
|
92
99
|
];
|
|
93
100
|
},
|
|
@@ -165,6 +172,10 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
165
172
|
items.set(item.id, next);
|
|
166
173
|
render();
|
|
167
174
|
});
|
|
175
|
+
unsubscribeModelEffort = onTauEvent(pi, "footer.model-effort", "tau:model-effort.changed", (state) => {
|
|
176
|
+
activeEffort = state.effort;
|
|
177
|
+
render();
|
|
178
|
+
});
|
|
168
179
|
|
|
169
180
|
pi.registerCommand(COMMAND, {
|
|
170
181
|
description: "Toggle Tau footer",
|
|
@@ -202,11 +213,22 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
202
213
|
pi.on("session_start", async (_event, ctx) => {
|
|
203
214
|
const settings = await loadTauExtensionSettings(ctx, footerSettings);
|
|
204
215
|
setEnabled(ctx, settings.enabled);
|
|
216
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
|
|
205
217
|
onStateChange(ctx);
|
|
218
|
+
emitTauEvent(pi, "tau:model-effort.snapshot.requested", {});
|
|
219
|
+
});
|
|
220
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
221
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
|
|
222
|
+
onStateChange(ctx, false);
|
|
223
|
+
});
|
|
224
|
+
pi.on("model_select", (_event, ctx) => {
|
|
225
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
|
|
226
|
+
rerender(ctx);
|
|
227
|
+
});
|
|
228
|
+
pi.on("thinking_level_select", (event, ctx) => {
|
|
229
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, event.level);
|
|
230
|
+
rerender(ctx);
|
|
206
231
|
});
|
|
207
|
-
pi.on("session_tree", (_event, ctx) => onStateChange(ctx, false));
|
|
208
|
-
pi.on("model_select", (_event, ctx) => rerender(ctx));
|
|
209
|
-
pi.on("thinking_level_select", (_event, ctx) => rerender(ctx));
|
|
210
232
|
pi.on("agent_start", (_event, ctx) => onStateChange(ctx, false));
|
|
211
233
|
pi.on("turn_end", (_event, ctx) => onStateChange(ctx));
|
|
212
234
|
pi.on("agent_end", (_event, ctx) => onStateChange(ctx));
|
|
@@ -214,6 +236,9 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
214
236
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
215
237
|
unsubscribeFooterItems?.();
|
|
216
238
|
unsubscribeFooterItems = undefined;
|
|
239
|
+
unsubscribeModelEffort?.();
|
|
240
|
+
unsubscribeModelEffort = undefined;
|
|
241
|
+
activeEffort = undefined;
|
|
217
242
|
requestRender = undefined;
|
|
218
243
|
activeCtx = undefined;
|
|
219
244
|
footerInstalled = false;
|
|
@@ -226,6 +251,11 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
226
251
|
}
|
|
227
252
|
}
|
|
228
253
|
|
|
254
|
+
function effortText(theme: Theme, effort: ModelEffort | undefined): string {
|
|
255
|
+
if (!effort) return "";
|
|
256
|
+
return theme.fg("dim", theme.bold(effort.toUpperCase()));
|
|
257
|
+
}
|
|
258
|
+
|
|
229
259
|
async function saveEnabled(ctx: ExtensionContext, enabled: boolean): Promise<void> {
|
|
230
260
|
await updateTauExtensionSettings("global", ctx, footerSettings, (current) => ({ ...current, enabled }));
|
|
231
261
|
}
|
|
@@ -13,6 +13,7 @@ Select local reference repositories, add new ones, and send a prompt that points
|
|
|
13
13
|
|
|
14
14
|
- Stores references in `~/.local/share/tau-agent/references/`.
|
|
15
15
|
- `/reference` opens a TUI picker.
|
|
16
|
+
- The picker shows each repository's name with its `origin` owner in muted parentheses when available, falling back to its local folder name.
|
|
16
17
|
- `n` adds a new reference from a git URL, showing clone progress in the list as `↓ 37%`.
|
|
17
18
|
- New references clone the remote default branch.
|
|
18
19
|
- `b` switches the highlighted reference to another branch.
|
|
@@ -27,6 +27,7 @@ const BRANCH_VISIBLE_ROWS = 5;
|
|
|
27
27
|
|
|
28
28
|
export interface ReferenceItem {
|
|
29
29
|
name: string;
|
|
30
|
+
displayName: string;
|
|
30
31
|
path: string;
|
|
31
32
|
dirty: boolean;
|
|
32
33
|
branch: string;
|
|
@@ -190,7 +191,7 @@ class ReferencePanel implements Component {
|
|
|
190
191
|
items,
|
|
191
192
|
emptyMessage: "No references. Press ctrl+n for new.",
|
|
192
193
|
selection: { kind: "multi", primaryLabel: "attach" },
|
|
193
|
-
filter: { searchText: (item) => `${item.name} ${item.branch} ${item.path}` },
|
|
194
|
+
filter: { searchText: (item) => `${item.displayName} ${item.name} ${item.branch} ${item.path}` },
|
|
194
195
|
actions: [
|
|
195
196
|
{ id: "new", key: Key.ctrl("n"), hint: rawHint("ctrl+n", "new") },
|
|
196
197
|
{ id: "open", key: Key.ctrl("o"), hint: rawHint("ctrl+o", "open"), target: "current" },
|
|
@@ -303,13 +304,16 @@ class ReferencePanel implements Component {
|
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
private renderReferenceRow(item: ReferenceListItem, active: boolean, width: number): string[] {
|
|
306
|
-
const
|
|
307
|
+
const separator = item.displayName.lastIndexOf("/");
|
|
308
|
+
const repository = separator < 0 ? item.displayName : item.displayName.slice(separator + 1);
|
|
309
|
+
const owner = separator < 0 ? "" : this.theme.fg("muted", ` (${item.displayName.slice(0, separator)})`);
|
|
310
|
+
const name = item.dirty ? `${repository} *` : repository;
|
|
307
311
|
const label = active ? this.theme.fg("accent", name) : this.theme.fg(item.dirty ? "warning" : "text", name);
|
|
308
312
|
const branch = item.branch ? this.theme.fg("muted", ` (${item.branch})`) : "";
|
|
309
313
|
const status = item.state
|
|
310
314
|
? ` ${this.theme.fg(item.state === "failed" ? "error" : item.state === "updated" ? "success" : "muted", item.state === "failed" ? "!" : item.state === "updated" ? "✓" : "…")}`
|
|
311
315
|
: "";
|
|
312
|
-
return [truncateToWidth(`${label}${branch}${status}`, width, "")];
|
|
316
|
+
return [truncateToWidth(`${label}${owner}${branch}${status}`, width, "")];
|
|
313
317
|
}
|
|
314
318
|
|
|
315
319
|
private handleBranchInput(data: string): void {
|
|
@@ -636,7 +640,7 @@ function toListItem(item: ReferenceItem, state?: ReferenceListItem["state"]): Re
|
|
|
636
640
|
}
|
|
637
641
|
|
|
638
642
|
function toReferenceItem(item: ReferenceListItem): ReferenceItem {
|
|
639
|
-
return { name: item.name, path: item.path, dirty: item.dirty, branch: item.branch };
|
|
643
|
+
return { name: item.name, displayName: item.displayName, path: item.path, dirty: item.dirty, branch: item.branch };
|
|
640
644
|
}
|
|
641
645
|
|
|
642
646
|
async function cloneReference(
|
|
@@ -747,8 +751,10 @@ async function loadReferences(git: GitRunner): Promise<ReferenceItem[]> {
|
|
|
747
751
|
async function loadReference(git: GitRunner, name: string, path: string): Promise<ReferenceItem> {
|
|
748
752
|
const branch = await git.run(["branch", "--show-current"], { cwd: path, optional: true });
|
|
749
753
|
const commit = branch ? "" : await git.run(["rev-parse", "--short", "HEAD"], { cwd: path, optional: true });
|
|
754
|
+
const remoteUrl = await git.run(["config", "--get", "remote.origin.url"], { cwd: path, optional: true });
|
|
750
755
|
return {
|
|
751
756
|
name,
|
|
757
|
+
displayName: referenceDisplayName(remoteUrl, name),
|
|
752
758
|
path,
|
|
753
759
|
dirty: (await git.run(["status", "--porcelain=v1"], { cwd: path, optional: true })).length > 0,
|
|
754
760
|
branch: branch || (commit ? `detached ${commit}` : ""),
|
|
@@ -865,6 +871,30 @@ function referenceNameFromUrl(url: string): string {
|
|
|
865
871
|
return name;
|
|
866
872
|
}
|
|
867
873
|
|
|
874
|
+
export function referenceDisplayName(remoteUrl: string, fallback: string): string {
|
|
875
|
+
const remote = remoteUrl
|
|
876
|
+
.trim()
|
|
877
|
+
.replace(/[?#].*$/, "")
|
|
878
|
+
.replace(/\/+$/, "")
|
|
879
|
+
.replace(/\.git$/i, "");
|
|
880
|
+
if (!remote) return fallback;
|
|
881
|
+
|
|
882
|
+
let path: string;
|
|
883
|
+
try {
|
|
884
|
+
const parsed = new URL(remote);
|
|
885
|
+
if (!parsed.host) return fallback;
|
|
886
|
+
path = parsed.pathname;
|
|
887
|
+
} catch {
|
|
888
|
+
const separator = remote.indexOf(":");
|
|
889
|
+
if (separator <= 0 || remote.slice(0, separator).includes("/")) return fallback;
|
|
890
|
+
path = remote.slice(separator + 1);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const segments = path.split("/").filter(Boolean);
|
|
894
|
+
if (segments.length < 2) return fallback;
|
|
895
|
+
return segments.slice(-2).join("/");
|
|
896
|
+
}
|
|
897
|
+
|
|
868
898
|
function shellQuote(value: string): string {
|
|
869
899
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
870
900
|
}
|
|
@@ -36,7 +36,11 @@ Adds `/context` to set branch-local reusable repository work scopes from `.pi/co
|
|
|
36
36
|
|
|
37
37
|
## working-memory
|
|
38
38
|
|
|
39
|
-
Gives agent `working_memory` for selective hard checkpoints.
|
|
39
|
+
Gives agent `working_memory` for selective hard checkpoints. Every checkpoint retains at least one useful user message or visible assistant text. Requested source files return as structural outlines, deferred files remain cheap conditional reminders, and a continuation note carries conclusions extracted from exploration. Tool history and full file reads leave future model input without changing saved session. Advisory reminders begin at 40k active-context tokens. Run `/prune` to request reassessment manually.
|
|
40
|
+
|
|
41
|
+
## effort
|
|
42
|
+
|
|
43
|
+
Adds `/effort [low|medium|high]` to select effort and a provider from current logins. Tau selects provider’s best available model for tier, then tries its configured model fallback. `Ctrl+Shift+E` cycles tiers on current provider. Footer derives effort from current provider, model, and thinking level, and hides it when no configured tier matches.
|
|
40
44
|
|
|
41
45
|
## explore
|
|
42
46
|
|
|
@@ -66,10 +70,6 @@ Adds `/manage-sessions` to browse saved sessions and `/sweep` to archive or dele
|
|
|
66
70
|
|
|
67
71
|
Replaces separate edit/write operations with one multi-file `patch` tool. It can create, rewrite, edit, move, and delete files in one structured call. Fewer tool calls means fewer turns, and each avoided turn prevents the full chat context from being sent again. Tau keeps `patch` disabled and uses `edit` and `write` for xAI and Grok models.
|
|
68
72
|
|
|
69
|
-
## publish
|
|
70
|
-
|
|
71
|
-
Adds `/publish` to create a tagged release, trigger trusted npm publishing in GitHub Actions, and show its status. It recommends a semantic version bump from commits since the prior release tag, but you must confirm the release type and publish action. If publishing fails, the agent investigates with read-only diagnostics and recommends a solution without applying it or retrying the release.
|
|
72
|
-
|
|
73
73
|
## qna
|
|
74
74
|
|
|
75
75
|
Adds `/qna` for when the agent has asked you several questions in chat and you want a friendly UI for answering them on your own terms. It is only active when you manually run the command.
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Working Memory gives agent selective checkpoints without changing saved conversation.
|
|
4
4
|
|
|
5
|
-
`working_memory`
|
|
5
|
+
`working_memory` retains at least one referenced user message or visible assistant text. Keep task framing, constraints, decisions, and active work chain; continuation and file tiers support that conversation context. It has three file tiers:
|
|
6
|
+
|
|
7
|
+
- Auto-read full source into next turn when agent needs its body to decide, edit, or debug.
|
|
8
|
+
- Carry a structural outline when symbols and locations will support a later scoped read.
|
|
9
|
+
- Defer inactive files with reason and condition that makes them relevant again.
|
|
10
|
+
|
|
11
|
+
Use one tier for each path. `readFiles` publishes Tau's built-in autoread request; `outlineFiles` injects a structural outline; `deferFiles` records a conditional reminder.
|
|
12
|
+
|
|
13
|
+
`continuation` is a working note for next step: keep durable decisions, findings, unresolved questions, remaining work, and next action. It should carry enough reasoning to resume without rereading discarded results.
|
|
6
14
|
|
|
7
15
|
Automatic reminders begin at 40,000 active-context tokens and remain advisory. Run `/prune` to request reassessment manually.
|
|
8
16
|
|