@mjasnikovs/pi-task 0.38.30 → 0.38.32

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.
Files changed (73) hide show
  1. package/README.md +2 -1
  2. package/dist/config/config.d.ts +16 -2
  3. package/dist/config/config.js +7 -2
  4. package/dist/config/group-args.d.ts +52 -0
  5. package/dist/config/group-args.js +110 -0
  6. package/dist/config/group-models.d.ts +88 -0
  7. package/dist/config/group-models.js +117 -0
  8. package/dist/config/groups.d.ts +76 -0
  9. package/dist/config/groups.js +110 -0
  10. package/dist/config/option-picker.d.ts +42 -0
  11. package/dist/config/option-picker.js +73 -0
  12. package/dist/config/reasoning.d.ts +22 -63
  13. package/dist/config/reasoning.js +37 -108
  14. package/dist/config/register.d.ts +98 -12
  15. package/dist/config/register.js +228 -23
  16. package/dist/index.js +4 -0
  17. package/dist/remote/push.js +1 -7
  18. package/dist/shared/command-watchdog.d.ts +63 -0
  19. package/dist/shared/command-watchdog.js +87 -0
  20. package/dist/shared/data-home.d.ts +8 -0
  21. package/dist/shared/data-home.js +14 -0
  22. package/dist/shared/model-endpoint.d.ts +53 -0
  23. package/dist/shared/model-endpoint.js +98 -2
  24. package/dist/shared/reasoning-capability.d.ts +25 -5
  25. package/dist/shared/reasoning-capability.js +18 -9
  26. package/dist/task/auto-orchestrator.js +14 -3
  27. package/dist/task/child-runner.d.ts +92 -15
  28. package/dist/task/child-runner.js +303 -66
  29. package/dist/task/context-usage.d.ts +46 -0
  30. package/dist/task/context-usage.js +41 -0
  31. package/dist/task/failure-classifier.js +24 -1
  32. package/dist/task/gate-child.d.ts +15 -4
  33. package/dist/task/gate-child.js +2 -2
  34. package/dist/task/gate-deps.js +7 -2
  35. package/dist/task/implementation-guards.d.ts +26 -0
  36. package/dist/task/implementation-guards.js +177 -0
  37. package/dist/task/implementation-hold.d.ts +118 -0
  38. package/dist/task/implementation-hold.js +165 -0
  39. package/dist/task/implementation-turn.d.ts +5 -0
  40. package/dist/task/implementation-turn.js +12 -1
  41. package/dist/task/loop-detector.d.ts +18 -0
  42. package/dist/task/loop-detector.js +22 -2
  43. package/dist/task/model-hold-stash.d.ts +43 -0
  44. package/dist/task/model-hold-stash.js +70 -0
  45. package/dist/task/orchestrator.d.ts +18 -5
  46. package/dist/task/orchestrator.js +63 -6
  47. package/dist/task/phases.js +18 -5
  48. package/dist/task/research-worker.d.ts +2 -2
  49. package/dist/task/research-worker.js +1 -1
  50. package/dist/workers/docs-core.js +2 -2
  51. package/dist/workers/docs-lookup.d.ts +4 -3
  52. package/dist/workers/docs-lookup.js +1 -1
  53. package/dist/workers/fetch-core.js +2 -2
  54. package/dist/workers/focused-extractor.d.ts +6 -4
  55. package/dist/workers/focused-extractor.js +17 -5
  56. package/dist/workers/index.js +2 -0
  57. package/dist/workers/model-warning.d.ts +69 -0
  58. package/dist/workers/model-warning.js +113 -0
  59. package/dist/workers/pi-worker-core.d.ts +9 -38
  60. package/dist/workers/pi-worker-core.js +8 -86
  61. package/dist/workers/pi-worker-docs.js +2 -2
  62. package/dist/workers/pi-worker.js +4 -4
  63. package/dist/workers/reasoning-warning.d.ts +17 -9
  64. package/dist/workers/reasoning-warning.js +69 -22
  65. package/dist/workers/single-read-guard.d.ts +6 -6
  66. package/dist/workers/single-read-guard.js +8 -8
  67. package/dist/workers/worker-profiles.d.ts +11 -3
  68. package/dist/workers/worker-profiles.js +33 -1
  69. package/package.json +1 -1
  70. package/dist/config/reasoning-args.d.ts +0 -23
  71. package/dist/config/reasoning-args.js +0 -28
  72. package/dist/task/implementation-thinking.d.ts +0 -56
  73. package/dist/task/implementation-thinking.js +0 -32
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The submenu a /task-config row opens on Enter, when cycling is the wrong verb.
3
+ *
4
+ * WHY A SUBMENU AND NOT `ctx.ui.select`
5
+ * -------------------------------------
6
+ * The settings panel already lives inside `ctx.ui.custom` with `overlay: true`.
7
+ * Opening a second dialog from inside a component's input handler races two
8
+ * overlays for focus. `SettingsList` already solves this: it renders the submenu
9
+ * in place of the list and delegates input to it. And its `done(v)` calls
10
+ * `onChange(id, v)` with exactly the returned value while `done(undefined)`
11
+ * writes nothing — the same one-value contract every cycling row uses, so the
12
+ * panel's dispatch needs no branch and a model list of 200 costs one write
13
+ * instead of 200.
14
+ *
15
+ * WHY NOT A BARE `SelectList`
16
+ * ---------------------------
17
+ * `SelectList.handleInput` matches up, down, confirm and cancel and DROPS every
18
+ * other key, so it can never fill its own `setFilter`. `Container` has no
19
+ * `handleInput` at all — it composes `render` and nothing else. pi's own model
20
+ * picker (`modes/interactive/components/model-selector.js`) hand-rolls exactly
21
+ * this: an `Input`, a filtered list, and a `handleInput` that routes between
22
+ * them. The three components that DO use `SelectList` bare — theme, thinking,
23
+ * show-images — are short fixed lists with no filter. A model list is not.
24
+ */
25
+ import { Container } from '@earendil-works/pi-tui';
26
+ import type { SelectItem } from '@earendil-works/pi-tui';
27
+ import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
28
+ type Theme = ExtensionCommandContext['ui']['theme'];
29
+ /**
30
+ * A filterable one-of picker.
31
+ *
32
+ * `currentValue` may match NO option — that is the vanished-model case, where
33
+ * the stored spec is still shown by the row and must still be openable. It
34
+ * simply starts at the top rather than refusing to render.
35
+ */
36
+ export declare class OptionPicker extends Container {
37
+ private readonly input;
38
+ private readonly list;
39
+ constructor(options: readonly SelectItem[], currentValue: string, theme: Theme, done: (value?: string) => void);
40
+ handleInput(data: string): void;
41
+ }
42
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The submenu a /task-config row opens on Enter, when cycling is the wrong verb.
3
+ *
4
+ * WHY A SUBMENU AND NOT `ctx.ui.select`
5
+ * -------------------------------------
6
+ * The settings panel already lives inside `ctx.ui.custom` with `overlay: true`.
7
+ * Opening a second dialog from inside a component's input handler races two
8
+ * overlays for focus. `SettingsList` already solves this: it renders the submenu
9
+ * in place of the list and delegates input to it. And its `done(v)` calls
10
+ * `onChange(id, v)` with exactly the returned value while `done(undefined)`
11
+ * writes nothing — the same one-value contract every cycling row uses, so the
12
+ * panel's dispatch needs no branch and a model list of 200 costs one write
13
+ * instead of 200.
14
+ *
15
+ * WHY NOT A BARE `SelectList`
16
+ * ---------------------------
17
+ * `SelectList.handleInput` matches up, down, confirm and cancel and DROPS every
18
+ * other key, so it can never fill its own `setFilter`. `Container` has no
19
+ * `handleInput` at all — it composes `render` and nothing else. pi's own model
20
+ * picker (`modes/interactive/components/model-selector.js`) hand-rolls exactly
21
+ * this: an `Input`, a filtered list, and a `handleInput` that routes between
22
+ * them. The three components that DO use `SelectList` bare — theme, thinking,
23
+ * show-images — are short fixed lists with no filter. A model list is not.
24
+ */
25
+ import { Container, getKeybindings, Input, SelectList } from '@earendil-works/pi-tui';
26
+ /** Rows visible before the list scrolls. Matches the panel's own MAX_VISIBLE. */
27
+ const PICKER_VISIBLE = 11;
28
+ function selectTheme(theme) {
29
+ return {
30
+ selectedPrefix: text => theme.fg('accent', text),
31
+ selectedText: text => theme.fg('accent', theme.bold(text)),
32
+ description: text => theme.fg('muted', text),
33
+ scrollInfo: text => theme.fg('dim', text),
34
+ noMatch: text => theme.fg('dim', text)
35
+ };
36
+ }
37
+ /**
38
+ * A filterable one-of picker.
39
+ *
40
+ * `currentValue` may match NO option — that is the vanished-model case, where
41
+ * the stored spec is still shown by the row and must still be openable. It
42
+ * simply starts at the top rather than refusing to render.
43
+ */
44
+ export class OptionPicker extends Container {
45
+ input = new Input();
46
+ list;
47
+ constructor(options, currentValue, theme, done) {
48
+ super();
49
+ this.list = new SelectList([...options], PICKER_VISIBLE, selectTheme(theme));
50
+ const at = options.findIndex(o => o.value === currentValue);
51
+ if (at !== -1)
52
+ this.list.setSelectedIndex(at);
53
+ this.list.onSelect = item => done(item.value);
54
+ this.list.onCancel = () => done(undefined);
55
+ this.addChild(this.input);
56
+ this.addChild(this.list);
57
+ }
58
+ handleInput(data) {
59
+ const kb = getKeybindings();
60
+ const forList = kb.matches(data, 'tui.select.up')
61
+ || kb.matches(data, 'tui.select.down')
62
+ || kb.matches(data, 'tui.select.confirm')
63
+ || kb.matches(data, 'tui.select.cancel');
64
+ if (forList) {
65
+ this.list.handleInput(data);
66
+ return;
67
+ }
68
+ // Everything else is typing. The list re-filters on every keystroke,
69
+ // which is what `setFilter` is for and what nothing else calls.
70
+ this.input.handleInput(data);
71
+ this.list.setFilter(this.input.getValue());
72
+ }
73
+ }
@@ -17,35 +17,21 @@
17
17
  *
