gentle-pi 0.2.7 → 0.3.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 +43 -13
- package/assets/orchestrator.md +13 -4
- package/extensions/gentle-ai.ts +274 -119
- package/extensions/sdd-init.ts +7 -0
- package/lib/sdd-preflight.ts +253 -0
- package/package.json +2 -1
- package/scripts/verify-package-files.mjs +1 -0
- package/tests/runtime-harness.mjs +277 -3
package/extensions/gentle-ai.ts
CHANGED
|
@@ -14,6 +14,14 @@ import type {
|
|
|
14
14
|
ToolCallEventResult,
|
|
15
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
17
|
+
import {
|
|
18
|
+
ensureSddPreflight,
|
|
19
|
+
getSddPreflightPreferences,
|
|
20
|
+
installSddAssets,
|
|
21
|
+
isSddPreflightTrigger,
|
|
22
|
+
renderSddPreflightPrompt,
|
|
23
|
+
type SddPreflightPreferences,
|
|
24
|
+
} from "../lib/sdd-preflight.ts";
|
|
17
25
|
|
|
18
26
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
19
27
|
const ASSETS_DIR = join(PACKAGE_ROOT, "assets");
|
|
@@ -101,7 +109,12 @@ const SDD_AGENT_NAMES = [
|
|
|
101
109
|
] as const;
|
|
102
110
|
|
|
103
111
|
type SddAgentName = (typeof SDD_AGENT_NAMES)[number];
|
|
104
|
-
type
|
|
112
|
+
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
113
|
+
interface AgentRoutingEntry {
|
|
114
|
+
model?: string;
|
|
115
|
+
thinking?: ThinkingLevel;
|
|
116
|
+
}
|
|
117
|
+
type AgentModelConfig = Record<string, AgentRoutingEntry>;
|
|
105
118
|
type AgentSource = "project" | "user" | "builtin";
|
|
106
119
|
|
|
107
120
|
interface AgentEntry {
|
|
@@ -113,6 +126,16 @@ interface AgentEntry {
|
|
|
113
126
|
const KEEP_CURRENT = "Keep current";
|
|
114
127
|
const INHERIT_MODEL = "Inherit active/default model";
|
|
115
128
|
const CUSTOM_MODEL = "Custom model id";
|
|
129
|
+
const INHERIT_THINKING = "Inherit effort";
|
|
130
|
+
const THINKING_OPTIONS: (ThinkingLevel | typeof INHERIT_THINKING)[] = [
|
|
131
|
+
INHERIT_THINKING,
|
|
132
|
+
"off",
|
|
133
|
+
"minimal",
|
|
134
|
+
"low",
|
|
135
|
+
"medium",
|
|
136
|
+
"high",
|
|
137
|
+
"xhigh",
|
|
138
|
+
];
|
|
116
139
|
|
|
117
140
|
const MODEL_CONTROL_OPTIONS = [
|
|
118
141
|
KEEP_CURRENT,
|
|
@@ -167,62 +190,6 @@ async function confirmCommand(
|
|
|
167
190
|
};
|
|
168
191
|
}
|
|
169
192
|
|
|
170
|
-
function copyDirectoryFiles(
|
|
171
|
-
sourceDir: string,
|
|
172
|
-
targetDir: string,
|
|
173
|
-
force: boolean,
|
|
174
|
-
): { copied: number; skipped: number } {
|
|
175
|
-
if (!existsSync(sourceDir)) return { copied: 0, skipped: 0 };
|
|
176
|
-
mkdirSync(targetDir, { recursive: true });
|
|
177
|
-
let copied = 0;
|
|
178
|
-
let skipped = 0;
|
|
179
|
-
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
180
|
-
const sourcePath = join(sourceDir, entry.name);
|
|
181
|
-
const targetPath = join(targetDir, entry.name);
|
|
182
|
-
if (entry.isDirectory()) {
|
|
183
|
-
const child = copyDirectoryFiles(sourcePath, targetPath, force);
|
|
184
|
-
copied += child.copied;
|
|
185
|
-
skipped += child.skipped;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (!entry.isFile()) continue;
|
|
189
|
-
if (!force && existsSync(targetPath)) {
|
|
190
|
-
skipped += 1;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
writeFileSync(targetPath, readFileSync(sourcePath));
|
|
194
|
-
copied += 1;
|
|
195
|
-
}
|
|
196
|
-
return { copied, skipped };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function installSddAssets(
|
|
200
|
-
cwd: string,
|
|
201
|
-
force: boolean,
|
|
202
|
-
): { agents: number; chains: number; support: number; skipped: number } {
|
|
203
|
-
const agents = copyDirectoryFiles(
|
|
204
|
-
join(ASSETS_DIR, "agents"),
|
|
205
|
-
join(cwd, ".pi", "agents"),
|
|
206
|
-
force,
|
|
207
|
-
);
|
|
208
|
-
const chains = copyDirectoryFiles(
|
|
209
|
-
join(ASSETS_DIR, "chains"),
|
|
210
|
-
join(cwd, ".pi", "chains"),
|
|
211
|
-
force,
|
|
212
|
-
);
|
|
213
|
-
const support = copyDirectoryFiles(
|
|
214
|
-
join(ASSETS_DIR, "support"),
|
|
215
|
-
join(cwd, ".pi", "gentle-ai", "support"),
|
|
216
|
-
force,
|
|
217
|
-
);
|
|
218
|
-
return {
|
|
219
|
-
agents: agents.copied,
|
|
220
|
-
chains: chains.copied,
|
|
221
|
-
support: support.copied,
|
|
222
|
-
skipped: agents.skipped + chains.skipped + support.skipped,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
193
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
227
194
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
228
195
|
}
|
|
@@ -253,7 +220,33 @@ function writePersonaMode(cwd: string, mode: PersonaMode): void {
|
|
|
253
220
|
writeFileSync(path, `${JSON.stringify({ mode }, null, 2)}\n`);
|
|
254
221
|
}
|
|
255
222
|
|
|
256
|
-
function
|
|
223
|
+
function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
224
|
+
return (
|
|
225
|
+
value === "off" ||
|
|
226
|
+
value === "minimal" ||
|
|
227
|
+
value === "low" ||
|
|
228
|
+
value === "medium" ||
|
|
229
|
+
value === "high" ||
|
|
230
|
+
value === "xhigh"
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeRoutingEntry(value: unknown): AgentRoutingEntry | undefined {
|
|
235
|
+
if (typeof value === "string") {
|
|
236
|
+
const model = value.trim();
|
|
237
|
+
return model.length > 0 ? { model } : undefined;
|
|
238
|
+
}
|
|
239
|
+
if (!isRecord(value)) return undefined;
|
|
240
|
+
const model =
|
|
241
|
+
typeof value.model === "string" && value.model.trim().length > 0
|
|
242
|
+
? value.model.trim()
|
|
243
|
+
: undefined;
|
|
244
|
+
const thinking = isThinkingLevel(value.thinking) ? value.thinking : undefined;
|
|
245
|
+
if (!model && !thinking) return undefined;
|
|
246
|
+
return { model, thinking };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function readModelConfig(cwd: string): AgentModelConfig {
|
|
257
250
|
const path = modelConfigPath(cwd);
|
|
258
251
|
if (!existsSync(path)) return {};
|
|
259
252
|
try {
|
|
@@ -261,9 +254,8 @@ function readModelConfig(cwd: string): AgentModelConfig {
|
|
|
261
254
|
if (!isRecord(parsed)) return {};
|
|
262
255
|
const config: AgentModelConfig = {};
|
|
263
256
|
for (const [name, value] of Object.entries(parsed)) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
257
|
+
const entry = normalizeRoutingEntry(value);
|
|
258
|
+
if (entry) config[name] = entry;
|
|
267
259
|
}
|
|
268
260
|
return config;
|
|
269
261
|
} catch {
|
|
@@ -274,12 +266,23 @@ function readModelConfig(cwd: string): AgentModelConfig {
|
|
|
274
266
|
function writeModelConfig(cwd: string, config: AgentModelConfig): void {
|
|
275
267
|
const path = modelConfigPath(cwd);
|
|
276
268
|
mkdirSync(dirname(path), { recursive: true });
|
|
277
|
-
|
|
269
|
+
const cleaned: AgentModelConfig = {};
|
|
270
|
+
for (const [name, value] of Object.entries(config)) {
|
|
271
|
+
const entry = normalizeRoutingEntry(value);
|
|
272
|
+
if (entry) cleaned[name] = entry;
|
|
273
|
+
}
|
|
274
|
+
writeFileSync(path, `${JSON.stringify(cleaned, null, 2)}\n`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function cloneModelConfig(config: AgentModelConfig): AgentModelConfig {
|
|
278
|
+
return Object.fromEntries(
|
|
279
|
+
Object.entries(config).map(([name, entry]) => [name, { ...entry }]),
|
|
280
|
+
);
|
|
278
281
|
}
|
|
279
282
|
|
|
280
|
-
function
|
|
283
|
+
function updateFrontmatterRouting(
|
|
281
284
|
content: string,
|
|
282
|
-
|
|
285
|
+
entry: AgentRoutingEntry | undefined,
|
|
283
286
|
): string {
|
|
284
287
|
if (!content.startsWith("---\n")) return content;
|
|
285
288
|
const endIndex = content.indexOf("\n---", 4);
|
|
@@ -288,14 +291,19 @@ function updateFrontmatterModel(
|
|
|
288
291
|
const body = content.slice(endIndex);
|
|
289
292
|
const lines = frontmatter
|
|
290
293
|
.split("\n")
|
|
291
|
-
.filter(
|
|
292
|
-
|
|
294
|
+
.filter(
|
|
295
|
+
(line) => !line.startsWith("model:") && !line.startsWith("thinking:"),
|
|
296
|
+
);
|
|
297
|
+
const toInsert: string[] = [];
|
|
298
|
+
if (entry?.model) toInsert.push(`model: ${entry.model}`);
|
|
299
|
+
if (entry?.thinking) toInsert.push(`thinking: ${entry.thinking}`);
|
|
300
|
+
if (toInsert.length > 0) {
|
|
293
301
|
const descriptionIndex = lines.findIndex((line) =>
|
|
294
302
|
line.startsWith("description:"),
|
|
295
303
|
);
|
|
296
304
|
const insertIndex =
|
|
297
305
|
descriptionIndex >= 0 ? descriptionIndex + 1 : Math.min(1, lines.length);
|
|
298
|
-
lines.splice(insertIndex, 0,
|
|
306
|
+
lines.splice(insertIndex, 0, ...toInsert);
|
|
299
307
|
}
|
|
300
308
|
return `---\n${lines.join("\n")}${body}`;
|
|
301
309
|
}
|
|
@@ -372,7 +380,7 @@ function projectSettingsPath(cwd: string): string {
|
|
|
372
380
|
function updateBuiltinModelOverride(
|
|
373
381
|
cwd: string,
|
|
374
382
|
name: string,
|
|
375
|
-
|
|
383
|
+
entry: AgentRoutingEntry | undefined,
|
|
376
384
|
): boolean {
|
|
377
385
|
const path = projectSettingsPath(cwd);
|
|
378
386
|
let settings: Record<string, unknown> = {};
|
|
@@ -393,8 +401,10 @@ function updateBuiltinModelOverride(
|
|
|
393
401
|
const current = isRecord(agentOverrides[name])
|
|
394
402
|
? { ...agentOverrides[name] }
|
|
395
403
|
: {};
|
|
396
|
-
if (model === undefined) delete current.model;
|
|
397
|
-
else current.model = model;
|
|
404
|
+
if (entry?.model === undefined) delete current.model;
|
|
405
|
+
else current.model = entry.model;
|
|
406
|
+
if (entry?.thinking === undefined) delete current.thinking;
|
|
407
|
+
else current.thinking = entry.thinking;
|
|
398
408
|
if (Object.keys(current).length > 0) agentOverrides[name] = current;
|
|
399
409
|
else delete agentOverrides[name];
|
|
400
410
|
if (Object.keys(agentOverrides).length > 0)
|
|
@@ -407,16 +417,16 @@ function updateBuiltinModelOverride(
|
|
|
407
417
|
return true;
|
|
408
418
|
}
|
|
409
419
|
|
|
410
|
-
function applyModelConfig(
|
|
420
|
+
export function applyModelConfig(
|
|
411
421
|
cwd: string,
|
|
412
422
|
config: AgentModelConfig,
|
|
413
423
|
): { updated: number; skipped: number } {
|
|
414
424
|
let updated = 0;
|
|
415
425
|
let skipped = 0;
|
|
416
426
|
for (const agent of listDiscoverableAgents(cwd)) {
|
|
417
|
-
const
|
|
427
|
+
const entry = config[agent.name];
|
|
418
428
|
if (agent.source === "builtin") {
|
|
419
|
-
if (updateBuiltinModelOverride(cwd, agent.name,
|
|
429
|
+
if (updateBuiltinModelOverride(cwd, agent.name, entry)) updated += 1;
|
|
420
430
|
else skipped += 1;
|
|
421
431
|
continue;
|
|
422
432
|
}
|
|
@@ -425,7 +435,7 @@ function applyModelConfig(
|
|
|
425
435
|
continue;
|
|
426
436
|
}
|
|
427
437
|
const original = readFileSync(agent.filePath, "utf8");
|
|
428
|
-
const next =
|
|
438
|
+
const next = updateFrontmatterRouting(original, entry);
|
|
429
439
|
if (next === original) {
|
|
430
440
|
skipped += 1;
|
|
431
441
|
continue;
|
|
@@ -437,9 +447,12 @@ function applyModelConfig(
|
|
|
437
447
|
}
|
|
438
448
|
|
|
439
449
|
function describeModelConfig(cwd: string, config: AgentModelConfig): string[] {
|
|
440
|
-
return listDiscoverableAgents(cwd).map(
|
|
441
|
-
|
|
442
|
-
|
|
450
|
+
return listDiscoverableAgents(cwd).map((agent) => {
|
|
451
|
+
const entry = config[agent.name];
|
|
452
|
+
const model = entry?.model ?? "inherit";
|
|
453
|
+
const thinking = entry?.thinking ?? "inherit";
|
|
454
|
+
return `${agent.name}: model=${model}, effort=${thinking}`;
|
|
455
|
+
});
|
|
443
456
|
}
|
|
444
457
|
|
|
445
458
|
async function getPiModelOptions(ctx: ExtensionContext): Promise<string[]> {
|
|
@@ -458,16 +471,17 @@ interface OverlayComponent {
|
|
|
458
471
|
|
|
459
472
|
type ModelPanelResult =
|
|
460
473
|
| { type: "save"; config: AgentModelConfig }
|
|
461
|
-
| { type: "custom"; agent: string | "all" }
|
|
474
|
+
| { type: "custom"; agent: string | "all"; config: AgentModelConfig }
|
|
462
475
|
| { type: "cancel" };
|
|
463
476
|
|
|
464
477
|
const SET_ALL_AGENTS = "Set all agents";
|
|
465
478
|
|
|
466
479
|
class SddModelPanel implements OverlayComponent {
|
|
467
480
|
private cursor = 0;
|
|
468
|
-
private mode: "agents" | "models" = "agents";
|
|
481
|
+
private mode: "agents" | "models" | "effort" = "agents";
|
|
469
482
|
private selectedRow = SET_ALL_AGENTS;
|
|
470
483
|
private modelCursor = 0;
|
|
484
|
+
private effortCursor = 0;
|
|
471
485
|
private query = "";
|
|
472
486
|
private readonly draft: AgentModelConfig;
|
|
473
487
|
private readonly rows: string[];
|
|
@@ -480,7 +494,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
480
494
|
agents: string[],
|
|
481
495
|
done: (result: ModelPanelResult) => void,
|
|
482
496
|
) {
|
|
483
|
-
this.draft =
|
|
497
|
+
this.draft = cloneModelConfig(initialConfig);
|
|
484
498
|
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
485
499
|
this.modelOptions = modelOptions;
|
|
486
500
|
this.done = done;
|
|
@@ -493,13 +507,17 @@ class SddModelPanel implements OverlayComponent {
|
|
|
493
507
|
this.handleModelInput(data);
|
|
494
508
|
return;
|
|
495
509
|
}
|
|
510
|
+
if (this.mode === "effort") {
|
|
511
|
+
this.handleEffortInput(data);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
496
514
|
this.handleAgentInput(data);
|
|
497
515
|
}
|
|
498
516
|
|
|
499
517
|
render(width: number): string[] {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
518
|
+
if (this.mode === "models") return this.renderModelPicker(width);
|
|
519
|
+
if (this.mode === "effort") return this.renderEffortPicker(width);
|
|
520
|
+
return this.renderAgentList(width);
|
|
503
521
|
}
|
|
504
522
|
|
|
505
523
|
private handleAgentInput(data: string): void {
|
|
@@ -521,13 +539,21 @@ class SddModelPanel implements OverlayComponent {
|
|
|
521
539
|
return;
|
|
522
540
|
}
|
|
523
541
|
if (data === "i") {
|
|
524
|
-
this.
|
|
542
|
+
this.applyInherit();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (data === "e") {
|
|
546
|
+
this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
|
|
547
|
+
this.mode = "effort";
|
|
548
|
+
this.effortCursor = 0;
|
|
525
549
|
return;
|
|
526
550
|
}
|
|
527
551
|
if (data === "c") {
|
|
528
552
|
const row = this.rows[this.cursor];
|
|
529
|
-
if (row === SET_ALL_AGENTS)
|
|
530
|
-
|
|
553
|
+
if (row === SET_ALL_AGENTS)
|
|
554
|
+
this.done({ type: "custom", agent: "all", config: this.draft });
|
|
555
|
+
else if (row)
|
|
556
|
+
this.done({ type: "custom", agent: row, config: this.draft });
|
|
531
557
|
return;
|
|
532
558
|
}
|
|
533
559
|
if (!matchesKey(data, "return")) return;
|
|
@@ -582,6 +608,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
582
608
|
this.done({
|
|
583
609
|
type: "custom",
|
|
584
610
|
agent: this.selectedRow === SET_ALL_AGENTS ? "all" : this.selectedRow,
|
|
611
|
+
config: this.draft,
|
|
585
612
|
});
|
|
586
613
|
return;
|
|
587
614
|
}
|
|
@@ -589,7 +616,9 @@ class SddModelPanel implements OverlayComponent {
|
|
|
589
616
|
this.mode = "agents";
|
|
590
617
|
return;
|
|
591
618
|
}
|
|
592
|
-
this.
|
|
619
|
+
this.applyModelSelection(
|
|
620
|
+
selected === INHERIT_MODEL ? undefined : selected,
|
|
621
|
+
);
|
|
593
622
|
this.mode = "agents";
|
|
594
623
|
return;
|
|
595
624
|
}
|
|
@@ -599,18 +628,52 @@ class SddModelPanel implements OverlayComponent {
|
|
|
599
628
|
}
|
|
600
629
|
}
|
|
601
630
|
|
|
602
|
-
private
|
|
631
|
+
private applyModelSelection(model: string | undefined): void {
|
|
603
632
|
const row = this.rows[this.cursor];
|
|
604
633
|
if (row === SET_ALL_AGENTS) {
|
|
605
|
-
for (const name of this.rows.slice(1))
|
|
606
|
-
if (model === undefined) delete this.draft[name];
|
|
607
|
-
else this.draft[name] = model;
|
|
608
|
-
}
|
|
634
|
+
for (const name of this.rows.slice(1)) this.setModel(name, model);
|
|
609
635
|
return;
|
|
610
636
|
}
|
|
611
637
|
if (!row) return;
|
|
612
|
-
|
|
613
|
-
|
|
638
|
+
this.setModel(row, model);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private applyThinkingSelection(thinking: ThinkingLevel | undefined): void {
|
|
642
|
+
const row = this.selectedRow;
|
|
643
|
+
if (row === SET_ALL_AGENTS) {
|
|
644
|
+
for (const name of this.rows.slice(1)) this.setThinking(name, thinking);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
this.setThinking(row, thinking);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private applyInherit(): void {
|
|
651
|
+
const row = this.rows[this.cursor];
|
|
652
|
+
if (row === SET_ALL_AGENTS) {
|
|
653
|
+
for (const name of this.rows.slice(1)) this.clearEntry(name);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (row) this.clearEntry(row);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private setModel(name: string, model: string | undefined): void {
|
|
660
|
+
const current = this.draft[name] ?? {};
|
|
661
|
+
if (model === undefined) delete current.model;
|
|
662
|
+
else current.model = model;
|
|
663
|
+
if (!current.model && !current.thinking) delete this.draft[name];
|
|
664
|
+
else this.draft[name] = current;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private setThinking(name: string, thinking: ThinkingLevel | undefined): void {
|
|
668
|
+
const current = this.draft[name] ?? {};
|
|
669
|
+
if (thinking === undefined) delete current.thinking;
|
|
670
|
+
else current.thinking = thinking;
|
|
671
|
+
if (!current.model && !current.thinking) delete this.draft[name];
|
|
672
|
+
else this.draft[name] = current;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private clearEntry(name: string): void {
|
|
676
|
+
delete this.draft[name];
|
|
614
677
|
}
|
|
615
678
|
|
|
616
679
|
private filteredModelOptions(): string[] {
|
|
@@ -648,7 +711,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
648
711
|
lines.push("");
|
|
649
712
|
lines.push(
|
|
650
713
|
line(
|
|
651
|
-
"j/k: navigate • enter: change model / confirm • i: inherit • c: custom • ctrl+s: save • esc: back",
|
|
714
|
+
"j/k: navigate • enter: change model / confirm • e: change effort • i: inherit all • c: custom model • ctrl+s: save • esc: back",
|
|
652
715
|
),
|
|
653
716
|
);
|
|
654
717
|
return lines;
|
|
@@ -684,17 +747,70 @@ class SddModelPanel implements OverlayComponent {
|
|
|
684
747
|
return lines;
|
|
685
748
|
}
|
|
686
749
|
|
|
750
|
+
private handleEffortInput(data: string): void {
|
|
751
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
752
|
+
this.done({ type: "cancel" });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (matchesKey(data, "escape")) {
|
|
756
|
+
this.mode = "agents";
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
760
|
+
this.effortCursor = Math.min(
|
|
761
|
+
Math.max(0, THINKING_OPTIONS.length - 1),
|
|
762
|
+
this.effortCursor + 1,
|
|
763
|
+
);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
767
|
+
this.effortCursor = Math.max(0, this.effortCursor - 1);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (!matchesKey(data, "return")) return;
|
|
771
|
+
const selected = THINKING_OPTIONS[this.effortCursor];
|
|
772
|
+
if (selected === INHERIT_THINKING) this.applyThinkingSelection(undefined);
|
|
773
|
+
else this.applyThinkingSelection(selected);
|
|
774
|
+
this.mode = "agents";
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private renderEffortPicker(width: number): string[] {
|
|
778
|
+
const lines: string[] = [];
|
|
779
|
+
const line = (text = "") =>
|
|
780
|
+
truncateToWidth(text, Math.max(1, width), "…", true);
|
|
781
|
+
lines.push(line(`Select effort for ${this.selectedRow}`));
|
|
782
|
+
lines.push("");
|
|
783
|
+
for (let i = 0; i < THINKING_OPTIONS.length; i++) {
|
|
784
|
+
const focused = i === this.effortCursor;
|
|
785
|
+
lines.push(line(`${focused ? "▸" : " "} ${THINKING_OPTIONS[i]}`));
|
|
786
|
+
}
|
|
787
|
+
lines.push("");
|
|
788
|
+
lines.push(line("j/k: navigate • enter: select • esc: back"));
|
|
789
|
+
return lines;
|
|
790
|
+
}
|
|
791
|
+
|
|
687
792
|
private renderSetAllLabel(row: string): string {
|
|
688
|
-
const
|
|
793
|
+
const models = this.rows
|
|
689
794
|
.slice(1)
|
|
690
|
-
.map((name) => this.draft[name] ?? "inherit");
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
795
|
+
.map((name) => this.draft[name]?.model ?? "inherit");
|
|
796
|
+
const efforts = this.rows
|
|
797
|
+
.slice(1)
|
|
798
|
+
.map((name) => this.draft[name]?.thinking ?? "inherit");
|
|
799
|
+
const firstModel = models[0] ?? "inherit";
|
|
800
|
+
const firstEffort = efforts[0] ?? "inherit";
|
|
801
|
+
const modelLabel = models.every((value) => value === firstModel)
|
|
802
|
+
? firstModel
|
|
803
|
+
: "mixed";
|
|
804
|
+
const effortLabel = efforts.every((value) => value === firstEffort)
|
|
805
|
+
? firstEffort
|
|
806
|
+
: "mixed";
|
|
807
|
+
return `${row.padEnd(20)} model=${modelLabel}, effort=${effortLabel}`;
|
|
694
808
|
}
|
|
695
809
|
|
|
696
810
|
private renderAgentLabel(row: string): string {
|
|
697
|
-
|
|
811
|
+
const model = this.draft[row]?.model ?? "inherit";
|
|
812
|
+
const effort = this.draft[row]?.thinking ?? "inherit";
|
|
813
|
+
return `${row.padEnd(20)} model=${model}, effort=${effort}`;
|
|
698
814
|
}
|
|
699
815
|
}
|
|
700
816
|
|
|
@@ -723,8 +839,11 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
723
839
|
let config = readModelConfig(ctx.cwd);
|
|
724
840
|
let result = await showSddModelPanel(ctx, config);
|
|
725
841
|
while (result.type === "custom") {
|
|
842
|
+
config = cloneModelConfig(result.config);
|
|
726
843
|
const current =
|
|
727
|
-
result.agent === "all"
|
|
844
|
+
result.agent === "all"
|
|
845
|
+
? "inherit"
|
|
846
|
+
: (config[result.agent]?.model ?? "inherit");
|
|
728
847
|
const custom = await ctx.ui.input(
|
|
729
848
|
`${result.agent === "all" ? "all agents" : result.agent} custom model id`,
|
|
730
849
|
current === "inherit" ? "provider/model" : current,
|
|
@@ -733,11 +852,22 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
733
852
|
const trimmed = custom.trim();
|
|
734
853
|
if (trimmed.length > 0) {
|
|
735
854
|
if (result.agent === "all") {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
855
|
+
const next: AgentModelConfig = { ...config };
|
|
856
|
+
for (const agent of listDiscoverableAgents(ctx.cwd)) {
|
|
857
|
+
next[agent.name] = {
|
|
858
|
+
...(next[agent.name] ?? {}),
|
|
859
|
+
model: trimmed,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
config = next;
|
|
739
863
|
} else {
|
|
740
|
-
config = {
|
|
864
|
+
config = {
|
|
865
|
+
...config,
|
|
866
|
+
[result.agent]: {
|
|
867
|
+
...(config[result.agent] ?? {}),
|
|
868
|
+
model: trimmed,
|
|
869
|
+
},
|
|
870
|
+
};
|
|
741
871
|
}
|
|
742
872
|
}
|
|
743
873
|
result = await showSddModelPanel(ctx, config);
|
|
@@ -775,18 +905,16 @@ async function handlePersonaCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
775
905
|
}
|
|
776
906
|
|
|
777
907
|
export default function gentleAi(pi: ExtensionAPI): void {
|
|
908
|
+
function runSddPreflight(ctx: ExtensionContext): Promise<SddPreflightPreferences> {
|
|
909
|
+
return ensureSddPreflight(ctx, {
|
|
910
|
+
pi,
|
|
911
|
+
installAssets: (cwd) => installSddAssets(cwd, false),
|
|
912
|
+
applyModelConfig: (cwd) => applyModelConfig(cwd, readModelConfig(cwd)),
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
778
916
|
pi.on("session_start", (_event, ctx) => {
|
|
779
|
-
const result = installSddAssets(ctx.cwd, false);
|
|
780
917
|
const modelResult = applyModelConfig(ctx.cwd, readModelConfig(ctx.cwd));
|
|
781
|
-
if (
|
|
782
|
-
ctx.hasUI &&
|
|
783
|
-
(result.agents > 0 || result.chains > 0 || result.support > 0)
|
|
784
|
-
) {
|
|
785
|
-
ctx.ui.notify(
|
|
786
|
-
`Gentle AI SDD assets auto-installed: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s).`,
|
|
787
|
-
"info",
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
918
|
if (ctx.hasUI && modelResult.updated > 0) {
|
|
791
919
|
ctx.ui.notify(
|
|
792
920
|
`el Gentleman applied SDD model config to ${modelResult.updated} agent(s).`,
|
|
@@ -795,9 +923,21 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
795
923
|
}
|
|
796
924
|
});
|
|
797
925
|
|
|
798
|
-
pi.on("
|
|
799
|
-
|
|
800
|
-
|
|
926
|
+
pi.on("input", async (event, ctx) => {
|
|
927
|
+
if (typeof event.text !== "string" || !isSddPreflightTrigger(event.text)) {
|
|
928
|
+
return { action: "continue" };
|
|
929
|
+
}
|
|
930
|
+
await runSddPreflight(ctx);
|
|
931
|
+
return { action: "continue" };
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
935
|
+
const prefs = getSddPreflightPreferences(ctx);
|
|
936
|
+
const sddPrompt = prefs ? `\n\n${renderSddPreflightPrompt(prefs)}` : "";
|
|
937
|
+
return {
|
|
938
|
+
systemPrompt: `${event.systemPrompt}\n\n${buildGentlePrompt(readPersonaMode(ctx.cwd))}${sddPrompt}`,
|
|
939
|
+
};
|
|
940
|
+
});
|
|
801
941
|
|
|
802
942
|
pi.on("tool_call", async (event, ctx) => {
|
|
803
943
|
if (event.toolName !== "bash") return undefined;
|
|
@@ -819,6 +959,21 @@ export default function gentleAi(pi: ExtensionAPI): void {
|
|
|
819
959
|
},
|
|
820
960
|
});
|
|
821
961
|
|
|
962
|
+
pi.registerCommand("gentle-ai:sdd-preflight", {
|
|
963
|
+
description:
|
|
964
|
+
"Run or reuse the lazy SDD preflight for this Pi session.",
|
|
965
|
+
handler: async (_args, ctx) => {
|
|
966
|
+
await runSddPreflight(ctx);
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
pi.registerCommand("gentle:sdd-preflight", {
|
|
971
|
+
description: "Compatibility alias for /gentle-ai:sdd-preflight.",
|
|
972
|
+
handler: async (_args, ctx) => {
|
|
973
|
+
await runSddPreflight(ctx);
|
|
974
|
+
},
|
|
975
|
+
});
|
|
976
|
+
|
|
822
977
|
pi.registerCommand("gentle:models", {
|
|
823
978
|
description: "Configure per-agent models for el Gentleman.",
|
|
824
979
|
handler: async (_args, ctx) => {
|
package/extensions/sdd-init.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
writeFileSync,
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { basename, dirname, join, relative } from "node:path";
|
|
9
|
+
import { applyModelConfig, readModelConfig } from "./gentle-ai.ts";
|
|
10
|
+
import { ensureSddPreflight, installSddAssets } from "../lib/sdd-preflight.ts";
|
|
9
11
|
type ExtensionAPI = any;
|
|
10
12
|
|
|
11
13
|
const CONFIG_REL_PATH = "openspec/config.yaml";
|
|
@@ -773,6 +775,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
773
775
|
description:
|
|
774
776
|
"Auto-detect project stack and bootstrap openspec/config.yaml for SDD.",
|
|
775
777
|
handler: async (_args: unknown, ctx: any) => {
|
|
778
|
+
await ensureSddPreflight(ctx, {
|
|
779
|
+
pi,
|
|
780
|
+
installAssets: (cwd) => installSddAssets(cwd, false),
|
|
781
|
+
applyModelConfig: (cwd) => applyModelConfig(cwd, readModelConfig(cwd)),
|
|
782
|
+
});
|
|
776
783
|
const configPath = join(ctx.cwd, CONFIG_REL_PATH);
|
|
777
784
|
if (existsSync(configPath)) {
|
|
778
785
|
ctx.ui.notify(
|