@andares/pi-toolkits 0.4.3 → 0.6.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/README.md +45 -3
- package/package.json +9 -6
- package/src/features/autostart/autostart.test.ts +315 -0
- package/src/features/autostart/autostart.ts +108 -0
- package/src/features/autostart/index.ts +109 -0
- package/src/features/compact/index.ts +11 -9
- package/src/features/compact/prompt.ts +3 -0
- package/src/features/favorites/patch-cycle.ts +7 -5
- package/src/features/favorites/patch-selector.ts +8 -4
- package/src/index.ts +3 -0
- package/src/lib/model-config.ts +6 -5
- package/src/lib/model-picker.ts +6 -5
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/@andares/pi-toolkits)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://pnpm.io)
|
|
11
|
-
[](https://github.com/earendil-works/pi)
|
|
12
12
|
[](https://github.com/andares/pi-toolkits)
|
|
13
13
|
|
|
14
14
|
一句话介绍:目前包含五个开箱即用的功能——**只读咨询模式** `ask`、**模型收藏** `favorites`、**提示词暂存** `stash`、**第三方代码评审** `third-review`、**独立压缩模型** `compact-model`,后续持续追加。
|
|
15
15
|
Currently ships five ready-to-use capabilities — **ask mode**, **model favorites**, **prompt stash**, **third-party code review** and a **dedicated compaction model** — with more to come.
|
|
16
16
|
|
|
17
|
-
**版本要求 · Requirements**:pi
|
|
17
|
+
**版本要求 · Requirements**:pi `>=0.86.0 <2.0.0`(覆盖最近 4 个中版本线 0.86.x / 0.87.x / 0.99.x / 1.0.x —— 0.88–0.98 从未发布。区间内全部 10 个已发布版本逐版本比对宿主 dist:用到的扩展 API 无差异,favorites 两处运行时 patch 目标 `ModelSelectorComponent` / `AgentSession.cycleModel` 字节级一致;并在 0.86.0、0.86.1、0.87.1、0.99.2、1.0.2 上逐一通过 typecheck + 全量测试,在真实 pi 1.0.2 上冒烟加载无警告)。注:pi ≥ 0.99 的宿主要求 `@earendil-works/pi-tui` 声明为 peerDependencies `"*"`(由宿主提供),本包已按要求声明。
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -52,6 +52,7 @@ pi -e ./src/index.ts
|
|
|
52
52
|
- [📥 stash — 提示词暂存](#stash)
|
|
53
53
|
- [🔎 review — 第三方代码评审](#review)
|
|
54
54
|
- [🗜️ compact — 独立压缩模型](#compact)
|
|
55
|
+
- [🚀 autostart — 会话启动自动化](#autostart)
|
|
55
56
|
- [开发 Development](#development)
|
|
56
57
|
- [贡献 Contributing](#contributing)
|
|
57
58
|
- [许可 License](#license)
|
|
@@ -69,6 +70,7 @@ pi -e ./src/index.ts
|
|
|
69
70
|
| 📥 **stash** 提示词暂存 | `ctrl+alt+y` | 一段提示词的暂存槽,按键交换 / 连按两次暂存并清空输入框 |
|
|
70
71
|
| 🔎 **review** 第三方代码评审 | `/third-review` | 召唤配置的评审模型,对刚完成的任务做查+修,重点检查「本轮需求实现 / 是否有遗漏 / 是否有错误」 |
|
|
71
72
|
| 🗜️ **compact** 独立压缩模型 | `/compact-model` | 给 `/compact` 与 auto-compact 配一个便宜快模型做总结,主模型专注对话;未配置零开销、任何异常回落 pi 默认 |
|
|
73
|
+
| 🚀 **autostart** 会话启动自动化 | `.pi/autostart.toml` | 项目内声明启动时自动执行的斜杠命令(首批用例:`/add-dir` 多仓库挂载);未信任不执行、未注册命令跳过不发给模型 |
|
|
72
74
|
|
|
73
75
|
---
|
|
74
76
|
|
|
@@ -191,7 +193,7 @@ pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文
|
|
|
191
193
|
| auto-compact 阈值触发 | 压缩模型总结 |
|
|
192
194
|
| context overflow 恢复 | 压缩模型总结,重试行为不变 |
|
|
193
195
|
|
|
194
|
-
**接管后与 pi 内建行为完全一致的部分**(逐字同步 0.87.1
|
|
196
|
+
**接管后与 pi 内建行为完全一致的部分**(逐字同步 pi 内建实现,0.87.1 起字节级不变、已核对至 1.0.2,只换执行模型):
|
|
195
197
|
|
|
196
198
|
- **摘要格式** —— Goal / Progress / Key Decisions 结构化 checkpoint;已有摘要时用迭代更新版 prompt,信息延续不丢
|
|
197
199
|
- **切分行为** —— 保留最近 `keepRecentTokens` 的策略、`firstKeptEntryId` / `tokensBefore` 原样回传,不动
|
|
@@ -222,6 +224,46 @@ pi 的 `/compact` 与 auto-compact 默认**用当前会话模型**做上下文
|
|
|
222
224
|
|
|
223
225
|
---
|
|
224
226
|
|
|
227
|
+
## 🚀 autostart — 会话启动自动化
|
|
228
|
+
|
|
229
|
+
<a name="autostart"></a>
|
|
230
|
+
|
|
231
|
+
多仓库项目的第一步:在项目内声明「开新会话时要自动执行的斜杠命令」,首批用例是 `/add-dir` 挂载兄弟仓库——新会话无需手动重放。
|
|
232
|
+
|
|
233
|
+
**配置**(`<项目根>/.pi/autostart.toml`,随项目进 git;`.pi/` 是 pi 的项目级配置惯例,先例 `.pi/mcp.json`):
|
|
234
|
+
|
|
235
|
+
```toml
|
|
236
|
+
version = 1
|
|
237
|
+
|
|
238
|
+
[command]
|
|
239
|
+
run = [
|
|
240
|
+
"/add-dir ../shop-frontend",
|
|
241
|
+
"/add-dir ../shop-api",
|
|
242
|
+
]
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
无文件 = 功能未启用,零开销、静默跳过。
|
|
246
|
+
|
|
247
|
+
**执行时机**:
|
|
248
|
+
|
|
249
|
+
| session_start reason | 是否重放 | 原因 |
|
|
250
|
+
| --- | --- | --- |
|
|
251
|
+
| `startup` / `new` | ✅ | 新会话,按声明重放 |
|
|
252
|
+
| `fork` | ✅ | 分支点可能早于配置出现 |
|
|
253
|
+
| `resume` | ❌ | 恢复的分支已携带当时的挂载状态,重放反而会冲突 |
|
|
254
|
+
| `reload` | ❌ | 扩展重建,重放会重复副作用 |
|
|
255
|
+
|
|
256
|
+
**安全门**(按序):
|
|
257
|
+
|
|
258
|
+
1. **project trust** —— 未信任目录不自动执行任何命令(warning 提示);配置解析失败同样不执行(error 提示)
|
|
259
|
+
2. **命令预校验** —— 只执行「扩展注册的命令」;未注册的命令名**跳过并警告**,绝不发给模型(sendUserMessage 对未处理的 `/x` 会当作普通消息发给 LLM,这是必须预校验的原因)
|
|
260
|
+
|
|
261
|
+
**关键约束:只支持扩展命令**。`/add-dir`(pi-add-dir)、`/cd`(pi-cd)等 `registerCommand` 注册的命令可以;内置命令(`/compact`、`/reload`、`/resume`…)在 TUI 层分发,扩展层触发不了;skill 命令(`/skill:name`)是展开机制而非执行机制。依赖的目标扩展(如 pi-add-dir)需要先安装。
|
|
262
|
+
|
|
263
|
+
> **实现说明**:执行用 `sendUserMessage(cmd, { expandPromptTemplates: true })` —— 该选项把 `/cmd args` 路由到命令分发而不是模型 prompt 路径(包装层默认 `false`,已对照 pi 0.86–1.0.4 源码核实,见 AGENTS.md 宿主兼容纪律)。预校验用 `pi.getCommands()`(只认 `source: "extension"`)。依赖 `smol-toml`(零依赖、~10KB、TOML 1.0)。
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
225
267
|
## 🛠️ 开发 Development
|
|
226
268
|
|
|
227
269
|
<a name="development"></a>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), model favorites, prompt stash, third-party code review, and a dedicated compaction model",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,18 +24,21 @@
|
|
|
24
24
|
"code-review",
|
|
25
25
|
"compaction",
|
|
26
26
|
"compact-model",
|
|
27
|
-
"toolkit"
|
|
27
|
+
"toolkit",
|
|
28
|
+
"autostart"
|
|
28
29
|
],
|
|
29
30
|
"peerDependencies": {
|
|
30
|
-
"@earendil-works/pi-coding-agent": "
|
|
31
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0 <2.0.0",
|
|
32
|
+
"@earendil-works/pi-tui": "*"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@dreki-gg/pi-command-sandbox": "^0.4.3",
|
|
34
|
-
"
|
|
36
|
+
"smol-toml": "^1.9.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
|
-
"@earendil-works/pi-ai": "^0.
|
|
38
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
39
|
+
"@earendil-works/pi-ai": "^1.0.2",
|
|
40
|
+
"@earendil-works/pi-coding-agent": "^1.0.2",
|
|
41
|
+
"@earendil-works/pi-tui": "^1.0.2",
|
|
39
42
|
"@types/node": "^26.6.2",
|
|
40
43
|
"typescript": "^5.6.0",
|
|
41
44
|
"vitest": "^4.1.10"
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autostart feature tests.
|
|
3
|
+
*
|
|
4
|
+
* Pure-logic coverage for parseAutostartConfig / commandNameOf, plus an
|
|
5
|
+
* integration pass over registerAutostart with a minimal mock of the pi
|
|
6
|
+
* extension surface (on/getCommands/sendUserMessage) and ExtensionContext
|
|
7
|
+
* (cwd/isProjectTrusted/ui.notify), using a temp directory for the config
|
|
8
|
+
* file.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import {
|
|
16
|
+
AUTOSTART_CONFIG_PATH,
|
|
17
|
+
AutostartParseError,
|
|
18
|
+
commandNameOf,
|
|
19
|
+
parseAutostartConfig,
|
|
20
|
+
RUNNABLE_REASONS,
|
|
21
|
+
} from "./autostart.js";
|
|
22
|
+
import { registerAutostart } from "./index.js";
|
|
23
|
+
|
|
24
|
+
describe("parseAutostartConfig", () => {
|
|
25
|
+
it("parses a full config into the ordered command list", () => {
|
|
26
|
+
const text = [
|
|
27
|
+
"# project autostart",
|
|
28
|
+
"version = 1",
|
|
29
|
+
"",
|
|
30
|
+
"[command]",
|
|
31
|
+
'run = ["/add-dir ../shop-frontend", "/add-dir ../shop-api"]',
|
|
32
|
+
].join("\n");
|
|
33
|
+
expect(parseAutostartConfig(text)).toEqual([
|
|
34
|
+
"/add-dir ../shop-frontend",
|
|
35
|
+
"/add-dir ../shop-api",
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("accepts a config without version (defaults to 1)", () => {
|
|
40
|
+
expect(parseAutostartConfig('[command]\nrun = ["/ask on"]')).toEqual([
|
|
41
|
+
"/ask on",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns empty for a config without [command] or run", () => {
|
|
46
|
+
expect(parseAutostartConfig("version = 1")).toEqual([]);
|
|
47
|
+
expect(parseAutostartConfig("[command]\n")).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("drops blank entries", () => {
|
|
51
|
+
expect(
|
|
52
|
+
parseAutostartConfig('[command]\nrun = ["/ask on", " "]'),
|
|
53
|
+
).toEqual(["/ask on"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("rejects [[command]] array tables instead of ignoring them", () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
parseAutostartConfig('[[command]]\nrun = ["/ask on"]'),
|
|
59
|
+
).toThrow(AutostartParseError);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("normalizes whitespace out of commands", () => {
|
|
63
|
+
expect(
|
|
64
|
+
parseAutostartConfig('[command]\nrun = [" /add-dir ../x "]'),
|
|
65
|
+
).toEqual(["/add-dir ../x"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("rejects version != 1", () => {
|
|
69
|
+
expect(() =>
|
|
70
|
+
parseAutostartConfig("version = 2\n[command]\nrun = []"),
|
|
71
|
+
).toThrow(AutostartParseError);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects invalid TOML", () => {
|
|
75
|
+
expect(() => parseAutostartConfig("[command\nrun = [")).toThrow(
|
|
76
|
+
AutostartParseError,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects [command].run that is not a string array", () => {
|
|
81
|
+
expect(() => parseAutostartConfig('[command]\nrun = "/ask on"')).toThrow(
|
|
82
|
+
AutostartParseError,
|
|
83
|
+
);
|
|
84
|
+
expect(() => parseAutostartConfig("[command]\nrun = [1, 2]")).toThrow(
|
|
85
|
+
AutostartParseError,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("commandNameOf", () => {
|
|
91
|
+
it("extracts the command name from a slash command with args", () => {
|
|
92
|
+
expect(commandNameOf("/add-dir ../shop-frontend")).toBe("add-dir");
|
|
93
|
+
});
|
|
94
|
+
it("tolerates missing slash, extra whitespace, and bare command", () => {
|
|
95
|
+
expect(commandNameOf("add-dir ../x")).toBe("add-dir");
|
|
96
|
+
expect(commandNameOf(" /add-dir ../x ")).toBe("add-dir");
|
|
97
|
+
expect(commandNameOf("/ask")).toBe("ask");
|
|
98
|
+
});
|
|
99
|
+
it("returns empty for blank input", () => {
|
|
100
|
+
expect(commandNameOf(" ")).toBe("");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("RUNNABLE_REASONS", () => {
|
|
105
|
+
it("replays startup/new/fork and not resume/reload", () => {
|
|
106
|
+
expect(RUNNABLE_REASONS.has("startup")).toBe(true);
|
|
107
|
+
expect(RUNNABLE_REASONS.has("new")).toBe(true);
|
|
108
|
+
expect(RUNNABLE_REASONS.has("fork")).toBe(true);
|
|
109
|
+
expect(RUNNABLE_REASONS.has("resume")).toBe(false);
|
|
110
|
+
expect(RUNNABLE_REASONS.has("reload")).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/* ------------------------------------------------------------------ */
|
|
115
|
+
/* registerAutostart integration (mocked pi surface) */
|
|
116
|
+
/* ------------------------------------------------------------------ */
|
|
117
|
+
|
|
118
|
+
type SessionStartHandler = (
|
|
119
|
+
event: { reason: string },
|
|
120
|
+
ctx: {
|
|
121
|
+
cwd: string;
|
|
122
|
+
isProjectTrusted: () => boolean;
|
|
123
|
+
ui: { notify: (message: string, type?: string) => void };
|
|
124
|
+
},
|
|
125
|
+
) => Promise<void>;
|
|
126
|
+
|
|
127
|
+
interface Harness {
|
|
128
|
+
cwd: string;
|
|
129
|
+
handler: SessionStartHandler;
|
|
130
|
+
sent: Array<{ command: string; options?: { expandPromptTemplates?: boolean } }>;
|
|
131
|
+
notifications: Array<{ message: string; type?: string }>;
|
|
132
|
+
commands: Array<{ name: string; source: string }>;
|
|
133
|
+
failCommands: Set<string>;
|
|
134
|
+
setTrusted(trusted: boolean): void;
|
|
135
|
+
start(reason: string): Promise<void>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function makeHarness(
|
|
139
|
+
config: string | null,
|
|
140
|
+
configAsDir = false,
|
|
141
|
+
): Promise<Harness> {
|
|
142
|
+
const cwd = await mkdtemp(join(tmpdir(), "pi-autostart-test-"));
|
|
143
|
+
if (configAsDir) {
|
|
144
|
+
// EISDIR setup: the config path exists but is a directory.
|
|
145
|
+
await mkdir(join(cwd, ".pi"), { recursive: true });
|
|
146
|
+
await mkdir(join(cwd, AUTOSTART_CONFIG_PATH));
|
|
147
|
+
} else if (config !== null) {
|
|
148
|
+
await mkdir(join(cwd, ".pi"), { recursive: true });
|
|
149
|
+
await writeFile(join(cwd, AUTOSTART_CONFIG_PATH), config, "utf8");
|
|
150
|
+
}
|
|
151
|
+
let trusted = true;
|
|
152
|
+
let handler: SessionStartHandler = async () => {};
|
|
153
|
+
const sent: Harness["sent"] = [];
|
|
154
|
+
const notifications: Harness["notifications"] = [];
|
|
155
|
+
const commands: Harness["commands"] = [
|
|
156
|
+
{ name: "add-dir", source: "extension" },
|
|
157
|
+
{ name: "ask", source: "extension" },
|
|
158
|
+
];
|
|
159
|
+
const failCommands = new Set<string>();
|
|
160
|
+
const pi = {
|
|
161
|
+
on: (event: string, h: SessionStartHandler) => {
|
|
162
|
+
if (event === "session_start") handler = h;
|
|
163
|
+
},
|
|
164
|
+
getCommands: () => commands,
|
|
165
|
+
sendUserMessage: (
|
|
166
|
+
command: string,
|
|
167
|
+
options?: { expandPromptTemplates?: boolean },
|
|
168
|
+
) => {
|
|
169
|
+
sent.push({ command, options }); // records attempts, including failures
|
|
170
|
+
if (failCommands.has(command)) {
|
|
171
|
+
return Promise.reject(new Error("boom"));
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
} as unknown as ExtensionAPI;
|
|
175
|
+
registerAutostart(pi);
|
|
176
|
+
return {
|
|
177
|
+
handler,
|
|
178
|
+
sent,
|
|
179
|
+
notifications,
|
|
180
|
+
commands,
|
|
181
|
+
failCommands,
|
|
182
|
+
setTrusted(value: boolean) {
|
|
183
|
+
trusted = value;
|
|
184
|
+
},
|
|
185
|
+
async start(reason: string) {
|
|
186
|
+
await handler({ reason }, {
|
|
187
|
+
cwd,
|
|
188
|
+
isProjectTrusted: () => trusted,
|
|
189
|
+
ui: {
|
|
190
|
+
notify: (message: string, type?: string) => {
|
|
191
|
+
notifications.push({ message, type });
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
cwd,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let current: Harness | undefined;
|
|
201
|
+
|
|
202
|
+
afterEach(async () => {
|
|
203
|
+
if (current) {
|
|
204
|
+
await rm(current.cwd, { recursive: true, force: true });
|
|
205
|
+
current = undefined;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
async function harness(
|
|
210
|
+
config: string | null,
|
|
211
|
+
configAsDir = false,
|
|
212
|
+
): Promise<Harness> {
|
|
213
|
+
const h = await makeHarness(config, configAsDir);
|
|
214
|
+
current = h;
|
|
215
|
+
return h;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
describe("registerAutostart", () => {
|
|
219
|
+
const fullConfig =
|
|
220
|
+
'version = 1\n[command]\nrun = ["/add-dir ../frontend", "/add-dir ../api"]';
|
|
221
|
+
|
|
222
|
+
it("dispatches configured commands with expandPromptTemplates on startup", async () => {
|
|
223
|
+
const h = await harness(fullConfig);
|
|
224
|
+
await h.start("startup");
|
|
225
|
+
expect(h.sent).toEqual([
|
|
226
|
+
{ command: "/add-dir ../frontend", options: { expandPromptTemplates: true } },
|
|
227
|
+
{ command: "/add-dir ../api", options: { expandPromptTemplates: true } },
|
|
228
|
+
]);
|
|
229
|
+
expect(h.notifications).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("reports parse errors and dispatches nothing", async () => {
|
|
233
|
+
const h = await harness("/[command]\nrun = [");
|
|
234
|
+
await h.start("startup");
|
|
235
|
+
expect(h.sent).toEqual([]);
|
|
236
|
+
expect(h.notifications).toHaveLength(1);
|
|
237
|
+
expect(h.notifications[0]?.type).toBe("error");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("replays on new and fork", async () => {
|
|
241
|
+
const h = await harness('[command]\nrun = ["/ask on"]');
|
|
242
|
+
await h.start("new");
|
|
243
|
+
await h.start("fork");
|
|
244
|
+
expect(h.sent).toEqual([
|
|
245
|
+
{ command: "/ask on", options: { expandPromptTemplates: true } },
|
|
246
|
+
{ command: "/ask on", options: { expandPromptTemplates: true } },
|
|
247
|
+
]);
|
|
248
|
+
expect(h.notifications).toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("does nothing on resume or reload", async () => {
|
|
252
|
+
const h = await harness(fullConfig);
|
|
253
|
+
await h.start("resume");
|
|
254
|
+
await h.start("reload");
|
|
255
|
+
expect(h.sent).toEqual([]);
|
|
256
|
+
expect(h.notifications).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("silently skips when no config file exists", async () => {
|
|
260
|
+
const h = await harness(null);
|
|
261
|
+
await h.start("startup");
|
|
262
|
+
expect(h.sent).toEqual([]);
|
|
263
|
+
expect(h.notifications).toEqual([]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("skips everything when the project is not trusted", async () => {
|
|
267
|
+
const h = await harness(fullConfig);
|
|
268
|
+
h.setTrusted(false);
|
|
269
|
+
await h.start("startup");
|
|
270
|
+
expect(h.sent).toEqual([]);
|
|
271
|
+
expect(h.notifications).toHaveLength(1);
|
|
272
|
+
expect(h.notifications[0]?.type).toBe("warning");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("skips unregistered commands instead of sending them to the model", async () => {
|
|
276
|
+
const h = await harness(
|
|
277
|
+
'[command]\nrun = ["/add-dir ../x", "/not-a-command y"]',
|
|
278
|
+
);
|
|
279
|
+
await h.start("startup");
|
|
280
|
+
expect(h.sent).toEqual([
|
|
281
|
+
{ command: "/add-dir ../x", options: { expandPromptTemplates: true } },
|
|
282
|
+
]);
|
|
283
|
+
expect(h.notifications).toHaveLength(1);
|
|
284
|
+
expect(h.notifications[0]?.type).toBe("warning");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("skips commands only registered as skill or prompt sources", async () => {
|
|
288
|
+
const h = await harness('[command]\nrun = ["/add-dir ../x"]');
|
|
289
|
+
h.commands.length = 0;
|
|
290
|
+
h.commands.push({ name: "add-dir", source: "skill" });
|
|
291
|
+
await h.start("startup");
|
|
292
|
+
expect(h.sent).toEqual([]);
|
|
293
|
+
expect(h.notifications).toHaveLength(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("warns when the config path cannot be read for a reason other than ENOENT", async () => {
|
|
297
|
+
const h = await harness(null, true); // .pi/autostart.toml is a directory → EISDIR
|
|
298
|
+
await h.start("startup");
|
|
299
|
+
expect(h.sent).toEqual([]);
|
|
300
|
+
expect(h.notifications).toHaveLength(1);
|
|
301
|
+
expect(h.notifications[0]?.type).toBe("warning");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("keeps dispatching after one command fails", async () => {
|
|
305
|
+
const h = await harness(fullConfig);
|
|
306
|
+
h.failCommands.add("/add-dir ../frontend");
|
|
307
|
+
await h.start("startup");
|
|
308
|
+
expect(h.sent.map((entry) => entry.command)).toEqual([
|
|
309
|
+
"/add-dir ../frontend",
|
|
310
|
+
"/add-dir ../api",
|
|
311
|
+
]);
|
|
312
|
+
expect(h.notifications).toHaveLength(1);
|
|
313
|
+
expect(h.notifications[0]?.type).toBe("error");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autostart feature — pure config logic, no pi imports.
|
|
3
|
+
*
|
|
4
|
+
* Schema (`.pi/autostart.toml`, checked into the project):
|
|
5
|
+
*
|
|
6
|
+
* ```toml
|
|
7
|
+
* version = 1
|
|
8
|
+
*
|
|
9
|
+
* [command]
|
|
10
|
+
* run = [
|
|
11
|
+
* "/add-dir ../shop-frontend",
|
|
12
|
+
* "/add-dir ../shop-api",
|
|
13
|
+
* ]
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `version` is optional (defaults to 1) but must be 1 when present, so a
|
|
17
|
+
* future schema change can fail loudly instead of silently misreading.
|
|
18
|
+
*/
|
|
19
|
+
import { parse as parseToml } from "smol-toml";
|
|
20
|
+
|
|
21
|
+
/** Project-relative location of the autostart config (`.pi/` is pi's
|
|
22
|
+
* project-config convention, precedent: `.pi/mcp.json`). */
|
|
23
|
+
export const AUTOSTART_CONFIG_PATH = ".pi/autostart.toml";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* session_start reasons that replay autostart commands:
|
|
27
|
+
* - startup / new: fresh session in the project — replay
|
|
28
|
+
* - fork: new branch of an earlier session — replay (the fork point may
|
|
29
|
+
* predate the config)
|
|
30
|
+
* - resume: the resumed branch already carries its /add-dir state —
|
|
31
|
+
* replaying would at best duplicate work (pi-add-dir is idempotent) and
|
|
32
|
+
* at worst re-add directories the user removed on that branch
|
|
33
|
+
* - reload: extensions are being rebuilt — re-running commands would
|
|
34
|
+
* duplicate side effects from the same session
|
|
35
|
+
*/
|
|
36
|
+
export const RUNNABLE_REASONS: ReadonlySet<string> = new Set([
|
|
37
|
+
"startup",
|
|
38
|
+
"new",
|
|
39
|
+
"fork",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Parse/validation failure with a user-facing message. */
|
|
43
|
+
export class AutostartParseError extends Error {}
|
|
44
|
+
|
|
45
|
+
function errMessage(err: unknown): string {
|
|
46
|
+
return err instanceof Error ? err.message : String(err);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse autostart config text into the ordered command list.
|
|
51
|
+
* @throws AutostartParseError on invalid TOML, wrong shape, or version ≠ 1.
|
|
52
|
+
*/
|
|
53
|
+
export function parseAutostartConfig(text: string): string[] {
|
|
54
|
+
let doc: unknown;
|
|
55
|
+
try {
|
|
56
|
+
doc = parseToml(text);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw new AutostartParseError(`invalid TOML: ${errMessage(err)}`);
|
|
59
|
+
}
|
|
60
|
+
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
|
|
61
|
+
throw new AutostartParseError("top-level value must be a table");
|
|
62
|
+
}
|
|
63
|
+
const { version, command } = doc as {
|
|
64
|
+
version?: unknown;
|
|
65
|
+
command?: unknown;
|
|
66
|
+
};
|
|
67
|
+
if (version !== undefined && version !== 1) {
|
|
68
|
+
throw new AutostartParseError(
|
|
69
|
+
`unsupported version ${String(version)} (expected 1)`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (command === undefined) return [];
|
|
73
|
+
if (Array.isArray(command)) {
|
|
74
|
+
throw new AutostartParseError(
|
|
75
|
+
"[[command]] array tables are not supported — use a single [command] table with a run array",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (command === null || typeof command !== "object") {
|
|
79
|
+
throw new AutostartParseError("[command] must be a table");
|
|
80
|
+
}
|
|
81
|
+
const run = (command as { run?: unknown }).run;
|
|
82
|
+
if (run === undefined) return [];
|
|
83
|
+
if (
|
|
84
|
+
!Array.isArray(run) ||
|
|
85
|
+
run.some((entry) => typeof entry !== "string")
|
|
86
|
+
) {
|
|
87
|
+
throw new AutostartParseError(
|
|
88
|
+
"[command].run must be an array of command strings",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
// Trim each entry: `prompt()` dispatches a command only when the text
|
|
92
|
+
// literally starts with "/", so a leading-space entry would silently fall
|
|
93
|
+
// through to the model as a plain message. Normalizing here keeps the
|
|
94
|
+
// "never sent to the model" guarantee (see index.ts).
|
|
95
|
+
return (run as string[])
|
|
96
|
+
.map((cmd) => cmd.trim())
|
|
97
|
+
.filter((cmd) => cmd !== "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract the command name a slash command dispatches to:
|
|
102
|
+
* `"/add-dir ../x"` → `"add-dir"`, tolerant of leading whitespace and an
|
|
103
|
+
* optional slash. Empty for blank input.
|
|
104
|
+
*/
|
|
105
|
+
export function commandNameOf(command: string): string {
|
|
106
|
+
const stripped = command.trim().replace(/^\//, "");
|
|
107
|
+
return stripped.split(/\s+/)[0] ?? "";
|
|
108
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autostart feature — run project-declared slash commands at session start.
|
|
3
|
+
*
|
|
4
|
+
* Config lives at `<project root>/.pi/autostart.toml` (see autostart.ts for
|
|
5
|
+
* the schema). Execution gates, in order:
|
|
6
|
+
*
|
|
7
|
+
* 1. reason filter — only startup/new/fork replay (see RUNNABLE_REASONS)
|
|
8
|
+
* 2. config file exists — a missing file is a silent opt-in skip
|
|
9
|
+
* 3. `ctx.isProjectTrusted()` — an untrusted project never auto-executes
|
|
10
|
+
* 4. per-command pre-validation against `pi.getCommands()`: only commands
|
|
11
|
+
* registered by an *extension* are dispatched. This is mandatory:
|
|
12
|
+
* sendUserMessage with an unhandled "/x" falls through prompt() to the
|
|
13
|
+
* model as a plain user message (pi agent-session.js), so an unknown
|
|
14
|
+
* command must be skipped here, never sent.
|
|
15
|
+
*
|
|
16
|
+
* Dispatch uses `sendUserMessage(cmd, { expandPromptTemplates: true })` —
|
|
17
|
+
* that flag is what routes "/cmd args" through _tryExecuteExtensionCommand
|
|
18
|
+
* instead of the model prompt path. The wrapper defaults it to false
|
|
19
|
+
* (verified against pi 1.0.4 agent-session.js; see AGENTS.md host-compat
|
|
20
|
+
* discipline for the version matrix).
|
|
21
|
+
*
|
|
22
|
+
* Only extension commands can run this way. Built-in commands (/compact,
|
|
23
|
+
* /reload, /resume, …) are dispatched at the TUI layer and cannot be
|
|
24
|
+
* triggered from the extension layer; skill commands (/skill:name) expand
|
|
25
|
+
* into prompts rather than execute. First-party use case: `/add-dir`
|
|
26
|
+
* (pi-add-dir), which is idempotent ("Already added" check).
|
|
27
|
+
*/
|
|
28
|
+
import { readFile } from "node:fs/promises";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
import {
|
|
32
|
+
AUTOSTART_CONFIG_PATH,
|
|
33
|
+
AutostartParseError,
|
|
34
|
+
commandNameOf,
|
|
35
|
+
parseAutostartConfig,
|
|
36
|
+
RUNNABLE_REASONS,
|
|
37
|
+
} from "./autostart.js";
|
|
38
|
+
|
|
39
|
+
export function registerAutostart(pi: ExtensionAPI): void {
|
|
40
|
+
pi.on("session_start", async (event, ctx) => {
|
|
41
|
+
if (!RUNNABLE_REASONS.has(event.reason)) return;
|
|
42
|
+
|
|
43
|
+
let text: string;
|
|
44
|
+
try {
|
|
45
|
+
text = await readFile(join(ctx.cwd, AUTOSTART_CONFIG_PATH), "utf8");
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// Missing file = opt-in skip. Any other read failure (EACCES, EISDIR,
|
|
48
|
+
// …) is a misconfiguration the operator should hear about.
|
|
49
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
50
|
+
ctx.ui.notify(
|
|
51
|
+
`autostart: skipped — cannot read ${AUTOSTART_CONFIG_PATH}: ${err instanceof Error ? err.message : String(err)}`,
|
|
52
|
+
"warning",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let commands: string[];
|
|
59
|
+
try {
|
|
60
|
+
commands = parseAutostartConfig(text);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const message = err instanceof AutostartParseError ? err.message : String(err);
|
|
63
|
+
ctx.ui.notify(
|
|
64
|
+
`autostart: skipped — ${message} (${AUTOSTART_CONFIG_PATH})`,
|
|
65
|
+
"error",
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (commands.length === 0) return;
|
|
70
|
+
|
|
71
|
+
if (!ctx.isProjectTrusted()) {
|
|
72
|
+
ctx.ui.notify(
|
|
73
|
+
`autostart: skipped — project not trusted (${AUTOSTART_CONFIG_PATH})`,
|
|
74
|
+
"warning",
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Only extension-registered commands are dispatchable; skill and
|
|
80
|
+
// prompt-template entries expand rather than execute.
|
|
81
|
+
const dispatchable = new Set(
|
|
82
|
+
pi
|
|
83
|
+
.getCommands()
|
|
84
|
+
.flatMap((cmd) =>
|
|
85
|
+
cmd.source === "extension" ? [cmd.name.replace(/^\//, "")] : [],
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
for (const command of commands) {
|
|
90
|
+
const name = commandNameOf(command);
|
|
91
|
+
if (!name || !dispatchable.has(name)) {
|
|
92
|
+
ctx.ui.notify(
|
|
93
|
+
`autostart: skipped "${command}" — not a registered extension command`,
|
|
94
|
+
"warning",
|
|
95
|
+
);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// One failing command must not cancel the rest of the list.
|
|
99
|
+
try {
|
|
100
|
+
await pi.sendUserMessage(command, { expandPromptTemplates: true });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
ctx.ui.notify(
|
|
103
|
+
`autostart: "${command}" failed — ${err instanceof Error ? err.message : String(err)}`,
|
|
104
|
+
"error",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* emits for ALL three trigger reasons (manual / threshold / overflow)
|
|
11
11
|
* before running its own compaction
|
|
12
12
|
* - prompts, message assembly, split-turn handling, output budgets, and the
|
|
13
|
-
* summary file-list tail are verbatim pi
|
|
14
|
-
* the executing model
|
|
13
|
+
* summary file-list tail are verbatim pi built-in behavior (copied from
|
|
14
|
+
* 0.87.1, verified unchanged through 1.0.2) — only the executing model
|
|
15
|
+
* differs
|
|
15
16
|
* - lossless fallback: unset config, unavailable model, empty/failed
|
|
16
17
|
* summary, or user abort → return undefined and pi's default compaction
|
|
17
18
|
* (session model) runs untouched. The extension can never make
|
|
@@ -115,8 +116,8 @@ async function completeSummary(
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
|
-
* pi's output-budget formula (0.87.1
|
|
119
|
-
* reserveTokens clamped by the model's own output cap.
|
|
119
|
+
* pi's output-budget formula (compaction.js, 0.87.1 → 1.0.2 unchanged): a
|
|
120
|
+
* fraction of reserveTokens clamped by the model's own output cap.
|
|
120
121
|
*/
|
|
121
122
|
function summarizationBudget(
|
|
122
123
|
fraction: number,
|
|
@@ -131,11 +132,12 @@ function summarizationBudget(
|
|
|
131
132
|
|
|
132
133
|
/**
|
|
133
134
|
* Run the full compaction summarization with the configured model,
|
|
134
|
-
* replicating pi 0.87.1
|
|
135
|
-
* iterative-update prompt, custom
|
|
136
|
-
* turn-prefix checkpoint when the cut point
|
|
137
|
-
* summary with the file-list tail. Returns
|
|
138
|
-
* notifying (pi default compaction then takes
|
|
135
|
+
* replicating pi's compact() (0.87.1, verified unchanged through 1.0.2):
|
|
136
|
+
* history summary (first-connection or iterative-update prompt, custom
|
|
137
|
+
* focus appended), plus a separate turn-prefix checkpoint when the cut point
|
|
138
|
+
* splits a turn, merged into one summary with the file-list tail. Returns
|
|
139
|
+
* undefined on any failure after notifying (pi default compaction then takes
|
|
140
|
+
* over).
|
|
139
141
|
*/
|
|
140
142
|
async function runCompactModelCompaction(
|
|
141
143
|
ctx: ExtensionContext,
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* compaction (dist/core/compaction/compaction.js and utils.js) so that
|
|
6
6
|
* summaries produced by the configured compact model are formatted exactly
|
|
7
7
|
* like pi's own compaction output. Only the executing model differs.
|
|
8
|
+
* Verified byte-identical against pi 1.0.2 (0.88–0.98 were never published;
|
|
9
|
+
* the only 0.87.1→1.0.2 change there is a combineUsage refactor into a
|
|
10
|
+
* shared module, no prompt or budget change).
|
|
8
11
|
*
|
|
9
12
|
* Update these strings whenever pi changes its built-in prompts (diff
|
|
10
13
|
* dist/core/compaction/compaction.js between versions).
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* favorite, backward to the last (user-confirmed)
|
|
14
14
|
* - with exactly one favorite, already on it → undefined (stock "only one
|
|
15
15
|
* model available" feedback); otherwise jump to it
|
|
16
|
-
* - the apply logic mirrors pi
|
|
17
|
-
* (
|
|
16
|
+
* - the apply logic mirrors pi's _cycleScopedModel/_cycleAvailableModel
|
|
17
|
+
* (byte-identical across every published version in 0.86.0 → 1.0.2:
|
|
18
|
+
* state swap, session persistence, default-model persistence only when
|
|
18
19
|
* `options.persist` is set, thinking-level re-clamp, model_select emit
|
|
19
20
|
* with source "cycle")
|
|
20
21
|
*
|
|
@@ -28,9 +29,10 @@ import {
|
|
|
28
29
|
import { getFavoritesStore, modelKey } from "./store.js";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* pi's `Model<any>` — the exact type of `ModelCycleResult.model`
|
|
32
|
-
* pi-coding-agent 0.
|
|
33
|
-
* @earendil-works/pi-ai,
|
|
32
|
+
* pi's `Model<any>` — the exact type of `ModelCycleResult.model`
|
|
33
|
+
* (pi-coding-agent 0.86–1.0; the field is unchanged across that span).
|
|
34
|
+
* Derived here instead of importing `Model` from @earendil-works/pi-ai,
|
|
35
|
+
* whose index.d.ts (0.86 through 1.0.2 alike) re-exports it via an
|
|
34
36
|
* extensioned `"./types.ts"` specifier that not every TS resolver substitutes
|
|
35
37
|
* to `types.d.ts` (tsc 5.9 bundler does; other tsserver builds don't).
|
|
36
38
|
*/
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Version guard: if the prototype no longer has the expected methods (pi
|
|
21
21
|
* upgraded internals), patches are skipped with a warning — the extension
|
|
22
|
-
* keeps loading and ctrl+p falls back to stock behavior.
|
|
22
|
+
* keeps loading and ctrl+p falls back to stock behavior. The prototype
|
|
23
|
+
* shape touched here (handleInput/updateList/filterModels and the instance
|
|
24
|
+
* fields below) is byte-identical across every published version in
|
|
25
|
+
* pi 0.86.0 → 1.0.2.
|
|
23
26
|
*/
|
|
24
27
|
import {
|
|
25
28
|
ModelSelectorComponent,
|
|
@@ -113,8 +116,9 @@ let patched = false;
|
|
|
113
116
|
export function applyModelSelectorPatches(): boolean {
|
|
114
117
|
if (patched) return true;
|
|
115
118
|
// SAFETY: ModelSelectorComponent's members are TS-private (compile-time
|
|
116
|
-
// only); the structural view matches pi 0.
|
|
117
|
-
//
|
|
119
|
+
// only); the structural view matches pi 0.86–1.0's prototype shape
|
|
120
|
+
// (byte-identical across that span) and every touched method is
|
|
121
|
+
// existence-checked below before any patching happens.
|
|
118
122
|
const proto =
|
|
119
123
|
ModelSelectorComponent.prototype as unknown as SelectorPatchTarget;
|
|
120
124
|
if (
|
|
@@ -151,7 +155,7 @@ export function applyModelSelectorPatches(): boolean {
|
|
|
151
155
|
};
|
|
152
156
|
|
|
153
157
|
// ── 2. List rendering: hint row + bold bright yellow favorites ────────
|
|
154
|
-
// Replicates the built-in updateList layout (pi 0.
|
|
158
|
+
// Replicates the built-in updateList layout (pi 0.86–1.0: fixed-width cursor
|
|
155
159
|
// and ✓ current-marker columns, provider badge, `· default` badge, scroll
|
|
156
160
|
// indicator, empty/error states, refresh status, Model Name footer) with
|
|
157
161
|
// one change: favorited model ids render in the favorite style.
|
package/src/index.ts
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* - stash — prompt stash (ctrl+alt+y swap / double-press stash-and-clear)
|
|
11
11
|
* - review — third-party code review with a configured review model (third-review)
|
|
12
12
|
* - compact — dedicated compact model for /compact + auto-compact summaries
|
|
13
|
+
* - autostart — replay project-declared slash commands at session start (/add-dir persistence)
|
|
13
14
|
*/
|
|
14
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import { registerAsk } from "./features/ask/index.js";
|
|
17
|
+
import { registerAutostart } from "./features/autostart/index.js";
|
|
16
18
|
import { registerCompact } from "./features/compact/index.js";
|
|
17
19
|
import { registerFavorites } from "./features/favorites/index.js";
|
|
18
20
|
import { registerReview } from "./features/review/index.js";
|
|
@@ -20,6 +22,7 @@ import { registerStash } from "./features/stash/index.js";
|
|
|
20
22
|
|
|
21
23
|
export default function piToolkits(pi: ExtensionAPI): void {
|
|
22
24
|
registerAsk(pi);
|
|
25
|
+
registerAutostart(pi);
|
|
23
26
|
registerCompact(pi);
|
|
24
27
|
registerFavorites(pi);
|
|
25
28
|
registerReview(pi);
|
package/src/lib/model-config.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* (review, compact-model — favorites keeps its own store-local key logic).
|
|
5
5
|
*
|
|
6
6
|
* Types are derived from pi-coding-agent's own exports (ModelRegistry)
|
|
7
|
-
* rather than imported from @earendil-works/pi-ai: pi-ai
|
|
8
|
-
* re-exports `Model`/`Api` via extensioned
|
|
9
|
-
* tsc 5.9 bundler resolution substitutes to
|
|
10
|
-
* builds do not. Deriving from ModelRegistry keeps
|
|
11
|
-
* agreement with the exact type
|
|
7
|
+
* rather than imported from @earendil-works/pi-ai: pi-ai's index.d.ts (0.87
|
|
8
|
+
* through 1.0.2 alike) re-exports `Model`/`Api` via extensioned
|
|
9
|
+
* `./types.ts` specifiers, which tsc 5.9 bundler resolution substitutes to
|
|
10
|
+
* `.d.ts` but some tsserver builds do not. Deriving from ModelRegistry keeps
|
|
11
|
+
* every resolver in agreement with the exact type
|
|
12
|
+
* `ctx.modelRegistry.getAvailable()` returns.
|
|
12
13
|
*/
|
|
13
14
|
import type {
|
|
14
15
|
ExtensionContext,
|
package/src/lib/model-picker.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The component is driven through a small ModelRuntime facade backed by
|
|
11
11
|
* ctx.modelRegistry (pi's extension-facing wrapper). SAFETY: verified
|
|
12
|
-
* against pi 0.
|
|
13
|
-
* model-catalog-refresh.js
|
|
14
|
-
* getModel, getError and refresh
|
|
15
|
-
* exactly those
|
|
12
|
+
* against pi 0.86–1.0 dist (modes/interactive/components/model-selector.js +
|
|
13
|
+
* model-catalog-refresh.js — byte-identical across that span) — the
|
|
14
|
+
* component only calls getAvailableSnapshot, getModel, getError and refresh
|
|
15
|
+
* on modelRuntime, and ModelRegistry provides exactly those
|
|
16
|
+
* (getAvailable/find/getError/refresh).
|
|
16
17
|
*
|
|
17
18
|
* `current` is passed as the component's currentModel: the built-in selector
|
|
18
19
|
* preselects, ✓-marks and sorts-first its currentModel, so one Enter accepts
|
|
@@ -41,7 +42,7 @@ function modelRuntimeFacade(ctx: ExtensionContext): ModelRuntime {
|
|
|
41
42
|
refresh: (options?: { signal?: AbortSignal }) =>
|
|
42
43
|
ctx.modelRegistry.refresh(options),
|
|
43
44
|
};
|
|
44
|
-
// SAFETY: the selector only touches the four members above (pi 0.
|
|
45
|
+
// SAFETY: the selector only touches the four members above (pi 0.86–1.0
|
|
45
46
|
// model-selector.js); everything else on ModelRuntime is never reached
|
|
46
47
|
// from this component.
|
|
47
48
|
return facade as unknown as ModelRuntime;
|