18
18
  * PURE MODULE — no imports with runtime side effects, so `config.ts` can import
19
19
  * the table and the sanitizers during its own module evaluation. `getConfig()`
20
- * lives one hop away in `reasoning-args.ts` for exactly this reason: importing
20
+ * lives one hop away in `group-args.ts` for exactly this reason: importing
21
21
  * it here would make config.ts ⇄ reasoning.ts a real cycle, and whichever module
22
22
  * a caller reached first would decide whether `DEFAULT_REASONING_TABLE` was
23
23
  * initialised before `DEFAULT_CONFIG` read it.
24
24
  */
25
25
  import type { PiTaskConfig } from './config.js';
26
+ import { CHILD_GROUPS, type ChildGroup } from './groups.js';
26
27
  /**
27
- * The child roles that share one reasoning setting.
28
- *
29
- * Grouped by JOB, not by spawn mechanism two children that both go through
30
- * `runWorker` (a research worker and a verify gate) want different amounts of
31
- * thinking, while `refine` in phases.ts and `compress-label` in title-label.ts
32
- * want the same amount and share the `phase` cell.
33
- *
34
- * - `research` the ad-hoc `pi-worker` subagent tool, and the fallback the four
35
- * research workers use when their own cell is unset
36
- * - `research:files` / `research:apis` / `research:context` / `research:tooling`
37
- * one cell per research worker, so a level can be paid for in
38
- * one worker without paying for it in the other three
39
- * - `phase` refine, verify-tooling, grill, compose, critique, compress-label
40
- * - `planning` /task-auto's planning children — clarify, decompose, and the
41
- * extract/coverage passes around them
42
- * - `plan` /task-plan's question and answer children
43
- * - `gate` enforce, verify, lint-fix, final-fix, recommend
44
- * - `extraction` the --no-tools focused docs/fetch extractors
45
- * - `implementation` the host-session turn that writes the code (not a child)
28
+ * Re-exported because this module's whole public surface — the default table,
29
+ * `resolveReasoning`, `effectiveReasoning` — is typed on them, so an importer of
30
+ * those already needs them. The roster itself lives in groups.ts; `GROUP_BY_CHILD`
31
+ * and `groupForChild` are deliberately NOT re-exported, so a caller that wants
32
+ * the roster reaches for the roster.
46
33
  */
