taskplane 0.24.18 → 0.24.20

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.
@@ -507,7 +507,23 @@ export interface TaskplaneConfig {
507
507
  * | mergeModel | orchestrator.merge.model | string |
508
508
  * | supervisorModel | orchestrator.supervisor.model | string |
509
509
  * | dashboardPort | (preferences-only; not yet in schema)| number |
510
+ * | initAgentDefaults | (preferences-only; used by init UX) | object |
510
511
  */
512
+ export interface InitAgentDefaultsPreferences {
513
+ /** Worker model default for `taskplane init` prompts (empty = inherit) */
514
+ workerModel?: string;
515
+ /** Reviewer model default for `taskplane init` prompts (empty = inherit) */
516
+ reviewerModel?: string;
517
+ /** Merger model default for `taskplane init` prompts (empty = inherit) */
518
+ mergeModel?: string;
519
+ /** Worker thinking default for `taskplane init` prompts (`""`/`on`/`off`) */
520
+ workerThinking?: string;
521
+ /** Reviewer thinking default for `taskplane init` prompts (`""`/`on`/`off`) */
522
+ reviewerThinking?: string;
523
+ /** Merger thinking default for `taskplane init` prompts (`""`/`on`/`off`) */
524
+ mergeThinking?: string;
525
+ }
526
+
511
527
  export interface UserPreferences {
512
528
  /** Operator identifier (overrides orchestrator.orchestrator.operatorId) */
513
529
  operatorId?: string;
@@ -521,10 +537,14 @@ export interface UserPreferences {
521
537
  reviewerModel?: string;
522
538
  /** Merge model override (overrides orchestrator.merge.model) */
523
539
  mergeModel?: string;
540
+ /** Merge thinking override (overrides orchestrator.merge.thinking) */
541
+ mergeThinking?: string;
524
542
  /** Supervisor model override (overrides orchestrator.supervisor.model) (TP-041) */
525
543
  supervisorModel?: string;
526
544
  /** Dashboard port (preferences-only; not yet wired into config schema) */
527
545
  dashboardPort?: number;
546
+ /** Saved defaults used to pre-populate `taskplane init` model/thinking prompts */
547
+ initAgentDefaults?: InitAgentDefaultsPreferences;
528
548
  }
529
549
 
530
550
  /** Default (empty) user preferences — all fields undefined means "no override". */
@@ -551,8 +571,8 @@ export const DEFAULT_TASK_RUNNER_SECTION: TaskRunnerSection = {
551
571
  testing: { commands: {} },
552
572
  standards: { docs: [], rules: [] },
553
573
  standardsOverrides: {},
554
- worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "off" },
555
- reviewer: { model: "openai/gpt-5.3-codex", tools: "read,bash,grep,find,ls", thinking: "on" },
574
+ worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "" },
575
+ reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on" },
556
576
  context: {
557
577
  workerContextWindow: 0,
558
578
  warnPercent: 85,
@@ -49,7 +49,7 @@ export type FieldSource = "default" | "project" | "user";
49
49
  export type FieldLayer = "L1" | "L2" | "L1+L2";
50
50
 
51
51
  /** UI control type for a field */
52
- export type FieldControl = "toggle" | "input";
52
+ export type FieldControl = "toggle" | "input" | "picker";
53
53
 
54
54
  /** Field definition for the settings TUI */
55
55
  export interface FieldDef {
@@ -129,7 +129,7 @@ export const SECTIONS: SectionDef[] = [
129
129
  fields: [
130
130
  { configPath: "orchestrator.merge.model", label: "Merge Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "mergeModel", description: "Merge-agent model (inherit = use session model)" },
131
131
  { configPath: "orchestrator.merge.tools", label: "Merge Tools", control: "input", layer: "L1", fieldType: "string", description: "Merge-agent tool allowlist" },
132
- { configPath: "orchestrator.merge.thinking", label: "Merge Thinking", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "mergeThinking", description: "Merge-agent thinking mode (empty = inherit session)" },
132
+ { configPath: "orchestrator.merge.thinking", label: "Merge Thinking", control: "picker", layer: "L1+L2", fieldType: "string", prefsKey: "mergeThinking", description: "Merge-agent thinking mode" },
133
133
  { configPath: "orchestrator.merge.order", label: "Merge Order", control: "toggle", layer: "L1", fieldType: "enum", values: ["fewest-files-first", "sequential"], description: "Lane merge ordering policy" },
134
134
  { configPath: "orchestrator.merge.timeoutMinutes", label: "Merge Timeout (minutes)", control: "input", layer: "L1", fieldType: "number", description: "Max time for merge agent to complete. Increase for large batches (default: 10)" },
135
135
  ],
@@ -162,7 +162,7 @@ export const SECTIONS: SectionDef[] = [
162
162
  fields: [
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
- { configPath: "taskRunner.worker.thinking", label: "Worker Thinking", control: "input", layer: "L1", fieldType: "string", description: "Worker thinking mode" },
165
+ { configPath: "taskRunner.worker.thinking", label: "Worker Thinking", control: "picker", layer: "L1", fieldType: "string", description: "Worker thinking mode" },
166
166
  { configPath: "taskRunner.worker.spawnMode", label: "Spawn Mode", control: "toggle", layer: "L1", fieldType: "enum", values: ["subprocess"], optional: true, description: "How /task spawns workers and reviewers. Runtime V2 supports subprocess only." },
167
167
  ],
168
168
  },
@@ -171,7 +171,7 @@ export const SECTIONS: SectionDef[] = [
171
171
  fields: [
172
172
  { configPath: "taskRunner.reviewer.model", label: "Reviewer Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "reviewerModel", description: "Reviewer model (inherit = use session model)" },
173
173
  { configPath: "taskRunner.reviewer.tools", label: "Reviewer Tools", control: "input", layer: "L1", fieldType: "string", description: "Reviewer tool allowlist" },
174
- { configPath: "taskRunner.reviewer.thinking", label: "Reviewer Thinking", control: "input", layer: "L1", fieldType: "string", description: "Reviewer thinking mode" },
174
+ { configPath: "taskRunner.reviewer.thinking", label: "Reviewer Thinking", control: "picker", layer: "L1", fieldType: "string", description: "Reviewer thinking mode" },
175
175
  ],
176
176
  },
177
177
  {
@@ -1003,6 +1003,131 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
1003
1003
  }
1004
1004
  }
1005
1005
 
1006
+ type ThinkingModeValue = "" | "on" | "off";
1007
+
1008
+ const THINKING_MODE_OPTIONS: Array<{ value: ThinkingModeValue; label: string }> = [
1009
+ { value: "", label: "inherit (use session thinking)" },
1010
+ { value: "on", label: "on" },
1011
+ { value: "off", label: "off" },
1012
+ ];
1013
+
1014
+ function normalizeThinkingMode(value: unknown): ThinkingModeValue {
1015
+ const cleaned = String(value ?? "").trim().toLowerCase();
1016
+ if (cleaned === "on") return "on";
1017
+ if (cleaned === "off") return "off";
1018
+ return "";
1019
+ }
1020
+
1021
+ async function pickThinkingMode(
1022
+ ctx: ExtensionContext,
1023
+ currentThinking: string,
1024
+ ): Promise<ThinkingModeValue | undefined> {
1025
+ const current = normalizeThinkingMode(currentThinking);
1026
+ const optionToValue = new Map<string, ThinkingModeValue>();
1027
+ const optionLabels: string[] = [];
1028
+
1029
+ for (const option of THINKING_MODE_OPTIONS) {
1030
+ const label = `${option.label}${option.value === current ? " ✓ current" : ""}`;
1031
+ optionLabels.push(label);
1032
+ optionToValue.set(label, option.value);
1033
+ }
1034
+
1035
+ const selected = await selectScrollable(ctx, "Choose thinking mode", optionLabels, 8);
1036
+ if (!selected) return undefined;
1037
+ return optionToValue.get(selected);
1038
+ }
1039
+
1040
+ const MODEL_THINKING_PATH_MAP: Record<string, { thinkingPath: string; label: string }> = {
1041
+ "taskRunner.worker.model": { thinkingPath: "taskRunner.worker.thinking", label: "Worker" },
1042
+ "taskRunner.reviewer.model": { thinkingPath: "taskRunner.reviewer.thinking", label: "Reviewer" },
1043
+ "orchestrator.merge.model": { thinkingPath: "orchestrator.merge.thinking", label: "Merge" },
1044
+ };
1045
+
1046
+ function resolveModelRecord(ctx: ExtensionContext, modelRef: string): any | undefined {
1047
+ const trimmed = modelRef.trim();
1048
+ if (!trimmed) return undefined;
1049
+
1050
+ const available = ctx.modelRegistry.getAvailable();
1051
+ const lower = trimmed.toLowerCase();
1052
+ const slashIdx = trimmed.indexOf("/");
1053
+
1054
+ if (slashIdx > 0) {
1055
+ const provider = trimmed.slice(0, slashIdx).toLowerCase();
1056
+ const id = trimmed.slice(slashIdx + 1).toLowerCase();
1057
+ return available.find((m: any) =>
1058
+ String(m?.provider ?? "").toLowerCase() === provider
1059
+ && String(m?.id ?? "").toLowerCase() === id,
1060
+ );
1061
+ }
1062
+
1063
+ return available.find((m: any) =>
1064
+ String(m?.id ?? "").toLowerCase() === lower
1065
+ || `${String(m?.provider ?? "").toLowerCase()}/${String(m?.id ?? "").toLowerCase()}` === lower,
1066
+ );
1067
+ }
1068
+
1069
+ export function modelSupportsThinking(model: any): boolean {
1070
+ if (!model || typeof model !== "object") return false;
1071
+
1072
+ const boolFlags = [
1073
+ "supportsThinking",
1074
+ "thinking",
1075
+ "supportsReasoning",
1076
+ "supportsReasoningEffort",
1077
+ "supportsReasoningTokens",
1078
+ "reasoning",
1079
+ ];
1080
+ const capabilityKeys = [
1081
+ "reasoningEffort",
1082
+ "reasoningTokens",
1083
+ "thinkingModes",
1084
+ "thinkingMode",
1085
+ "reasoning_effort",
1086
+ "reasoning_tokens",
1087
+ ];
1088
+
1089
+ const candidateObjects = [
1090
+ model,
1091
+ model.capabilities,
1092
+ model.features,
1093
+ model.metadata,
1094
+ ].filter((entry) => entry && typeof entry === "object");
1095
+
1096
+ for (const candidate of candidateObjects) {
1097
+ for (const key of boolFlags) {
1098
+ if (typeof candidate[key] === "boolean" && candidate[key]) return true;
1099
+ }
1100
+ for (const key of capabilityKeys) {
1101
+ if (candidate[key] !== undefined && candidate[key] !== null) return true;
1102
+ }
1103
+ }
1104
+
1105
+ return false;
1106
+ }
1107
+
1108
+ export function buildThinkingSuggestionForModelChange(
1109
+ ctx: ExtensionContext,
1110
+ field: FieldDef,
1111
+ previousModelValue: string,
1112
+ nextModelValue: string,
1113
+ mergedConfig: TaskplaneConfig,
1114
+ ): string | null {
1115
+ const mapping = MODEL_THINKING_PATH_MAP[field.configPath];
1116
+ if (!mapping) return null;
1117
+
1118
+ const previousNormalized = previousModelValue.trim().toLowerCase();
1119
+ const nextNormalized = nextModelValue.trim().toLowerCase();
1120
+ if (!nextNormalized || previousNormalized === nextNormalized) return null;
1121
+
1122
+ const modelRecord = resolveModelRecord(ctx, nextModelValue);
1123
+ if (!modelRecord || !modelSupportsThinking(modelRecord)) return null;
1124
+
1125
+ const currentThinking = normalizeThinkingMode(getNestedValue(mergedConfig, mapping.thinkingPath));
1126
+ if (currentThinking === "on") return null;
1127
+
1128
+ return `${mapping.label} model supports thinking. Consider setting ${mapping.label} Thinking to \"on\".`;
1129
+ }
1130
+
1006
1131
  /**
1007
1132
  * Scrollable select list for model/provider picking.
1008
1133
  * Uses pi's TUI custom widget API.
@@ -1247,20 +1372,26 @@ async function showSectionSettingsLoop(
1247
1372
  const field = section.fields.find((f) => f.configPath === result.fieldId);
1248
1373
  if (!field) continue; // Safety: field not found
1249
1374
 
1250
- // Input fields: the submenu returned a sentinel — use ctx.ui.input() for actual editing
1251
- if (result.rawValue === "__EDIT_REQUESTED__" && field.control === "input") {
1375
+ let previousModelValue = "";
1376
+
1377
+ // Input/picker fields: the submenu returned a sentinel — open the editor picker.
1378
+ if (result.rawValue === "__EDIT_REQUESTED__" && (field.control === "input" || field.control === "picker")) {
1379
+ const state = loadConfigState(configRoot, pointerConfigRoot);
1380
+ const currentDisplay = getFieldDisplayValue(field, state.mergedConfig, state.prefs);
1381
+ const currentClean = String(currentDisplay).replace(/\s+\((?:default|project|user)\)$/, "");
1382
+ const normalizedCurrent = currentClean === "(inherit)" ? "" : currentClean;
1383
+
1252
1384
  // Model fields: use interactive provider → model picker instead of free-text
1253
1385
  if (field.configPath.endsWith(".model")) {
1254
- const state = loadConfigState(configRoot, pointerConfigRoot);
1255
- const currentDisplay = getFieldDisplayValue(field, state.mergedConfig, state.prefs);
1256
- const currentClean = String(currentDisplay).replace(/\s+\((?:default|project|user)\)$/, "");
1257
- const selected = await pickModel(ctx, currentClean === "(inherit)" ? "" : currentClean);
1386
+ previousModelValue = normalizedCurrent;
1387
+ const selected = await pickModel(ctx, normalizedCurrent);
1388
+ if (selected === undefined) continue; // Cancelled
1389
+ result.rawValue = selected;
1390
+ } else if (field.control === "picker" && field.configPath.endsWith(".thinking")) {
1391
+ const selected = await pickThinkingMode(ctx, normalizedCurrent);
1258
1392
  if (selected === undefined) continue; // Cancelled
1259
1393
  result.rawValue = selected;
1260
1394
  } else {
1261
- const state = loadConfigState(configRoot, pointerConfigRoot);
1262
- const currentDisplay = getFieldDisplayValue(field, state.mergedConfig, state.prefs);
1263
- const currentClean = String(currentDisplay).replace(/\s+\((?:default|project|user)\)$/, "");
1264
1395
  const placeholder = currentClean === "(not set)" || currentClean === "(inherit)" ? "" : currentClean;
1265
1396
 
1266
1397
  const newValue = await ctx.ui.input(
@@ -1327,6 +1458,18 @@ async function showSectionSettingsLoop(
1327
1458
  `ℹ Restart session to apply changes.`,
1328
1459
  "info",
1329
1460
  );
1461
+
1462
+ const refreshedState = loadConfigState(configRoot, pointerConfigRoot);
1463
+ const suggestion = buildThinkingSuggestionForModelChange(
1464
+ ctx,
1465
+ field,
1466
+ previousModelValue,
1467
+ String(result.rawValue ?? ""),
1468
+ refreshedState.mergedConfig,
1469
+ );
1470
+ if (suggestion) {
1471
+ ctx.ui.notify(`💡 ${suggestion}`, "info");
1472
+ }
1330
1473
  } catch (err: any) {
1331
1474
  ctx.ui.notify(`❌ Failed to save: ${err.message}`, "error");
1332
1475
  }
@@ -1375,7 +1518,7 @@ async function showSectionSettingsOnce(
1375
1518
  // The inline submenu approach freezes on Windows/tmux (issue #57).
1376
1519
  // We set a single sentinel value so pressing Enter/Space triggers onChange,
1377
1520
  // which exits the TUI. The caller then uses ctx.ui.input() for actual editing.
1378
- if (field.control === "input") {
1521
+ if (field.control === "input" || field.control === "picker") {
1379
1522
  item.values = [`__EDIT_REQUESTED__`];
1380
1523
  }
1381
1524
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.24.18",
3
+ "version": "0.24.20",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -47,7 +47,7 @@ standards_overrides: {}
47
47
  worker:
48
48
  model: "" # empty = inherit from parent pi session
49
49
  tools: "read,write,edit,bash,grep,find,ls"
50
- thinking: "off"
50
+ thinking: "" # empty = inherit from parent pi session
51
51
  # spawn_mode: "subprocess" # currently supported runtime mode
52
52
 
53
53
  reviewer: