@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.
@@ -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 ThinkingLevel, type Tool, Type } from "@earendil-works/pi-ai";
2
+ import { type Tool, Type } from "@earendil-works/pi-ai";
3
3
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
- import { generateToolValidated, generateValidated, resolveCandidates } from "../../shared/model-fallback/index.ts";
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 resolveCandidates(ctx, COMMIT_MODELS, true),
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 resolveCandidates(ctx, COMMIT_MODELS, true),
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
- .slice(index, index + EVIDENCE_CONCURRENCY)
120
- .map(async (file) => ({ ...file, evidence: await loadEvidenceForFile(git, root, file, hasHead) })),
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), ...messages] };
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
+ }
@@ -14,7 +14,7 @@ Replaces Pi’s default footer with Tau’s compact two-line footer.
14
14
  ## Layout
15
15
 
16
16
  ```text
17
- git • model • thinking S $0.18 · D $2.43
17
+ git • EFFORT • provider/model (thinking) S $0.18 · D $2.43
18
18
  cwd • session footer items
19
19
  ```
20
20
 
@@ -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 topLeft = [gitText(git), `${model} (${thinking})`].filter(Boolean).join(" • ");
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, theme.fg("dim", topLeft), topRight),
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 name = item.dirty ? `${item.name} *` : item.name;
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. Model-only references identify useful user messages and 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.
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` keeps referenced user messages and visible assistant text, carries requested source files as structural outlines, and records deferred files as conditional reminders. Tool history and full file reads leave model context; useful findings from them are distilled into a compact continuation note.
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