@shanepadgett/tau-agent 0.33.0 → 0.34.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/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 +79 -0
- package/extensions/checkpoint/checkpoint.ts +277 -0
- package/extensions/checkpoint/index.ts +102 -0
- package/extensions/checkpoint/messages.ts +169 -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/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/panel.ts +28 -16
- package/extensions/review/index.ts +63 -51
- package/extensions/review/panel.ts +5 -34
- package/extensions/runtime-context/context.ts +30 -26
- package/extensions/script-runner/README.md +2 -2
- package/extensions/script-runner/index.ts +130 -93
- package/extensions/silent-command-runner/index.ts +38 -23
- package/extensions/subagent/agents/context-sync.md +185 -33
- package/extensions/subagent/agents/scout.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 +304 -193
- package/extensions/subagent/runtime.ts +781 -343
- package/extensions/tau/index.ts +47 -38
- package/extensions/tau-help/help.md +14 -6
- 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 +20 -33
- 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 +71 -29
- package/shared/model-effort.ts +9 -19
- 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/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
package/docs/context.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# Context management
|
|
2
2
|
|
|
3
|
-
Tau uses `.pi/contexts` as a reusable map of the repository.
|
|
3
|
+
Tau uses `.pi/contexts` as a reusable map of the repository. Selecting a context injects a small bootstrap for a work scope and points toward related files without loading everything.
|
|
4
4
|
|
|
5
5
|
## Structure
|
|
6
6
|
|
|
7
7
|
The catalog has three levels:
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
.pi/contexts/<
|
|
11
|
-
|
|
10
|
+
.pi/contexts/<NN_domain>/<concept>.toml
|
|
11
|
+
└── [entry]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
- **Domain**: a stable top-level area of the repository, such as `commerce`, `platform`, or `documentation`.
|
|
14
|
+
- **Domain**: a stable top-level area of the repository, such as `commerce`, `platform`, or `documentation`. Domain folders are named `NN_slug` with a two-digit order (`01_extensions`, `02_core`, …). `/context` sorts tabs by that prefix and shows the slug only.
|
|
15
15
|
- **Concept**: a coherent subsystem or capability inside a domain. Each concept is one TOML file.
|
|
16
16
|
- **Entry**: a selectable work scope inside that concept. Each TOML section defines one entry.
|
|
17
17
|
|
|
18
|
-
Domain
|
|
18
|
+
Domain slugs (after `NN_`), concept filenames, and entry section names use lowercase kebab-case. Orders are contiguous from `01` with no gaps or duplicate slugs. Entry ids use the slug (`extensions/checkpoint/checkpoint-tool`), not the folder prefix.
|
|
19
19
|
|
|
20
20
|
```toml
|
|
21
21
|
name = "Checkout"
|
|
@@ -23,45 +23,54 @@ description = "Checkout calculation and order submission"
|
|
|
23
23
|
|
|
24
24
|
[orchestration]
|
|
25
25
|
description = "Building and submitting a checkout order"
|
|
26
|
-
read = []
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
read = ["src/checkout/order.ts"]
|
|
27
|
+
show = [
|
|
28
|
+
{ path = "src/payments/client.ts", name = "submitPayment" },
|
|
29
|
+
]
|
|
30
|
+
outline = ["src/checkout/service.ts"]
|
|
31
|
+
references = ["test/checkout/service.test.ts"]
|
|
29
32
|
|
|
30
33
|
[discounts]
|
|
31
34
|
description = "Applying coupons and account discounts"
|
|
32
|
-
read = []
|
|
33
|
-
|
|
35
|
+
read = ["src/checkout/discounts.ts"]
|
|
36
|
+
show = []
|
|
37
|
+
outline = []
|
|
34
38
|
references = ["test/checkout/discounts.test.ts"]
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
Saved as `.pi/contexts/
|
|
41
|
+
Saved as `.pi/contexts/01_commerce/checkout.toml`, these entries have the IDs `commerce/checkout/orchestration` and `commerce/checkout/discounts`.
|
|
38
42
|
|
|
39
43
|
## Build a useful taxonomy
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
Each entry is a **work pack**: enough primary material that selecting only that entry lets an agent start one recurring job with little search. Taxonomy groups packs; it is not a file index. Gold-standard example: `.pi/contexts/01_extensions/checkpoint.toml`.
|
|
46
|
+
|
|
47
|
+
Classify from the top down:
|
|
42
48
|
|
|
43
49
|
1. **Domain:** Which stable product or technical area owns this work?
|
|
44
50
|
2. **Concept:** Which subsystem inside that area has a clear shared purpose?
|
|
45
|
-
3. **Entry:** Which
|
|
51
|
+
3. **Entry:** Which job would someone select on purpose, and which files form its start pack?
|
|
46
52
|
|
|
47
53
|
Reuse existing terms when they still describe the code honestly. Create, move, split, or merge taxonomy when ownership or subsystem boundaries change. Directory layout can inform the decision, though domains and concepts should describe responsibility rather than copy the source tree.
|
|
48
54
|
|
|
49
|
-
Keep entries focused. Names such as `misc`, `shared`, and `
|
|
55
|
+
Keep entries focused on jobs. Names such as `misc`, `shared`, `other`, and a lone `[feature]` bag hide missing boundaries. A broad `all` entry can support deliberate subsystem-wide work, but focused job entries should remain available. If an entry keeps collecting unrelated paths, split it by work scope. If several tiny entries are always selected together, merge them. Overlap across entries is fine when two jobs share files.
|
|
50
56
|
|
|
51
57
|
Catalog durable code, configuration, tests, standards, and long-lived documentation. Leave scratch files, working plans, interviews, generated output, and rough ideas out of the catalog.
|
|
52
58
|
|
|
53
59
|
## Choose loading modes
|
|
54
60
|
|
|
55
|
-
- `read` supplies exact complete file contents.
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
61
|
+
- `read` supplies exact complete file contents. Default for code the agent must edit or deeply understand for that work scope. Prefer complete reads on clean, bounded modules.
|
|
62
|
+
- `show` supplies one resolved declaration slice (`path` + `name`, optional `view`) through Explore. Use it for a fat neighbor's thin contract — not to microslice a file you should just `read`. Default `view` is `declaration`. Allowed views: `signature`, `signatureWithDocs`, `declaration`, `declarationWithImports`.
|
|
63
|
+
- `outline` supplies structural outlines through Explore. Use it when a full read would drown the job (large or noisy files), not as the default for small clean modules.
|
|
64
|
+
- `references` supplies unloaded navigation paths. Default for secondary code, tests, callers, and spill edges the agent should not load unless the task expands.
|
|
65
|
+
|
|
66
|
+
Every entry declares all four arrays, including empty arrays. Keep injected material small enough that one selected entry is a start pack for real work. A path may appear in only one of `read`, `outline`, and `references`. The same path may also appear in `show` with `outline` or `references` (structure or nav plus hot symbols). When selected entries disagree, precedence is `read`, then `show`, then `outline`, then `references`. A full `read` drops `show` targets for that path.
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
Do not store raw line ranges in the catalog — they drift. `show` stores durable symbol identity and resolves to current lines at inject time.
|
|
60
69
|
|
|
61
|
-
`/context`
|
|
70
|
+
`/context` injects the selected entries once, as soon as you confirm the selector. Tau adds one hidden note naming the selected entries and their `references`, then appends complete `read` files, `show` declaration slices, and `outline` structures as visible rows. Those messages stay in the conversation exactly as injected, so the prompt prefix stays cacheable. Run `/context` again to inject more entries.
|
|
62
71
|
|
|
63
72
|
## Maintain the map
|
|
64
73
|
|
|
65
74
|
Inspect the existing catalog before placing new paths. Re-evaluate domain, concept, and entry boundaries after moves, ownership changes, or a coherent batch of new work; avoid stuffing paths into the nearest existing bucket.
|
|
66
75
|
|
|
67
|
-
Run `/context-sync` to update the catalog from uncommitted repository changes. Tau can also delegate to the `context-sync` subagent when automation is enabled.
|
|
76
|
+
Run `/context-sync` to update the catalog from uncommitted repository changes (editor is replaced until it finishes; Escape or Ctrl+C cancels). Tau can also delegate to the `context-sync` subagent when automation is enabled. The agent uses normal repo tools, checks every eligible changed file for membership, removes stale paths, and re-evaluates taxonomy and work-pack quality (including `read` / `show` / `outline` / `references`) before editing `.pi/contexts`. Nudge with `/context-sync <note>` when you want a quality rewrite of weak bags, not only dirty-path coverage.
|
|
@@ -32,45 +32,84 @@ Fields:
|
|
|
32
32
|
|
|
33
33
|
The promise resolves after publication with the generated bytes, absolute path, provider, model, operation, MIME type, width, and height. The bytes are the same bytes written to `result.path`, so callers can attach the image without reopening the file. Failures reject with a bounded, sanitized error. Cancellation before publication rejects with the signal's abort reason and removes staged files.
|
|
34
34
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
External event integration uses Pi's native `pi.events` bus. The caller and Tau Agent must be loaded in the same Pi runtime. Event callers use string channel names and documented payloads. They do not import Tau Agent internals.
|
|
38
|
-
|
|
39
|
-
Only events documented in this file are public. Extensions run trusted in-process; event emitters can ask Tau Agent to do work.
|
|
40
|
-
|
|
41
|
-
Related:
|
|
42
|
-
|
|
43
|
-
- [Custom subagents](./subagents.md)
|
|
44
|
-
- [TUI components](./tui.md)
|
|
45
|
-
|
|
46
|
-
## `tau:autoread.requested`
|
|
35
|
+
## File injection
|
|
47
36
|
|
|
48
|
-
|
|
37
|
+
`injectFiles()` reads or outlines files and injects them as visible `tau.file` messages, in request order. `prepareFileInjection(pi, request)` builds the same messages without sending them, for callers that need to inspect or append the prepared rows themselves. Both APIs use the supplied Pi runtime to reach Explore's runtime-scoped outline provider.
|
|
49
38
|
|
|
50
39
|
```ts
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
import { injectFiles } from "@shanepadgett/tau-agent";
|
|
41
|
+
|
|
42
|
+
await injectFiles(pi, {
|
|
54
43
|
cwd: ctx.cwd,
|
|
44
|
+
source: "my-extension",
|
|
55
45
|
batchId,
|
|
56
|
-
files: [
|
|
46
|
+
files: [
|
|
47
|
+
{ path: "src/server.ts", mode: "full" },
|
|
48
|
+
{ path: "src/router.ts", mode: "outline" },
|
|
49
|
+
],
|
|
57
50
|
});
|
|
58
51
|
```
|
|
59
52
|
|
|
60
53
|
Fields:
|
|
61
54
|
|
|
62
|
-
- `source`: caller identifier shown in Tau metadata.
|
|
63
|
-
- `title`: optional display/context label.
|
|
64
55
|
- `cwd`: root used to resolve file paths.
|
|
65
|
-
- `
|
|
56
|
+
- `source`: caller identifier recorded on each message.
|
|
57
|
+
- `batchId`: groups the visible rows produced by one call.
|
|
66
58
|
- `files[].path`: file path relative to `cwd`.
|
|
59
|
+
- `files[].mode`: `full` injects complete contents, `outline` injects structure, `auto` injects contents unless the file is larger than Explore's structural read threshold.
|
|
60
|
+
- `signal`: optional cancellation signal.
|
|
67
61
|
|
|
68
62
|
Behavior:
|
|
69
63
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
|
|
64
|
+
- Messages are sent in request order and resolve to the prepared messages.
|
|
65
|
+
- Missing, unreadable, or oversized files produce visible failed rows instead of rejecting.
|
|
66
|
+
- Outlining requires Tau's Explore extension to be loaded.
|
|
67
|
+
|
|
68
|
+
## Deferred tool groups
|
|
69
|
+
|
|
70
|
+
Extensions can register tools with Tau's `load_tools` registry while keeping their schemas out of the active tool set until the agent needs them. The extension and Tau must run in the same Pi runtime.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
74
|
+
import { registerDeferredToolGroup } from "@shanepadgett/tau-agent";
|
|
75
|
+
import { Type } from "typebox";
|
|
76
|
+
|
|
77
|
+
const confluenceSearch = defineTool({
|
|
78
|
+
name: "confluence_search",
|
|
79
|
+
label: "Confluence Search",
|
|
80
|
+
description: "Search and read Confluence pages.",
|
|
81
|
+
parameters: Type.Object({ query: Type.String() }),
|
|
82
|
+
async execute(_toolCallId, params) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: await searchConfluence(params.query) }],
|
|
85
|
+
details: {},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export default function confluenceExtension(pi: ExtensionAPI): void {
|
|
91
|
+
registerDeferredToolGroup(pi, {
|
|
92
|
+
id: "confluence",
|
|
93
|
+
description: "Search and read Confluence pages",
|
|
94
|
+
tools: [confluenceSearch],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`registerDeferredToolGroup()` registers tool definitions with Pi and exposes the group through `load_tools`. Call it during extension initialization. Project-local and global package extensions use the same API. `id` must be unique in the runtime, and tool names must be unique within the group.
|
|
100
|
+
|
|
101
|
+
Deferred loading affects model-visible tool schemas, not JavaScript package loading. Initialize expensive clients, authentication, and network connections inside tool execution when possible. Pi handles provider-specific deferred-tool behavior after Tau additively activates the group.
|
|
102
|
+
|
|
103
|
+
## Events
|
|
104
|
+
|
|
105
|
+
External event integration uses Pi's native `pi.events` bus. The caller and Tau Agent must be loaded in the same Pi runtime. Event callers use string channel names and documented payloads. They do not import Tau Agent internals.
|
|
106
|
+
|
|
107
|
+
Only events documented in this file are public. Extensions run trusted in-process; event emitters can ask Tau Agent to do work.
|
|
108
|
+
|
|
109
|
+
Related:
|
|
110
|
+
|
|
111
|
+
- [Custom subagents](./subagents.md)
|
|
112
|
+
- [TUI components](./tui.md)
|
|
74
113
|
|
|
75
114
|
## `tau:footer-item`
|
|
76
115
|
|
|
@@ -11,6 +11,7 @@ import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
|
|
11
11
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
12
|
import { type Static, Type } from "typebox";
|
|
13
13
|
import { createToolRowStateStore, formatToolRowTitle } from "../../shared/tool-row-state.js";
|
|
14
|
+
import { registerDeferredToolGroup } from "../../src/tool-loading/index.ts";
|
|
14
15
|
import { createNativeHelper, type RunHelper } from "./native-helper.ts";
|
|
15
16
|
|
|
16
17
|
const MAX_PNG_BYTES = 12 * 1024 * 1024;
|
|
@@ -105,121 +106,121 @@ function encodeWindowList(windows: WindowInfo[]): string {
|
|
|
105
106
|
function registerAppshotTools(pi: ExtensionAPI, runHelper: RunHelper): void {
|
|
106
107
|
const rowState = createToolRowStateStore(pi, "appshot.tool-row-state");
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
const listWindowsTool = defineTool<typeof listWindowsSchema, undefined>({
|
|
110
|
+
name: "list_windows",
|
|
111
|
+
label: "List Windows",
|
|
112
|
+
description:
|
|
113
|
+
"List visible normal macOS windows as compact JSON with window IDs, titles, application identity, process IDs, and bounds. Use list_windows to discover exact window IDs and application PIDs before screenshot_window or activate_app. Requires macOS 14 or newer and Screen & System Audio Recording permission.",
|
|
114
|
+
parameters: listWindowsSchema,
|
|
115
|
+
async execute(_toolCallId, _params, signal) {
|
|
116
|
+
if (process.platform !== "darwin") throw new Error("list_windows is only available on macOS");
|
|
117
|
+
const result = await runHelper(["list"], signal, 30_000);
|
|
118
|
+
if (result.code !== 0)
|
|
119
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || "Window listing failed");
|
|
120
|
+
let parsed: unknown;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(result.stdout);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error("Window listing helper returned invalid data");
|
|
125
|
+
}
|
|
126
|
+
if (!Array.isArray(parsed) || !parsed.every(isWindowInfo)) {
|
|
127
|
+
throw new Error("Window listing helper returned invalid data");
|
|
128
|
+
}
|
|
129
|
+
return { content: [{ type: "text", text: encodeWindowList(parsed) }], details: undefined };
|
|
130
|
+
},
|
|
131
|
+
renderCall(_args, theme, context) {
|
|
132
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
133
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
134
|
+
text.setText(formatToolRowTitle(rowState, context.toolCallId, "list_windows", theme));
|
|
135
|
+
return text;
|
|
136
|
+
},
|
|
137
|
+
renderResult(result, _options, _theme, context) {
|
|
138
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
139
|
+
const content = result.content.find((item) => item.type === "text");
|
|
140
|
+
text.setText(context.expanded && content?.type === "text" ? content.text : "");
|
|
141
|
+
return text;
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const screenshotWindowTool = defineTool<typeof screenshotWindowSchema, ScreenshotDetails | undefined>({
|
|
146
|
+
name: "screenshot_window",
|
|
147
|
+
label: "Screenshot Window",
|
|
148
|
+
description:
|
|
149
|
+
"Capture one visible macOS window by an exact ID returned by list_windows, resize it to fit within 1568×1568 pixels, save it to the required PNG path, and inspect the image. Call list_windows first.",
|
|
150
|
+
parameters: screenshotWindowSchema,
|
|
151
|
+
async execute(_toolCallId, params: ScreenshotWindowParams, signal, onUpdate, ctx) {
|
|
152
|
+
if (process.platform !== "darwin") throw new Error("screenshot_window is only available on macOS");
|
|
153
|
+
const rawPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
|
|
154
|
+
if (!rawPath.trim()) throw new Error("Screenshot path cannot be empty");
|
|
155
|
+
const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(ctx.cwd, rawPath);
|
|
156
|
+
if (!absolutePath.toLowerCase().endsWith(".png")) throw new Error("Screenshot path must end in .png");
|
|
157
|
+
|
|
158
|
+
await onUpdate?.({
|
|
159
|
+
content: [{ type: "text", text: `Capturing window ${params.window_id}...` }],
|
|
160
|
+
details: undefined,
|
|
161
|
+
});
|
|
162
|
+
return withFileMutationQueue(absolutePath, async () => {
|
|
163
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
164
|
+
const temporaryPath = join(dirname(absolutePath), `.${basename(absolutePath)}.${randomUUID()}.tmp.png`);
|
|
121
165
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new Error("Window listing helper returned invalid data");
|
|
128
|
-
}
|
|
129
|
-
return { content: [{ type: "text", text: encodeWindowList(parsed) }], details: undefined };
|
|
130
|
-
},
|
|
131
|
-
renderCall(_args, theme, context) {
|
|
132
|
-
rowState.watch(context.toolCallId, context.invalidate);
|
|
133
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
134
|
-
text.setText(formatToolRowTitle(rowState, context.toolCallId, "list_windows", theme));
|
|
135
|
-
return text;
|
|
136
|
-
},
|
|
137
|
-
renderResult(result, _options, _theme, context) {
|
|
138
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
139
|
-
const content = result.content.find((item) => item.type === "text");
|
|
140
|
-
text.setText(context.expanded && content?.type === "text" ? content.text : "");
|
|
141
|
-
return text;
|
|
142
|
-
},
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
pi.registerTool(
|
|
147
|
-
defineTool<typeof screenshotWindowSchema, ScreenshotDetails | undefined>({
|
|
148
|
-
name: "screenshot_window",
|
|
149
|
-
label: "Screenshot Window",
|
|
150
|
-
description:
|
|
151
|
-
"Capture one visible macOS window by an exact ID returned by list_windows, resize it to fit within 1568×1568 pixels, save it to the required PNG path, and inspect the image. Call list_windows first.",
|
|
152
|
-
parameters: screenshotWindowSchema,
|
|
153
|
-
async execute(_toolCallId, params: ScreenshotWindowParams, signal, onUpdate, ctx) {
|
|
154
|
-
if (process.platform !== "darwin") throw new Error("screenshot_window is only available on macOS");
|
|
155
|
-
const rawPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
|
|
156
|
-
if (!rawPath.trim()) throw new Error("Screenshot path cannot be empty");
|
|
157
|
-
const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(ctx.cwd, rawPath);
|
|
158
|
-
if (!absolutePath.toLowerCase().endsWith(".png")) throw new Error("Screenshot path must end in .png");
|
|
159
|
-
|
|
160
|
-
await onUpdate?.({
|
|
161
|
-
content: [{ type: "text", text: `Capturing window ${params.window_id}...` }],
|
|
162
|
-
details: undefined,
|
|
163
|
-
});
|
|
164
|
-
return withFileMutationQueue(absolutePath, async () => {
|
|
165
|
-
await mkdir(dirname(absolutePath), { recursive: true });
|
|
166
|
-
const temporaryPath = join(dirname(absolutePath), `.${basename(absolutePath)}.${randomUUID()}.tmp.png`);
|
|
166
|
+
const result = await runHelper(["capture", String(params.window_id), temporaryPath], signal, 30_000);
|
|
167
|
+
if (result.code !== 0) {
|
|
168
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || "Window capture failed");
|
|
169
|
+
}
|
|
170
|
+
let imageSize: number;
|
|
167
171
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const image = await readFile(temporaryPath);
|
|
182
|
-
if (
|
|
183
|
-
image.length < PNG_SIGNATURE.length ||
|
|
184
|
-
!image.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
|
|
185
|
-
) {
|
|
186
|
-
throw new Error("Window capture produced an invalid PNG file");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
await rename(temporaryPath, absolutePath);
|
|
190
|
-
return {
|
|
191
|
-
content: [
|
|
192
|
-
{ type: "text", text: `Captured window ${params.window_id} to ${absolutePath}` },
|
|
193
|
-
{ type: "image", data: image.toString("base64"), mimeType: "image/png" },
|
|
194
|
-
],
|
|
195
|
-
details: { path: absolutePath, window_id: params.window_id },
|
|
196
|
-
};
|
|
197
|
-
} finally {
|
|
198
|
-
await rm(temporaryPath, { force: true });
|
|
172
|
+
imageSize = (await stat(temporaryPath)).size;
|
|
173
|
+
} catch {
|
|
174
|
+
throw new Error(result.stderr.trim() || "Window capture produced no PNG file");
|
|
175
|
+
}
|
|
176
|
+
if (imageSize > MAX_PNG_BYTES) {
|
|
177
|
+
throw new Error(`Window capture exceeds the ${MAX_PNG_BYTES / 1024 / 1024} MiB attachment limit`);
|
|
178
|
+
}
|
|
179
|
+
const image = await readFile(temporaryPath);
|
|
180
|
+
if (
|
|
181
|
+
image.length < PNG_SIGNATURE.length ||
|
|
182
|
+
!image.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)
|
|
183
|
+
) {
|
|
184
|
+
throw new Error("Window capture produced an invalid PNG file");
|
|
199
185
|
}
|
|
200
|
-
});
|
|
201
|
-
},
|
|
202
|
-
}),
|
|
203
|
-
);
|
|
204
186
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
187
|
+
await rename(temporaryPath, absolutePath);
|
|
188
|
+
return {
|
|
189
|
+
content: [
|
|
190
|
+
{ type: "text", text: `Captured window ${params.window_id} to ${absolutePath}` },
|
|
191
|
+
{ type: "image", data: image.toString("base64"), mimeType: "image/png" },
|
|
192
|
+
],
|
|
193
|
+
details: { path: absolutePath, window_id: params.window_id },
|
|
194
|
+
};
|
|
195
|
+
} finally {
|
|
196
|
+
await rm(temporaryPath, { force: true });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const activateAppTool = defineTool<typeof activateAppSchema, ActivationDetails>({
|
|
203
|
+
name: "activate_app",
|
|
204
|
+
label: "Activate App",
|
|
205
|
+
description:
|
|
206
|
+
"Bring a running macOS application and its windows to the foreground by a process ID returned by list_windows. Use only when foregrounding is required for visual validation because activate_app changes user focus.",
|
|
207
|
+
parameters: activateAppSchema,
|
|
208
|
+
async execute(_toolCallId, params, signal) {
|
|
209
|
+
if (process.platform !== "darwin") throw new Error("activate_app is only available on macOS");
|
|
210
|
+
const result = await runHelper(["activate", String(params.pid)], signal, 5000);
|
|
211
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || result.stdout.trim() || "Activation failed");
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: `Activated application PID ${params.pid}` }],
|
|
214
|
+
details: { pid: params.pid },
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
registerDeferredToolGroup(pi, {
|
|
220
|
+
id: "appshot",
|
|
221
|
+
description: "macOS window discovery, capture, and activation",
|
|
222
|
+
tools: [listWindowsTool, screenshotWindowTool, activateAppTool],
|
|
223
|
+
});
|
|
223
224
|
|
|
224
225
|
pi.on("session_start", () => rowState.clear());
|
|
225
226
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# aside Extension
|
|
2
|
+
|
|
3
|
+
Ask the current model a one-off side question without adding it or its answer to the current conversation.
|
|
4
|
+
|
|
5
|
+
Run `/aside <question>`, then choose whether to include the current conversation branch or ask with no context. The request uses the current model without tools and runs in the background. A status widget remains above the editor while the model is thinking, then the answer opens in an overlay.
|
|
6
|
+
|
|
7
|
+
Run `/aside` to open the latest answer. Run `/aside clear` to cancel a running aside and clear its in-memory result. Aside results also clear when the session changes or Tau reloads.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Context, Message, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import {
|
|
4
|
+
buildSessionContext,
|
|
5
|
+
convertToLlm,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionCommandContext,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { errorText } from "../../shared/text.ts";
|
|
11
|
+
import { AsideResultPanel, AsideWidget, type AsideResult } from "./panel.ts";
|
|
12
|
+
|
|
13
|
+
const WIDGET_KEY = "aside";
|
|
14
|
+
const CURRENT_CONVERSATION = "Current conversation branch";
|
|
15
|
+
const NO_CONTEXT = "No context";
|
|
16
|
+
|
|
17
|
+
function buildAsideRequest(messages: readonly Message[], question: string, systemPrompt: string | undefined): Context {
|
|
18
|
+
const toolResults = new Set(
|
|
19
|
+
messages.flatMap((message) => (message.role === "toolResult" ? [message.toolCallId] : [])),
|
|
20
|
+
);
|
|
21
|
+
let validLength = messages.length;
|
|
22
|
+
for (const [index, message] of messages.entries()) {
|
|
23
|
+
if (
|
|
24
|
+
message.role === "assistant" &&
|
|
25
|
+
message.content.some((part) => part.type === "toolCall" && !toolResults.has(part.id))
|
|
26
|
+
) {
|
|
27
|
+
validLength = index;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const userMessage: Message = {
|
|
33
|
+
role: "user",
|
|
34
|
+
content: [{ type: "text", text: question }],
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
};
|
|
37
|
+
const request = { messages: [...messages.slice(0, validLength), userMessage] };
|
|
38
|
+
return systemPrompt === undefined ? request : { ...request, systemPrompt };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function asideExtension(pi: ExtensionAPI): void {
|
|
42
|
+
let activeController: AbortController | undefined;
|
|
43
|
+
let latest: AsideResult | undefined;
|
|
44
|
+
let generation = 0;
|
|
45
|
+
|
|
46
|
+
const setWidget = (ctx: ExtensionContext, question: string | undefined): void => {
|
|
47
|
+
ctx.ui.setWidget(WIDGET_KEY, question ? (_tui, theme) => new AsideWidget(theme, question) : undefined, {
|
|
48
|
+
placement: "aboveEditor",
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const clear = (ctx: ExtensionContext): void => {
|
|
53
|
+
generation += 1;
|
|
54
|
+
activeController?.abort();
|
|
55
|
+
activeController = undefined;
|
|
56
|
+
latest = undefined;
|
|
57
|
+
setWidget(ctx, undefined);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
pi.on("session_start", (_event, ctx) => clear(ctx));
|
|
61
|
+
pi.on("session_shutdown", (_event, ctx) => clear(ctx));
|
|
62
|
+
|
|
63
|
+
pi.registerCommand("aside", {
|
|
64
|
+
description: "Ask the current model a side question outside the transcript",
|
|
65
|
+
handler: async (args, ctx) => {
|
|
66
|
+
if (ctx.mode !== "tui") {
|
|
67
|
+
ctx.ui.notify("/aside requires TUI mode", "warning");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const question = args.trim();
|
|
72
|
+
if (question === "clear") {
|
|
73
|
+
clear(ctx);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!question) {
|
|
77
|
+
if (!latest) {
|
|
78
|
+
ctx.ui.notify(activeController ? "Aside is still running" : "No aside answer available", "info");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await showResult(ctx, latest);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (activeController) {
|
|
85
|
+
ctx.ui.notify("An aside is already running", "warning");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const contextChoice = await ctx.ui.select("Aside context", [CURRENT_CONVERSATION, NO_CONTEXT]);
|
|
90
|
+
if (!contextChoice) return;
|
|
91
|
+
const model = ctx.model;
|
|
92
|
+
if (!model) {
|
|
93
|
+
ctx.ui.notify("No model selected", "error");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
activeController = controller;
|
|
99
|
+
const requestGeneration = ++generation;
|
|
100
|
+
setWidget(ctx, question);
|
|
101
|
+
void runAside(ctx, model, question, contextChoice === CURRENT_CONVERSATION, controller.signal)
|
|
102
|
+
.then((result) => {
|
|
103
|
+
if (generation !== requestGeneration || controller.signal.aborted) return;
|
|
104
|
+
latest = result;
|
|
105
|
+
activeController = undefined;
|
|
106
|
+
setWidget(ctx, undefined);
|
|
107
|
+
void showResult(ctx, result).catch((error: unknown) => {
|
|
108
|
+
if (generation !== requestGeneration) return;
|
|
109
|
+
ctx.ui.notify(`Could not open aside: ${errorText(error)}`, "error");
|
|
110
|
+
});
|
|
111
|
+
})
|
|
112
|
+
.catch((error: unknown) => {
|
|
113
|
+
if (generation !== requestGeneration || controller.signal.aborted) return;
|
|
114
|
+
setWidget(ctx, undefined);
|
|
115
|
+
ctx.ui.notify(`Aside failed: ${errorText(error)}`, "error");
|
|
116
|
+
})
|
|
117
|
+
.finally(() => {
|
|
118
|
+
if (generation === requestGeneration) activeController = undefined;
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function runAside(
|
|
125
|
+
ctx: ExtensionCommandContext,
|
|
126
|
+
model: Model<string>,
|
|
127
|
+
question: string,
|
|
128
|
+
withConversation: boolean,
|
|
129
|
+
signal: AbortSignal,
|
|
130
|
+
): Promise<AsideResult> {
|
|
131
|
+
const provider = ctx.modelRegistry.getProvider(model.provider);
|
|
132
|
+
if (!provider) throw new Error(`Provider ${model.provider} is unavailable`);
|
|
133
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
134
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
135
|
+
|
|
136
|
+
const request = withConversation
|
|
137
|
+
? buildAsideRequest(
|
|
138
|
+
convertToLlm(buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages),
|
|
139
|
+
question,
|
|
140
|
+
ctx.getSystemPrompt(),
|
|
141
|
+
)
|
|
142
|
+
: buildAsideRequest([], question, undefined);
|
|
143
|
+
const response = await provider
|
|
144
|
+
.streamSimple(model, request, {
|
|
145
|
+
apiKey: auth.apiKey,
|
|
146
|
+
headers: auth.headers,
|
|
147
|
+
env: auth.env,
|
|
148
|
+
signal,
|
|
149
|
+
reasoning: ctx.thinkingLevel === "off" ? undefined : ctx.thinkingLevel,
|
|
150
|
+
sessionId: withConversation ? ctx.sessionManager.getSessionId() : randomUUID(),
|
|
151
|
+
})
|
|
152
|
+
.result();
|
|
153
|
+
if (response.stopReason === "error") throw new Error(response.errorMessage || "model returned an error");
|
|
154
|
+
if (response.stopReason === "aborted") throw new Error("Cancelled");
|
|
155
|
+
const answer = response.content
|
|
156
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
157
|
+
.join("\n")
|
|
158
|
+
.trim();
|
|
159
|
+
if (!answer) throw new Error("model returned no text");
|
|
160
|
+
return { question, answer, context: withConversation ? CURRENT_CONVERSATION : NO_CONTEXT };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function showResult(ctx: ExtensionCommandContext, result: AsideResult): Promise<void> {
|
|
164
|
+
await ctx.ui.custom<void>((tui, theme, keys, done) => new AsideResultPanel(tui, theme, keys, result, done), {
|
|
165
|
+
overlay: true,
|
|
166
|
+
overlayOptions: { anchor: "top-center", width: "80%", minWidth: 68, maxHeight: "90%", margin: 1 },
|
|
167
|
+
});
|
|
168
|
+
}
|