47
- export type ReasoningGroup = 'research' | 'research:files' | 'research:apis' | 'research:context' | 'research:tooling' | 'phase' | 'planning' | 'plan' | 'gate' | 'extraction' | 'implementation';
48
- export declare const REASONING_GROUPS: readonly ReasoningGroup[];
34
+ export { CHILD_GROUPS, type ChildGroup };
49
35
  /**
50
36
  * The four profiles offered by /task-config.
51
37
  * - `default` the per-group table below
@@ -82,44 +68,9 @@ export declare const REASONING_ON_LEVEL: GroupSetting;
82
68
  * host's level. Any other value is a decision this project made for that
83
69
  * group's job, and `/task-config` mode `custom` overrides all of it.
84
70
  */
85
- export declare const DEFAULT_REASONING_TABLE: Readonly<Record<ReasoningGroup, GroupSetting>>;
71
+ export declare const DEFAULT_REASONING_TABLE: Readonly<Record<ChildGroup, GroupSetting>>;
86
72
  /** A stored mode, or `default` when the value is not one. */
87
73
  export declare function sanitizeReasoningMode(value: unknown): ReasoningMode;
88
- /**
89
- * Child NAME → reasoning group, for every child spawned under a name:
90
- * `runPhaseChild`, `runPlanningChild`, and the research workers' `spec.label`.
91
- *
92
- * WHY KEYED ON THE NAME
93
- * ---------------------
94
- * The name is the only identifier in scope at all three spawn paths (phases,
95
- * /task-auto planning, /task-plan), it is what the loader and the debug trail
96
- * already print, and it is the one thing a reader can check against the phase
97
- * list without following the call graph. Threading a group parameter through
98
- * `PhaseDeps` / `AutoDeps` instead would touch both orchestrators' dep bags to
99
- * express something the call site already says out loud.
100
- *
101
- * AN UNMAPPED NAME IS A BUILD FAILURE, not a silent `inherit`.
102
- * `reasoning-groups.test.ts` scans every literal child name under `src/task`
103
- * and fails if it is missing here. A defaulting lookup would let a phase added
104
- * later opt itself out of the table without anyone deciding to.
105
- *
106
- * The gate and extraction groups are NOT here: those children reach the model
107
- * through `groupThinkingArgs('gate' | 'extraction')` at call sites with no name
108
- * in scope (gate-deps.ts, fetch-core.ts, docs-core.ts, pi-worker-docs.ts). The
109
- * four research workers do have a name — their `spec.label` — so they are here.
110
- */
111
- export declare const REASONING_GROUP_BY_CHILD: Readonly<Record<string, ReasoningGroup>>;
112
- /**
113
- * The group a named child belongs to.
114
- *
115
- * Returns `undefined` for a name the table does not know, and the CALLER decides
116
- * what that means. `runPhaseChild` treats it as `inherit` — a child that reaches
117
- * the model with today's argv is always safe — while the test treats it as a
118
- * failure. That split is deliberate: the guard belongs at build time, where
119
- * someone can fix it, not at run time, where it would abort a user's task over a
120
- * missing table row.
121
- */
122
- export declare function reasoningGroupForChild(name: string): ReasoningGroup | undefined;
123
74
  /**
124
75
  * Always returns a COMPLETE record, never a partial one.
125
76
  *
@@ -128,16 +79,16 @@ export declare function reasoningGroupForChild(name: string): ReasoningGroup | u
128
79
  * `undefined`, and every call site would need its own fallback. Filling the gaps
129
80
  * here means the type is true at the only place that constructs the value.
130
81
  */
