gentle-pi 0.2.7 → 0.2.8
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 +26 -10
- package/extensions/gentle-ai.ts +226 -48
- package/package.json +1 -1
- package/tests/runtime-harness.mjs +101 -1
package/README.md
CHANGED
|
@@ -246,7 +246,7 @@ Saved at:
|
|
|
246
246
|
|
|
247
247
|
Run `/reload` or start a new Pi session after switching persona.
|
|
248
248
|
|
|
249
|
-
## Model assignment
|
|
249
|
+
## Model and effort assignment
|
|
250
250
|
|
|
251
251
|
```text
|
|
252
252
|
/gentle:models
|
|
@@ -258,15 +258,15 @@ The modal discovers:
|
|
|
258
258
|
- user agents in `~/.pi/agent/agents/` and `~/.agents/`;
|
|
259
259
|
- built-in agents from `pi-subagents`.
|
|
260
260
|
|
|
261
|
-
Recommended model shape:
|
|
261
|
+
Recommended model/effort shape:
|
|
262
262
|
|
|
263
|
-
| Agent kind | Recommended model |
|
|
264
|
-
| -------------------------- | ---------------------------------------------------- |
|
|
265
|
-
| Explore, proposal, archive | Fast and cheap is usually enough. |
|
|
266
|
-
| Spec, design, tasks | Strong reasoning model. |
|
|
267
|
-
| Apply | Strong coding and tool-use model. |
|
|
268
|
-
| Verify / review | Strong fresh-context model. |
|
|
269
|
-
| Tiny utilities | Inherit active/default model unless they bottleneck. |
|
|
263
|
+
| Agent kind | Recommended model | Recommended effort (`thinking`) |
|
|
264
|
+
| -------------------------- | ---------------------------------------------------- | ------------------------------- |
|
|
265
|
+
| Explore, proposal, archive | Fast and cheap is usually enough. | `off` to `low` |
|
|
266
|
+
| Spec, design, tasks | Strong reasoning model. | `medium` to `high` |
|
|
267
|
+
| Apply | Strong coding and tool-use model. | `medium` to `high` |
|
|
268
|
+
| Verify / review | Strong fresh-context model. | `high` |
|
|
269
|
+
| Tiny utilities | Inherit active/default model unless they bottleneck. | `inherit` |
|
|
270
270
|
|
|
271
271
|
Saved at:
|
|
272
272
|
|
|
@@ -274,12 +274,28 @@ Saved at:
|
|
|
274
274
|
.pi/gentle-ai/models.json
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
+
Config shape (per agent):
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"sdd-design": {
|
|
282
|
+
"model": "anthropic/claude-sonnet-4",
|
|
283
|
+
"thinking": "high"
|
|
284
|
+
},
|
|
285
|
+
"sdd-archive": {
|
|
286
|
+
"model": "openai/gpt-5-mini"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Legacy string entries are still accepted and treated as `model`-only config.
|
|
292
|
+
|
|
277
293
|
## Commands
|
|
278
294
|
|
|
279
295
|
| Command | What it does |
|
|
280
296
|
| -------------------------------- | ------------------------------------------------------------ |
|
|
281
297
|
| `/gentle-ai:status` | Shows package, SDD asset, OpenSpec, and model config status. |
|
|
282
|
-
| `/gentle:models` | Opens model assignment UI.
|
|
298
|
+
| `/gentle:models` | Opens model + effort assignment UI. |
|
|
283
299
|
| `/gentle:persona` | Switches persona mode. |
|
|
284
300
|
| `/sdd-init` | Initializes or refreshes `openspec/config.yaml`. |
|
|
285
301
|
| `/gentle-ai:install-sdd` | Reinstalls SDD assets without overwriting local files. |
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -101,7 +101,12 @@ const SDD_AGENT_NAMES = [
|
|
|
101
101
|
] as const;
|
|
102
102
|
|
|
103
103
|
type SddAgentName = (typeof SDD_AGENT_NAMES)[number];
|
|
104
|
-
type
|
|
104
|
+
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
105
|
+
interface AgentRoutingEntry {
|
|
106
|
+
model?: string;
|
|
107
|
+
thinking?: ThinkingLevel;
|
|
108
|
+
}
|
|
109
|
+
type AgentModelConfig = Record<string, AgentRoutingEntry>;
|
|
105
110
|
type AgentSource = "project" | "user" | "builtin";
|
|
106
111
|
|
|
107
112
|
interface AgentEntry {
|
|
@@ -113,6 +118,16 @@ interface AgentEntry {
|
|
|
113
118
|
const KEEP_CURRENT = "Keep current";
|
|
114
119
|
const INHERIT_MODEL = "Inherit active/default model";
|
|
115
120
|
const CUSTOM_MODEL = "Custom model id";
|
|
121
|
+
const INHERIT_THINKING = "Inherit effort";
|
|
122
|
+
const THINKING_OPTIONS: (ThinkingLevel | typeof INHERIT_THINKING)[] = [
|
|
123
|
+
INHERIT_THINKING,
|
|
124
|
+
"off",
|
|
125
|
+
"minimal",
|
|
126
|
+
"low",
|
|
127
|
+
"medium",
|
|
128
|
+
"high",
|
|
129
|
+
"xhigh",
|
|
130
|
+
];
|
|
116
131
|
|
|
117
132
|
const MODEL_CONTROL_OPTIONS = [
|
|
118
133
|
KEEP_CURRENT,
|
|
@@ -253,6 +268,32 @@ function writePersonaMode(cwd: string, mode: PersonaMode): void {
|
|
|
253
268
|
writeFileSync(path, `${JSON.stringify({ mode }, null, 2)}\n`);
|
|
254
269
|
}
|
|
255
270
|
|
|
271
|
+
function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
272
|
+
return (
|
|
273
|
+
value === "off" ||
|
|
274
|
+
value === "minimal" ||
|
|
275
|
+
value === "low" ||
|
|
276
|
+
value === "medium" ||
|
|
277
|
+
value === "high" ||
|
|
278
|
+
value === "xhigh"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeRoutingEntry(value: unknown): AgentRoutingEntry | undefined {
|
|
283
|
+
if (typeof value === "string") {
|
|
284
|
+
const model = value.trim();
|
|
285
|
+
return model.length > 0 ? { model } : undefined;
|
|
286
|
+
}
|
|
287
|
+
if (!isRecord(value)) return undefined;
|
|
288
|
+
const model =
|
|
289
|
+
typeof value.model === "string" && value.model.trim().length > 0
|
|
290
|
+
? value.model.trim()
|
|
291
|
+
: undefined;
|
|
292
|
+
const thinking = isThinkingLevel(value.thinking) ? value.thinking : undefined;
|
|
293
|
+
if (!model && !thinking) return undefined;
|
|
294
|
+
return { model, thinking };
|
|
295
|
+
}
|
|
296
|
+
|
|
256
297
|
function readModelConfig(cwd: string): AgentModelConfig {
|
|
257
298
|
const path = modelConfigPath(cwd);
|
|
258
299
|
if (!existsSync(path)) return {};
|
|
@@ -261,9 +302,8 @@ function readModelConfig(cwd: string): AgentModelConfig {
|
|
|
261
302
|
if (!isRecord(parsed)) return {};
|
|
262
303
|
const config: AgentModelConfig = {};
|
|
263
304
|
for (const [name, value] of Object.entries(parsed)) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
305
|
+
const entry = normalizeRoutingEntry(value);
|
|
306
|
+
if (entry) config[name] = entry;
|
|
267
307
|
}
|
|
268
308
|
return config;
|
|
269
309
|
} catch {
|
|
@@ -274,12 +314,23 @@ function readModelConfig(cwd: string): AgentModelConfig {
|
|
|
274
314
|
function writeModelConfig(cwd: string, config: AgentModelConfig): void {
|
|
275
315
|
const path = modelConfigPath(cwd);
|
|
276
316
|
mkdirSync(dirname(path), { recursive: true });
|
|
277
|
-
|
|
317
|
+
const cleaned: AgentModelConfig = {};
|
|
318
|
+
for (const [name, value] of Object.entries(config)) {
|
|
319
|
+
const entry = normalizeRoutingEntry(value);
|
|
320
|
+
if (entry) cleaned[name] = entry;
|
|
321
|
+
}
|
|
322
|
+
writeFileSync(path, `${JSON.stringify(cleaned, null, 2)}\n`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function cloneModelConfig(config: AgentModelConfig): AgentModelConfig {
|
|
326
|
+
return Object.fromEntries(
|
|
327
|
+
Object.entries(config).map(([name, entry]) => [name, { ...entry }]),
|
|
328
|
+
);
|
|
278
329
|
}
|
|
279
330
|
|
|
280
|
-
function
|
|
331
|
+
function updateFrontmatterRouting(
|
|
281
332
|
content: string,
|
|
282
|
-
|
|
333
|
+
entry: AgentRoutingEntry | undefined,
|
|
283
334
|
): string {
|
|
284
335
|
if (!content.startsWith("---\n")) return content;
|
|
285
336
|
const endIndex = content.indexOf("\n---", 4);
|
|
@@ -288,14 +339,19 @@ function updateFrontmatterModel(
|
|
|
288
339
|
const body = content.slice(endIndex);
|
|
289
340
|
const lines = frontmatter
|
|
290
341
|
.split("\n")
|
|
291
|
-
.filter(
|
|
292
|
-
|
|
342
|
+
.filter(
|
|
343
|
+
(line) => !line.startsWith("model:") && !line.startsWith("thinking:"),
|
|
344
|
+
);
|
|
345
|
+
const toInsert: string[] = [];
|
|
346
|
+
if (entry?.model) toInsert.push(`model: ${entry.model}`);
|
|
347
|
+
if (entry?.thinking) toInsert.push(`thinking: ${entry.thinking}`);
|
|
348
|
+
if (toInsert.length > 0) {
|
|
293
349
|
const descriptionIndex = lines.findIndex((line) =>
|
|
294
350
|
line.startsWith("description:"),
|
|
295
351
|
);
|
|
296
352
|
const insertIndex =
|
|
297
353
|
descriptionIndex >= 0 ? descriptionIndex + 1 : Math.min(1, lines.length);
|
|
298
|
-
lines.splice(insertIndex, 0,
|
|
354
|
+
lines.splice(insertIndex, 0, ...toInsert);
|
|
299
355
|
}
|
|
300
356
|
return `---\n${lines.join("\n")}${body}`;
|
|
301
357
|
}
|
|
@@ -372,7 +428,7 @@ function projectSettingsPath(cwd: string): string {
|
|
|
372
428
|
function updateBuiltinModelOverride(
|
|
373
429
|
cwd: string,
|
|
374
430
|
name: string,
|
|
375
|
-
|
|
431
|
+
entry: AgentRoutingEntry | undefined,
|
|
376
432
|
): boolean {
|
|
377
433
|
const path = projectSettingsPath(cwd);
|
|
378
434
|
let settings: Record<string, unknown> = {};
|
|
@@ -393,8 +449,10 @@ function updateBuiltinModelOverride(
|
|
|
393
449
|
const current = isRecord(agentOverrides[name])
|
|
394
450
|
? { ...agentOverrides[name] }
|
|
395
451
|
: {};
|
|
396
|
-
if (model === undefined) delete current.model;
|
|
397
|
-
else current.model = model;
|
|
452
|
+
if (entry?.model === undefined) delete current.model;
|
|
453
|
+
else current.model = entry.model;
|
|
454
|
+
if (entry?.thinking === undefined) delete current.thinking;
|
|
455
|
+
else current.thinking = entry.thinking;
|
|
398
456
|
if (Object.keys(current).length > 0) agentOverrides[name] = current;
|
|
399
457
|
else delete agentOverrides[name];
|
|
400
458
|
if (Object.keys(agentOverrides).length > 0)
|
|
@@ -414,9 +472,9 @@ function applyModelConfig(
|
|
|
414
472
|
let updated = 0;
|
|
415
473
|
let skipped = 0;
|
|
416
474
|
for (const agent of listDiscoverableAgents(cwd)) {
|
|
417
|
-
const
|
|
475
|
+
const entry = config[agent.name];
|
|
418
476
|
if (agent.source === "builtin") {
|
|
419
|
-
if (updateBuiltinModelOverride(cwd, agent.name,
|
|
477
|
+
if (updateBuiltinModelOverride(cwd, agent.name, entry)) updated += 1;
|
|
420
478
|
else skipped += 1;
|
|
421
479
|
continue;
|
|
422
480
|
}
|
|
@@ -425,7 +483,7 @@ function applyModelConfig(
|
|
|
425
483
|
continue;
|
|
426
484
|
}
|
|
427
485
|
const original = readFileSync(agent.filePath, "utf8");
|
|
428
|
-
const next =
|
|
486
|
+
const next = updateFrontmatterRouting(original, entry);
|
|
429
487
|
if (next === original) {
|
|
430
488
|
skipped += 1;
|
|
431
489
|
continue;
|
|
@@ -437,9 +495,12 @@ function applyModelConfig(
|
|
|
437
495
|
}
|
|
438
496
|
|
|
439
497
|
function describeModelConfig(cwd: string, config: AgentModelConfig): string[] {
|
|
440
|
-
return listDiscoverableAgents(cwd).map(
|
|
441
|
-
|
|
442
|
-
|
|
498
|
+
return listDiscoverableAgents(cwd).map((agent) => {
|
|
499
|
+
const entry = config[agent.name];
|
|
500
|
+
const model = entry?.model ?? "inherit";
|
|
501
|
+
const thinking = entry?.thinking ?? "inherit";
|
|
502
|
+
return `${agent.name}: model=${model}, effort=${thinking}`;
|
|
503
|
+
});
|
|
443
504
|
}
|
|
444
505
|
|
|
445
506
|
async function getPiModelOptions(ctx: ExtensionContext): Promise<string[]> {
|
|
@@ -458,16 +519,17 @@ interface OverlayComponent {
|
|
|
458
519
|
|
|
459
520
|
type ModelPanelResult =
|
|
460
521
|
| { type: "save"; config: AgentModelConfig }
|
|
461
|
-
| { type: "custom"; agent: string | "all" }
|
|
522
|
+
| { type: "custom"; agent: string | "all"; config: AgentModelConfig }
|
|
462
523
|
| { type: "cancel" };
|
|
463
524
|
|
|
464
525
|
const SET_ALL_AGENTS = "Set all agents";
|
|
465
526
|
|
|
466
527
|
class SddModelPanel implements OverlayComponent {
|
|
467
528
|
private cursor = 0;
|
|
468
|
-
private mode: "agents" | "models" = "agents";
|
|
529
|
+
private mode: "agents" | "models" | "effort" = "agents";
|
|
469
530
|
private selectedRow = SET_ALL_AGENTS;
|
|
470
531
|
private modelCursor = 0;
|
|
532
|
+
private effortCursor = 0;
|
|
471
533
|
private query = "";
|
|
472
534
|
private readonly draft: AgentModelConfig;
|
|
473
535
|
private readonly rows: string[];
|
|
@@ -480,7 +542,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
480
542
|
agents: string[],
|
|
481
543
|
done: (result: ModelPanelResult) => void,
|
|
482
544
|
) {
|
|
483
|
-
this.draft =
|
|
545
|
+
this.draft = cloneModelConfig(initialConfig);
|
|
484
546
|
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
485
547
|
this.modelOptions = modelOptions;
|
|
486
548
|
this.done = done;
|
|
@@ -493,13 +555,17 @@ class SddModelPanel implements OverlayComponent {
|
|
|
493
555
|
this.handleModelInput(data);
|
|
494
556
|
return;
|
|
495
557
|
}
|
|
558
|
+
if (this.mode === "effort") {
|
|
559
|
+
this.handleEffortInput(data);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
496
562
|
this.handleAgentInput(data);
|
|
497
563
|
}
|
|
498
564
|
|
|
499
565
|
render(width: number): string[] {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
566
|
+
if (this.mode === "models") return this.renderModelPicker(width);
|
|
567
|
+
if (this.mode === "effort") return this.renderEffortPicker(width);
|
|
568
|
+
return this.renderAgentList(width);
|
|
503
569
|
}
|
|
504
570
|
|
|
505
571
|
private handleAgentInput(data: string): void {
|
|
@@ -521,13 +587,21 @@ class SddModelPanel implements OverlayComponent {
|
|
|
521
587
|
return;
|
|
522
588
|
}
|
|
523
589
|
if (data === "i") {
|
|
524
|
-
this.
|
|
590
|
+
this.applyInherit();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (data === "e") {
|
|
594
|
+
this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
|
|
595
|
+
this.mode = "effort";
|
|
596
|
+
this.effortCursor = 0;
|
|
525
597
|
return;
|
|
526
598
|
}
|
|
527
599
|
if (data === "c") {
|
|
528
600
|
const row = this.rows[this.cursor];
|
|
529
|
-
if (row === SET_ALL_AGENTS)
|
|
530
|
-
|
|
601
|
+
if (row === SET_ALL_AGENTS)
|
|
602
|
+
this.done({ type: "custom", agent: "all", config: this.draft });
|
|
603
|
+
else if (row)
|
|
604
|
+
this.done({ type: "custom", agent: row, config: this.draft });
|
|
531
605
|
return;
|
|
532
606
|
}
|
|
533
607
|
if (!matchesKey(data, "return")) return;
|
|
@@ -582,6 +656,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
582
656
|
this.done({
|
|
583
657
|
type: "custom",
|
|
584
658
|
agent: this.selectedRow === SET_ALL_AGENTS ? "all" : this.selectedRow,
|
|
659
|
+
config: this.draft,
|
|
585
660
|
});
|
|
586
661
|
return;
|
|
587
662
|
}
|
|
@@ -589,7 +664,9 @@ class SddModelPanel implements OverlayComponent {
|
|
|
589
664
|
this.mode = "agents";
|
|
590
665
|
return;
|
|
591
666
|
}
|
|
592
|
-
this.
|
|
667
|
+
this.applyModelSelection(
|
|
668
|
+
selected === INHERIT_MODEL ? undefined : selected,
|
|
669
|
+
);
|
|
593
670
|
this.mode = "agents";
|
|
594
671
|
return;
|
|
595
672
|
}
|
|
@@ -599,18 +676,52 @@ class SddModelPanel implements OverlayComponent {
|
|
|
599
676
|
}
|
|
600
677
|
}
|
|
601
678
|
|
|
602
|
-
private
|
|
679
|
+
private applyModelSelection(model: string | undefined): void {
|
|
603
680
|
const row = this.rows[this.cursor];
|
|
604
681
|
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
|
-
}
|
|
682
|
+
for (const name of this.rows.slice(1)) this.setModel(name, model);
|
|
609
683
|
return;
|
|
610
684
|
}
|
|
611
685
|
if (!row) return;
|
|
612
|
-
|
|
613
|
-
|
|
686
|
+
this.setModel(row, model);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private applyThinkingSelection(thinking: ThinkingLevel | undefined): void {
|
|
690
|
+
const row = this.selectedRow;
|
|
691
|
+
if (row === SET_ALL_AGENTS) {
|
|
692
|
+
for (const name of this.rows.slice(1)) this.setThinking(name, thinking);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
this.setThinking(row, thinking);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private applyInherit(): void {
|
|
699
|
+
const row = this.rows[this.cursor];
|
|
700
|
+
if (row === SET_ALL_AGENTS) {
|
|
701
|
+
for (const name of this.rows.slice(1)) this.clearEntry(name);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (row) this.clearEntry(row);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private setModel(name: string, model: string | undefined): void {
|
|
708
|
+
const current = this.draft[name] ?? {};
|
|
709
|
+
if (model === undefined) delete current.model;
|
|
710
|
+
else current.model = model;
|
|
711
|
+
if (!current.model && !current.thinking) delete this.draft[name];
|
|
712
|
+
else this.draft[name] = current;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private setThinking(name: string, thinking: ThinkingLevel | undefined): void {
|
|
716
|
+
const current = this.draft[name] ?? {};
|
|
717
|
+
if (thinking === undefined) delete current.thinking;
|
|
718
|
+
else current.thinking = thinking;
|
|
719
|
+
if (!current.model && !current.thinking) delete this.draft[name];
|
|
720
|
+
else this.draft[name] = current;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private clearEntry(name: string): void {
|
|
724
|
+
delete this.draft[name];
|
|
614
725
|
}
|
|
615
726
|
|
|
616
727
|
private filteredModelOptions(): string[] {
|
|
@@ -648,7 +759,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
648
759
|
lines.push("");
|
|
649
760
|
lines.push(
|
|
650
761
|
line(
|
|
651
|
-
"j/k: navigate • enter: change model / confirm • i: inherit • c: custom • ctrl+s: save • esc: back",
|
|
762
|
+
"j/k: navigate • enter: change model / confirm • e: change effort • i: inherit all • c: custom model • ctrl+s: save • esc: back",
|
|
652
763
|
),
|
|
653
764
|
);
|
|
654
765
|
return lines;
|
|
@@ -684,17 +795,70 @@ class SddModelPanel implements OverlayComponent {
|
|
|
684
795
|
return lines;
|
|
685
796
|
}
|
|
686
797
|
|
|
798
|
+
private handleEffortInput(data: string): void {
|
|
799
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
800
|
+
this.done({ type: "cancel" });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (matchesKey(data, "escape")) {
|
|
804
|
+
this.mode = "agents";
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
808
|
+
this.effortCursor = Math.min(
|
|
809
|
+
Math.max(0, THINKING_OPTIONS.length - 1),
|
|
810
|
+
this.effortCursor + 1,
|
|
811
|
+
);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
815
|
+
this.effortCursor = Math.max(0, this.effortCursor - 1);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (!matchesKey(data, "return")) return;
|
|
819
|
+
const selected = THINKING_OPTIONS[this.effortCursor];
|
|
820
|
+
if (selected === INHERIT_THINKING) this.applyThinkingSelection(undefined);
|
|
821
|
+
else this.applyThinkingSelection(selected);
|
|
822
|
+
this.mode = "agents";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private renderEffortPicker(width: number): string[] {
|
|
826
|
+
const lines: string[] = [];
|
|
827
|
+
const line = (text = "") =>
|
|
828
|
+
truncateToWidth(text, Math.max(1, width), "…", true);
|
|
829
|
+
lines.push(line(`Select effort for ${this.selectedRow}`));
|
|
830
|
+
lines.push("");
|
|
831
|
+
for (let i = 0; i < THINKING_OPTIONS.length; i++) {
|
|
832
|
+
const focused = i === this.effortCursor;
|
|
833
|
+
lines.push(line(`${focused ? "▸" : " "} ${THINKING_OPTIONS[i]}`));
|
|
834
|
+
}
|
|
835
|
+
lines.push("");
|
|
836
|
+
lines.push(line("j/k: navigate • enter: select • esc: back"));
|
|
837
|
+
return lines;
|
|
838
|
+
}
|
|
839
|
+
|
|
687
840
|
private renderSetAllLabel(row: string): string {
|
|
688
|
-
const
|
|
841
|
+
const models = this.rows
|
|
842
|
+
.slice(1)
|
|
843
|
+
.map((name) => this.draft[name]?.model ?? "inherit");
|
|
844
|
+
const efforts = this.rows
|
|
689
845
|
.slice(1)
|
|
690
|
-
.map((name) => this.draft[name] ?? "inherit");
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
|
|
846
|
+
.map((name) => this.draft[name]?.thinking ?? "inherit");
|
|
847
|
+
const firstModel = models[0] ?? "inherit";
|
|
848
|
+
const firstEffort = efforts[0] ?? "inherit";
|
|
849
|
+
const modelLabel = models.every((value) => value === firstModel)
|
|
850
|
+
? firstModel
|
|
851
|
+
: "mixed";
|
|
852
|
+
const effortLabel = efforts.every((value) => value === firstEffort)
|
|
853
|
+
? firstEffort
|
|
854
|
+
: "mixed";
|
|
855
|
+
return `${row.padEnd(20)} model=${modelLabel}, effort=${effortLabel}`;
|
|
694
856
|
}
|
|
695
857
|
|
|
696
858
|
private renderAgentLabel(row: string): string {
|
|
697
|
-
|
|
859
|
+
const model = this.draft[row]?.model ?? "inherit";
|
|
860
|
+
const effort = this.draft[row]?.thinking ?? "inherit";
|
|
861
|
+
return `${row.padEnd(20)} model=${model}, effort=${effort}`;
|
|
698
862
|
}
|
|
699
863
|
}
|
|
700
864
|
|
|
@@ -723,8 +887,11 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
723
887
|
let config = readModelConfig(ctx.cwd);
|
|
724
888
|
let result = await showSddModelPanel(ctx, config);
|
|
725
889
|
while (result.type === "custom") {
|
|
890
|
+
config = cloneModelConfig(result.config);
|
|
726
891
|
const current =
|
|
727
|
-
result.agent === "all"
|
|
892
|
+
result.agent === "all"
|
|
893
|
+
? "inherit"
|
|
894
|
+
: (config[result.agent]?.model ?? "inherit");
|
|
728
895
|
const custom = await ctx.ui.input(
|
|
729
896
|
`${result.agent === "all" ? "all agents" : result.agent} custom model id`,
|
|
730
897
|
current === "inherit" ? "provider/model" : current,
|
|
@@ -733,11 +900,22 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
733
900
|
const trimmed = custom.trim();
|
|
734
901
|
if (trimmed.length > 0) {
|
|
735
902
|
if (result.agent === "all") {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
903
|
+
const next: AgentModelConfig = { ...config };
|
|
904
|
+
for (const agent of listDiscoverableAgents(ctx.cwd)) {
|
|
905
|
+
next[agent.name] = {
|
|
906
|
+
...(next[agent.name] ?? {}),
|
|
907
|
+
model: trimmed,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
config = next;
|
|
739
911
|
} else {
|
|
740
|
-
config = {
|
|
912
|
+
config = {
|
|
913
|
+
...config,
|
|
914
|
+
[result.agent]: {
|
|
915
|
+
...(config[result.agent] ?? {}),
|
|
916
|
+
model: trimmed,
|
|
917
|
+
},
|
|
918
|
+
};
|
|
741
919
|
}
|
|
742
920
|
}
|
|
743
921
|
result = await showSddModelPanel(ctx, config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -176,6 +176,106 @@ async function run() {
|
|
|
176
176
|
await rm(sddCwd, { recursive: true, force: true });
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
const modelsCwd = await tempWorkspace();
|
|
180
|
+
try {
|
|
181
|
+
await mkdir(join(modelsCwd, ".pi", "agents"), { recursive: true });
|
|
182
|
+
await mkdir(
|
|
183
|
+
join(modelsCwd, ".pi", "npm", "node_modules", "pi-subagents", "agents"),
|
|
184
|
+
{ recursive: true },
|
|
185
|
+
);
|
|
186
|
+
await writeFile(
|
|
187
|
+
join(
|
|
188
|
+
modelsCwd,
|
|
189
|
+
".pi",
|
|
190
|
+
"npm",
|
|
191
|
+
"node_modules",
|
|
192
|
+
"pi-subagents",
|
|
193
|
+
"agents",
|
|
194
|
+
"worker.md",
|
|
195
|
+
),
|
|
196
|
+
`---\nname: worker\ndescription: Builtin worker\n---\n`,
|
|
197
|
+
);
|
|
198
|
+
await writeFile(
|
|
199
|
+
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
200
|
+
`---\nname: sdd-apply\ndescription: Apply phase\n---\n\nbody\n`,
|
|
201
|
+
);
|
|
202
|
+
await mkdir(join(modelsCwd, ".pi", "gentle-ai"), { recursive: true });
|
|
203
|
+
await writeFile(
|
|
204
|
+
join(modelsCwd, ".pi", "gentle-ai", "models.json"),
|
|
205
|
+
JSON.stringify({ "sdd-apply": "openai/gpt-5" }, null, 2),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const ctx = createCtx(modelsCwd, true);
|
|
209
|
+
await hooks.get("session_start")[0]({ reason: "startup" }, ctx);
|
|
210
|
+
const legacyAppliedAgent = await readFile(
|
|
211
|
+
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
212
|
+
"utf8",
|
|
213
|
+
);
|
|
214
|
+
assert.match(legacyAppliedAgent, /model: openai\/gpt-5/);
|
|
215
|
+
assert.doesNotMatch(legacyAppliedAgent, /thinking:/);
|
|
216
|
+
|
|
217
|
+
ctx.ui.custom = () =>
|
|
218
|
+
Promise.resolve({
|
|
219
|
+
type: "save",
|
|
220
|
+
config: {
|
|
221
|
+
"sdd-apply": { model: "openai/gpt-5", thinking: "high" },
|
|
222
|
+
worker: { model: "openai/gpt-5-mini", thinking: "low" },
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
await commands.get("gentle:models").handler("", ctx);
|
|
226
|
+
|
|
227
|
+
const savedConfig = JSON.parse(
|
|
228
|
+
await readFile(join(modelsCwd, ".pi", "gentle-ai", "models.json"), "utf8"),
|
|
229
|
+
);
|
|
230
|
+
assert.deepEqual(savedConfig["sdd-apply"], {
|
|
231
|
+
model: "openai/gpt-5",
|
|
232
|
+
thinking: "high",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const applyAgent = await readFile(
|
|
236
|
+
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
237
|
+
"utf8",
|
|
238
|
+
);
|
|
239
|
+
assert.match(applyAgent, /model: openai\/gpt-5/);
|
|
240
|
+
assert.match(applyAgent, /thinking: high/);
|
|
241
|
+
|
|
242
|
+
const settings = JSON.parse(
|
|
243
|
+
await readFile(join(modelsCwd, ".pi", "settings.json"), "utf8"),
|
|
244
|
+
);
|
|
245
|
+
assert.equal(
|
|
246
|
+
settings.subagents.agentOverrides.worker.model,
|
|
247
|
+
"openai/gpt-5-mini",
|
|
248
|
+
);
|
|
249
|
+
assert.equal(settings.subagents.agentOverrides.worker.thinking, "low");
|
|
250
|
+
|
|
251
|
+
let customPanelCalls = 0;
|
|
252
|
+
ctx.ui.input = async () => "custom/provider-model";
|
|
253
|
+
ctx.ui.custom = (factory) =>
|
|
254
|
+
new Promise((resolve) => {
|
|
255
|
+
customPanelCalls += 1;
|
|
256
|
+
const panel = factory(null, null, null, resolve);
|
|
257
|
+
if (customPanelCalls === 1) {
|
|
258
|
+
panel.handleInput("e"); // effort picker for all agents
|
|
259
|
+
for (let i = 0; i < 4; i++) panel.handleInput("j"); // medium
|
|
260
|
+
panel.handleInput("\r");
|
|
261
|
+
panel.handleInput("c"); // custom model from the same unsaved draft
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
panel.handleInput("\u0013"); // ctrl+s saves the draft reopened after custom model input
|
|
265
|
+
});
|
|
266
|
+
await commands.get("gentle:models").handler("", ctx);
|
|
267
|
+
|
|
268
|
+
const customSavedConfig = JSON.parse(
|
|
269
|
+
await readFile(join(modelsCwd, ".pi", "gentle-ai", "models.json"), "utf8"),
|
|
270
|
+
);
|
|
271
|
+
assert.deepEqual(customSavedConfig["sdd-apply"], {
|
|
272
|
+
model: "custom/provider-model",
|
|
273
|
+
thinking: "medium",
|
|
274
|
+
});
|
|
275
|
+
} finally {
|
|
276
|
+
await rm(modelsCwd, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
|
|
179
279
|
const registryCwd = await tempWorkspace();
|
|
180
280
|
try {
|
|
181
281
|
const ctx = createCtx(registryCwd, true);
|