taskplane 0.24.21 → 0.24.22
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 +1 -1
- package/bin/taskplane.mjs +239 -103
- package/extensions/task-runner.ts +2 -110
- package/extensions/taskplane/config-loader.ts +306 -269
- package/extensions/taskplane/config-schema.ts +49 -32
- package/extensions/taskplane/merge.ts +1 -1
- package/extensions/taskplane/settings-tui.ts +149 -172
- package/extensions/taskplane/supervisor.ts +2 -2
- package/extensions/taskplane/types.ts +1 -1
- package/package.json +1 -1
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* 2. Per-section SettingsList with field display, source badges,
|
|
7
7
|
* and inline editing for enum/boolean/string/number fields
|
|
8
8
|
*
|
|
9
|
-
* Source detection reads raw config
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Source detection reads raw project config to determine whether each
|
|
10
|
+
* field is explicitly overridden in project config (`(project)`) or
|
|
11
|
+
* inherited from global baseline (`(global)`).
|
|
12
12
|
*
|
|
13
|
-
* Write-back
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Write-back defaults to global preferences for all editable fields.
|
|
14
|
+
* "Save to project override" and "Remove project override" are
|
|
15
|
+
* explicit actions in the destination picker.
|
|
16
16
|
*
|
|
17
17
|
* @module settings/tui
|
|
18
18
|
*/
|
|
@@ -29,21 +29,21 @@ import {
|
|
|
29
29
|
DEFAULT_PROJECT_CONFIG,
|
|
30
30
|
PROJECT_CONFIG_FILENAME,
|
|
31
31
|
type TaskplaneConfig,
|
|
32
|
-
type
|
|
32
|
+
type GlobalPreferences,
|
|
33
33
|
} from "./config-schema.ts";
|
|
34
34
|
import {
|
|
35
|
-
|
|
35
|
+
loadGlobalPreferences,
|
|
36
36
|
loadProjectConfig,
|
|
37
|
-
|
|
37
|
+
loadProjectOverrides,
|
|
38
38
|
resolveConfigRoot,
|
|
39
|
-
|
|
39
|
+
resolveGlobalPreferencesPath,
|
|
40
40
|
} from "./config-loader.ts";
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
// ── Types ────────────────────────────────────────────────────────────
|
|
44
44
|
|
|
45
45
|
/** Source of a field's current value */
|
|
46
|
-
export type FieldSource = "
|
|
46
|
+
export type FieldSource = "project" | "global";
|
|
47
47
|
|
|
48
48
|
/** Layer assignment for a field */
|
|
49
49
|
export type FieldLayer = "L1" | "L2" | "L1+L2";
|
|
@@ -67,8 +67,8 @@ export interface FieldDef {
|
|
|
67
67
|
fieldType: "string" | "number" | "boolean" | "enum";
|
|
68
68
|
/** Whether the field is optional (can be unset) */
|
|
69
69
|
optional?: boolean;
|
|
70
|
-
/** For L1+L2 fields: the
|
|
71
|
-
prefsKey?: keyof
|
|
70
|
+
/** For L1+L2 fields: the global preferences key */
|
|
71
|
+
prefsKey?: keyof GlobalPreferences;
|
|
72
72
|
/** Description shown when selected */
|
|
73
73
|
description?: string;
|
|
74
74
|
}
|
|
@@ -163,7 +163,8 @@ export const SECTIONS: SectionDef[] = [
|
|
|
163
163
|
{ configPath: "taskRunner.worker.model", label: "Worker Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "workerModel", description: "Worker model (inherit = use session model)" },
|
|
164
164
|
{ configPath: "taskRunner.worker.tools", label: "Worker Tools", control: "input", layer: "L1", fieldType: "string", description: "Worker tool allowlist" },
|
|
165
165
|
{ configPath: "taskRunner.worker.thinking", label: "Worker Thinking", control: "picker", layer: "L1", fieldType: "string", description: "Worker thinking mode" },
|
|
166
|
-
|
|
166
|
+
// spawnMode removed — /task is deprecated, Runtime V2 is subprocess-only.
|
|
167
|
+
// { configPath: "taskRunner.worker.spawnMode" ... } was here.
|
|
167
168
|
],
|
|
168
169
|
},
|
|
169
170
|
{
|
|
@@ -187,7 +188,7 @@ export const SECTIONS: SectionDef[] = [
|
|
|
187
188
|
],
|
|
188
189
|
},
|
|
189
190
|
{
|
|
190
|
-
name: "
|
|
191
|
+
name: "Global Preferences",
|
|
191
192
|
fields: [
|
|
192
193
|
{ configPath: "preferences.dashboardPort", label: "Dashboard Port", control: "input", layer: "L2", fieldType: "number", prefsKey: "dashboardPort", optional: true, description: "Dashboard server port" },
|
|
193
194
|
],
|
|
@@ -321,10 +322,10 @@ function snakeKeysToCamel(obj: Record<string, any>): Record<string, any> {
|
|
|
321
322
|
}
|
|
322
323
|
|
|
323
324
|
/**
|
|
324
|
-
* Read the raw
|
|
325
|
+
* Read the raw global preferences JSON.
|
|
325
326
|
*/
|
|
326
327
|
function readRawPreferences(): Record<string, any> | null {
|
|
327
|
-
const prefsPath =
|
|
328
|
+
const prefsPath = resolveGlobalPreferencesPath();
|
|
328
329
|
if (!existsSync(prefsPath)) return null;
|
|
329
330
|
try {
|
|
330
331
|
const raw = readFileSync(prefsPath, "utf-8");
|
|
@@ -361,23 +362,33 @@ function setNestedValue(obj: Record<string, any>, path: string, value: any): voi
|
|
|
361
362
|
}
|
|
362
363
|
}
|
|
363
364
|
|
|
365
|
+
function pruneEmptyObjects(node: unknown): boolean {
|
|
366
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return false;
|
|
367
|
+
const obj = node as Record<string, any>;
|
|
368
|
+
for (const key of Object.keys(obj)) {
|
|
369
|
+
const child = obj[key];
|
|
370
|
+
if (child && typeof child === "object" && !Array.isArray(child)) {
|
|
371
|
+
if (pruneEmptyObjects(child)) {
|
|
372
|
+
delete obj[key];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return Object.keys(obj).length === 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function toGlobalPreferencePath(field: FieldDef): string {
|
|
380
|
+
if (field.configPath.startsWith("preferences.")) {
|
|
381
|
+
return field.configPath.slice("preferences.".length);
|
|
382
|
+
}
|
|
383
|
+
return field.configPath;
|
|
384
|
+
}
|
|
385
|
+
|
|
364
386
|
/**
|
|
365
|
-
* Write a
|
|
366
|
-
*
|
|
367
|
-
* Writes to the resolved config root using the active layout:
|
|
368
|
-
* - standard: `<configRoot>/.pi/taskplane-config.json`
|
|
369
|
-
* - flat: `<configRoot>/taskplane-config.json` (pointer `.taskplane/` roots)
|
|
370
|
-
*
|
|
371
|
-
* When no JSON config exists (YAML-only scenario), bootstraps the new
|
|
372
|
-
* JSON file from the full current Layer 1 config (YAML values + defaults).
|
|
373
|
-
* This preserves ALL existing YAML-set values — because JSON takes
|
|
374
|
-
* precedence on next load, a partial skeleton would silently reset
|
|
375
|
-
* non-edited fields to defaults.
|
|
387
|
+
* Write a single project override field to `taskplane-config.json`.
|
|
376
388
|
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* Uses atomic tmp+rename write pattern to prevent partial writes.
|
|
389
|
+
* This performs sparse writes only: when no JSON file exists yet, a new
|
|
390
|
+
* file is created with `{ configVersion }` plus the specific override path.
|
|
391
|
+
* It does NOT bootstrap full config values from YAML/global/default layers.
|
|
381
392
|
*/
|
|
382
393
|
export function writeProjectConfigField(
|
|
383
394
|
configRoot: string,
|
|
@@ -402,22 +413,14 @@ export function writeProjectConfigField(
|
|
|
402
413
|
: join(resolvedRoot, ".pi", PROJECT_CONFIG_FILENAME);
|
|
403
414
|
const tmpPath = jsonPath + ".tmp";
|
|
404
415
|
|
|
405
|
-
// Ensure parent directory exists
|
|
406
416
|
mkdirSync(dirname(jsonPath), { recursive: true });
|
|
407
417
|
|
|
408
|
-
// Load existing JSON config, or bootstrap from full L1 config.
|
|
409
|
-
// When YAML-only, we seed from loadLayer1Config to preserve all
|
|
410
|
-
// YAML-sourced values (since JSON takes precedence on next load
|
|
411
|
-
// and YAML values would otherwise be lost).
|
|
412
418
|
let configObj: Record<string, any>;
|
|
413
419
|
if (existsSync(jsonPath)) {
|
|
414
420
|
try {
|
|
415
421
|
const raw = readFileSync(jsonPath, "utf-8");
|
|
416
422
|
configObj = JSON.parse(raw);
|
|
417
423
|
} catch (e: any) {
|
|
418
|
-
// Malformed JSON — cannot safely bootstrap because loadLayer1Config
|
|
419
|
-
// would also fail on the same corrupt file. Surface the error so the
|
|
420
|
-
// user can fix/delete the malformed JSON file first.
|
|
421
424
|
throw new Error(
|
|
422
425
|
`Cannot write settings: ${jsonPath} contains malformed JSON. ` +
|
|
423
426
|
`Please fix or delete the file and try again. ` +
|
|
@@ -425,49 +428,42 @@ export function writeProjectConfigField(
|
|
|
425
428
|
);
|
|
426
429
|
}
|
|
427
430
|
} else {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
+
const yamlSeed = loadProjectOverrides(resolvedRoot);
|
|
432
|
+
configObj = {
|
|
433
|
+
configVersion: CONFIG_VERSION,
|
|
434
|
+
...JSON.parse(JSON.stringify(yamlSeed)),
|
|
435
|
+
};
|
|
431
436
|
}
|
|
432
437
|
|
|
433
|
-
// Set the value at the config path
|
|
434
438
|
setNestedValue(configObj, configPath, value);
|
|
439
|
+
pruneEmptyObjects(configObj);
|
|
440
|
+
if (configObj.configVersion === undefined) {
|
|
441
|
+
configObj.configVersion = CONFIG_VERSION;
|
|
442
|
+
}
|
|
435
443
|
|
|
436
|
-
// Atomic write: tmp + rename
|
|
437
444
|
const json = JSON.stringify(configObj, null, 2) + "\n";
|
|
438
445
|
writeFileSync(tmpPath, json, "utf-8");
|
|
439
446
|
try {
|
|
440
447
|
renameSync(tmpPath, jsonPath);
|
|
441
448
|
} catch {
|
|
442
|
-
// Windows fallback: direct write if rename fails
|
|
443
449
|
writeFileSync(jsonPath, json, "utf-8");
|
|
444
450
|
try { if (existsSync(tmpPath)) unlinkSync(tmpPath); } catch { /* cleanup best-effort */ }
|
|
445
451
|
}
|
|
446
452
|
}
|
|
447
453
|
|
|
448
454
|
/**
|
|
449
|
-
* Write a
|
|
450
|
-
*
|
|
451
|
-
* Writes to `resolveUserPreferencesPath()`.
|
|
452
|
-
* If `value` is undefined, deletes the key from the preferences file
|
|
453
|
-
* (for clearing preferences).
|
|
454
|
-
*
|
|
455
|
-
* Uses atomic tmp+rename write pattern to prevent partial writes.
|
|
455
|
+
* Write a global preference at a dot-path (e.g. `taskRunner.worker.model`).
|
|
456
|
+
* Uses sparse JSON updates and prunes empty objects on delete.
|
|
456
457
|
*/
|
|
457
|
-
export function
|
|
458
|
-
|
|
459
|
-
value: any,
|
|
460
|
-
): void {
|
|
461
|
-
const prefsPath = resolveUserPreferencesPath();
|
|
458
|
+
export function writeGlobalPreference(path: string, value: any): void {
|
|
459
|
+
const prefsPath = resolveGlobalPreferencesPath();
|
|
462
460
|
const tmpPath = prefsPath + ".tmp";
|
|
463
461
|
|
|
464
|
-
// Ensure directory exists
|
|
465
462
|
const prefsDir = dirname(prefsPath);
|
|
466
463
|
if (!existsSync(prefsDir)) {
|
|
467
464
|
mkdirSync(prefsDir, { recursive: true });
|
|
468
465
|
}
|
|
469
466
|
|
|
470
|
-
// Load existing prefs or create empty object
|
|
471
467
|
let prefsObj: Record<string, any> = {};
|
|
472
468
|
if (existsSync(prefsPath)) {
|
|
473
469
|
try {
|
|
@@ -477,25 +473,18 @@ export function writeUserPreference(
|
|
|
477
473
|
prefsObj = parsed;
|
|
478
474
|
}
|
|
479
475
|
} catch {
|
|
480
|
-
// Malformed — start fresh (preserve what we can't parse)
|
|
481
476
|
prefsObj = {};
|
|
482
477
|
}
|
|
483
478
|
}
|
|
484
479
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
delete prefsObj[prefsKey];
|
|
488
|
-
} else {
|
|
489
|
-
prefsObj[prefsKey] = value;
|
|
490
|
-
}
|
|
480
|
+
setNestedValue(prefsObj, path, value);
|
|
481
|
+
pruneEmptyObjects(prefsObj);
|
|
491
482
|
|
|
492
|
-
// Atomic write: tmp + rename
|
|
493
483
|
const json = JSON.stringify(prefsObj, null, 2) + "\n";
|
|
494
484
|
writeFileSync(tmpPath, json, "utf-8");
|
|
495
485
|
try {
|
|
496
486
|
renameSync(tmpPath, prefsPath);
|
|
497
487
|
} catch {
|
|
498
|
-
// Windows fallback: direct write if rename fails
|
|
499
488
|
writeFileSync(prefsPath, json, "utf-8");
|
|
500
489
|
try { if (existsSync(tmpPath)) unlinkSync(tmpPath); } catch { /* cleanup best-effort */ }
|
|
501
490
|
}
|
|
@@ -512,7 +501,7 @@ export function writeUserPreference(
|
|
|
512
501
|
*/
|
|
513
502
|
export function coerceValueForWrite(field: FieldDef, rawValue: string): any {
|
|
514
503
|
// Strip source badge if present
|
|
515
|
-
const cleaned = rawValue.replace(/\s+\((?:default|project|
|
|
504
|
+
const cleaned = rawValue.replace(/\s+\((?:default|project|global)\)$/, "").trim();
|
|
516
505
|
|
|
517
506
|
// Unset / inherit → undefined (delete key)
|
|
518
507
|
if (cleaned === "(not set)" || cleaned === "(inherit)") {
|
|
@@ -534,35 +523,20 @@ export function coerceValueForWrite(field: FieldDef, rawValue: string): any {
|
|
|
534
523
|
}
|
|
535
524
|
|
|
536
525
|
/**
|
|
537
|
-
*
|
|
526
|
+
* Write destinations for settings edits.
|
|
538
527
|
*
|
|
539
|
-
* -
|
|
540
|
-
* -
|
|
541
|
-
* -
|
|
528
|
+
* - `prefs`: write to global preferences (default)
|
|
529
|
+
* - `project`: write a project-specific override
|
|
530
|
+
* - `remove-project`: delete an existing project override (revert to global)
|
|
542
531
|
*/
|
|
543
|
-
export type WriteDestination = "project" | "prefs";
|
|
532
|
+
export type WriteDestination = "project" | "prefs" | "remove-project";
|
|
544
533
|
|
|
545
|
-
export function getDefaultWriteDestination(
|
|
546
|
-
|
|
547
|
-
if (field.layer === "L2") return "prefs";
|
|
548
|
-
// L1+L2 → user must choose
|
|
549
|
-
return null;
|
|
534
|
+
export function getDefaultWriteDestination(_field: FieldDef): WriteDestination {
|
|
535
|
+
return "prefs";
|
|
550
536
|
}
|
|
551
537
|
|
|
552
538
|
/**
|
|
553
539
|
* Resolve the write action for a field change.
|
|
554
|
-
*
|
|
555
|
-
* Encapsulates the destination + confirmation decision tree from
|
|
556
|
-
* showSectionSettingsLoop as a pure function for testability.
|
|
557
|
-
*
|
|
558
|
-
* @param field - The field being edited
|
|
559
|
-
* @param destinationChoice - For L1+L2 fields: the user's choice from the
|
|
560
|
-
* destination select ("User preferences (personal)", "Project config (shared)",
|
|
561
|
-
* "Cancel", or null). Ignored for L1-only and L2-only fields.
|
|
562
|
-
* @param projectConfirmed - For project-destination writes: whether the user
|
|
563
|
-
* confirmed the project config change. Ignored for prefs-destination writes.
|
|
564
|
-
* @returns The resolved destination ("project" | "prefs") or "skip" if the
|
|
565
|
-
* user cancelled or declined confirmation.
|
|
566
540
|
*/
|
|
567
541
|
export function resolveWriteAction(
|
|
568
542
|
field: FieldDef,
|
|
@@ -570,18 +544,17 @@ export function resolveWriteAction(
|
|
|
570
544
|
projectConfirmed: boolean,
|
|
571
545
|
): WriteDestination | "skip" {
|
|
572
546
|
const defaultDest = getDefaultWriteDestination(field);
|
|
573
|
-
let dest: WriteDestination | null = defaultDest;
|
|
574
547
|
|
|
575
|
-
//
|
|
576
|
-
if (
|
|
577
|
-
if (!destinationChoice || destinationChoice === "Cancel") return "skip";
|
|
578
|
-
dest = destinationChoice.startsWith("User") ? "prefs" : "project";
|
|
579
|
-
}
|
|
548
|
+
// L2-only fields have no project layer
|
|
549
|
+
if (field.layer === "L2") return "prefs";
|
|
580
550
|
|
|
581
|
-
|
|
582
|
-
if (
|
|
551
|
+
if (!destinationChoice || destinationChoice === "Cancel") return "skip";
|
|
552
|
+
if (destinationChoice.startsWith("Global") || destinationChoice.startsWith("User")) return "prefs";
|
|
553
|
+
if (destinationChoice.startsWith("Remove project override")) return "remove-project";
|
|
554
|
+
if (destinationChoice.startsWith("Project") && !projectConfirmed) return "skip";
|
|
555
|
+
if (destinationChoice.startsWith("Project")) return "project";
|
|
583
556
|
|
|
584
|
-
return
|
|
557
|
+
return defaultDest;
|
|
585
558
|
}
|
|
586
559
|
|
|
587
560
|
|
|
@@ -604,47 +577,21 @@ function getNestedValue(obj: any, path: string): any {
|
|
|
604
577
|
/**
|
|
605
578
|
* Determine the source of a field's current value.
|
|
606
579
|
*
|
|
607
|
-
*
|
|
608
|
-
* -
|
|
609
|
-
* -
|
|
610
|
-
* - Fallback to default
|
|
580
|
+
* Source badge policy:
|
|
581
|
+
* - `(project)` when the field is explicitly present in project config JSON/YAML
|
|
582
|
+
* - `(global)` otherwise (global preferences baseline + schema defaults)
|
|
611
583
|
*/
|
|
612
584
|
export function detectFieldSource(
|
|
613
585
|
field: FieldDef,
|
|
614
586
|
rawProjectConfig: Record<string, any> | null,
|
|
615
|
-
|
|
587
|
+
_rawPrefs: Record<string, any> | null,
|
|
616
588
|
): FieldSource {
|
|
617
|
-
|
|
618
|
-
// Type guards MUST match extractAllowlistedPreferences() in config-loader.ts
|
|
619
|
-
// to avoid showing "(user)" for values that the merge layer would reject.
|
|
620
|
-
if ((field.layer === "L1+L2" || field.layer === "L2") && field.prefsKey && rawPrefs) {
|
|
621
|
-
const prefVal = rawPrefs[field.prefsKey];
|
|
622
|
-
if (field.fieldType === "string") {
|
|
623
|
-
// String rule: must be typeof string, non-empty → (user)
|
|
624
|
-
// Matches: `typeof raw.X === "string"` AND applyUserPreferences `val !== "" `
|
|
625
|
-
if (typeof prefVal === "string" && prefVal !== "") return "user";
|
|
626
|
-
} else if (field.fieldType === "enum") {
|
|
627
|
-
// Enum rule: must be a valid enum value from the field's values array.
|
|
628
|
-
// Matches extractAllowlistedPreferences which checks exact enum membership
|
|
629
|
-
// (e.g., raw.spawnMode === "subprocess").
|
|
630
|
-
if (prefVal !== undefined && field.values && field.values.includes(String(prefVal))) return "user";
|
|
631
|
-
} else if (field.fieldType === "number") {
|
|
632
|
-
// Number rule: must be typeof number and finite → (user)
|
|
633
|
-
// Matches: `typeof raw.X === "number" && Number.isFinite(raw.X)`
|
|
634
|
-
if (typeof prefVal === "number" && Number.isFinite(prefVal)) return "user";
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// L2-only fields have no project layer
|
|
639
|
-
if (field.layer === "L2") return "default";
|
|
640
|
-
|
|
641
|
-
// L1 check: look in raw project config
|
|
642
|
-
if (rawProjectConfig) {
|
|
589
|
+
if (field.layer !== "L2" && rawProjectConfig) {
|
|
643
590
|
const val = getNestedValue(rawProjectConfig, field.configPath);
|
|
644
591
|
if (val !== undefined) return "project";
|
|
645
592
|
}
|
|
646
593
|
|
|
647
|
-
return "
|
|
594
|
+
return "global";
|
|
648
595
|
}
|
|
649
596
|
|
|
650
597
|
|
|
@@ -656,7 +603,7 @@ export function detectFieldSource(
|
|
|
656
603
|
export function getFieldDisplayValue(
|
|
657
604
|
field: FieldDef,
|
|
658
605
|
mergedConfig: TaskplaneConfig,
|
|
659
|
-
prefs:
|
|
606
|
+
prefs: GlobalPreferences,
|
|
660
607
|
): string {
|
|
661
608
|
// Special case: dashboardPort (L2-only, not in merged config)
|
|
662
609
|
if (field.configPath === "preferences.dashboardPort") {
|
|
@@ -668,9 +615,6 @@ export function getFieldDisplayValue(
|
|
|
668
615
|
|
|
669
616
|
// Optional fields may be undefined
|
|
670
617
|
if (val === undefined) {
|
|
671
|
-
if (field.optional && field.configPath === "taskRunner.worker.spawnMode") {
|
|
672
|
-
return "(inherit)";
|
|
673
|
-
}
|
|
674
618
|
return "(not set)";
|
|
675
619
|
}
|
|
676
620
|
|
|
@@ -1003,18 +947,25 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
|
|
|
1003
947
|
}
|
|
1004
948
|
}
|
|
1005
949
|
|
|
1006
|
-
type ThinkingModeValue = "" | "
|
|
950
|
+
type ThinkingModeValue = "" | "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
1007
951
|
|
|
1008
952
|
const THINKING_MODE_OPTIONS: Array<{ value: ThinkingModeValue; label: string }> = [
|
|
1009
953
|
{ value: "", label: "inherit (use session thinking)" },
|
|
1010
|
-
{ value: "on", label: "on" },
|
|
1011
954
|
{ value: "off", label: "off" },
|
|
955
|
+
{ value: "minimal", label: "minimal" },
|
|
956
|
+
{ value: "low", label: "low" },
|
|
957
|
+
{ value: "medium", label: "medium" },
|
|
958
|
+
{ value: "high", label: "high" },
|
|
959
|
+
{ value: "xhigh", label: "xhigh" },
|
|
1012
960
|
];
|
|
1013
961
|
|
|
1014
962
|
function normalizeThinkingMode(value: unknown): ThinkingModeValue {
|
|
1015
963
|
const cleaned = String(value ?? "").trim().toLowerCase();
|
|
1016
|
-
if (cleaned === "
|
|
1017
|
-
if (cleaned === "
|
|
964
|
+
if (!cleaned || cleaned === "inherit") return "";
|
|
965
|
+
if (cleaned === "on") return "high";
|
|
966
|
+
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(cleaned)) {
|
|
967
|
+
return cleaned as ThinkingModeValue;
|
|
968
|
+
}
|
|
1018
969
|
return "";
|
|
1019
970
|
}
|
|
1020
971
|
|
|
@@ -1023,11 +974,12 @@ async function pickThinkingMode(
|
|
|
1023
974
|
currentThinking: string,
|
|
1024
975
|
): Promise<ThinkingModeValue | undefined> {
|
|
1025
976
|
const current = normalizeThinkingMode(currentThinking);
|
|
977
|
+
const resolvedCurrent: ThinkingModeValue = current || "high";
|
|
1026
978
|
const optionToValue = new Map<string, ThinkingModeValue>();
|
|
1027
979
|
const optionLabels: string[] = [];
|
|
1028
980
|
|
|
1029
981
|
for (const option of THINKING_MODE_OPTIONS) {
|
|
1030
|
-
const label = `${option.label}${option.value ===
|
|
982
|
+
const label = `${option.label}${option.value === resolvedCurrent ? " ✓ current" : ""}`;
|
|
1031
983
|
optionLabels.push(label);
|
|
1032
984
|
optionToValue.set(label, option.value);
|
|
1033
985
|
}
|
|
@@ -1043,6 +995,12 @@ const MODEL_THINKING_PATH_MAP: Record<string, { thinkingPath: string; label: str
|
|
|
1043
995
|
"orchestrator.merge.model": { thinkingPath: "orchestrator.merge.thinking", label: "Merge" },
|
|
1044
996
|
};
|
|
1045
997
|
|
|
998
|
+
const THINKING_MODEL_PATH_MAP: Record<string, { modelPath: string; label: string }> = {
|
|
999
|
+
"taskRunner.worker.thinking": { modelPath: "taskRunner.worker.model", label: "Worker" },
|
|
1000
|
+
"taskRunner.reviewer.thinking": { modelPath: "taskRunner.reviewer.model", label: "Reviewer" },
|
|
1001
|
+
"orchestrator.merge.thinking": { modelPath: "orchestrator.merge.model", label: "Merge" },
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1046
1004
|
function resolveModelRecord(ctx: ExtensionContext, modelRef: string): any | undefined {
|
|
1047
1005
|
const trimmed = modelRef.trim();
|
|
1048
1006
|
if (!trimmed) return undefined;
|
|
@@ -1096,6 +1054,10 @@ export function modelSupportsThinking(model: any): boolean {
|
|
|
1096
1054
|
for (const candidate of candidateObjects) {
|
|
1097
1055
|
for (const key of boolFlags) {
|
|
1098
1056
|
if (typeof candidate[key] === "boolean" && candidate[key]) return true;
|
|
1057
|
+
if (typeof candidate[key] === "string") {
|
|
1058
|
+
const normalized = candidate[key].trim().toLowerCase();
|
|
1059
|
+
if (["yes", "true", "on", "supported"].includes(normalized)) return true;
|
|
1060
|
+
}
|
|
1099
1061
|
}
|
|
1100
1062
|
for (const key of capabilityKeys) {
|
|
1101
1063
|
if (candidate[key] !== undefined && candidate[key] !== null) return true;
|
|
@@ -1123,9 +1085,26 @@ export function buildThinkingSuggestionForModelChange(
|
|
|
1123
1085
|
if (!modelRecord || !modelSupportsThinking(modelRecord)) return null;
|
|
1124
1086
|
|
|
1125
1087
|
const currentThinking = normalizeThinkingMode(getNestedValue(mergedConfig, mapping.thinkingPath));
|
|
1126
|
-
if (currentThinking === "
|
|
1088
|
+
if (currentThinking === "high") return null;
|
|
1127
1089
|
|
|
1128
|
-
return `${mapping.label} model supports thinking. Consider setting ${mapping.label} Thinking to \"
|
|
1090
|
+
return `${mapping.label} model supports thinking. Consider setting ${mapping.label} Thinking to \"high\".`;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export function buildThinkingUnsupportedNoteForThinkingField(
|
|
1094
|
+
ctx: ExtensionContext,
|
|
1095
|
+
field: FieldDef,
|
|
1096
|
+
mergedConfig: TaskplaneConfig,
|
|
1097
|
+
): string | null {
|
|
1098
|
+
const mapping = THINKING_MODEL_PATH_MAP[field.configPath];
|
|
1099
|
+
if (!mapping) return null;
|
|
1100
|
+
|
|
1101
|
+
const modelRef = String(getNestedValue(mergedConfig, mapping.modelPath) ?? "").trim();
|
|
1102
|
+
if (!modelRef) return null;
|
|
1103
|
+
|
|
1104
|
+
const modelRecord = resolveModelRecord(ctx, modelRef);
|
|
1105
|
+
if (!modelRecord || modelSupportsThinking(modelRecord)) return null;
|
|
1106
|
+
|
|
1107
|
+
return `${mapping.label} model does not advertise thinking support. You can still set thinking; unsupported models ignore it at runtime.`;
|
|
1129
1108
|
}
|
|
1130
1109
|
|
|
1131
1110
|
/**
|
|
@@ -1198,14 +1177,14 @@ export async function openSettingsTui(
|
|
|
1198
1177
|
*/
|
|
1199
1178
|
function loadConfigState(configRoot: string, pointerConfigRoot?: string): {
|
|
1200
1179
|
mergedConfig: TaskplaneConfig;
|
|
1201
|
-
prefs:
|
|
1180
|
+
prefs: GlobalPreferences;
|
|
1202
1181
|
rawProject: Record<string, any> | null;
|
|
1203
1182
|
rawPrefs: Record<string, any> | null;
|
|
1204
1183
|
} {
|
|
1205
1184
|
const resolvedRoot = resolveConfigRoot(configRoot, pointerConfigRoot);
|
|
1206
1185
|
return {
|
|
1207
1186
|
mergedConfig: loadProjectConfig(configRoot, pointerConfigRoot),
|
|
1208
|
-
prefs:
|
|
1187
|
+
prefs: loadGlobalPreferences(),
|
|
1209
1188
|
rawProject: readRawProjectJson(resolvedRoot) || readRawYamlConfigs(resolvedRoot),
|
|
1210
1189
|
rawPrefs: readRawPreferences(),
|
|
1211
1190
|
};
|
|
@@ -1340,9 +1319,8 @@ async function showAdvancedSection(
|
|
|
1340
1319
|
*/
|
|
1341
1320
|
function formatSourceBadge(source: FieldSource): string {
|
|
1342
1321
|
switch (source) {
|
|
1343
|
-
case "default": return "(default)";
|
|
1344
1322
|
case "project": return "(project)";
|
|
1345
|
-
case "
|
|
1323
|
+
case "global": return "(global)";
|
|
1346
1324
|
}
|
|
1347
1325
|
}
|
|
1348
1326
|
|
|
@@ -1378,7 +1356,7 @@ async function showSectionSettingsLoop(
|
|
|
1378
1356
|
if (result.rawValue === "__EDIT_REQUESTED__" && (field.control === "input" || field.control === "picker")) {
|
|
1379
1357
|
const state = loadConfigState(configRoot, pointerConfigRoot);
|
|
1380
1358
|
const currentDisplay = getFieldDisplayValue(field, state.mergedConfig, state.prefs);
|
|
1381
|
-
const currentClean = String(currentDisplay).replace(/\s+\((?:default|project|
|
|
1359
|
+
const currentClean = String(currentDisplay).replace(/\s+\((?:default|project|global)\)$/, "");
|
|
1382
1360
|
const normalizedCurrent = currentClean === "(inherit)" ? "" : currentClean;
|
|
1383
1361
|
|
|
1384
1362
|
// Model fields: use interactive provider → model picker instead of free-text
|
|
@@ -1388,6 +1366,8 @@ async function showSectionSettingsLoop(
|
|
|
1388
1366
|
if (selected === undefined) continue; // Cancelled
|
|
1389
1367
|
result.rawValue = selected;
|
|
1390
1368
|
} else if (field.control === "picker" && field.configPath.endsWith(".thinking")) {
|
|
1369
|
+
const note = buildThinkingUnsupportedNoteForThinkingField(ctx, field, state.mergedConfig);
|
|
1370
|
+
if (note) ctx.ui.notify(note, "info");
|
|
1391
1371
|
const selected = await pickThinkingMode(ctx, normalizedCurrent);
|
|
1392
1372
|
if (selected === undefined) continue; // Cancelled
|
|
1393
1373
|
result.rawValue = selected;
|
|
@@ -1421,30 +1401,28 @@ async function showSectionSettingsLoop(
|
|
|
1421
1401
|
}
|
|
1422
1402
|
|
|
1423
1403
|
const typedValue = coerceValueForWrite(field, result.rawValue);
|
|
1404
|
+
const hasProjectOverride =
|
|
1405
|
+
field.layer !== "L2" &&
|
|
1406
|
+
!!state.rawProject &&
|
|
1407
|
+
getNestedValue(state.rawProject, field.configPath) !== undefined;
|
|
1424
1408
|
|
|
1425
1409
|
// Collect UI answers for the write-decision contract
|
|
1426
1410
|
let destinationChoice: string | null = null;
|
|
1427
|
-
if (
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
"
|
|
1431
|
-
[
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
],
|
|
1436
|
-
);
|
|
1411
|
+
if (field.layer !== "L2") {
|
|
1412
|
+
const options = [
|
|
1413
|
+
"Global preferences (default)",
|
|
1414
|
+
"Project override (this project only)",
|
|
1415
|
+
...(hasProjectOverride ? ["Remove project override (revert to global)"] : []),
|
|
1416
|
+
"Cancel",
|
|
1417
|
+
];
|
|
1418
|
+
destinationChoice = await ctx.ui.select("Save this change to:", options);
|
|
1437
1419
|
}
|
|
1438
1420
|
|
|
1439
1421
|
let projectConfirmed = true;
|
|
1440
|
-
|
|
1441
|
-
const needsProjectConfirm =
|
|
1442
|
-
(field.layer === "L1") ||
|
|
1443
|
-
(field.layer === "L1+L2" && destinationChoice?.startsWith("Project"));
|
|
1444
|
-
if (needsProjectConfirm) {
|
|
1422
|
+
if (destinationChoice?.startsWith("Project override")) {
|
|
1445
1423
|
projectConfirmed = await ctx.ui.confirm(
|
|
1446
|
-
"Confirm project
|
|
1447
|
-
"This writes to .pi/taskplane-config.json
|
|
1424
|
+
"Confirm project override",
|
|
1425
|
+
"This writes to .pi/taskplane-config.json as a project override. Continue?",
|
|
1448
1426
|
);
|
|
1449
1427
|
}
|
|
1450
1428
|
|
|
@@ -1455,11 +1433,10 @@ async function showSectionSettingsLoop(
|
|
|
1455
1433
|
try {
|
|
1456
1434
|
if (dest === "project") {
|
|
1457
1435
|
writeProjectConfigField(configRoot, field.configPath, typedValue, pointerConfigRoot);
|
|
1436
|
+
} else if (dest === "remove-project") {
|
|
1437
|
+
writeProjectConfigField(configRoot, field.configPath, undefined, pointerConfigRoot);
|
|
1458
1438
|
} else {
|
|
1459
|
-
|
|
1460
|
-
if (field.prefsKey) {
|
|
1461
|
-
writeUserPreference(field.prefsKey, typedValue);
|
|
1462
|
-
}
|
|
1439
|
+
writeGlobalPreference(toGlobalPreferencePath(field), typedValue);
|
|
1463
1440
|
}
|
|
1464
1441
|
ctx.ui.notify(
|
|
1465
1442
|
`✅ ${field.label} updated.\n` +
|
|
@@ -1500,7 +1477,7 @@ async function showSectionSettingsOnce(
|
|
|
1500
1477
|
ctx: ExtensionContext,
|
|
1501
1478
|
section: SectionDef,
|
|
1502
1479
|
mergedConfig: TaskplaneConfig,
|
|
1503
|
-
prefs:
|
|
1480
|
+
prefs: GlobalPreferences,
|
|
1504
1481
|
rawProject: Record<string, any> | null,
|
|
1505
1482
|
rawPrefs: Record<string, any> | null,
|
|
1506
1483
|
): Promise<PendingChange | null> {
|
|
@@ -1597,7 +1574,7 @@ function createInputSubmenu(
|
|
|
1597
1574
|
done: (selectedValue?: string) => void,
|
|
1598
1575
|
): any {
|
|
1599
1576
|
// Strip source badge from current value for editing
|
|
1600
|
-
const cleanValue = currentValue.replace(/\s+\((?:default|project|
|
|
1577
|
+
const cleanValue = currentValue.replace(/\s+\((?:default|project|global)\)$/, "");
|
|
1601
1578
|
let inputBuffer = cleanValue === "(not set)" || cleanValue === "(inherit)" ? "" : cleanValue;
|
|
1602
1579
|
let errorMsg = "";
|
|
1603
1580
|
let cursorPos = inputBuffer.length;
|
|
@@ -1845,7 +1845,7 @@ export function presentBatchSummary(
|
|
|
1845
1845
|
export type SupervisorAutonomyLevel = "interactive" | "supervised" | "autonomous";
|
|
1846
1846
|
|
|
1847
1847
|
/**
|
|
1848
|
-
* Supervisor configuration resolved from project config +
|
|
1848
|
+
* Supervisor configuration resolved from project config + global preferences.
|
|
1849
1849
|
*
|
|
1850
1850
|
* @since TP-041
|
|
1851
1851
|
*/
|
|
@@ -3070,7 +3070,7 @@ export function registerSupervisorPromptHook(
|
|
|
3070
3070
|
* Resolve supervisor configuration from available sources.
|
|
3071
3071
|
*
|
|
3072
3072
|
* Resolution order (highest precedence first):
|
|
3073
|
-
* 1.
|
|
3073
|
+
* 1. Global preferences (supervisorModel → orchestrator.supervisor.model)
|
|
3074
3074
|
* 2. Project config (orchestrator.supervisor section in taskplane-config.json)
|
|
3075
3075
|
* 3. Defaults (model="" = inherit session model, autonomy="supervised")
|
|
3076
3076
|
*
|
|
@@ -282,7 +282,7 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
|
|
|
282
282
|
merge: {
|
|
283
283
|
model: "",
|
|
284
284
|
tools: "read,write,edit,bash,grep,find,ls",
|
|
285
|
-
thinking: "",
|
|
285
|
+
thinking: "off",
|
|
286
286
|
verify: [],
|
|
287
287
|
order: "fewest-files-first",
|
|
288
288
|
timeout_minutes: 90,
|