@nmzpy/pi-ember-stack 0.1.6 → 0.2.2
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/README.md +83 -83
- package/package.json +62 -48
- package/plugins/devin-auth/extensions/index.ts +68 -7
- package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
- package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
- package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
- package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
- package/plugins/devin-auth/src/models.ts +42 -196
- package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
- package/plugins/devin-auth/src/oauth/types.ts +71 -71
- package/plugins/devin-auth/src/stream.ts +1 -1
- package/plugins/index.ts +29 -6
- package/plugins/pi-compact-tools/index.ts +35 -192
- package/plugins/pi-compact-tools/renderer.ts +589 -0
- package/plugins/pi-custom-agents/index.ts +727 -373
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
- package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +118 -226
- package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
- package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +260 -170
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +975 -0
- package/plugins/pi-ember-fff/query.ts +247 -0
- package/plugins/pi-ember-fff/test/query.test.ts +222 -0
- package/plugins/pi-ember-fff/test/renderer.test.ts +727 -0
- package/plugins/pi-ember-tps/index.ts +144 -0
- package/plugins/pi-ember-ui/ember.json +99 -0
- package/plugins/pi-ember-ui/index.ts +805 -0
- package/plugins/pi-ember-ui/mode-colors.ts +196 -0
- package/tsconfig.json +2 -1
- package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types for the OAuth login flow + persisted credentials.
|
|
3
|
-
*
|
|
4
|
-
* Two distinct token shapes appear in this codebase:
|
|
5
|
-
*
|
|
6
|
-
* - `firebaseIdToken` — the short-lived JWT minted by Auth0 / Firebase Auth
|
|
7
|
-
* during browser sign-in. Lives in the OAuth callback URL fragment/query.
|
|
8
|
-
* Treated as opaque and discarded once exchanged.
|
|
9
|
-
*
|
|
10
|
-
* - `apiKey` — the long-lived credential returned by
|
|
11
|
-
* `SeatManagementService.RegisterUser`. Used inside every Cascade RPC's
|
|
12
|
-
* `Metadata.api_key` field. Format is provider-defined:
|
|
13
|
-
* * Cognition era: `devin-session-token$<JWT>`
|
|
14
|
-
* * Codeium classic: bare UUID v4
|
|
15
|
-
* * Older Windsurf: `sk-ws-01-<...>` / `cog_<...>`
|
|
16
|
-
* The plugin treats it as an opaque string — only the cloud cares about format.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export interface OAuthLoginResult {
|
|
20
|
-
/** The opaque API key used as `Metadata.api_key` in every Cascade RPC. */
|
|
21
|
-
apiKey: string;
|
|
22
|
-
/** Human-readable account name (`Satvik Kapoor`). */
|
|
23
|
-
name: string;
|
|
24
|
-
/**
|
|
25
|
-
* Cloud API server (`https://server.codeium.com`, `https://eu.windsurf.com/_route/api_server`,
|
|
26
|
-
* `https://windsurf.fedstart.com/_route/api_server`). Driven by the user's
|
|
27
|
-
* tenant — language_server needs this as `--api_server_url`.
|
|
28
|
-
*/
|
|
29
|
-
apiServerUrl: string;
|
|
30
|
-
/** Optional cleanup redirect URL returned by RegisterUser. Informational. */
|
|
31
|
-
redirectUrl?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface PersistedCredentials extends OAuthLoginResult {
|
|
35
|
-
/** ISO timestamp the credentials were minted at — purely informational. */
|
|
36
|
-
issuedAt: string;
|
|
37
|
-
/** Optional tag tracking the OAuth client id used (so a future client rotation can invalidate). */
|
|
38
|
-
oauthClientId: string;
|
|
39
|
-
/**
|
|
40
|
-
* True when these credentials were written as part of the
|
|
41
|
-
* `opencode auth login` → authorize() flow (so opencode's auth.json is the
|
|
42
|
-
* authoritative copy and `opencode auth logout windsurf` should mirror-clear
|
|
43
|
-
* this file). False / absent for credentials written by our standalone
|
|
44
|
-
* `opencode-windsurf-auth login` CLI; those survive opencode auth state
|
|
45
|
-
* changes.
|
|
46
|
-
*/
|
|
47
|
-
syncedViaOpencodeAuth?: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface WindsurfRegion {
|
|
51
|
-
/** Where to send users for browser sign-in. */
|
|
52
|
-
website: string;
|
|
53
|
-
/** Where to POST RegisterUser. */
|
|
54
|
-
registerApiServerUrl: string;
|
|
55
|
-
/** Auth0 client id passed in the OAuth URL. */
|
|
56
|
-
oauthClientId: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The single tenant (free / personal) configuration. EU, FedStart, and arbitrary
|
|
61
|
-
* portal URLs override `website` + `registerApiServerUrl` at runtime when the
|
|
62
|
-
* user passes `--portal-url` to the login command.
|
|
63
|
-
*/
|
|
64
|
-
export const DEFAULT_REGION: WindsurfRegion = {
|
|
65
|
-
website: 'https://windsurf.com',
|
|
66
|
-
registerApiServerUrl: 'https://register.windsurf.com',
|
|
67
|
-
// From /Applications/Windsurf.app/.../extension.js — the public Windsurf
|
|
68
|
-
// Auth0 client. If Windsurf rotates this, sign-in will start failing until
|
|
69
|
-
// we re-extract it.
|
|
70
|
-
oauthClientId: '3GUryQ7ldAeKEuD2obYnppsnmj58eP5u',
|
|
71
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the OAuth login flow + persisted credentials.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct token shapes appear in this codebase:
|
|
5
|
+
*
|
|
6
|
+
* - `firebaseIdToken` — the short-lived JWT minted by Auth0 / Firebase Auth
|
|
7
|
+
* during browser sign-in. Lives in the OAuth callback URL fragment/query.
|
|
8
|
+
* Treated as opaque and discarded once exchanged.
|
|
9
|
+
*
|
|
10
|
+
* - `apiKey` — the long-lived credential returned by
|
|
11
|
+
* `SeatManagementService.RegisterUser`. Used inside every Cascade RPC's
|
|
12
|
+
* `Metadata.api_key` field. Format is provider-defined:
|
|
13
|
+
* * Cognition era: `devin-session-token$<JWT>`
|
|
14
|
+
* * Codeium classic: bare UUID v4
|
|
15
|
+
* * Older Windsurf: `sk-ws-01-<...>` / `cog_<...>`
|
|
16
|
+
* The plugin treats it as an opaque string — only the cloud cares about format.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface OAuthLoginResult {
|
|
20
|
+
/** The opaque API key used as `Metadata.api_key` in every Cascade RPC. */
|
|
21
|
+
apiKey: string;
|
|
22
|
+
/** Human-readable account name (`Satvik Kapoor`). */
|
|
23
|
+
name: string;
|
|
24
|
+
/**
|
|
25
|
+
* Cloud API server (`https://server.codeium.com`, `https://eu.windsurf.com/_route/api_server`,
|
|
26
|
+
* `https://windsurf.fedstart.com/_route/api_server`). Driven by the user's
|
|
27
|
+
* tenant — language_server needs this as `--api_server_url`.
|
|
28
|
+
*/
|
|
29
|
+
apiServerUrl: string;
|
|
30
|
+
/** Optional cleanup redirect URL returned by RegisterUser. Informational. */
|
|
31
|
+
redirectUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PersistedCredentials extends OAuthLoginResult {
|
|
35
|
+
/** ISO timestamp the credentials were minted at — purely informational. */
|
|
36
|
+
issuedAt: string;
|
|
37
|
+
/** Optional tag tracking the OAuth client id used (so a future client rotation can invalidate). */
|
|
38
|
+
oauthClientId: string;
|
|
39
|
+
/**
|
|
40
|
+
* True when these credentials were written as part of the
|
|
41
|
+
* `opencode auth login` → authorize() flow (so opencode's auth.json is the
|
|
42
|
+
* authoritative copy and `opencode auth logout windsurf` should mirror-clear
|
|
43
|
+
* this file). False / absent for credentials written by our standalone
|
|
44
|
+
* `opencode-windsurf-auth login` CLI; those survive opencode auth state
|
|
45
|
+
* changes.
|
|
46
|
+
*/
|
|
47
|
+
syncedViaOpencodeAuth?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WindsurfRegion {
|
|
51
|
+
/** Where to send users for browser sign-in. */
|
|
52
|
+
website: string;
|
|
53
|
+
/** Where to POST RegisterUser. */
|
|
54
|
+
registerApiServerUrl: string;
|
|
55
|
+
/** Auth0 client id passed in the OAuth URL. */
|
|
56
|
+
oauthClientId: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The single tenant (free / personal) configuration. EU, FedStart, and arbitrary
|
|
61
|
+
* portal URLs override `website` + `registerApiServerUrl` at runtime when the
|
|
62
|
+
* user passes `--portal-url` to the login command.
|
|
63
|
+
*/
|
|
64
|
+
export const DEFAULT_REGION: WindsurfRegion = {
|
|
65
|
+
website: 'https://windsurf.com',
|
|
66
|
+
registerApiServerUrl: 'https://register.windsurf.com',
|
|
67
|
+
// From /Applications/Windsurf.app/.../extension.js — the public Windsurf
|
|
68
|
+
// Auth0 client. If Windsurf rotates this, sign-in will start failing until
|
|
69
|
+
// we re-extract it.
|
|
70
|
+
oauthClientId: '3GUryQ7ldAeKEuD2obYnppsnmj58eP5u',
|
|
71
|
+
};
|
|
@@ -265,7 +265,7 @@ export function streamDevin(
|
|
|
265
265
|
output.usage.cacheRead = ev.cachedInputTokens ?? 0;
|
|
266
266
|
output.usage.cacheWrite = ev.cacheCreationInputTokens ?? 0;
|
|
267
267
|
output.usage.totalTokens =
|
|
268
|
-
|
|
268
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
269
269
|
// calculateCost mutates output.usage.cost in place.
|
|
270
270
|
calculateCost(model, output.usage);
|
|
271
271
|
break;
|
package/plugins/index.ts
CHANGED
|
@@ -3,10 +3,15 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
import devinAuthPlugin from "./devin-auth/extensions/index.ts";
|
|
6
|
-
import piCompactToolsPlugin from "./pi-compact-tools/index.ts";
|
|
6
|
+
import piCompactToolsPlugin, { getSharedRenderer } from "./pi-compact-tools/index.ts";
|
|
7
7
|
import piCustomAgentsPlugin from "./pi-custom-agents/index.ts";
|
|
8
|
+
import piEmberFffPlugin from "./pi-ember-fff/index.ts";
|
|
9
|
+
import piEmberTpsPlugin from "./pi-ember-tps/index.ts";
|
|
10
|
+
import piEmberUiPlugin from "./pi-ember-ui/index.ts";
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
export { getSharedRenderer };
|
|
13
|
+
|
|
14
|
+
type PluginId = "pi-compact-tools" | "pi-custom-agents" | "devin-auth" | "pi-ember-fff" | "pi-ember-ui" | "pi-ember-tps";
|
|
10
15
|
type StackPlugin = {
|
|
11
16
|
id: PluginId;
|
|
12
17
|
description: string;
|
|
@@ -20,8 +25,11 @@ type StackPluginConfig = {
|
|
|
20
25
|
const CONFIG_RELATIVE_PATH = path.join(".pi", "ember-stack.json");
|
|
21
26
|
const DEFAULT_PLUGIN_IDS: readonly PluginId[] = [
|
|
22
27
|
"pi-compact-tools",
|
|
23
|
-
"pi-custom-agents",
|
|
24
28
|
"devin-auth",
|
|
29
|
+
"pi-custom-agents",
|
|
30
|
+
"pi-ember-fff",
|
|
31
|
+
"pi-ember-ui",
|
|
32
|
+
"pi-ember-tps",
|
|
25
33
|
];
|
|
26
34
|
|
|
27
35
|
const PLUGINS: readonly StackPlugin[] = [
|
|
@@ -30,15 +38,30 @@ const PLUGINS: readonly StackPlugin[] = [
|
|
|
30
38
|
description: "Collapsed native edit rendering",
|
|
31
39
|
extension: piCompactToolsPlugin,
|
|
32
40
|
},
|
|
41
|
+
{
|
|
42
|
+
id: "devin-auth",
|
|
43
|
+
description: "Devin OAuth provider, model catalog, and streaming transport",
|
|
44
|
+
extension: devinAuthPlugin,
|
|
45
|
+
},
|
|
33
46
|
{
|
|
34
47
|
id: "pi-custom-agents",
|
|
35
48
|
description: "Questionnaire, primary modes, plans, subagents, and bundled agent definitions",
|
|
36
49
|
extension: piCustomAgentsPlugin,
|
|
37
50
|
},
|
|
38
51
|
{
|
|
39
|
-
id: "
|
|
40
|
-
description: "
|
|
41
|
-
extension:
|
|
52
|
+
id: "pi-ember-fff",
|
|
53
|
+
description: "FFF-powered grep and find with compact rendering",
|
|
54
|
+
extension: piEmberFffPlugin,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "pi-ember-ui",
|
|
58
|
+
description: "Ember accent theme — orange reasoning colors, accent borders",
|
|
59
|
+
extension: piEmberUiPlugin,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "pi-ember-tps",
|
|
63
|
+
description: "Tokens-per-second meter with sparkline trend and live gauge",
|
|
64
|
+
extension: piEmberTpsPlugin,
|
|
42
65
|
},
|
|
43
66
|
];
|
|
44
67
|
|
|
@@ -10,199 +10,18 @@ import {
|
|
|
10
10
|
createWriteTool,
|
|
11
11
|
type ExtensionAPI,
|
|
12
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
13
|
+
import { type Component } from "@earendil-works/pi-tui";
|
|
14
|
+
import {
|
|
15
|
+
CompactRenderer,
|
|
16
|
+
GROUPABLE_TOOLS,
|
|
17
|
+
bashGrepInfo,
|
|
18
|
+
type ToolRenderContext,
|
|
19
|
+
type ToolRenderResultOptions,
|
|
20
|
+
} from "./renderer.ts";
|
|
14
21
|
|
|
15
22
|
const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const BULLET = "• ";
|
|
17
|
-
const DISCOVERY_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
18
23
|
|
|
19
24
|
type ToolFactory = (cwd: string) => any;
|
|
20
|
-
type ToolRenderContext = {
|
|
21
|
-
args: any;
|
|
22
|
-
toolCallId: string;
|
|
23
|
-
invalidate: () => void;
|
|
24
|
-
};
|
|
25
|
-
type ToolRenderResultOptions = {
|
|
26
|
-
isPartial: boolean;
|
|
27
|
-
};
|
|
28
|
-
type CompactCall = {
|
|
29
|
-
id: string;
|
|
30
|
-
name: string;
|
|
31
|
-
args: any;
|
|
32
|
-
group?: DiscoveryGroup;
|
|
33
|
-
invalidate?: () => void;
|
|
34
|
-
isError: boolean;
|
|
35
|
-
};
|
|
36
|
-
type DiscoveryGroup = {
|
|
37
|
-
records: CompactCall[];
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
function textValue(value: unknown, fallback = ""): string {
|
|
41
|
-
if (value === undefined || value === null) return fallback;
|
|
42
|
-
return String(value).replace(/[\r\n]+/g, " ");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function toolPath(args: any): string {
|
|
46
|
-
return textValue(args?.file_path ?? args?.path, ".");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function errorText(result: any, isError: boolean): string | undefined {
|
|
50
|
-
const content = result?.content?.find((item: any) => item.type === "text");
|
|
51
|
-
if (!isError && !content?.text?.startsWith("Error")) return undefined;
|
|
52
|
-
return textValue(content?.text, "Tool failed").split("\n")[0];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function diffStats(result: any): { additions: number; removals: number } {
|
|
56
|
-
const diff = typeof result?.details?.diff === "string" ? result.details.diff : "";
|
|
57
|
-
let additions = 0;
|
|
58
|
-
let removals = 0;
|
|
59
|
-
for (const line of diff.split("\n")) {
|
|
60
|
-
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
61
|
-
if (line.startsWith("-") && !line.startsWith("---")) removals++;
|
|
62
|
-
}
|
|
63
|
-
return { additions, removals };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function formatCallBody(name: string, args: any, theme: any): string {
|
|
67
|
-
const pathName = toolPath(args);
|
|
68
|
-
switch (name) {
|
|
69
|
-
case "read":
|
|
70
|
-
return theme.fg("toolTitle", theme.bold("Read")) +
|
|
71
|
-
theme.fg("accent", ` ${pathName}`);
|
|
72
|
-
case "grep":
|
|
73
|
-
return theme.fg("toolTitle", theme.bold("Search")) +
|
|
74
|
-
theme.fg("accent", ` ${textValue(args?.pattern)}`) +
|
|
75
|
-
theme.fg("toolOutput", ` in ${pathName}`);
|
|
76
|
-
case "find":
|
|
77
|
-
return theme.fg("toolTitle", theme.bold("Find")) +
|
|
78
|
-
theme.fg("accent", ` ${textValue(args?.pattern)}`) +
|
|
79
|
-
theme.fg("toolOutput", ` in ${pathName}`);
|
|
80
|
-
case "ls":
|
|
81
|
-
return theme.fg("toolTitle", theme.bold("List")) +
|
|
82
|
-
theme.fg("accent", ` ${pathName}`);
|
|
83
|
-
case "bash":
|
|
84
|
-
return theme.fg("toolTitle", theme.bold("Run")) +
|
|
85
|
-
theme.fg("accent", ` $ ${textValue(args?.command)}`);
|
|
86
|
-
case "edit":
|
|
87
|
-
return theme.fg("toolTitle", theme.bold("edit")) +
|
|
88
|
-
theme.fg("accent", ` ${pathName}`);
|
|
89
|
-
case "write":
|
|
90
|
-
return theme.fg("toolTitle", theme.bold("write")) +
|
|
91
|
-
theme.fg("accent", ` ${pathName}`);
|
|
92
|
-
default:
|
|
93
|
-
return theme.fg("toolTitle", theme.bold(name));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function formatGroup(group: DiscoveryGroup, theme: any): string {
|
|
98
|
-
const lines = [
|
|
99
|
-
theme.fg("muted", BULLET) + theme.fg("toolTitle", theme.bold("Explored")),
|
|
100
|
-
];
|
|
101
|
-
for (const [index, record] of group.records.entries()) {
|
|
102
|
-
const prefix = index === 0 ? " └ " : " ";
|
|
103
|
-
lines.push(theme.fg("dim", prefix) + formatCallBody(record.name, record.args, theme));
|
|
104
|
-
}
|
|
105
|
-
return lines.join("\n");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
class CompactRenderer {
|
|
109
|
-
private readonly calls = new Map<string, CompactCall>();
|
|
110
|
-
private lastCall: CompactCall | undefined;
|
|
111
|
-
|
|
112
|
-
beginTurn(): void {
|
|
113
|
-
this.lastCall = undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
observeCall(name: string, id: string, args: any): CompactCall {
|
|
117
|
-
return this.registerCall(name, id, args);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
registerCall(
|
|
121
|
-
name: string,
|
|
122
|
-
id: string,
|
|
123
|
-
args: any,
|
|
124
|
-
invalidate?: () => void,
|
|
125
|
-
): CompactCall {
|
|
126
|
-
const existing = this.calls.get(id);
|
|
127
|
-
if (existing) {
|
|
128
|
-
existing.args = args;
|
|
129
|
-
existing.invalidate = invalidate ?? existing.invalidate;
|
|
130
|
-
return existing;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const record: CompactCall = { id, name, args, isError: false };
|
|
134
|
-
this.calls.set(id, record);
|
|
135
|
-
if (DISCOVERY_TOOLS.has(name) && this.lastCall && DISCOVERY_TOOLS.has(this.lastCall.name)) {
|
|
136
|
-
const group = this.lastCall.group ?? { records: [this.lastCall] };
|
|
137
|
-
this.lastCall.group = group;
|
|
138
|
-
group.records.push(record);
|
|
139
|
-
record.group = group;
|
|
140
|
-
for (const groupedCall of group.records) groupedCall.invalidate?.();
|
|
141
|
-
}
|
|
142
|
-
this.lastCall = record;
|
|
143
|
-
record.invalidate = invalidate;
|
|
144
|
-
return record;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
setResult(record: CompactCall, result: any, isError: boolean): void {
|
|
148
|
-
record.isError = isError;
|
|
149
|
-
if (record.group) {
|
|
150
|
-
record.group.records[0]?.invalidate?.();
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
renderCall(
|
|
155
|
-
name: string,
|
|
156
|
-
args: any,
|
|
157
|
-
theme: any,
|
|
158
|
-
context: ToolRenderContext,
|
|
159
|
-
): Component {
|
|
160
|
-
const record = this.registerCall(name, context.toolCallId, args, context.invalidate);
|
|
161
|
-
if (record.group && record.group.records.length > 1) {
|
|
162
|
-
if (record.group.records[0] !== record) return new Text("", 0, 0);
|
|
163
|
-
return new Text(formatGroup(record.group, theme), 0, 0);
|
|
164
|
-
}
|
|
165
|
-
return new Text(
|
|
166
|
-
theme.fg("muted", BULLET) + formatCallBody(name, args, theme),
|
|
167
|
-
0,
|
|
168
|
-
0,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
renderResult(
|
|
173
|
-
name: string,
|
|
174
|
-
args: any,
|
|
175
|
-
result: any,
|
|
176
|
-
options: ToolRenderResultOptions,
|
|
177
|
-
theme: any,
|
|
178
|
-
context: ToolRenderContext & { isError: boolean },
|
|
179
|
-
): Component {
|
|
180
|
-
const record = this.registerCall(name, context.toolCallId, args, context.invalidate);
|
|
181
|
-
this.setResult(record, result, context.isError);
|
|
182
|
-
if (record.group && record.group.records.length > 1) {
|
|
183
|
-
if (record.group.records[0] !== record) return new Text("", 0, 0);
|
|
184
|
-
const error = errorText(result, context.isError);
|
|
185
|
-
return error ? new Text(theme.fg("error", error), 0, 0) : new Text("", 0, 0);
|
|
186
|
-
}
|
|
187
|
-
if (options.isPartial) return new Text("", 0, 0);
|
|
188
|
-
|
|
189
|
-
const error = errorText(result, context.isError);
|
|
190
|
-
if (error) return new Text(theme.fg("error", error), 0, 0);
|
|
191
|
-
if (name === "edit") {
|
|
192
|
-
const { additions, removals } = diffStats(result);
|
|
193
|
-
return new Text(
|
|
194
|
-
theme.fg("success", `+${additions}`) +
|
|
195
|
-
theme.fg("dim", " / ") +
|
|
196
|
-
theme.fg("error", `-${removals}`),
|
|
197
|
-
0,
|
|
198
|
-
0,
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
if (name === "bash") return new Text(theme.fg("success", "Done"), 0, 0);
|
|
202
|
-
if (name === "write") return new Text(theme.fg("success", "Written"), 0, 0);
|
|
203
|
-
return new Text("", 0, 0);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
25
|
|
|
207
26
|
const TOOL_FACTORIES: Record<string, ToolFactory> = {
|
|
208
27
|
bash: createBashTool,
|
|
@@ -253,14 +72,38 @@ function registerCompactTool(
|
|
|
253
72
|
});
|
|
254
73
|
}
|
|
255
74
|
|
|
75
|
+
let sharedRenderer: CompactRenderer | null = null;
|
|
76
|
+
|
|
77
|
+
export { bashGrepInfo };
|
|
78
|
+
|
|
79
|
+
export function getSharedRenderer(): CompactRenderer {
|
|
80
|
+
if (!sharedRenderer) sharedRenderer = new CompactRenderer();
|
|
81
|
+
return sharedRenderer;
|
|
82
|
+
}
|
|
83
|
+
|
|
256
84
|
export default function piCompactToolsPlugin(pi: ExtensionAPI): void {
|
|
257
|
-
const renderer =
|
|
85
|
+
const renderer = getSharedRenderer();
|
|
258
86
|
pi.on("turn_start", () => renderer.beginTurn());
|
|
87
|
+
pi.on("message_update", (event: any) => {
|
|
88
|
+
// When the model streams visible text (not just thinking tokens),
|
|
89
|
+
// mark the turn as having visible output so the next discovery
|
|
90
|
+
// call starts a fresh group instead of appending to the previous
|
|
91
|
+
// turn's group. Thinking-only turns keep grouping coherent.
|
|
92
|
+
const ev = event?.assistantMessageEvent;
|
|
93
|
+
if (ev && (ev.type === "text_start" || ev.type === "text_delta")) {
|
|
94
|
+
renderer.noteVisibleText();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
259
97
|
pi.on("tool_call", (event: any) => {
|
|
260
|
-
if (TOOL_FACTORIES[event.toolName]) {
|
|
261
|
-
renderer.
|
|
98
|
+
if (GROUPABLE_TOOLS.has(event.toolName) || TOOL_FACTORIES[event.toolName]) {
|
|
99
|
+
renderer.registerCall(event.toolName, event.toolCallId, event.input);
|
|
262
100
|
}
|
|
263
101
|
});
|
|
102
|
+
// Reset the shared renderer on session replacement so stale call rows
|
|
103
|
+
// from the previous session do not leak into the new one. The renderer
|
|
104
|
+
// is module-level (shared across sessions because jiti caches the
|
|
105
|
+
// module), so it must be explicitly cleared.
|
|
106
|
+
pi.on("session_start", () => renderer.resetForSession());
|
|
264
107
|
for (const [name, factory] of Object.entries(TOOL_FACTORIES)) {
|
|
265
108
|
registerCompactTool(pi, name, factory, renderer);
|
|
266
109
|
}
|