131
- export declare function sanitizeReasoningLevels(value: unknown): Record<ReasoningGroup, GroupSetting>;
82
+ export declare function sanitizeReasoningLevels(value: unknown): Record<ChildGroup, GroupSetting>;
132
83
  /**
133
84
  * What one group is actually set to, given a config. The ONLY place the four
134
85
  * modes are interpreted.
135
86
  *
136
87
  * `cfg` is required rather than defaulted to `getConfig()` so this module stays
137
88
  * import-free (see the header). Callers that want the live config use
138
- * `groupThinkingArgs` from reasoning-args.ts.
89
+ * `groupThinkingArgs` from group-args.ts.
139
90
  */
140
- export declare function resolveReasoning(group: ReasoningGroup, cfg: PiTaskConfig): GroupSetting;
91
+ export declare function resolveReasoning(group: ChildGroup, cfg: PiTaskConfig): GroupSetting;
141
92
  /**
142
93
  * The WHOLE table, as this config will actually run it.
143
94
  *
@@ -146,7 +97,7 @@ export declare function resolveReasoning(group: ReasoningGroup, cfg: PiTaskConfi
146
97
  * asks the single-group question instead, through `resolveReasoning` — which is
147
98
  * the only place the four modes are interpreted.
148
99
  */
149
- export declare function effectiveReasoning(cfg: PiTaskConfig): Record<ReasoningGroup, GroupSetting>;
100
+ export declare function effectiveReasoning(cfg: PiTaskConfig): Record<ChildGroup, GroupSetting>;
150
101
  /**
151
102
  * The argv fragment for a setting. `inherit` is the empty fragment — no flag at
152
103
  * all — which is what makes an all-`inherit` config byte-identical to the
@@ -154,4 +105,12 @@ export declare function effectiveReasoning(cfg: PiTaskConfig): Record<ReasoningG
154
105
  */
155
106
  export declare function thinkingArgs(setting: GroupSetting): string[];
156
107
  /** One honest sentence per group, for the /task-config rows. */
157
- export declare const REASONING_GROUP_HELP: Readonly<Record<ReasoningGroup, string>>;
108
+ export declare const REASONING_GROUP_HELP: Readonly<Record<ChildGroup, string>>;
109
+ /**
110
+ * One honest sentence per model row.
111
+ *
112
+ * They are NOT the reasoning help reworded. A model cell answers a different
113
+ * question — which machine does this work — and one of them costs money every
114
+ * turn rather than once per change, which is a thing the row has to say.
115
+ */
116
+ export declare const MODEL_GROUP_HELP: Readonly<Record<ChildGroup, string>>;
@@ -1,16 +1,12 @@
1
- export const REASONING_GROUPS = [
2
- 'research',
3
- 'research:files',
4
- 'research:apis',
5
- 'research:context',
6
- 'research:tooling',
7
- 'phase',
8
- 'planning',
9
- 'plan',
10
- 'gate',
11
- 'extraction',
12
- 'implementation'
13
- ];
1
+ import { CHILD_GROUPS, sanitizeGroupRecord } from './groups.js';
2
+ /**
3
+ * Re-exported because this module's whole public surface — the default table,
4
+ * `resolveReasoning`, `effectiveReasoning` — is typed on them, so an importer of
5
+ * those already needs them. The roster itself lives in groups.ts; `GROUP_BY_CHILD`
6
+ * and `groupForChild` are deliberately NOT re-exported, so a caller that wants
7
+ * the roster reaches for the roster.
8
+ */
9
+ export { CHILD_GROUPS };
14
10
  export const REASONING_MODES = ['default', 'on', 'off', 'custom'];
15
11
  /**
16
12
  * The settings offered in /task-config: `inherit` plus the standard part of pi's
@@ -56,80 +52,6 @@ export const DEFAULT_REASONING_TABLE = {
56
52
  export function sanitizeReasoningMode(value) {
57
53
  return REASONING_MODES.includes(value) ? value : 'default';
58
54
  }
59
- /**
60
- * Child NAME → reasoning group, for every child spawned under a name:
61
- * `runPhaseChild`, `runPlanningChild`, and the research workers' `spec.label`.
62
- *
63
- * WHY KEYED ON THE NAME
64
- * ---------------------
65
- * The name is the only identifier in scope at all three spawn paths (phases,
66
- * /task-auto planning, /task-plan), it is what the loader and the debug trail
67
- * already print, and it is the one thing a reader can check against the phase
68
- * list without following the call graph. Threading a group parameter through
69
- * `PhaseDeps` / `AutoDeps` instead would touch both orchestrators' dep bags to
70
- * express something the call site already says out loud.
71
- *
72
- * AN UNMAPPED NAME IS A BUILD FAILURE, not a silent `inherit`.
73
- * `reasoning-groups.test.ts` scans every literal child name under `src/task`
74
- * and fails if it is missing here. A defaulting lookup would let a phase added
75
- * later opt itself out of the table without anyone deciding to.
76
- *
77
- * The gate and extraction groups are NOT here: those children reach the model
78
- * through `groupThinkingArgs('gate' | 'extraction')` at call sites with no name
79
- * in scope (gate-deps.ts, fetch-core.ts, docs-core.ts, pi-worker-docs.ts). The
80
- * four research workers do have a name — their `spec.label` — so they are here.
81
- */
82
- export const REASONING_GROUP_BY_CHILD = {
83
- // ── phase: task/phases.ts + task/title-label.ts ──────────────────────────
84
- refine: 'phase',
85
- 'verify-tooling': 'phase',
86
- 'grill-auto': 'phase',
87
- 'grill-gen': 'phase',
88
- compose: 'phase',
89
- critique: 'phase',
90
- 'critique-triage': 'phase',
91
- 'compress-label': 'phase',
92
- // ── planning: task/auto-orchestrator.ts ──────────────────────────────────
93
- 'clarify-triage': 'planning',
94
- 'auto-clarify': 'planning',
95
- 'auto-decompose': 'planning',
96
- 'requirement-extract': 'planning',
97
- 'decompose-coverage': 'planning',
98
- 'coverage-map': 'planning',
99
- 'contract-extract': 'planning',
100
- 'launch-extract': 'planning',
101
- // ── plan: task/plan-orchestrator.ts ──────────────────────────────────────
102
- 'plan-question': 'plan',
103
- 'plan-answer': 'plan',
104
- // ── research: task/phases.ts `workerSpecs`, keyed on the spec's LABEL ─────
105
- 'worker:files': 'research:files',
106
- 'worker:apis': 'research:apis',
107
- 'worker:context': 'research:context',
108
- 'worker:tooling': 'research:tooling'
109
- };
110
- /**
111
- * The group a named child belongs to.
112
- *
113
- * Returns `undefined` for a name the table does not know, and the CALLER decides
114
- * what that means. `runPhaseChild` treats it as `inherit` — a child that reaches
115
- * the model with today's argv is always safe — while the test treats it as a
116
- * failure. That split is deliberate: the guard belongs at build time, where
117
- * someone can fix it, not at run time, where it would abort a user's task over a
118
- * missing table row.
119
- */
120
- export function reasoningGroupForChild(name) {
121
- return REASONING_GROUP_BY_CHILD[name];
122
- }
123
- /**
124
- * For a `research:*` group, the group a stored config falls back to when its own
125
- * key is missing. Every other group maps to `undefined`.
126
- */
127
- const RESEARCH_SUBGROUP_PARENT = {
128
- 'research:files': 'research',
129
- 'research:apis': 'research',
130
- 'research:context': 'research',
131
- 'research:tooling': 'research'
132
- };
133
55
  /**
134
56
  * Always returns a COMPLETE record, never a partial one.
135
57
  *
@@ -139,25 +61,7 @@ const RESEARCH_SUBGROUP_PARENT = {
139
61
  * here means the type is true at the only place that constructs the value.
140
62
  */
141
63
  export function sanitizeReasoningLevels(value) {
142
- const stored = typeof value === 'object' && value !== null && !Array.isArray(value) ?
143
- value
144
- : {};
145
- const valid = (v) => REASONING_SETTINGS.includes(v);
146
- const out = {};
147
- for (const group of REASONING_GROUPS) {
148
- const stored_ = stored[group];
149
- if (valid(stored_)) {
150
- out[group] = stored_;
151
- continue;
152
- }
153
- // A `research:*` key the stored config never had falls back to its
154
- // parent `research` value, so a config written before the split keeps
155
- // meaning what it meant.
156
- const parent = RESEARCH_SUBGROUP_PARENT[group];
157
- out[group] =
158
- parent && valid(stored[parent]) ? stored[parent] : DEFAULT_REASONING_TABLE[group];
159
- }
160
- return out;
64
+ return sanitizeGroupRecord(value, (v) => REASONING_SETTINGS.includes(v), group => DEFAULT_REASONING_TABLE[group]);
161
65
  }
162
66
  /**
163
67
  * What one group is actually set to, given a config. The ONLY place the four
@@ -165,7 +69,7 @@ export function sanitizeReasoningLevels(value) {
165
69
  *
166
70
  * `cfg` is required rather than defaulted to `getConfig()` so this module stays
167
71
  * import-free (see the header). Callers that want the live config use
168
- * `groupThinkingArgs` from reasoning-args.ts.
72
+ * `groupThinkingArgs` from group-args.ts.
169
73
  */
