@shanepadgett/tau-agent 0.33.1 → 0.35.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/docs/context.md +29 -20
- package/docs/extending-tau-agent.md +63 -24
- package/extensions/appshot/index.ts +112 -111
- package/extensions/aside/README.md +7 -0
- package/extensions/aside/index.ts +168 -0
- package/extensions/aside/panel.ts +83 -0
- package/extensions/bash-approval/README.md +20 -0
- package/extensions/bash-approval/index.ts +235 -0
- package/extensions/bash-approval/settings.ts +24 -0
- package/extensions/branch/index.ts +38 -21
- package/extensions/cache-diagnostics/index.ts +70 -58
- package/extensions/checkpoint/README.md +9 -0
- package/extensions/checkpoint/checkpoint-budget.ts +66 -0
- package/extensions/checkpoint/checkpoint.ts +279 -0
- package/extensions/checkpoint/index.ts +102 -0
- package/extensions/checkpoint/messages.ts +170 -0
- package/extensions/checkpoint/prompt.ts +24 -0
- package/extensions/checkpoint/settings.ts +27 -0
- package/extensions/commit/commit-effort.ts +3 -3
- package/extensions/commit/commit-plan.ts +2 -2
- package/extensions/context/README.md +12 -8
- package/extensions/context/definitions.ts +178 -55
- package/extensions/context/index.ts +192 -160
- package/extensions/context/panel.ts +57 -37
- package/extensions/context/sync.ts +139 -89
- package/extensions/context/validation.ts +37 -25
- package/extensions/effort/README.md +1 -1
- package/extensions/effort/index.ts +3 -3
- package/extensions/effort/state.ts +3 -18
- package/extensions/explore/guidance.ts +6 -47
- package/extensions/explore/index.ts +38 -53
- package/extensions/explore/{ast/read → read}/hook.ts +12 -7
- package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
- package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
- package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
- package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
- package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
- package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
- package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
- package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
- package/extensions/explore/tools/target-params.ts +9 -0
- package/extensions/footer/index.ts +75 -56
- package/extensions/handoff/index.ts +8 -30
- package/extensions/image-gen/index.ts +46 -40
- package/extensions/patch/executor.ts +190 -135
- package/extensions/patch/matcher.ts +82 -48
- package/extensions/patch/parser.ts +149 -105
- package/extensions/patch/render.ts +32 -39
- package/extensions/patch/summary.ts +30 -26
- package/extensions/ready/README.md +7 -0
- package/extensions/ready/index.ts +58 -0
- package/extensions/ready/model.ts +128 -0
- package/extensions/ready/packs.ts +272 -0
- package/extensions/ready/render.ts +445 -0
- package/extensions/ready/scan.ts +760 -0
- package/extensions/reference/index.ts +2 -0
- package/extensions/reference/panel.ts +94 -31
- package/extensions/review/README.md +14 -6
- package/extensions/review/index.ts +58 -99
- package/extensions/review/model.ts +13 -33
- package/extensions/review/session.ts +9 -23
- package/extensions/runtime-context/context.ts +30 -26
- package/extensions/script-runner/README.md +2 -2
- package/extensions/script-runner/index.ts +122 -82
- package/extensions/silent-command-runner/index.ts +38 -23
- package/extensions/soul/README.md +2 -2
- package/extensions/soul/index.ts +4 -4
- package/extensions/soul/prompt.ts +6 -10
- package/extensions/soul/settings.ts +6 -3
- package/extensions/subagent/agents/context-sync.md +185 -33
- package/extensions/subagent/agents/scout.md +2 -2
- package/extensions/subagent/agents/web-research.md +2 -2
- package/extensions/subagent/agents.ts +137 -84
- package/extensions/subagent/cmux-dashboard.ts +39 -30
- package/extensions/subagent/index.ts +79 -59
- package/extensions/subagent/resume.ts +1 -1
- package/extensions/subagent/run.ts +308 -193
- package/extensions/subagent/runtime.ts +781 -343
- package/extensions/tau/index.ts +47 -38
- package/extensions/tau-help/help.md +20 -8
- package/extensions/tool-loader/README.md +4 -2
- package/extensions/tool-loader/index.ts +77 -52
- package/extensions/web/index.ts +6 -3
- package/extensions/web/webfetch.ts +114 -86
- package/package.json +3 -3
- package/schemas/tau.schema.json +38 -35
- package/shared/bounded-text-result.ts +1 -0
- package/shared/checkpoint-visibility.ts +9 -0
- package/shared/events.ts +28 -10
- package/shared/isolated-session.ts +72 -30
- package/shared/model-effort.ts +23 -22
- package/shared/model-fallback/index.ts +66 -41
- package/shared/settings/load.ts +45 -38
- package/shared/temporary-output-store.ts +16 -10
- package/{extensions/explore → src}/ast/adapter.ts +11 -2
- package/{extensions/explore → src}/ast/engine.ts +54 -38
- package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
- package/{extensions/explore → src}/ast/format/context.ts +3 -6
- package/src/ast/format/deps.ts +94 -0
- package/{extensions/explore → src}/ast/format/discover.ts +2 -2
- package/{extensions/explore → src}/ast/format/impact.ts +9 -11
- package/{extensions/explore → src}/ast/format/outline.ts +2 -2
- package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
- package/{extensions/explore → src}/ast/format/show.ts +1 -1
- package/src/ast/format/targeted-result.ts +20 -0
- package/src/ast/grammars/c_sharp.wasm +0 -0
- package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
- package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
- package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
- package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
- package/{extensions/explore → src}/ast/ir.ts +6 -0
- package/src/ast/languages/csharp-file-deps.ts +90 -0
- package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
- package/src/ast/languages/dotted-index.ts +131 -0
- package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
- package/src/ast/languages/fixtures/sample.cs +53 -0
- package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
- package/src/ast/languages/fixtures/sample.go +37 -0
- package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
- package/src/ast/languages/fixtures/sample.java +40 -0
- package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
- package/src/ast/languages/fixtures/sample.kt +31 -0
- package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
- package/src/ast/languages/fixtures/sample.md +23 -0
- package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
- package/src/ast/languages/fixtures/sample.rs +54 -0
- package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
- package/src/ast/languages/fixtures/sample.swift +45 -0
- package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
- package/src/ast/languages/fixtures/sample.ts +45 -0
- package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
- package/src/ast/languages/fixtures/sample.tsx +36 -0
- package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
- package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
- package/{extensions/explore → src}/ast/languages/go.ts +21 -3
- package/{extensions/explore → src}/ast/languages/java.ts +5 -38
- package/src/ast/languages/jvm-file-deps.ts +135 -0
- package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
- package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
- package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
- package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
- package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
- package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
- package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
- package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
- package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
- package/{extensions/explore → src}/ast/markdown.ts +72 -47
- package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
- package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
- package/{extensions/explore → src}/ast/queries/context.ts +116 -80
- package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
- package/src/ast/queries/impact.ts +291 -0
- package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
- package/{extensions/explore → src}/ast/queries/show.ts +46 -42
- package/{extensions/explore → src}/ast/query.ts +1 -1
- package/{extensions/explore → src}/ast/registry.ts +1 -2
- package/{extensions/explore → src}/ast/scan.ts +1 -1
- package/src/ast/session.ts +50 -0
- package/{extensions/explore → src}/ast/slice.ts +8 -0
- package/{extensions/explore → src/ast}/traverse.ts +32 -20
- package/src/file-injection/index.ts +547 -0
- package/src/image-generation/client.ts +50 -31
- package/src/image-generation/index.ts +177 -134
- package/src/index.ts +2 -0
- package/src/tool-loading/index.ts +58 -0
- package/extensions/context/evidence.ts +0 -517
- package/extensions/context/projection.ts +0 -141
- package/extensions/context/state.ts +0 -30
- package/extensions/context/write-scope.ts +0 -109
- package/extensions/explore/ast/format/deps.ts +0 -86
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
- package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
- package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
- package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
- package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
- package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
- package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
- package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
- package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
- package/extensions/explore/ast/languages/odin.ts +0 -407
- package/extensions/explore/ast/queries/impact.ts +0 -250
- package/extensions/explore/outline-injection.ts +0 -151
- package/extensions/explore/read/autoread.ts +0 -118
- package/extensions/review/panel.ts +0 -128
- package/extensions/working-memory/README.md +0 -17
- package/extensions/working-memory/checkpoint.ts +0 -242
- package/extensions/working-memory/index.ts +0 -341
- package/extensions/working-memory/memory.ts +0 -235
- package/extensions/working-memory/render.ts +0 -185
- package/extensions/working-memory/settings.ts +0 -38
- package/extensions/working-memory/state.ts +0 -158
- package/shared/autoread.ts +0 -234
- package/shared/context-messages.ts +0 -19
- package/shared/outline-injection.ts +0 -56
- /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
- /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
- /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
- /package/{extensions/explore → src}/ast/identity.ts +0 -0
- /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
- /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
|
@@ -1,36 +1,163 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
4
|
+
import { prepareFileInjection } from "@shanepadgett/tau-agent";
|
|
4
5
|
import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
|
|
5
6
|
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
7
|
+
import { findProjectRoot, loadContextEntries, type ContextEntry, type ContextShowTarget } from "./definitions.ts";
|
|
6
8
|
import { ContextPanel, ContextSyncStatusPanel } from "./panel.ts";
|
|
7
|
-
import { findProjectRoot, loadContextEntries, type ContextEntry } from "./definitions.ts";
|
|
8
|
-
import { hideContextSyncEvidenceTool, registerContextSyncEvidenceTool } from "./evidence.ts";
|
|
9
|
-
import {
|
|
10
|
-
buildContextProjection,
|
|
11
|
-
contextProjectionKey,
|
|
12
|
-
contextProjectionMessage,
|
|
13
|
-
removeLegacyContextMessages,
|
|
14
|
-
selectContextEntries,
|
|
15
|
-
} from "./projection.ts";
|
|
16
9
|
import contextSettings from "./settings.ts";
|
|
17
|
-
import {
|
|
18
|
-
import { runContextSync } from "./sync.ts";
|
|
10
|
+
import { type ContextSyncDetails, runContextSync } from "./sync.ts";
|
|
19
11
|
import { formatContextValidationFailure, validateContextCatalog } from "./validation.ts";
|
|
20
12
|
|
|
13
|
+
const CONTEXT_BRIEF_TYPE = "tau.context.brief";
|
|
14
|
+
|
|
15
|
+
const SHOW_RANK = {
|
|
16
|
+
signature: 0,
|
|
17
|
+
signatureWithDocs: 1,
|
|
18
|
+
declaration: 2,
|
|
19
|
+
declarationWithImports: 3,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
function collectInjectionPlan(selected: readonly ContextEntry[]): {
|
|
23
|
+
read: string[];
|
|
24
|
+
shows: ContextShowTarget[];
|
|
25
|
+
outline: string[];
|
|
26
|
+
references: string[];
|
|
27
|
+
} {
|
|
28
|
+
const read = new Set(selected.flatMap((entry) => entry.read));
|
|
29
|
+
const showByKey = new Map<string, ContextShowTarget>();
|
|
30
|
+
for (const entry of selected) {
|
|
31
|
+
for (const target of entry.show) {
|
|
32
|
+
if (read.has(target.path)) continue;
|
|
33
|
+
const key = `${target.path}\0${target.name}`;
|
|
34
|
+
const existing = showByKey.get(key);
|
|
35
|
+
if (!existing || SHOW_RANK[target.view] > SHOW_RANK[existing.view]) showByKey.set(key, target);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const shows = [...showByKey.values()].sort(
|
|
39
|
+
(left, right) => left.path.localeCompare(right.path) || left.name.localeCompare(right.name),
|
|
40
|
+
);
|
|
41
|
+
const outline = new Set(selected.flatMap((entry) => entry.outline).filter((path) => !read.has(path)));
|
|
42
|
+
const references = [
|
|
43
|
+
...new Set(selected.flatMap((entry) => entry.references).filter((path) => !read.has(path) && !outline.has(path))),
|
|
44
|
+
].sort((left, right) => left.localeCompare(right));
|
|
45
|
+
return {
|
|
46
|
+
read: [...read].sort((left, right) => left.localeCompare(right)),
|
|
47
|
+
shows,
|
|
48
|
+
outline: [...outline].sort((left, right) => left.localeCompare(right)),
|
|
49
|
+
references,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function contextBriefContent(selected: readonly ContextEntry[], references: readonly string[], failed: number): string {
|
|
54
|
+
const authority =
|
|
55
|
+
failed === 0
|
|
56
|
+
? "The complete files, show targets, and outlines that follow are current. Treat them as authoritative and do not read them again. Show rows are declaration slices only — do not assume the rest of the file is loaded. Use a ranged read for bodies an outline omits, and re-read a file only after you change it."
|
|
57
|
+
: "Successful complete-file, show, and outline rows that follow are current; failed rows contain no source context. Treat successful rows as authoritative and do not read them again. Show rows are declaration slices only — do not assume the rest of the file is loaded. Use a ranged read for bodies an outline omits, and re-read a file only after you change it.";
|
|
58
|
+
return [
|
|
59
|
+
"Active repository context, injected once from the current catalog:",
|
|
60
|
+
...selected.map((entry) => `- ${entry.id}: ${entry.description}`),
|
|
61
|
+
"",
|
|
62
|
+
"Unloaded references:",
|
|
63
|
+
...(references.length ? references.map((path) => `- ${path}`) : ["(none)"]),
|
|
64
|
+
"",
|
|
65
|
+
authority,
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function injectSelectedContext(
|
|
70
|
+
pi: ExtensionAPI,
|
|
71
|
+
ctx: ExtensionContext,
|
|
72
|
+
root: string,
|
|
73
|
+
selected: readonly ContextEntry[],
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const plan = collectInjectionPlan(selected);
|
|
76
|
+
const prepared = await prepareFileInjection(pi, {
|
|
77
|
+
cwd: root,
|
|
78
|
+
source: "context",
|
|
79
|
+
batchId: randomUUID(),
|
|
80
|
+
files: [
|
|
81
|
+
...plan.read.map((path) => ({ path, mode: "full" as const })),
|
|
82
|
+
...plan.shows.map((target) => ({
|
|
83
|
+
path: target.path,
|
|
84
|
+
mode: "show" as const,
|
|
85
|
+
name: target.name,
|
|
86
|
+
view: target.view,
|
|
87
|
+
})),
|
|
88
|
+
...plan.outline.map((path) => ({ path, mode: "outline" as const })),
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
const failed = prepared.filter((message) => message.details.status === "failed").length;
|
|
92
|
+
pi.sendMessage({
|
|
93
|
+
customType: CONTEXT_BRIEF_TYPE,
|
|
94
|
+
content: contextBriefContent(selected, plan.references, failed),
|
|
95
|
+
display: false,
|
|
96
|
+
});
|
|
97
|
+
for (const message of prepared) pi.sendMessage(message);
|
|
98
|
+
if (failed > 0) {
|
|
99
|
+
ctx.ui.notify(
|
|
100
|
+
`Injected ${prepared.length - failed} of ${prepared.length} files from ${selected.length} context entries; ${failed} failed`,
|
|
101
|
+
"warning",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasAbortedAssistant(messages: readonly unknown[]): boolean {
|
|
107
|
+
return messages.some(
|
|
108
|
+
(message) =>
|
|
109
|
+
typeof message === "object" &&
|
|
110
|
+
message !== null &&
|
|
111
|
+
"role" in message &&
|
|
112
|
+
message.role === "assistant" &&
|
|
113
|
+
"stopReason" in message &&
|
|
114
|
+
message.stopReason === "aborted",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function handleAgentEndValidation(
|
|
119
|
+
pi: ExtensionAPI,
|
|
120
|
+
ctx: ExtensionContext,
|
|
121
|
+
settings: typeof contextSettings.defaults,
|
|
122
|
+
lastValidationFailure: string | undefined,
|
|
123
|
+
): Promise<string | undefined> {
|
|
124
|
+
if (!settings.sync.enabled || !settings.validation.enabled || !ctx.isProjectTrusted()) return undefined;
|
|
125
|
+
const root = await findProjectRoot(ctx.cwd);
|
|
126
|
+
const git = createGitRunner(pi, ctx);
|
|
127
|
+
if (!(await loadRepoStatus(git))) return lastValidationFailure;
|
|
128
|
+
const failure = formatContextValidationFailure(
|
|
129
|
+
await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
|
|
130
|
+
);
|
|
131
|
+
if (!failure) return undefined;
|
|
132
|
+
if (failure === lastValidationFailure) return lastValidationFailure;
|
|
133
|
+
ctx.ui.notify("Context catalog validation failed; running context-sync", "error");
|
|
134
|
+
const result = ctx.mode === "tui" ? await runContextSyncWithEditor(pi, ctx, {}) : await runContextSync(pi, ctx, {});
|
|
135
|
+
if (!result || result.outcome === "cancelled") {
|
|
136
|
+
ctx.ui.notify("Context sync cancelled", "info");
|
|
137
|
+
return failure;
|
|
138
|
+
}
|
|
139
|
+
const afterFailure = formatContextValidationFailure(
|
|
140
|
+
await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
|
|
141
|
+
);
|
|
142
|
+
if (result.outcome === "failed" || afterFailure) {
|
|
143
|
+
ctx.ui.notify(
|
|
144
|
+
result.outcome === "failed" ? result.summary : "Context catalog still invalid after context-sync",
|
|
145
|
+
"error",
|
|
146
|
+
);
|
|
147
|
+
return afterFailure ?? `${failure}\n${result.reason}`;
|
|
148
|
+
}
|
|
149
|
+
ctx.ui.notify(result.summary, "info");
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
21
153
|
export default function contextExtension(pi: ExtensionAPI): void {
|
|
22
154
|
let settings = contextSettings.defaults;
|
|
23
155
|
let lastValidationFailure: string | undefined;
|
|
24
156
|
let syncCommandRegistered = false;
|
|
25
|
-
let lifecycleGeneration = 0;
|
|
26
|
-
let projectionCache: { key: string; content: string } | undefined;
|
|
27
|
-
|
|
28
|
-
registerContextSyncEvidenceTool(pi);
|
|
29
157
|
|
|
30
158
|
const refreshSettings = async (ctx: { cwd: string; isProjectTrusted(): boolean }) => {
|
|
31
159
|
settings = await loadTauExtensionSettings(ctx, contextSettings);
|
|
32
160
|
if (settings.sync.enabled) registerContextSyncCommand();
|
|
33
|
-
hideContextSyncEvidenceTool(pi);
|
|
34
161
|
};
|
|
35
162
|
|
|
36
163
|
const registerContextSyncCommand = () => {
|
|
@@ -48,45 +175,21 @@ export default function contextExtension(pi: ExtensionAPI): void {
|
|
|
48
175
|
return;
|
|
49
176
|
}
|
|
50
177
|
await ctx.waitForIdle();
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const stopListening = ctx.ui.onTerminalInput((data) => {
|
|
54
|
-
if (!getKeybindings().matches(data, "tui.select.cancel")) return;
|
|
55
|
-
if (!controller.signal.aborted) widget.update("Cancelling context sync");
|
|
56
|
-
controller.abort();
|
|
57
|
-
return { consume: true };
|
|
178
|
+
const result = await runContextSyncWithEditor(pi, ctx, {
|
|
179
|
+
nudge: args.trim() || undefined,
|
|
58
180
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
onStatus: (status) => {
|
|
64
|
-
widget.update(status);
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
if (controller.signal.aborted) {
|
|
68
|
-
ctx.ui.notify("Context sync cancelled", "info");
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const level = result.outcome === "failed" ? "error" : "info";
|
|
72
|
-
ctx.ui.notify(result.summary, level);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
ctx.ui.notify(
|
|
75
|
-
controller.signal.aborted
|
|
76
|
-
? "Context sync cancelled"
|
|
77
|
-
: `Context sync failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
78
|
-
controller.signal.aborted ? "info" : "error",
|
|
79
|
-
);
|
|
80
|
-
} finally {
|
|
81
|
-
stopListening();
|
|
82
|
-
widget.clear();
|
|
181
|
+
if (!result) return;
|
|
182
|
+
if (result.outcome === "cancelled") {
|
|
183
|
+
ctx.ui.notify(result.summary, "info");
|
|
184
|
+
return;
|
|
83
185
|
}
|
|
186
|
+
ctx.ui.notify(result.summary, result.outcome === "failed" ? "error" : "info");
|
|
84
187
|
},
|
|
85
188
|
});
|
|
86
189
|
};
|
|
87
190
|
|
|
88
191
|
pi.registerCommand("context", {
|
|
89
|
-
description: "
|
|
192
|
+
description: "Inject repository context entries into the conversation",
|
|
90
193
|
handler: async (_args, ctx) => {
|
|
91
194
|
if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
|
|
92
195
|
ctx.ui.notify("/context requires a trusted TUI project", "warning");
|
|
@@ -99,21 +202,15 @@ export default function contextExtension(pi: ExtensionAPI): void {
|
|
|
99
202
|
ctx.ui.notify(`No context entries found in ${join(root, ".pi", "contexts")}`, "warning");
|
|
100
203
|
return;
|
|
101
204
|
}
|
|
102
|
-
const active = replayContextSelection(ctx.sessionManager.getBranch()).entryIds;
|
|
103
205
|
const selected = await ctx.ui.custom<ContextEntry[] | undefined>(
|
|
104
|
-
(tui, theme, _keys, done) => new ContextPanel(tui, theme, entries,
|
|
206
|
+
(tui, theme, _keys, done) => new ContextPanel(tui, theme, entries, done),
|
|
105
207
|
{
|
|
106
208
|
overlay: true,
|
|
107
209
|
overlayOptions: { anchor: "top-center", width: "70%", minWidth: 64, maxHeight: "80%", margin: 2 },
|
|
108
210
|
},
|
|
109
211
|
);
|
|
110
|
-
if (selected === undefined) return;
|
|
111
|
-
pi
|
|
112
|
-
projectionCache = undefined;
|
|
113
|
-
ctx.ui.notify(
|
|
114
|
-
selected.length ? `${selected.length} context entries active` : "Active context cleared",
|
|
115
|
-
"info",
|
|
116
|
-
);
|
|
212
|
+
if (selected === undefined || selected.length === 0) return;
|
|
213
|
+
await injectSelectedContext(pi, ctx, root, selected);
|
|
117
214
|
},
|
|
118
215
|
});
|
|
119
216
|
|
|
@@ -121,124 +218,59 @@ export default function contextExtension(pi: ExtensionAPI): void {
|
|
|
121
218
|
if (settings.sync.enabled) registerContextSyncCommand();
|
|
122
219
|
|
|
123
220
|
pi.on("session_start", async (_event, ctx) => {
|
|
124
|
-
lifecycleGeneration += 1;
|
|
125
|
-
projectionCache = undefined;
|
|
126
221
|
await refreshSettings(ctx);
|
|
127
222
|
});
|
|
128
|
-
pi.on("context", async (event, ctx) => {
|
|
129
|
-
const messages = removeLegacyContextMessages(event.messages);
|
|
130
|
-
if (!ctx.isProjectTrusted()) return { messages };
|
|
131
|
-
const entryIds = replayContextSelection(ctx.sessionManager.getBranch()).entryIds;
|
|
132
|
-
if (entryIds.length === 0) return { messages };
|
|
133
|
-
const generation = lifecycleGeneration;
|
|
134
|
-
const root = await findProjectRoot(ctx.cwd);
|
|
135
|
-
const selection = selectContextEntries(await loadContextEntries(root), entryIds);
|
|
136
|
-
const key = await contextProjectionKey(root, selection, ctx.signal, () => generation === lifecycleGeneration);
|
|
137
|
-
if (generation !== lifecycleGeneration) return { messages };
|
|
138
|
-
if (projectionCache?.key !== key) {
|
|
139
|
-
projectionCache = {
|
|
140
|
-
key,
|
|
141
|
-
content: await buildContextProjection(
|
|
142
|
-
pi,
|
|
143
|
-
root,
|
|
144
|
-
selection,
|
|
145
|
-
ctx.signal,
|
|
146
|
-
() => generation === lifecycleGeneration,
|
|
147
|
-
),
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
return { messages: [...messages, contextProjectionMessage(projectionCache.content)] };
|
|
151
|
-
});
|
|
152
223
|
pi.on("agent_start", async (_event, ctx) => {
|
|
153
224
|
await refreshSettings(ctx);
|
|
154
225
|
});
|
|
155
226
|
pi.on("agent_end", async (event, ctx) => {
|
|
156
|
-
if (
|
|
157
|
-
event.messages.some(
|
|
158
|
-
(message) =>
|
|
159
|
-
typeof message === "object" &&
|
|
160
|
-
message !== null &&
|
|
161
|
-
"role" in message &&
|
|
162
|
-
message.role === "assistant" &&
|
|
163
|
-
"stopReason" in message &&
|
|
164
|
-
message.stopReason === "aborted",
|
|
165
|
-
)
|
|
166
|
-
)
|
|
167
|
-
return;
|
|
227
|
+
if (hasAbortedAssistant(event.messages)) return;
|
|
168
228
|
settings = await loadTauExtensionSettings(ctx, contextSettings);
|
|
169
|
-
if (!settings.sync.enabled || !settings.validation.enabled || !ctx.isProjectTrusted()) {
|
|
170
|
-
lastValidationFailure = undefined;
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
229
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
230
|
+
lastValidationFailure = await handleAgentEndValidation(pi, ctx, settings, lastValidationFailure);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
ctx.ui.notify(`Context validation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function runContextSyncWithEditor(
|
|
238
|
+
pi: ExtensionAPI,
|
|
239
|
+
ctx: ExtensionContext,
|
|
240
|
+
options: { nudge?: string },
|
|
241
|
+
): Promise<ContextSyncDetails | undefined> {
|
|
242
|
+
return ctx.ui.custom<ContextSyncDetails | undefined>((tui, theme, _keys, done) => {
|
|
243
|
+
const panel = new ContextSyncStatusPanel(tui, theme, "Synchronizing repository context");
|
|
244
|
+
void (async () => {
|
|
189
245
|
try {
|
|
190
246
|
const result = await runContextSync(pi, ctx, {
|
|
247
|
+
nudge: options.nudge,
|
|
248
|
+
signal: panel.signal,
|
|
191
249
|
onStatus: (status) => {
|
|
192
|
-
|
|
250
|
+
panel.update(status);
|
|
193
251
|
},
|
|
194
252
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
);
|
|
253
|
+
done(result);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (panel.signal.aborted) {
|
|
256
|
+
done({
|
|
257
|
+
outcome: "cancelled",
|
|
258
|
+
summary: "Context sync cancelled",
|
|
259
|
+
reason: "Cancelled by user.",
|
|
260
|
+
changedContextFiles: [],
|
|
261
|
+
});
|
|
204
262
|
return;
|
|
205
263
|
}
|
|
206
|
-
|
|
207
|
-
|
|
264
|
+
done({
|
|
265
|
+
outcome: "failed",
|
|
266
|
+
summary: `Context sync failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
267
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
268
|
+
changedContextFiles: [],
|
|
269
|
+
});
|
|
208
270
|
} finally {
|
|
209
|
-
|
|
271
|
+
panel.dispose();
|
|
210
272
|
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
pi.on("session_tree", () => {
|
|
216
|
-
lifecycleGeneration += 1;
|
|
217
|
-
projectionCache = undefined;
|
|
218
|
-
});
|
|
219
|
-
pi.on("session_compact", () => {
|
|
220
|
-
lifecycleGeneration += 1;
|
|
221
|
-
projectionCache = undefined;
|
|
222
|
-
});
|
|
223
|
-
pi.on("session_shutdown", () => {
|
|
224
|
-
lifecycleGeneration += 1;
|
|
225
|
-
projectionCache = undefined;
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function showContextSyncWidget(ctx: ExtensionContext): { update(status: string): void; clear(): void } {
|
|
230
|
-
let widget: ContextSyncStatusPanel | undefined;
|
|
231
|
-
ctx.ui.setWidget("context-sync", (tui, theme) => {
|
|
232
|
-
widget = new ContextSyncStatusPanel(tui, theme, "Synchronizing repository context");
|
|
233
|
-
return widget;
|
|
273
|
+
})();
|
|
274
|
+
return panel;
|
|
234
275
|
});
|
|
235
|
-
return {
|
|
236
|
-
update(status) {
|
|
237
|
-
widget?.update(status);
|
|
238
|
-
},
|
|
239
|
-
clear() {
|
|
240
|
-
ctx.ui.setWidget("context-sync", undefined);
|
|
241
|
-
widget = undefined;
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
276
|
}
|
|
@@ -21,7 +21,6 @@ export class ContextPanel implements Component {
|
|
|
21
21
|
tui: TUI,
|
|
22
22
|
theme: Theme,
|
|
23
23
|
entries: readonly ContextEntry[],
|
|
24
|
-
activeEntryIds: readonly string[],
|
|
25
24
|
done: (result: ContextEntry[] | undefined) => void,
|
|
26
25
|
) {
|
|
27
26
|
const firstEntry = entries[0];
|
|
@@ -30,7 +29,8 @@ export class ContextPanel implements Component {
|
|
|
30
29
|
this.theme = theme;
|
|
31
30
|
this.done = done;
|
|
32
31
|
this.current = firstEntry;
|
|
33
|
-
|
|
32
|
+
// loadContextEntries already emits tabs in NN_ folder order; keep first-seen order.
|
|
33
|
+
const tabNames = [...new Set(entries.map((entry) => entry.tab))];
|
|
34
34
|
this.tabs = new Tabs(
|
|
35
35
|
theme,
|
|
36
36
|
tabNames.map((tab) => {
|
|
@@ -68,42 +68,43 @@ export class ContextPanel implements Component {
|
|
|
68
68
|
border: "box",
|
|
69
69
|
};
|
|
70
70
|
this.panel = new ToolPanel(theme, this.config);
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private handlePanelChromeKey(data: string): boolean {
|
|
74
|
+
const keys = getKeybindings();
|
|
75
|
+
const paths = this.currentPaths();
|
|
76
|
+
const pageSize = this.pathPageSize();
|
|
77
|
+
if (paths.length > pageSize && matchesKey(data, Key.alt("up"))) {
|
|
78
|
+
this.fileOffset = Math.max(0, this.fileOffset - pageSize);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (paths.length > pageSize && matchesKey(data, Key.alt("down"))) {
|
|
82
|
+
this.fileOffset = Math.min(paths.length - pageSize, this.fileOffset + pageSize);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
86
|
+
for (const [tab, tabList] of this.lists) {
|
|
87
|
+
tabList.setSelectedIds([]);
|
|
88
|
+
this.selected.set(tab, []);
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (keys.matches(data, "tui.select.confirm")) {
|
|
93
|
+
this.done([...this.selected.values()].flatMap((items) => [...items]));
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (keys.matches(data, "tui.select.cancel")) {
|
|
97
|
+
this.done(undefined);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
73
101
|
}
|
|
74
102
|
|
|
75
103
|
handleInput(data: string): void {
|
|
76
104
|
const list = this.activeList();
|
|
77
|
-
if (!list?.isFilterFocused()) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const pageSize = this.pathPageSize();
|
|
81
|
-
if (paths.length > pageSize && matchesKey(data, Key.alt("up"))) {
|
|
82
|
-
this.fileOffset = Math.max(0, this.fileOffset - pageSize);
|
|
83
|
-
this.sync();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (paths.length > pageSize && matchesKey(data, Key.alt("down"))) {
|
|
87
|
-
this.fileOffset = Math.min(paths.length - pageSize, this.fileOffset + pageSize);
|
|
88
|
-
this.sync();
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (matchesKey(data, Key.ctrl("c"))) {
|
|
92
|
-
for (const [tab, tabList] of this.lists) {
|
|
93
|
-
tabList.setSelectedIds([]);
|
|
94
|
-
this.selected.set(tab, []);
|
|
95
|
-
}
|
|
96
|
-
this.sync();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (keys.matches(data, "tui.select.confirm")) {
|
|
100
|
-
this.done([...this.selected.values()].flatMap((items) => [...items]));
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (keys.matches(data, "tui.select.cancel")) {
|
|
104
|
-
this.done(undefined);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
105
|
+
if (!list?.isFilterFocused() && this.handlePanelChromeKey(data)) {
|
|
106
|
+
this.sync();
|
|
107
|
+
return;
|
|
107
108
|
}
|
|
108
109
|
this.tabs.handleInput(data);
|
|
109
110
|
const current = this.activeList()?.getCurrentItem() ?? this.current;
|
|
@@ -154,9 +155,16 @@ export class ContextPanel implements Component {
|
|
|
154
155
|
invalidate: () => this.tabs.invalidate(),
|
|
155
156
|
};
|
|
156
157
|
}
|
|
157
|
-
private currentPaths(): Array<{ kind: "read" | "outline" | "reference"; path: string }> {
|
|
158
|
+
private currentPaths(): Array<{ kind: "read" | "show" | "outline" | "reference"; path: string }> {
|
|
158
159
|
return [
|
|
159
160
|
...this.current.read.map((path) => ({ kind: "read" as const, path })),
|
|
161
|
+
...this.current.show.map((target) => ({
|
|
162
|
+
kind: "show" as const,
|
|
163
|
+
path:
|
|
164
|
+
target.view === "declaration"
|
|
165
|
+
? `${target.name} · ${target.path}`
|
|
166
|
+
: `${target.name} [${target.view}] · ${target.path}`,
|
|
167
|
+
})),
|
|
160
168
|
...this.current.outline.map((path) => ({ kind: "outline" as const, path })),
|
|
161
169
|
...this.current.references.map((path) => ({ kind: "reference" as const, path })),
|
|
162
170
|
];
|
|
@@ -168,7 +176,7 @@ export class ContextPanel implements Component {
|
|
|
168
176
|
return this.currentPaths().length > pageSize ? Math.max(1, pageSize - 1) : pageSize;
|
|
169
177
|
}
|
|
170
178
|
private secondary(): string {
|
|
171
|
-
return `${[...this.selected.values()].reduce((sum, items) => sum + items.length, 0)} selected · ${this.current.read.length} read · ${this.current.outline.length} outline · ${this.current.references.length} references`;
|
|
179
|
+
return `${[...this.selected.values()].reduce((sum, items) => sum + items.length, 0)} selected · ${this.current.read.length} read · ${this.current.show.length} show · ${this.current.outline.length} outline · ${this.current.references.length} references`;
|
|
172
180
|
}
|
|
173
181
|
private hints() {
|
|
174
182
|
const list = this.activeList();
|
|
@@ -193,6 +201,7 @@ export class ContextSyncStatusPanel implements Component {
|
|
|
193
201
|
private readonly tui: TUI;
|
|
194
202
|
private readonly panel: ToolPanel;
|
|
195
203
|
private readonly config: ToolPanelConfig;
|
|
204
|
+
private readonly controller = new AbortController();
|
|
196
205
|
private readonly startedAt = Date.now();
|
|
197
206
|
private readonly timer: ReturnType<typeof setInterval>;
|
|
198
207
|
private lines: string[] = [];
|
|
@@ -214,7 +223,7 @@ export class ContextSyncStatusPanel implements Component {
|
|
|
214
223
|
},
|
|
215
224
|
invalidate: () => {},
|
|
216
225
|
},
|
|
217
|
-
footer: { kind: "hints", hints: [] },
|
|
226
|
+
footer: { kind: "hints", hints: [bindingHint("tui.select.cancel", "cancel")] },
|
|
218
227
|
border: "horizontal",
|
|
219
228
|
};
|
|
220
229
|
this.panel = new ToolPanel(theme, this.config);
|
|
@@ -223,6 +232,17 @@ export class ContextSyncStatusPanel implements Component {
|
|
|
223
232
|
this.timer.unref();
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
get signal(): AbortSignal {
|
|
236
|
+
return this.controller.signal;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
handleInput(data: string): void {
|
|
240
|
+
if (!getKeybindings().matches(data, "tui.select.cancel")) return;
|
|
241
|
+
if (this.controller.signal.aborted) return;
|
|
242
|
+
this.update("Cancelling context sync");
|
|
243
|
+
this.controller.abort();
|
|
244
|
+
}
|
|
245
|
+
|
|
226
246
|
update(status: string): void {
|
|
227
247
|
const added = status
|
|
228
248
|
.split(/\r?\n/)
|