taskplane 0.29.2 → 0.30.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/bin/gitignore-patterns.mjs +11 -8
- package/bin/rpc-wrapper.mjs +410 -357
- package/bin/taskplane.mjs +533 -250
- package/extensions/reviewer-extension.ts +17 -11
- package/extensions/taskplane/abort.ts +50 -18
- package/extensions/taskplane/agent-bridge-extension.ts +232 -105
- package/extensions/taskplane/agent-host.ts +224 -97
- package/extensions/taskplane/cleanup.ts +71 -42
- package/extensions/taskplane/config-loader.ts +142 -58
- package/extensions/taskplane/config-schema.ts +6 -13
- package/extensions/taskplane/config.ts +10 -2
- package/extensions/taskplane/diagnostic-reports.ts +59 -47
- package/extensions/taskplane/diagnostics.ts +13 -13
- package/extensions/taskplane/discovery.ts +35 -61
- package/extensions/taskplane/engine-worker.ts +53 -46
- package/extensions/taskplane/engine.ts +1760 -602
- package/extensions/taskplane/execution.ts +426 -206
- package/extensions/taskplane/extension.ts +1073 -598
- package/extensions/taskplane/formatting.ts +136 -124
- package/extensions/taskplane/git.ts +0 -2
- package/extensions/taskplane/lane-runner.ts +542 -311
- package/extensions/taskplane/mailbox.ts +57 -49
- package/extensions/taskplane/merge.ts +662 -383
- package/extensions/taskplane/messages.ts +109 -51
- package/extensions/taskplane/migrations.ts +1 -1
- package/extensions/taskplane/path-resolver.ts +8 -9
- package/extensions/taskplane/persistence.ts +425 -262
- package/extensions/taskplane/process-registry.ts +36 -7
- package/extensions/taskplane/quality-gate.ts +107 -55
- package/extensions/taskplane/resume.ts +774 -267
- package/extensions/taskplane/sessions.ts +1 -1
- package/extensions/taskplane/settings-tui.ts +505 -164
- package/extensions/taskplane/sidecar-telemetry.ts +25 -10
- package/extensions/taskplane/supervisor.ts +477 -270
- package/extensions/taskplane/task-executor-core.ts +178 -53
- package/extensions/taskplane/types.ts +186 -108
- package/extensions/taskplane/verification.ts +27 -22
- package/extensions/taskplane/waves.ts +59 -43
- package/extensions/taskplane/workspace.ts +14 -12
- package/extensions/taskplane/worktree.ts +218 -196
- package/package.json +14 -2
|
@@ -19,7 +19,14 @@
|
|
|
19
19
|
|
|
20
20
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
21
21
|
import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
Container,
|
|
24
|
+
type SelectItem,
|
|
25
|
+
SelectList,
|
|
26
|
+
type SettingItem,
|
|
27
|
+
SettingsList,
|
|
28
|
+
Text,
|
|
29
|
+
} from "@mariozechner/pi-tui";
|
|
23
30
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from "fs";
|
|
24
31
|
import { join, dirname } from "path";
|
|
25
32
|
import { parse as yamlParse } from "yaml";
|
|
@@ -40,7 +47,6 @@ import {
|
|
|
40
47
|
} from "./config-loader.ts";
|
|
41
48
|
import { loadPiSettingsPackages } from "./settings-loader.ts";
|
|
42
49
|
|
|
43
|
-
|
|
44
50
|
// ── Types ────────────────────────────────────────────────────────────
|
|
45
51
|
|
|
46
52
|
/** Source of a field's current value */
|
|
@@ -84,7 +90,6 @@ export interface SectionDef {
|
|
|
84
90
|
readOnly?: boolean;
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
|
|
88
93
|
// ── Section & Field Definitions ──────────────────────────────────────
|
|
89
94
|
|
|
90
95
|
/**
|
|
@@ -95,114 +100,401 @@ export const SECTIONS: SectionDef[] = [
|
|
|
95
100
|
{
|
|
96
101
|
name: "Orchestrator",
|
|
97
102
|
fields: [
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
{
|
|
104
|
+
configPath: "orchestrator.orchestrator.maxLanes",
|
|
105
|
+
label: "Max Lanes",
|
|
106
|
+
control: "input",
|
|
107
|
+
layer: "L1",
|
|
108
|
+
fieldType: "number",
|
|
109
|
+
description: "Maximum parallel execution lanes",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
configPath: "orchestrator.orchestrator.worktreeLocation",
|
|
113
|
+
label: "Worktree Location",
|
|
114
|
+
control: "toggle",
|
|
115
|
+
layer: "L1",
|
|
116
|
+
fieldType: "enum",
|
|
117
|
+
values: ["sibling", "subdirectory"],
|
|
118
|
+
description: "Where lane worktree directories are created",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
configPath: "orchestrator.orchestrator.worktreePrefix",
|
|
122
|
+
label: "Worktree Prefix",
|
|
123
|
+
control: "input",
|
|
124
|
+
layer: "L1",
|
|
125
|
+
fieldType: "string",
|
|
126
|
+
description: "Prefix for worktree directory names",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
configPath: "orchestrator.orchestrator.batchIdFormat",
|
|
130
|
+
label: "Batch ID Format",
|
|
131
|
+
control: "toggle",
|
|
132
|
+
layer: "L1",
|
|
133
|
+
fieldType: "enum",
|
|
134
|
+
values: ["timestamp", "sequential"],
|
|
135
|
+
description: "Batch ID format for logs/branch naming",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
configPath: "orchestrator.orchestrator.sessionPrefix",
|
|
139
|
+
label: "Session Prefix",
|
|
140
|
+
control: "input",
|
|
141
|
+
layer: "L1+L2",
|
|
142
|
+
fieldType: "string",
|
|
143
|
+
prefsKey: "sessionPrefix",
|
|
144
|
+
description: "Prefix for orchestrator session names",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
configPath: "orchestrator.orchestrator.operatorId",
|
|
148
|
+
label: "Operator ID",
|
|
149
|
+
control: "input",
|
|
150
|
+
layer: "L1+L2",
|
|
151
|
+
fieldType: "string",
|
|
152
|
+
prefsKey: "operatorId",
|
|
153
|
+
description: "Operator identifier (empty = auto-detect)",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
configPath: "orchestrator.orchestrator.integration",
|
|
157
|
+
label: "Integration",
|
|
158
|
+
control: "picker",
|
|
159
|
+
layer: "L1",
|
|
160
|
+
fieldType: "enum",
|
|
161
|
+
values: ["manual", "supervised", "auto"],
|
|
162
|
+
description:
|
|
163
|
+
"How completed batches are integrated. manual = user runs /orch-integrate. supervised = supervisor proposes plan, asks confirmation. auto = supervisor executes without asking.",
|
|
164
|
+
},
|
|
105
165
|
],
|
|
106
166
|
},
|
|
107
167
|
{
|
|
108
168
|
name: "Agent: Supervisor",
|
|
109
169
|
fields: [
|
|
110
|
-
{
|
|
111
|
-
|
|
170
|
+
{
|
|
171
|
+
configPath: "orchestrator.supervisor.model",
|
|
172
|
+
label: "Supervisor Model",
|
|
173
|
+
control: "input",
|
|
174
|
+
layer: "L1+L2",
|
|
175
|
+
fieldType: "string",
|
|
176
|
+
prefsKey: "supervisorModel",
|
|
177
|
+
description: "Supervisor model (inherit = use session model)",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
configPath: "orchestrator.supervisor.autonomy",
|
|
181
|
+
label: "Autonomy Level",
|
|
182
|
+
control: "picker",
|
|
183
|
+
layer: "L1",
|
|
184
|
+
fieldType: "enum",
|
|
185
|
+
values: ["interactive", "supervised", "autonomous"],
|
|
186
|
+
description: "Recovery action confirmation behavior",
|
|
187
|
+
},
|
|
112
188
|
],
|
|
113
189
|
},
|
|
114
190
|
{
|
|
115
191
|
name: "Agent: Worker",
|
|
116
192
|
fields: [
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
193
|
+
{
|
|
194
|
+
configPath: "taskRunner.worker.model",
|
|
195
|
+
label: "Worker Model",
|
|
196
|
+
control: "input",
|
|
197
|
+
layer: "L1+L2",
|
|
198
|
+
fieldType: "string",
|
|
199
|
+
prefsKey: "workerModel",
|
|
200
|
+
description: "Worker model (inherit = use session model)",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
configPath: "taskRunner.worker.tools",
|
|
204
|
+
label: "Worker Tools",
|
|
205
|
+
control: "input",
|
|
206
|
+
layer: "L1",
|
|
207
|
+
fieldType: "string",
|
|
208
|
+
description: "Worker tool allowlist",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
configPath: "taskRunner.worker.thinking",
|
|
212
|
+
label: "Worker Thinking",
|
|
213
|
+
control: "picker",
|
|
214
|
+
layer: "L1",
|
|
215
|
+
fieldType: "string",
|
|
216
|
+
description: "Worker thinking mode",
|
|
217
|
+
},
|
|
120
218
|
],
|
|
121
219
|
},
|
|
122
220
|
{
|
|
123
221
|
name: "Agent: Reviewer",
|
|
124
222
|
fields: [
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
223
|
+
{
|
|
224
|
+
configPath: "taskRunner.reviewer.model",
|
|
225
|
+
label: "Reviewer Model",
|
|
226
|
+
control: "input",
|
|
227
|
+
layer: "L1+L2",
|
|
228
|
+
fieldType: "string",
|
|
229
|
+
prefsKey: "reviewerModel",
|
|
230
|
+
description: "Reviewer model (inherit = use session model)",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
configPath: "taskRunner.reviewer.tools",
|
|
234
|
+
label: "Reviewer Tools",
|
|
235
|
+
control: "input",
|
|
236
|
+
layer: "L1",
|
|
237
|
+
fieldType: "string",
|
|
238
|
+
description: "Reviewer tool allowlist",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
configPath: "taskRunner.reviewer.thinking",
|
|
242
|
+
label: "Reviewer Thinking",
|
|
243
|
+
control: "picker",
|
|
244
|
+
layer: "L1",
|
|
245
|
+
fieldType: "string",
|
|
246
|
+
description: "Reviewer thinking mode",
|
|
247
|
+
},
|
|
128
248
|
],
|
|
129
249
|
},
|
|
130
250
|
{
|
|
131
251
|
name: "Agent: Merge",
|
|
132
252
|
fields: [
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
253
|
+
{
|
|
254
|
+
configPath: "orchestrator.merge.model",
|
|
255
|
+
label: "Merge Model",
|
|
256
|
+
control: "input",
|
|
257
|
+
layer: "L1+L2",
|
|
258
|
+
fieldType: "string",
|
|
259
|
+
prefsKey: "mergeModel",
|
|
260
|
+
description: "Merge-agent model (inherit = use session model)",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
configPath: "orchestrator.merge.tools",
|
|
264
|
+
label: "Merge Tools",
|
|
265
|
+
control: "input",
|
|
266
|
+
layer: "L1",
|
|
267
|
+
fieldType: "string",
|
|
268
|
+
description: "Merge-agent tool allowlist",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
configPath: "orchestrator.merge.thinking",
|
|
272
|
+
label: "Merge Thinking",
|
|
273
|
+
control: "picker",
|
|
274
|
+
layer: "L1+L2",
|
|
275
|
+
fieldType: "string",
|
|
276
|
+
prefsKey: "mergeThinking",
|
|
277
|
+
description: "Merge-agent thinking mode",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
configPath: "orchestrator.merge.order",
|
|
281
|
+
label: "Merge Order",
|
|
282
|
+
control: "toggle",
|
|
283
|
+
layer: "L1",
|
|
284
|
+
fieldType: "enum",
|
|
285
|
+
values: ["fewest-files-first", "sequential"],
|
|
286
|
+
description: "Lane merge ordering policy",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
configPath: "orchestrator.merge.timeoutMinutes",
|
|
290
|
+
label: "Merge Timeout (minutes)",
|
|
291
|
+
control: "input",
|
|
292
|
+
layer: "L1",
|
|
293
|
+
fieldType: "number",
|
|
294
|
+
description: "Max time for merge agent to complete. Increase for large batches (default: 10)",
|
|
295
|
+
},
|
|
138
296
|
],
|
|
139
297
|
},
|
|
140
298
|
{
|
|
141
299
|
name: "Agent Extensions",
|
|
142
|
-
readOnly: true,
|
|
300
|
+
readOnly: true, // Dynamically handled — no fixed fields
|
|
143
301
|
fields: [],
|
|
144
302
|
},
|
|
145
303
|
{
|
|
146
304
|
name: "Context Limits",
|
|
147
305
|
fields: [
|
|
148
|
-
{
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
306
|
+
{
|
|
307
|
+
configPath: "taskRunner.context.workerContextWindow",
|
|
308
|
+
label: "Context Window",
|
|
309
|
+
control: "input",
|
|
310
|
+
layer: "L1",
|
|
311
|
+
fieldType: "number",
|
|
312
|
+
description: "Worker context window size",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
configPath: "taskRunner.context.warnPercent",
|
|
316
|
+
label: "Warn %",
|
|
317
|
+
control: "input",
|
|
318
|
+
layer: "L1",
|
|
319
|
+
fieldType: "number",
|
|
320
|
+
description: "Context utilization warn threshold (%)",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
configPath: "taskRunner.context.killPercent",
|
|
324
|
+
label: "Kill %",
|
|
325
|
+
control: "input",
|
|
326
|
+
layer: "L1",
|
|
327
|
+
fieldType: "number",
|
|
328
|
+
description: "Context utilization hard-stop threshold (%)",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
configPath: "taskRunner.context.maxWorkerIterations",
|
|
332
|
+
label: "Max Iterations",
|
|
333
|
+
control: "input",
|
|
334
|
+
layer: "L1",
|
|
335
|
+
fieldType: "number",
|
|
336
|
+
description: "Max worker iterations per step",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
configPath: "taskRunner.context.maxReviewCycles",
|
|
340
|
+
label: "Max Review Cycles",
|
|
341
|
+
control: "input",
|
|
342
|
+
layer: "L1",
|
|
343
|
+
fieldType: "number",
|
|
344
|
+
description: "Max revise loops per review stage",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
configPath: "taskRunner.context.noProgressLimit",
|
|
348
|
+
label: "No Progress Limit",
|
|
349
|
+
control: "input",
|
|
350
|
+
layer: "L1",
|
|
351
|
+
fieldType: "number",
|
|
352
|
+
description: "Max no-progress iterations before failure",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
configPath: "taskRunner.context.maxWorkerMinutes",
|
|
356
|
+
label: "Max Worker Min (ctx)",
|
|
357
|
+
control: "input",
|
|
358
|
+
layer: "L1",
|
|
359
|
+
fieldType: "number",
|
|
360
|
+
optional: true,
|
|
361
|
+
description: "Per-worker wall-clock cap (minutes, empty = no cap)",
|
|
362
|
+
},
|
|
155
363
|
],
|
|
156
364
|
},
|
|
157
365
|
{
|
|
158
366
|
name: "Failure Policy",
|
|
159
367
|
fields: [
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
368
|
+
{
|
|
369
|
+
configPath: "orchestrator.failure.onTaskFailure",
|
|
370
|
+
label: "On Task Failure",
|
|
371
|
+
control: "toggle",
|
|
372
|
+
layer: "L1",
|
|
373
|
+
fieldType: "enum",
|
|
374
|
+
values: ["skip-dependents", "stop-wave", "stop-all"],
|
|
375
|
+
description: "Batch behavior when a task fails",
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
configPath: "orchestrator.failure.onMergeFailure",
|
|
379
|
+
label: "On Merge Failure",
|
|
380
|
+
control: "toggle",
|
|
381
|
+
layer: "L1",
|
|
382
|
+
fieldType: "enum",
|
|
383
|
+
values: ["pause", "abort"],
|
|
384
|
+
description: "Behavior when a merge step fails",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
configPath: "orchestrator.failure.stallTimeout",
|
|
388
|
+
label: "Stall Timeout (min)",
|
|
389
|
+
control: "input",
|
|
390
|
+
layer: "L1",
|
|
391
|
+
fieldType: "number",
|
|
392
|
+
description: "Stall detection threshold (minutes)",
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
configPath: "orchestrator.failure.maxWorkerMinutes",
|
|
396
|
+
label: "Max Worker Min",
|
|
397
|
+
control: "input",
|
|
398
|
+
layer: "L1",
|
|
399
|
+
fieldType: "number",
|
|
400
|
+
description: "Max worker runtime budget per task (minutes)",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
configPath: "orchestrator.failure.abortGracePeriod",
|
|
404
|
+
label: "Abort Grace (sec)",
|
|
405
|
+
control: "input",
|
|
406
|
+
layer: "L1",
|
|
407
|
+
fieldType: "number",
|
|
408
|
+
description: "Graceful abort wait time (seconds)",
|
|
409
|
+
},
|
|
165
410
|
],
|
|
166
411
|
},
|
|
167
412
|
{
|
|
168
413
|
name: "Dependencies",
|
|
169
414
|
fields: [
|
|
170
|
-
{
|
|
171
|
-
|
|
415
|
+
{
|
|
416
|
+
configPath: "orchestrator.dependencies.source",
|
|
417
|
+
label: "Dep Source",
|
|
418
|
+
control: "toggle",
|
|
419
|
+
layer: "L1",
|
|
420
|
+
fieldType: "enum",
|
|
421
|
+
values: ["prompt", "agent"],
|
|
422
|
+
description: "Dependency extraction source",
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
configPath: "orchestrator.dependencies.cache",
|
|
426
|
+
label: "Dep Cache",
|
|
427
|
+
control: "toggle",
|
|
428
|
+
layer: "L1",
|
|
429
|
+
fieldType: "boolean",
|
|
430
|
+
values: ["true", "false"],
|
|
431
|
+
description: "Cache dependency analysis results",
|
|
432
|
+
},
|
|
172
433
|
],
|
|
173
434
|
},
|
|
174
435
|
{
|
|
175
436
|
name: "Assignment",
|
|
176
437
|
fields: [
|
|
177
|
-
{
|
|
438
|
+
{
|
|
439
|
+
configPath: "orchestrator.assignment.strategy",
|
|
440
|
+
label: "Strategy",
|
|
441
|
+
control: "toggle",
|
|
442
|
+
layer: "L1",
|
|
443
|
+
fieldType: "enum",
|
|
444
|
+
values: ["affinity-first", "round-robin", "load-balanced"],
|
|
445
|
+
description: "Lane assignment strategy",
|
|
446
|
+
},
|
|
178
447
|
],
|
|
179
448
|
},
|
|
180
449
|
{
|
|
181
450
|
name: "Pre-Warm",
|
|
182
451
|
fields: [
|
|
183
|
-
{
|
|
452
|
+
{
|
|
453
|
+
configPath: "orchestrator.preWarm.autoDetect",
|
|
454
|
+
label: "Auto-Detect",
|
|
455
|
+
control: "toggle",
|
|
456
|
+
layer: "L1",
|
|
457
|
+
fieldType: "boolean",
|
|
458
|
+
values: ["true", "false"],
|
|
459
|
+
description: "Enable automatic pre-warm command detection",
|
|
460
|
+
},
|
|
184
461
|
],
|
|
185
462
|
},
|
|
186
463
|
{
|
|
187
464
|
name: "Monitoring",
|
|
188
465
|
fields: [
|
|
189
|
-
{
|
|
466
|
+
{
|
|
467
|
+
configPath: "orchestrator.monitoring.pollInterval",
|
|
468
|
+
label: "Poll Interval (sec)",
|
|
469
|
+
control: "input",
|
|
470
|
+
layer: "L1",
|
|
471
|
+
fieldType: "number",
|
|
472
|
+
description: "Poll interval for lane/task monitoring (seconds)",
|
|
473
|
+
},
|
|
190
474
|
],
|
|
191
475
|
},
|
|
192
476
|
{
|
|
193
477
|
name: "Global Preferences",
|
|
194
478
|
fields: [
|
|
195
|
-
{
|
|
479
|
+
{
|
|
480
|
+
configPath: "preferences.dashboardPort",
|
|
481
|
+
label: "Dashboard Port",
|
|
482
|
+
control: "input",
|
|
483
|
+
layer: "L2",
|
|
484
|
+
fieldType: "number",
|
|
485
|
+
prefsKey: "dashboardPort",
|
|
486
|
+
optional: true,
|
|
487
|
+
description: "Dashboard server port",
|
|
488
|
+
},
|
|
196
489
|
],
|
|
197
490
|
},
|
|
198
491
|
{
|
|
199
492
|
name: "Advanced (JSON Only)",
|
|
200
493
|
readOnly: true,
|
|
201
|
-
fields: [],
|
|
494
|
+
fields: [], // Populated dynamically in getAdvancedItems()
|
|
202
495
|
},
|
|
203
496
|
];
|
|
204
497
|
|
|
205
|
-
|
|
206
498
|
// ── Raw Config Readers (Source Detection) ────────────────────────────
|
|
207
499
|
|
|
208
500
|
/**
|
|
@@ -256,7 +548,9 @@ export function readRawYamlConfigs(configRoot: string): Record<string, any> | nu
|
|
|
256
548
|
if (parsed && typeof parsed === "object") {
|
|
257
549
|
result.taskRunner = convertYamlKeys(parsed, "taskRunner");
|
|
258
550
|
}
|
|
259
|
-
} catch {
|
|
551
|
+
} catch {
|
|
552
|
+
/* ignore */
|
|
553
|
+
}
|
|
260
554
|
}
|
|
261
555
|
|
|
262
556
|
if (hasOrch) {
|
|
@@ -266,7 +560,9 @@ export function readRawYamlConfigs(configRoot: string): Record<string, any> | nu
|
|
|
266
560
|
if (parsed && typeof parsed === "object") {
|
|
267
561
|
result.orchestrator = convertYamlKeys(parsed, "orchestrator");
|
|
268
562
|
}
|
|
269
|
-
} catch {
|
|
563
|
+
} catch {
|
|
564
|
+
/* ignore */
|
|
565
|
+
}
|
|
270
566
|
}
|
|
271
567
|
|
|
272
568
|
return Object.keys(result).length > 0 ? result : null;
|
|
@@ -338,7 +634,6 @@ function readRawPreferences(): Record<string, any> | null {
|
|
|
338
634
|
}
|
|
339
635
|
}
|
|
340
636
|
|
|
341
|
-
|
|
342
637
|
// ── Write-Back ───────────────────────────────────────────────────────
|
|
343
638
|
|
|
344
639
|
/**
|
|
@@ -425,8 +720,8 @@ export function writeProjectConfigField(
|
|
|
425
720
|
} catch (e: any) {
|
|
426
721
|
throw new Error(
|
|
427
722
|
`Cannot write settings: ${jsonPath} contains malformed JSON. ` +
|
|
428
|
-
|
|
429
|
-
|
|
723
|
+
`Please fix or delete the file and try again. ` +
|
|
724
|
+
`(Parse error: ${e.message ?? "unknown"})`,
|
|
430
725
|
);
|
|
431
726
|
}
|
|
432
727
|
} else {
|
|
@@ -449,7 +744,11 @@ export function writeProjectConfigField(
|
|
|
449
744
|
renameSync(tmpPath, jsonPath);
|
|
450
745
|
} catch {
|
|
451
746
|
writeFileSync(jsonPath, json, "utf-8");
|
|
452
|
-
try {
|
|
747
|
+
try {
|
|
748
|
+
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
749
|
+
} catch {
|
|
750
|
+
/* cleanup best-effort */
|
|
751
|
+
}
|
|
453
752
|
}
|
|
454
753
|
}
|
|
455
754
|
|
|
@@ -488,7 +787,11 @@ export function writeGlobalPreference(path: string, value: any): void {
|
|
|
488
787
|
renameSync(tmpPath, prefsPath);
|
|
489
788
|
} catch {
|
|
490
789
|
writeFileSync(prefsPath, json, "utf-8");
|
|
491
|
-
try {
|
|
790
|
+
try {
|
|
791
|
+
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
792
|
+
} catch {
|
|
793
|
+
/* cleanup best-effort */
|
|
794
|
+
}
|
|
492
795
|
}
|
|
493
796
|
}
|
|
494
797
|
|
|
@@ -559,7 +862,6 @@ export function resolveWriteAction(
|
|
|
559
862
|
return defaultDest;
|
|
560
863
|
}
|
|
561
864
|
|
|
562
|
-
|
|
563
865
|
// ── Source Detection ─────────────────────────────────────────────────
|
|
564
866
|
|
|
565
867
|
/**
|
|
@@ -596,7 +898,6 @@ export function detectFieldSource(
|
|
|
596
898
|
return "global";
|
|
597
899
|
}
|
|
598
900
|
|
|
599
|
-
|
|
600
901
|
// ── Value Formatting ─────────────────────────────────────────────────
|
|
601
902
|
|
|
602
903
|
/**
|
|
@@ -628,7 +929,6 @@ export function getFieldDisplayValue(
|
|
|
628
929
|
return String(val);
|
|
629
930
|
}
|
|
630
931
|
|
|
631
|
-
|
|
632
932
|
// ── Validation ───────────────────────────────────────────────────────
|
|
633
933
|
|
|
634
934
|
export interface ValidationResult {
|
|
@@ -683,7 +983,6 @@ export function validateFieldInput(field: FieldDef, input: string): ValidationRe
|
|
|
683
983
|
}
|
|
684
984
|
}
|
|
685
985
|
|
|
686
|
-
|
|
687
986
|
// ── Advanced Section Items ───────────────────────────────────────────
|
|
688
987
|
|
|
689
988
|
export interface AdvancedItem {
|
|
@@ -772,11 +1071,7 @@ export function getAdvancedItems(config: TaskplaneConfig): AdvancedItem[] {
|
|
|
772
1071
|
* Known subsection objects (like `taskRunner.worker`, `orchestrator.merge`)
|
|
773
1072
|
* are recursed into, not reported as leaves themselves.
|
|
774
1073
|
*/
|
|
775
|
-
function walkConfig(
|
|
776
|
-
obj: any,
|
|
777
|
-
prefix: string,
|
|
778
|
-
visitor: (path: string, value: any) => void,
|
|
779
|
-
): void {
|
|
1074
|
+
function walkConfig(obj: any, prefix: string, visitor: (path: string, value: any) => void): void {
|
|
780
1075
|
if (obj === null || obj === undefined) return;
|
|
781
1076
|
|
|
782
1077
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -861,7 +1156,6 @@ function summarizeArray(arr: any[]): string {
|
|
|
861
1156
|
return `${arr.length} items`;
|
|
862
1157
|
}
|
|
863
1158
|
|
|
864
|
-
|
|
865
1159
|
// ── TUI Rendering ────────────────────────────────────────────────────
|
|
866
1160
|
|
|
867
1161
|
/**
|
|
@@ -890,7 +1184,10 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
|
|
|
890
1184
|
if (available.length === 0) {
|
|
891
1185
|
ctx.ui.notify("No available models found in pi model registry", "warning");
|
|
892
1186
|
// Fall back to manual input
|
|
893
|
-
const manual = await ctx.ui.input(
|
|
1187
|
+
const manual = await ctx.ui.input(
|
|
1188
|
+
"Model (provider/model-id, or empty for inherit)",
|
|
1189
|
+
currentModel || "",
|
|
1190
|
+
);
|
|
894
1191
|
if (manual === null || manual === undefined) return undefined;
|
|
895
1192
|
return manual;
|
|
896
1193
|
}
|
|
@@ -909,10 +1206,10 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
|
|
|
909
1206
|
];
|
|
910
1207
|
|
|
911
1208
|
const providerChoice = await selectScrollable(ctx, "Choose model provider", providerOptions);
|
|
912
|
-
if (!providerChoice) return undefined;
|
|
1209
|
+
if (!providerChoice) return undefined; // Cancelled
|
|
913
1210
|
|
|
914
1211
|
if (providerChoice.startsWith("inherit")) {
|
|
915
|
-
return "";
|
|
1212
|
+
return ""; // Empty string = inherit
|
|
916
1213
|
}
|
|
917
1214
|
|
|
918
1215
|
// Extract provider name (strip " (N models)" suffix)
|
|
@@ -941,7 +1238,7 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
|
|
|
941
1238
|
}
|
|
942
1239
|
|
|
943
1240
|
const modelChoice = await selectScrollable(ctx, `Choose model (${provider})`, modelOptions);
|
|
944
|
-
if (!modelChoice) continue;
|
|
1241
|
+
if (!modelChoice) continue; // Cancelled → back to providers
|
|
945
1242
|
if (modelChoice === "← Back to providers") continue;
|
|
946
1243
|
|
|
947
1244
|
const resolved = modelOptionMap.get(modelChoice);
|
|
@@ -962,7 +1259,9 @@ const THINKING_MODE_OPTIONS: Array<{ value: ThinkingModeValue; label: string }>
|
|
|
962
1259
|
];
|
|
963
1260
|
|
|
964
1261
|
function normalizeThinkingMode(value: unknown): ThinkingModeValue {
|
|
965
|
-
const cleaned = String(value ?? "")
|
|
1262
|
+
const cleaned = String(value ?? "")
|
|
1263
|
+
.trim()
|
|
1264
|
+
.toLowerCase();
|
|
966
1265
|
if (!cleaned || cleaned === "inherit") return "";
|
|
967
1266
|
if (cleaned === "on") return "high";
|
|
968
1267
|
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(cleaned)) {
|
|
@@ -1014,15 +1313,17 @@ function resolveModelRecord(ctx: ExtensionContext, modelRef: string): any | unde
|
|
|
1014
1313
|
if (slashIdx > 0) {
|
|
1015
1314
|
const provider = trimmed.slice(0, slashIdx).toLowerCase();
|
|
1016
1315
|
const id = trimmed.slice(slashIdx + 1).toLowerCase();
|
|
1017
|
-
return available.find(
|
|
1018
|
-
|
|
1019
|
-
|
|
1316
|
+
return available.find(
|
|
1317
|
+
(m: any) =>
|
|
1318
|
+
String(m?.provider ?? "").toLowerCase() === provider &&
|
|
1319
|
+
String(m?.id ?? "").toLowerCase() === id,
|
|
1020
1320
|
);
|
|
1021
1321
|
}
|
|
1022
1322
|
|
|
1023
|
-
return available.find(
|
|
1024
|
-
|
|
1025
|
-
|
|
1323
|
+
return available.find(
|
|
1324
|
+
(m: any) =>
|
|
1325
|
+
String(m?.id ?? "").toLowerCase() === lower ||
|
|
1326
|
+
`${String(m?.provider ?? "").toLowerCase()}/${String(m?.id ?? "").toLowerCase()}` === lower,
|
|
1026
1327
|
);
|
|
1027
1328
|
}
|
|
1028
1329
|
|
|
@@ -1046,12 +1347,9 @@ export function modelSupportsThinking(model: any): boolean {
|
|
|
1046
1347
|
"reasoning_tokens",
|
|
1047
1348
|
];
|
|
1048
1349
|
|
|
1049
|
-
const candidateObjects = [
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
model.features,
|
|
1053
|
-
model.metadata,
|
|
1054
|
-
].filter((entry) => entry && typeof entry === "object");
|
|
1350
|
+
const candidateObjects = [model, model.capabilities, model.features, model.metadata].filter(
|
|
1351
|
+
(entry) => entry && typeof entry === "object",
|
|
1352
|
+
);
|
|
1055
1353
|
|
|
1056
1354
|
for (const candidate of candidateObjects) {
|
|
1057
1355
|
for (const key of boolFlags) {
|
|
@@ -1145,7 +1443,9 @@ async function selectScrollable(
|
|
|
1145
1443
|
container.addChild(selectList);
|
|
1146
1444
|
|
|
1147
1445
|
container.addChild(new Text("", 0, 0));
|
|
1148
|
-
container.addChild(
|
|
1446
|
+
container.addChild(
|
|
1447
|
+
new Text(theme.fg("dim", "↑↓ navigate • type to filter • enter select • esc back"), 1, 0),
|
|
1448
|
+
);
|
|
1149
1449
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1150
1450
|
|
|
1151
1451
|
return {
|
|
@@ -1160,7 +1460,8 @@ async function selectScrollable(
|
|
|
1160
1460
|
|
|
1161
1461
|
if (selectedValue === undefined) return undefined;
|
|
1162
1462
|
const selectedIndex = Number(selectedValue);
|
|
1163
|
-
if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length)
|
|
1463
|
+
if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length)
|
|
1464
|
+
return undefined;
|
|
1164
1465
|
return options[selectedIndex];
|
|
1165
1466
|
}
|
|
1166
1467
|
|
|
@@ -1178,7 +1479,10 @@ export async function openSettingsTui(
|
|
|
1178
1479
|
* Reload all config state from disk. Called after write-back to
|
|
1179
1480
|
* refresh the TUI display.
|
|
1180
1481
|
*/
|
|
1181
|
-
function loadConfigState(
|
|
1482
|
+
function loadConfigState(
|
|
1483
|
+
configRoot: string,
|
|
1484
|
+
pointerConfigRoot?: string,
|
|
1485
|
+
): {
|
|
1182
1486
|
mergedConfig: TaskplaneConfig;
|
|
1183
1487
|
prefs: GlobalPreferences;
|
|
1184
1488
|
rawProject: Record<string, any> | null;
|
|
@@ -1211,11 +1515,12 @@ async function showSectionSelectorLoop(
|
|
|
1211
1515
|
const sectionItems: SelectItem[] = SECTIONS.map((section, i) => ({
|
|
1212
1516
|
value: String(i),
|
|
1213
1517
|
label: section.name,
|
|
1214
|
-
description:
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1518
|
+
description:
|
|
1519
|
+
section.name === "Agent Extensions"
|
|
1520
|
+
? "Toggle extensions per agent type"
|
|
1521
|
+
: section.readOnly
|
|
1522
|
+
? "Read-only collection/record fields"
|
|
1523
|
+
: `${section.fields.length} setting${section.fields.length === 1 ? "" : "s"}`,
|
|
1219
1524
|
}));
|
|
1220
1525
|
|
|
1221
1526
|
const selectedSection = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
@@ -1226,7 +1531,9 @@ async function showSectionSelectorLoop(
|
|
|
1226
1531
|
|
|
1227
1532
|
// Title
|
|
1228
1533
|
container.addChild(new Text(theme.fg("accent", theme.bold("⚙ Settings")), 1, 0));
|
|
1229
|
-
container.addChild(
|
|
1534
|
+
container.addChild(
|
|
1535
|
+
new Text(theme.fg("dim", "Navigate sections to view and edit configuration"), 1, 0),
|
|
1536
|
+
);
|
|
1230
1537
|
container.addChild(new Text("", 0, 0));
|
|
1231
1538
|
|
|
1232
1539
|
// SelectList
|
|
@@ -1251,11 +1558,14 @@ async function showSectionSelectorLoop(
|
|
|
1251
1558
|
return {
|
|
1252
1559
|
render: (w: number) => container.render(w),
|
|
1253
1560
|
invalidate: () => container.invalidate(),
|
|
1254
|
-
handleInput: (data: string) => {
|
|
1561
|
+
handleInput: (data: string) => {
|
|
1562
|
+
selectList.handleInput(data);
|
|
1563
|
+
tui.requestRender();
|
|
1564
|
+
},
|
|
1255
1565
|
};
|
|
1256
1566
|
});
|
|
1257
1567
|
|
|
1258
|
-
if (selectedSection === null) return;
|
|
1568
|
+
if (selectedSection === null) return; // User pressed Esc
|
|
1259
1569
|
|
|
1260
1570
|
const sectionIndex = parseInt(selectedSection, 10);
|
|
1261
1571
|
const section = SECTIONS[sectionIndex];
|
|
@@ -1295,15 +1605,17 @@ async function showAdvancedSection(
|
|
|
1295
1605
|
|
|
1296
1606
|
// Title
|
|
1297
1607
|
container.addChild(new Text(theme.fg("accent", theme.bold("Advanced (JSON Only)")), 1, 0));
|
|
1298
|
-
container.addChild(
|
|
1608
|
+
container.addChild(
|
|
1609
|
+
new Text(theme.fg("dim", "These fields can only be edited directly in the config file"), 1, 0),
|
|
1610
|
+
);
|
|
1299
1611
|
container.addChild(new Text("", 0, 0));
|
|
1300
1612
|
|
|
1301
1613
|
const settingsList = new SettingsList(
|
|
1302
1614
|
settingsItems,
|
|
1303
1615
|
Math.min(settingsItems.length + 2, 20),
|
|
1304
1616
|
getSettingsListTheme(),
|
|
1305
|
-
() => {},
|
|
1306
|
-
() => done(undefined),
|
|
1617
|
+
() => {}, // onChange — no-op (read-only)
|
|
1618
|
+
() => done(undefined), // onCancel
|
|
1307
1619
|
);
|
|
1308
1620
|
container.addChild(settingsList);
|
|
1309
1621
|
|
|
@@ -1317,7 +1629,10 @@ async function showAdvancedSection(
|
|
|
1317
1629
|
return {
|
|
1318
1630
|
render: (w: number) => container.render(w),
|
|
1319
1631
|
invalidate: () => container.invalidate(),
|
|
1320
|
-
handleInput: (data: string) => {
|
|
1632
|
+
handleInput: (data: string) => {
|
|
1633
|
+
settingsList.handleInput?.(data);
|
|
1634
|
+
tui.requestRender();
|
|
1635
|
+
},
|
|
1321
1636
|
};
|
|
1322
1637
|
});
|
|
1323
1638
|
}
|
|
@@ -1351,14 +1666,18 @@ async function showExtensionsSection(
|
|
|
1351
1666
|
container.addChild(new Text(theme.fg("accent", theme.bold("Agent Extensions")), 1, 0));
|
|
1352
1667
|
container.addChild(new Text("", 0, 0));
|
|
1353
1668
|
container.addChild(new Text(theme.fg("dim", "No third-party extensions found."), 1, 0));
|
|
1354
|
-
container.addChild(
|
|
1669
|
+
container.addChild(
|
|
1670
|
+
new Text(theme.fg("dim", "Install extensions via pi settings to see them here."), 1, 0),
|
|
1671
|
+
);
|
|
1355
1672
|
container.addChild(new Text("", 0, 0));
|
|
1356
1673
|
container.addChild(new Text(theme.fg("dim", "esc back"), 1, 0));
|
|
1357
1674
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1358
1675
|
return {
|
|
1359
1676
|
render: (w: number) => container.render(w),
|
|
1360
1677
|
invalidate: () => container.invalidate(),
|
|
1361
|
-
handleInput: (data: string) => {
|
|
1678
|
+
handleInput: (data: string) => {
|
|
1679
|
+
if (data === "\x1b" || data === "\x1b\x1b") done(undefined);
|
|
1680
|
+
},
|
|
1362
1681
|
};
|
|
1363
1682
|
});
|
|
1364
1683
|
return;
|
|
@@ -1371,7 +1690,11 @@ async function showExtensionsSection(
|
|
|
1371
1690
|
|
|
1372
1691
|
const agentTypes = [
|
|
1373
1692
|
{ name: "Worker", exclude: workerExclude, configPath: "taskRunner.worker.excludeExtensions" },
|
|
1374
|
-
{
|
|
1693
|
+
{
|
|
1694
|
+
name: "Reviewer",
|
|
1695
|
+
exclude: reviewerExclude,
|
|
1696
|
+
configPath: "taskRunner.reviewer.excludeExtensions",
|
|
1697
|
+
},
|
|
1375
1698
|
{ name: "Merger", exclude: mergeExclude, configPath: "orchestrator.merge.excludeExtensions" },
|
|
1376
1699
|
];
|
|
1377
1700
|
|
|
@@ -1391,32 +1714,37 @@ async function showExtensionsSection(
|
|
|
1391
1714
|
}
|
|
1392
1715
|
}
|
|
1393
1716
|
|
|
1394
|
-
const result = await ctx.ui.custom<{ id: string; value: string } | null>(
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1717
|
+
const result = await ctx.ui.custom<{ id: string; value: string } | null>(
|
|
1718
|
+
(tui, theme, _kb, done) => {
|
|
1719
|
+
const container = new Container();
|
|
1720
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1721
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Agent Extensions")), 1, 0));
|
|
1722
|
+
container.addChild(new Text(theme.fg("dim", "Toggle extensions on/off per agent type"), 1, 0));
|
|
1723
|
+
container.addChild(new Text("", 0, 0));
|
|
1400
1724
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1725
|
+
const settingsList = new SettingsList(
|
|
1726
|
+
settingsItems,
|
|
1727
|
+
Math.min(settingsItems.length + 2, 20),
|
|
1728
|
+
getSettingsListTheme(),
|
|
1729
|
+
(id, newValue) => done({ id, value: newValue }),
|
|
1730
|
+
() => done(null),
|
|
1731
|
+
);
|
|
1732
|
+
container.addChild(settingsList);
|
|
1409
1733
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1734
|
+
container.addChild(new Text("", 0, 0));
|
|
1735
|
+
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • space toggle • esc back"), 1, 0));
|
|
1736
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1413
1737
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1738
|
+
return {
|
|
1739
|
+
render: (w: number) => container.render(w),
|
|
1740
|
+
invalidate: () => container.invalidate(),
|
|
1741
|
+
handleInput: (data: string) => {
|
|
1742
|
+
settingsList.handleInput?.(data);
|
|
1743
|
+
tui.requestRender();
|
|
1744
|
+
},
|
|
1745
|
+
};
|
|
1746
|
+
},
|
|
1747
|
+
);
|
|
1420
1748
|
|
|
1421
1749
|
if (!result) return; // User pressed Esc
|
|
1422
1750
|
|
|
@@ -1428,7 +1756,8 @@ async function showExtensionsSection(
|
|
|
1428
1756
|
|
|
1429
1757
|
// Read current exclusion array from merged effective config (handles YAML+JSON)
|
|
1430
1758
|
const freshConfig = loadProjectConfig(configRoot, pointerConfigRoot);
|
|
1431
|
-
const currentExcludeList: string[] =
|
|
1759
|
+
const currentExcludeList: string[] =
|
|
1760
|
+
(getNestedValue(freshConfig, configPath) as string[] | undefined) ?? [];
|
|
1432
1761
|
|
|
1433
1762
|
let newExcludeList: string[];
|
|
1434
1763
|
if (enabling) {
|
|
@@ -1444,7 +1773,11 @@ async function showExtensionsSection(
|
|
|
1444
1773
|
try {
|
|
1445
1774
|
writeProjectConfigField(configRoot, configPath, newExcludeList, pointerConfigRoot);
|
|
1446
1775
|
if (onConfigChanged) {
|
|
1447
|
-
try {
|
|
1776
|
+
try {
|
|
1777
|
+
onConfigChanged();
|
|
1778
|
+
} catch {
|
|
1779
|
+
/* non-fatal */
|
|
1780
|
+
}
|
|
1448
1781
|
}
|
|
1449
1782
|
ctx.ui.notify(
|
|
1450
1783
|
`${enabling ? "✅ Enabled" : "❌ Disabled"} ${pkg} for ${configPath.includes("worker") ? "Worker" : configPath.includes("reviewer") ? "Reviewer" : "Merger"}`,
|
|
@@ -1463,8 +1796,10 @@ async function showExtensionsSection(
|
|
|
1463
1796
|
*/
|
|
1464
1797
|
function formatSourceBadge(source: FieldSource): string {
|
|
1465
1798
|
switch (source) {
|
|
1466
|
-
case "project":
|
|
1467
|
-
|
|
1799
|
+
case "project":
|
|
1800
|
+
return "(project)";
|
|
1801
|
+
case "global":
|
|
1802
|
+
return "(global)";
|
|
1468
1803
|
}
|
|
1469
1804
|
}
|
|
1470
1805
|
|
|
@@ -1487,18 +1822,28 @@ async function showSectionSettingsLoop(
|
|
|
1487
1822
|
): Promise<void> {
|
|
1488
1823
|
while (true) {
|
|
1489
1824
|
const state = loadConfigState(configRoot, pointerConfigRoot);
|
|
1490
|
-
const result = await showSectionSettingsOnce(
|
|
1825
|
+
const result = await showSectionSettingsOnce(
|
|
1826
|
+
ctx,
|
|
1827
|
+
section,
|
|
1828
|
+
state.mergedConfig,
|
|
1829
|
+
state.prefs,
|
|
1830
|
+
state.rawProject,
|
|
1831
|
+
state.rawPrefs,
|
|
1832
|
+
);
|
|
1491
1833
|
|
|
1492
|
-
if (result === null) return;
|
|
1834
|
+
if (result === null) return; // User pressed Esc → back to sections
|
|
1493
1835
|
|
|
1494
1836
|
// Process the pending change
|
|
1495
1837
|
const field = section.fields.find((f) => f.configPath === result.fieldId);
|
|
1496
|
-
if (!field) continue;
|
|
1838
|
+
if (!field) continue; // Safety: field not found
|
|
1497
1839
|
|
|
1498
1840
|
let previousModelValue = "";
|
|
1499
1841
|
|
|
1500
1842
|
// Input/picker fields: the submenu returned a sentinel — open the editor picker.
|
|
1501
|
-
if (
|
|
1843
|
+
if (
|
|
1844
|
+
result.rawValue === "__EDIT_REQUESTED__" &&
|
|
1845
|
+
(field.control === "input" || field.control === "picker")
|
|
1846
|
+
) {
|
|
1502
1847
|
const state = loadConfigState(configRoot, pointerConfigRoot);
|
|
1503
1848
|
const currentDisplay = getFieldDisplayValue(field, state.mergedConfig, state.prefs);
|
|
1504
1849
|
const currentClean = String(currentDisplay).replace(/\s+\((?:default|project|global)\)$/, "");
|
|
@@ -1508,31 +1853,30 @@ async function showSectionSettingsLoop(
|
|
|
1508
1853
|
if (field.configPath.endsWith(".model")) {
|
|
1509
1854
|
previousModelValue = normalizedCurrent;
|
|
1510
1855
|
const selected = await pickModel(ctx, normalizedCurrent);
|
|
1511
|
-
if (selected === undefined) continue;
|
|
1856
|
+
if (selected === undefined) continue; // Cancelled
|
|
1512
1857
|
result.rawValue = selected;
|
|
1513
1858
|
} else if (field.control === "picker" && field.configPath.endsWith(".thinking")) {
|
|
1514
1859
|
const note = buildThinkingUnsupportedNoteForThinkingField(ctx, field, state.mergedConfig);
|
|
1515
1860
|
if (note) ctx.ui.notify(note, "info");
|
|
1516
1861
|
const selected = await pickThinkingMode(ctx, normalizedCurrent);
|
|
1517
|
-
if (selected === undefined) continue;
|
|
1862
|
+
if (selected === undefined) continue; // Cancelled
|
|
1518
1863
|
result.rawValue = selected;
|
|
1519
1864
|
} else if (field.control === "picker" && field.values && field.values.length > 0) {
|
|
1520
1865
|
// Enum picker: show scrollable list of allowed values
|
|
1521
|
-
const options = field.values.map((v) =>
|
|
1522
|
-
`${v}${v === normalizedCurrent ? " ✓ current" : ""}`
|
|
1523
|
-
);
|
|
1866
|
+
const options = field.values.map((v) => `${v}${v === normalizedCurrent ? " ✓ current" : ""}`);
|
|
1524
1867
|
const selected = await selectScrollable(ctx, field.label, options);
|
|
1525
|
-
if (!selected) continue;
|
|
1868
|
+
if (!selected) continue; // Cancelled
|
|
1526
1869
|
result.rawValue = selected.replace(/\s+✓ current$/, "");
|
|
1527
1870
|
} else {
|
|
1528
|
-
const placeholder =
|
|
1871
|
+
const placeholder =
|
|
1872
|
+
currentClean === "(not set)" || currentClean === "(inherit)" ? "" : currentClean;
|
|
1529
1873
|
|
|
1530
1874
|
const newValue = await ctx.ui.input(
|
|
1531
1875
|
`${field.label}${field.description ? ` — ${field.description}` : ""}`,
|
|
1532
1876
|
placeholder,
|
|
1533
1877
|
);
|
|
1534
1878
|
|
|
1535
|
-
if (newValue === null || newValue === undefined) continue;
|
|
1879
|
+
if (newValue === null || newValue === undefined) continue; // Cancelled
|
|
1536
1880
|
|
|
1537
1881
|
// Validate
|
|
1538
1882
|
const validation = validateFieldInput(field, newValue);
|
|
@@ -1585,13 +1929,14 @@ async function showSectionSettingsLoop(
|
|
|
1585
1929
|
}
|
|
1586
1930
|
// Notify caller to reload in-memory config from disk
|
|
1587
1931
|
if (onConfigChanged) {
|
|
1588
|
-
try {
|
|
1932
|
+
try {
|
|
1933
|
+
onConfigChanged();
|
|
1934
|
+
} catch {
|
|
1935
|
+
/* non-fatal */
|
|
1936
|
+
}
|
|
1589
1937
|
}
|
|
1590
1938
|
|
|
1591
|
-
ctx.ui.notify(
|
|
1592
|
-
`✅ ${field.label} updated.`,
|
|
1593
|
-
"info",
|
|
1594
|
-
);
|
|
1939
|
+
ctx.ui.notify(`✅ ${field.label} updated.`, "info");
|
|
1595
1940
|
|
|
1596
1941
|
const refreshedState = loadConfigState(configRoot, pointerConfigRoot);
|
|
1597
1942
|
const suggestion = buildThinkingSuggestionForModelChange(
|
|
@@ -1657,17 +2002,13 @@ async function showSectionSettingsOnce(
|
|
|
1657
2002
|
}));
|
|
1658
2003
|
// Return SelectList directly — Container doesn't forward
|
|
1659
2004
|
// handleInput to children, which would freeze the TUI.
|
|
1660
|
-
const list = new SelectList(
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
{
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
scrollInfo: (t: string) => `\x1b[2m${t}\x1b[0m`,
|
|
1668
|
-
noMatch: (t: string) => `\x1b[33m${t}\x1b[0m`,
|
|
1669
|
-
},
|
|
1670
|
-
);
|
|
2005
|
+
const list = new SelectList(selectItems, Math.min(selectItems.length + 1, 10), {
|
|
2006
|
+
selectedPrefix: (t: string) => `\x1b[36m${t}\x1b[0m`,
|
|
2007
|
+
selectedText: (t: string) => `\x1b[36m${t}\x1b[0m`,
|
|
2008
|
+
description: (t: string) => `\x1b[2m${t}\x1b[0m`,
|
|
2009
|
+
scrollInfo: (t: string) => `\x1b[2m${t}\x1b[0m`,
|
|
2010
|
+
noMatch: (t: string) => `\x1b[33m${t}\x1b[0m`,
|
|
2011
|
+
});
|
|
1671
2012
|
const currentIdx = field.values!.indexOf(displayValue);
|
|
1672
2013
|
if (currentIdx >= 0) list.setSelectedIndex(currentIdx);
|
|
1673
2014
|
list.onSelect = (selected) => done(selected.value);
|
|
@@ -1710,7 +2051,7 @@ async function showSectionSettingsOnce(
|
|
|
1710
2051
|
// Exit TUI with the change so the caller can handle write-back
|
|
1711
2052
|
done({ fieldId: id, rawValue: newValue });
|
|
1712
2053
|
},
|
|
1713
|
-
() => done(null),
|
|
2054
|
+
() => done(null), // onCancel → back to section selector
|
|
1714
2055
|
{ enableSearch: settingsItems.length > 5 },
|
|
1715
2056
|
);
|
|
1716
2057
|
container.addChild(settingsList);
|
|
@@ -1723,10 +2064,9 @@ async function showSectionSettingsOnce(
|
|
|
1723
2064
|
|
|
1724
2065
|
// Help text
|
|
1725
2066
|
container.addChild(new Text("", 0, 0));
|
|
1726
|
-
container.addChild(
|
|
1727
|
-
theme.fg("dim", "↑↓ navigate • ←→/space cycle • enter edit • esc back"),
|
|
1728
|
-
|
|
1729
|
-
));
|
|
2067
|
+
container.addChild(
|
|
2068
|
+
new Text(theme.fg("dim", "↑↓ navigate • ←→/space cycle • enter edit • esc back"), 1, 0),
|
|
2069
|
+
);
|
|
1730
2070
|
|
|
1731
2071
|
// Bottom border
|
|
1732
2072
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
@@ -1734,12 +2074,14 @@ async function showSectionSettingsOnce(
|
|
|
1734
2074
|
return {
|
|
1735
2075
|
render: (w: number) => container.render(w),
|
|
1736
2076
|
invalidate: () => container.invalidate(),
|
|
1737
|
-
handleInput: (data: string) => {
|
|
2077
|
+
handleInput: (data: string) => {
|
|
2078
|
+
settingsList.handleInput?.(data);
|
|
2079
|
+
tui.requestRender();
|
|
2080
|
+
},
|
|
1738
2081
|
};
|
|
1739
2082
|
});
|
|
1740
2083
|
}
|
|
1741
2084
|
|
|
1742
|
-
|
|
1743
2085
|
// ── Input Submenu ────────────────────────────────────────────────────
|
|
1744
2086
|
|
|
1745
2087
|
/**
|
|
@@ -1761,7 +2103,7 @@ function createInputSubmenu(
|
|
|
1761
2103
|
render(width: number): string[] {
|
|
1762
2104
|
const lines: string[] = [];
|
|
1763
2105
|
const prompt = ` Enter ${field.label}: `;
|
|
1764
|
-
const inputDisplay = inputBuffer + "█";
|
|
2106
|
+
const inputDisplay = inputBuffer + "█"; // Simple cursor
|
|
1765
2107
|
lines.push(truncateLine(prompt + inputDisplay, width));
|
|
1766
2108
|
|
|
1767
2109
|
if (field.optional) {
|
|
@@ -1818,7 +2160,6 @@ function truncateLine(text: string, width: number): string {
|
|
|
1818
2160
|
return text.substring(0, width - 3) + "...";
|
|
1819
2161
|
}
|
|
1820
2162
|
|
|
1821
|
-
|
|
1822
2163
|
// ── JSON-Only Footer ─────────────────────────────────────────────────
|
|
1823
2164
|
|
|
1824
2165
|
/**
|
|
@@ -1826,17 +2167,17 @@ function truncateLine(text: string, width: number): string {
|
|
|
1826
2167
|
* Used to dynamically discover JSON-only sibling fields.
|
|
1827
2168
|
*/
|
|
1828
2169
|
const SECTION_CONFIG_PREFIXES: Record<string, string[]> = {
|
|
1829
|
-
|
|
2170
|
+
Orchestrator: ["orchestrator.orchestrator"],
|
|
1830
2171
|
"Agent: Supervisor": ["orchestrator.supervisor"],
|
|
1831
2172
|
"Agent: Worker": ["taskRunner.worker"],
|
|
1832
2173
|
"Agent: Reviewer": ["taskRunner.reviewer"],
|
|
1833
2174
|
"Agent: Merge": ["orchestrator.merge"],
|
|
1834
2175
|
"Context Limits": ["taskRunner.context"],
|
|
1835
2176
|
"Failure Policy": ["orchestrator.failure"],
|
|
1836
|
-
|
|
1837
|
-
|
|
2177
|
+
Dependencies: ["orchestrator.dependencies"],
|
|
2178
|
+
Assignment: ["orchestrator.assignment"],
|
|
1838
2179
|
"Pre-Warm": ["orchestrator.preWarm"],
|
|
1839
|
-
|
|
2180
|
+
Monitoring: ["orchestrator.monitoring"],
|
|
1840
2181
|
};
|
|
1841
2182
|
|
|
1842
2183
|
/**
|