170
74
  export function resolveReasoning(group, cfg) {
171
75
  switch (cfg.reasoningMode) {
@@ -189,7 +93,7 @@ export function resolveReasoning(group, cfg) {
189
93
  */
190
94
  export function effectiveReasoning(cfg) {
191
95
  const out = {};
192
- for (const group of REASONING_GROUPS)
96
+ for (const group of CHILD_GROUPS)
193
97
  out[group] = resolveReasoning(group, cfg);
194
98
  return out;
195
99
  }
@@ -224,3 +128,28 @@ export const REASONING_GROUP_HELP = {
224
128
  implementation: 'The main session turn that actually writes the code. Changing this briefly '
225
129
  + "changes pi's own thinking level, and puts it back afterwards."
226
130
  };
131
+ /**
132
+ * One honest sentence per model row.
133
+ *
134
+ * They are NOT the reasoning help reworded. A model cell answers a different
135
+ * question — which machine does this work — and one of them costs money every
136
+ * turn rather than once per change, which is a thing the row has to say.
137
+ */
138
+ export const MODEL_GROUP_HELP = {
139
+ research: 'The pi-worker subagent tool, and the fallback for any research worker below. '
140
+ + 'Long read-only loops: a cheap fast model pays off here.',
141
+ 'research:files': 'Research worker 1 of 4: maps which files the task will touch.',
142
+ 'research:apis': 'Research worker 2 of 4: the symbols and signatures the task must call.',
143
+ 'research:context': 'Research worker 3 of 4: how the project is put together.',
144
+ 'research:tooling': 'Research worker 4 of 4: the commands that build, test and run it.',
145
+ phase: 'Refining your request, generating and answering the clarifying questions, '
146
+ + 'writing the spec, and critiquing it.',
147
+ planning: "/task-auto's planners: splitting a design document into tasks.",
148
+ plan: "/task-plan's interactive question-and-answer children.",
149
+ gate: 'The checks that run after code is written: verify, enforce, lint-fix, autofix.',
150
+ extraction: 'The small no-tools children that pull one answer out of a page or a docs chunk.',
151
+ implementation: 'The main session turn that writes the code — YOUR session, switched for the turn and '
152
+ + 'switched back. Unlike every row above, this one is not free: a model switch re-bills '
153
+ + 'the whole prompt as a cache miss, twice per task. Leave it on inherit unless you '
154
+ + 'want a different model than the one you are reading this in.'
155
+ };
@@ -1,10 +1,11 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
2
2
  import { SettingsList } from '@earendil-works/pi-tui';
3
- import type { Component } from '@earendil-works/pi-tui';
3
+ import type { Component, SelectItem } from '@earendil-works/pi-tui';
4
+ import { type ReasoningModelFacts } from '../shared/reasoning-capability.js';
4
5
  import { type PiTaskConfig } from './config.js';
5
6
  import { type InstalledExtension } from './extension-list.js';
6
7
  import { type GuardableTool } from './tool-list.js';
7
- import { type ReasoningGroup } from './reasoning.js';
8
+ import { type GroupSetting, type ChildGroup } from './reasoning.js';
8
9
  type Theme = ExtensionCommandContext['ui']['theme'];
9
10
  /**
10
11
  * Frames a child component (the settings list) in a rounded border with a title
@@ -73,6 +74,17 @@ export interface ConfigItem {
73
74
  description: string;
74
75
  /** Offered values. Omitted for a boolean, which is always on/off. */
75
76
  values?: string[];
77
+ /**
78
+ * Rows whose choice is a LIST, not a cycle. Present ⇒ Enter opens a picker
79
+ * instead of stepping `values`.
80
+ *
81
+ * A FUNCTION of the draft config, evaluated at Enter-time, because what a row
82
+ * may offer can depend on another row the user changed while the panel was
83
+ * open: a group's thinking options narrow to what its chosen model declares.
84
+ * `values` stays the STATIC complete vocabulary, because that is what the
85
+ * round-trip property in config-items.test.ts quantifies over.
86
+ */
87
+ submenu?: (cfg: PiTaskConfig) => SelectItem[];
76
88
  /** What the panel shows for the current value. */
77
89
  format: (cfg: PiTaskConfig) => string;
78
90
  /** Write the chosen label back. A value it does not recognise is ignored. */
@@ -92,7 +104,7 @@ export interface ConfigItem {
92
104
  * exemptions from it) end up separated by rows that have nothing to do with
93
105
  * them. The headers are inert rows: no `values`, so Enter does nothing on them.
94
106
  */
95
- export type Section = 'session' | 'checks' | 'research' | 'reasoning' | 'timeouts' | 'unattended' | 'logging' | 'extensions';
107
+ export type Section = 'session' | 'checks' | 'research' | 'models' | 'reasoning' | 'timeouts' | 'unattended' | 'logging' | 'extensions';
96
108
  /** Section order, and the label each header renders. */
97
109
  export declare const SECTIONS: ReadonlyArray<{
98
110
  key: Section;
@@ -100,6 +112,18 @@ export declare const SECTIONS: ReadonlyArray<{
100
112
  }>;
101
113
  /** Marks a header row, so onChange can ignore one and tests can find them. */
102
114
  export declare const SECTION_ID_PREFIX = "section:";
115
+ /**
116
+ * Is this row scenery rather than a setting?
117
+ *
118
+ * Reads the ID, not `values`. Those two agreed only while every real row cycled
119
+ * a list: a picker row has a `submenu` and may carry `values` purely for the
120
+ * round-trip contract, so "no values" stopped meaning "not a row". Both
121
+ * `sectionHeader` and `sectionGap` already stamp the prefix, so this is exact
122
+ * rather than a heuristic, and all three consumers ask the same question.
123
+ */
124
+ export declare const isSectionRow: (item: {
125
+ id: string;
126
+ }) => boolean;
103
127
  /**
104
128
  * Every setting rendered by /task-config, in display order.
105
129
  *
@@ -125,13 +149,60 @@ export declare function applyToolToggle(exempt: readonly string[], toolName: str
125
149
  * So a child is drawn as a tree branch under its parent and loses the repeated
126
150
  * `think: research:` prefix, the same text at the head of four consecutive
127
151
  * lines. `└─` on the last child, `├─` on the rest, decided from the group's
128
- * position in {@link REASONING_GROUPS} rather than a hand-kept list — adding a
152
+ * position in {@link CHILD_GROUPS} rather than a hand-kept list — adding a
129
153
  * fifth worker moves the corner on its own.
130
154
  *
131
155
  * Leading spaces survive: SettingsList pads the label right, never trims it.
132
156
  */
133
- export declare function reasoningRowLabel(group: ReasoningGroup): string;
134
- export declare function reasoningItems(): ConfigItem[];
157
+ export declare function reasoningRowLabel(group: ChildGroup): string;
158
+ /**
159
+ * What this machine can offer a model row, and what each offer can DO.
160
+ *
161
+ * `facts` is what lets the thinking rows narrow: a group pinned to a
162
+ * `reasoning: false` model may only be offered `off`. It answers `undefined` for
163
+ * a spec it cannot resolve — a vanished model, or `inherit` before a session
164
+ * exists — and every consumer reads that as "offer everything", which is exactly
165
+ * today's behaviour.
166
+ */
167
+ export interface ModelCatalog {
168
+ /** Offerable `provider/id` specs, without `inherit`. */
169
+ specs: readonly string[];
170
+ /** Human note per spec, e.g. an extension-provided provider's warning. */
171
+ note?: (spec: string) => string | undefined;
172
+ facts: (spec: string) => ReasoningModelFacts | undefined;
173
+ }
174
+ /** No registry reachable. Every row still renders; nothing narrows. */
175
+ export declare const EMPTY_CATALOG: ModelCatalog;
176
+ export declare function modelItems(catalog: ModelCatalog): ConfigItem[];
177
+ /**
178
+ * Write a group's model, and RE-CLAMP its thinking cell in the same write.
179
+ *
180
+ * Narrowing the picker does nothing about a level already stored from before the
181
+ * model was chosen: on its own it would freeze a lie into a cell it has just
182
+ * made unconfigurable. `syncRows` re-renders every row, so the user sees the
183
+ * level move.
184
+ *
185
+ * The clamp reads `resolveReasoning`, NOT `cfg.reasoningLevels[group]`. In mode
186
+ * `on` or `off` the stored table is ignored entirely, so a user in `on` who
187
+ * picks a non-reasoning model has a stored cell that says nothing and an
188
+ * effective `medium` the model will erase — and comparing the stored cell would
189
+ * see no clamp and stay silent about a real lie.
190
+ *
191
+ * Guarded on the clamp actually MOVING something, because `applyReasoningLevel`
192
+ * flips the whole table to `custom`, and picking a fully-capable model must not
193
+ * do that as a side effect.
194
+ */
195
+ export declare function applyGroupModel(cfg: PiTaskConfig, group: ChildGroup, chosen: string, catalog: ModelCatalog): void;
196
+ /**
197
+ * The levels a row may offer, given the model that row's group will run on.
198
+ *
199
+ * The INTERSECTION with `REASONING_SETTINGS`, not `supportedThinkingLevels`
200
+ * directly: that returns the whole ladder including `xhigh` and `max`, which
201
+ * this menu excludes on purpose (see reasoning.ts) because pi's own UI may not
202
+ * offer them. A model declaring `xhigh` must not smuggle it in here.
203
+ */
204
+ export declare function offeredLevels(facts: ReasoningModelFacts | undefined): GroupSetting[];
205
+ export declare function reasoningItems(catalog?: ModelCatalog): ConfigItem[];
135
206
  /**
136
207
  * Apply one group row's new value.
137
208
  *
@@ -142,7 +213,7 @@ export declare function reasoningItems(): ConfigItem[];
142
213
  * it, nudging `research` while in `off` would silently return every other group
143
214
  * to whatever the stored table happened to hold.
144
215
  */
145
- export declare function applyReasoningLevel(cfg: PiTaskConfig, group: ReasoningGroup, chosen: string): void;
216
+ export declare function applyReasoningLevel(cfg: PiTaskConfig, group: ChildGroup, chosen: string): void;
146
217
  /**
147
218
  * Tallest body the settings list can render, so {@link BorderedBox} can pad
148
219
  * every frame to it and hold the border still. Mirrors SettingsList's own
@@ -157,11 +228,26 @@ export type PanelItem = {
157
228
  description: string;
158
229
  currentValue: string;
159
230
  /**
160
- * Omitted ONLY by a section header. SettingsList cycles a row on Enter when
161
- * this is a non-empty array, so leaving it off is what makes a header inert
162
- * the header does not need its own branch anywhere.
231
+ * The values a cycling row steps through. A section header carries none.
232
+ *
233
+ * It is NOT what marks a header any more {@link isSectionRow} reads the id
234
+ * for that. A model row legitimately has both a `values` list and a
235
+ * `submenu`, so "no values" and "not a row" stopped being the same fact the
236
+ * moment pickers existed.
163
237
  */
164
238
  values?: string[];
239
+ /**
240
+ * Enter-time options for a picker row, already closed over the draft config
241
+ * by {@link renderRows}.
242
+ *
243
+ * Deliberately NOT called `submenu`: pi-tui's `SettingItem.submenu` is a
244
+ * COMPONENT FACTORY, and a PanelItem is handed to `SettingsList` directly by
245
+ * tests and by the headless path. Two different things under one name would
246
+ * make PanelItem stop being assignable to SettingItem, for no gain.
247
+ * {@link createSettingsPanel} is where a theme exists, so it is where these
248
+ * options become a component.
249
+ */
250
+ submenuOptions?: () => SelectItem[];
165
251
  /**
166
252
  * What the headless one-line rendering calls this row, when `label` reads
167
253
  * only in the panel. A tree branch means nothing on a line of `|`-joined
@@ -198,11 +284,11 @@ onChange: (id: string, newValue: string, list: SettingsList) => void, onCancel:
198
284
  * Fixed rows come before discovered ones within a section, so a freshly
199
285
  * installed extension appends rather than reshuffling the menu.
200
286
  */
201
- export declare function configRows(installed: InstalledExtension[], tools?: readonly GuardableTool[]): ConfigItem[];
287
+ export declare function configRows(installed: InstalledExtension[], tools?: readonly GuardableTool[], catalog?: ModelCatalog): ConfigItem[];
202
288
  /** Render `rows` for the current config, grouped under their section headers. */
203
289
  export declare function renderRows(cfg: PiTaskConfig, rows: readonly ConfigItem[]): PanelItem[];
204
290
  /** The full settings row list for the current config, in menu order. */
205
- export declare function panelItems(cfg: PiTaskConfig, installed: InstalledExtension[], tools?: readonly GuardableTool[]): PanelItem[];
291
+ export declare function panelItems(cfg: PiTaskConfig, installed: InstalledExtension[], tools?: readonly GuardableTool[], catalog?: ModelCatalog): PanelItem[];
206
292
  /**
207
293
  * Re-ask every row what it now displays, and write the answers back.
208
294
  *