taskplane 0.24.16 → 0.24.18

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.
@@ -1017,6 +1017,7 @@ export function toOrchestratorConfig(config: TaskplaneConfig): import("./types.t
1017
1017
  merge: {
1018
1018
  model: o.merge.model,
1019
1019
  tools: o.merge.tools,
1020
+ thinking: o.merge.thinking,
1020
1021
  verify: [...o.merge.verify],
1021
1022
  order: o.merge.order,
1022
1023
  timeout_minutes: o.merge.timeoutMinutes ?? 90,
@@ -302,6 +302,8 @@ export interface MergeConfig {
302
302
  model: string;
303
303
  /** Merge-agent tool allowlist */
304
304
  tools: string;
305
+ /** Merge-agent thinking mode (empty = inherit session thinking) */
306
+ thinking: string;
305
307
  /** Verification commands run after merge operations */
306
308
  verify: string[];
307
309
  /** Lane merge ordering policy */
@@ -602,6 +604,7 @@ export const DEFAULT_ORCHESTRATOR_SECTION: OrchestratorSection = {
602
604
  merge: {
603
605
  model: "",
604
606
  tools: "read,write,edit,bash,grep,find,ls",
607
+ thinking: "",
605
608
  verify: [],
606
609
  order: "fewest-files-first",
607
610
  timeoutMinutes: 90,
@@ -602,6 +602,7 @@ export async function spawnMergeAgentV2(
602
602
  systemPrompt,
603
603
  model: config.merge.model || undefined,
604
604
  tools: config.merge.tools || undefined,
605
+ thinking: config.merge.thinking || undefined,
605
606
  mailboxDir,
606
607
  eventsPath,
607
608
  exitSummaryPath,
@@ -127,8 +127,9 @@ export const SECTIONS: SectionDef[] = [
127
127
  {
128
128
  name: "Merge",
129
129
  fields: [
130
- { configPath: "orchestrator.merge.model", label: "Merge Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "mergeModel", description: "Merge-agent model (empty = inherit session)" },
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
133
  { configPath: "orchestrator.merge.order", label: "Merge Order", control: "toggle", layer: "L1", fieldType: "enum", values: ["fewest-files-first", "sequential"], description: "Lane merge ordering policy" },
133
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)" },
134
135
  ],
@@ -152,14 +153,14 @@ export const SECTIONS: SectionDef[] = [
152
153
  {
153
154
  name: "Supervisor",
154
155
  fields: [
155
- { configPath: "orchestrator.supervisor.model", label: "Supervisor Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "supervisorModel", description: "Supervisor model (empty = inherit session)" },
156
+ { configPath: "orchestrator.supervisor.model", label: "Supervisor Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "supervisorModel", description: "Supervisor model (inherit = use session model)" },
156
157
  { configPath: "orchestrator.supervisor.autonomy", label: "Autonomy Level", control: "toggle", layer: "L1", fieldType: "enum", values: ["interactive", "supervised", "autonomous"], description: "Recovery action confirmation behavior" },
157
158
  ],
158
159
  },
159
160
  {
160
161
  name: "Worker",
161
162
  fields: [
162
- { configPath: "taskRunner.worker.model", label: "Worker Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "workerModel", description: "Worker model (empty = inherit session)" },
163
+ { configPath: "taskRunner.worker.model", label: "Worker Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "workerModel", description: "Worker model (inherit = use session model)" },
163
164
  { configPath: "taskRunner.worker.tools", label: "Worker Tools", control: "input", layer: "L1", fieldType: "string", description: "Worker tool allowlist" },
164
165
  { configPath: "taskRunner.worker.thinking", label: "Worker Thinking", control: "input", layer: "L1", fieldType: "string", description: "Worker thinking mode" },
165
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." },
@@ -168,7 +169,7 @@ export const SECTIONS: SectionDef[] = [
168
169
  {
169
170
  name: "Reviewer",
170
171
  fields: [
171
- { configPath: "taskRunner.reviewer.model", label: "Reviewer Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "reviewerModel", description: "Reviewer model (empty = inherit session)" },
172
+ { configPath: "taskRunner.reviewer.model", label: "Reviewer Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "reviewerModel", description: "Reviewer model (inherit = use session model)" },
172
173
  { configPath: "taskRunner.reviewer.tools", label: "Reviewer Tools", control: "input", layer: "L1", fieldType: "string", description: "Reviewer tool allowlist" },
173
174
  { configPath: "taskRunner.reviewer.thinking", label: "Reviewer Thinking", control: "input", layer: "L1", fieldType: "string", description: "Reviewer thinking mode" },
174
175
  ],
@@ -929,6 +930,134 @@ function summarizeArray(arr: any[]): string {
929
930
  * @param configRoot - Workspace/repo root (from execCtx.workspaceRoot)
930
931
  * @param pointerConfigRoot - Optional pointer-resolved config root (workspace mode)
931
932
  */
933
+ // ── Model Picker (Sage-style provider → model selection) ────────────
934
+
935
+ /**
936
+ * Interactive two-level model picker: provider first, then model within provider.
937
+ * Returns the selected model string (e.g., "anthropic/claude-sonnet-4-20250514")
938
+ * or "" for inherit, or undefined if cancelled.
939
+ *
940
+ * Adapted from Sage's pickModel implementation.
941
+ */
942
+ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<string | undefined> {
943
+ const available = ctx.modelRegistry.getAvailable();
944
+ if (available.length === 0) {
945
+ ctx.ui.notify("No available models found in pi model registry", "warning");
946
+ // Fall back to manual input
947
+ const manual = await ctx.ui.input("Model (provider/model-id, or empty for inherit)", currentModel || "");
948
+ if (manual === null || manual === undefined) return undefined;
949
+ return manual;
950
+ }
951
+
952
+ const currentLower = (currentModel || "").trim().toLowerCase();
953
+ const providers = [...new Set(available.map((m: any) => m.provider))].sort();
954
+
955
+ while (true) {
956
+ // Level 1: Provider selection (with "inherit" as first option)
957
+ const providerOptions: string[] = [
958
+ "inherit (use current session model)",
959
+ ...providers.map((p: string) => {
960
+ const count = available.filter((m: any) => m.provider === p).length;
961
+ return `${p} (${count} models)`;
962
+ }),
963
+ ];
964
+
965
+ const providerChoice = await selectScrollable(ctx, "Choose model provider", providerOptions);
966
+ if (!providerChoice) return undefined; // Cancelled
967
+
968
+ if (providerChoice.startsWith("inherit")) {
969
+ return ""; // Empty string = inherit
970
+ }
971
+
972
+ // Extract provider name (strip " (N models)" suffix)
973
+ const provider = providerChoice.replace(/\s*\(\d+ models?\)$/, "");
974
+ const providerModels = available
975
+ .filter((m: any) => m.provider === provider)
976
+ .sort((a: any, b: any) => {
977
+ // Current model first, then alphabetical
978
+ const aComposite = `${a.provider}/${a.id}`.toLowerCase();
979
+ const bComposite = `${b.provider}/${b.id}`.toLowerCase();
980
+ if (aComposite === currentLower) return -1;
981
+ if (bComposite === currentLower) return 1;
982
+ return a.id.localeCompare(b.id);
983
+ });
984
+
985
+ // Level 2: Model selection within provider
986
+ const modelOptionMap = new Map<string, string>();
987
+ const modelOptions = ["← Back to providers"];
988
+
989
+ for (const model of providerModels) {
990
+ const composite = `${model.provider}/${model.id}`;
991
+ const isCurrent = composite.toLowerCase() === currentLower;
992
+ const label = `${model.id}${isCurrent ? " ✓ current" : ""}`;
993
+ modelOptions.push(label);
994
+ modelOptionMap.set(label, composite);
995
+ }
996
+
997
+ const modelChoice = await selectScrollable(ctx, `Choose model (${provider})`, modelOptions);
998
+ if (!modelChoice) continue; // Cancelled → back to providers
999
+ if (modelChoice === "← Back to providers") continue;
1000
+
1001
+ const resolved = modelOptionMap.get(modelChoice);
1002
+ if (resolved) return resolved;
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Scrollable select list for model/provider picking.
1008
+ * Uses pi's TUI custom widget API.
1009
+ */
1010
+ async function selectScrollable(
1011
+ ctx: ExtensionContext,
1012
+ title: string,
1013
+ options: string[],
1014
+ maxVisible = 12,
1015
+ ): Promise<string | undefined> {
1016
+ if (options.length === 0) return undefined;
1017
+
1018
+ const items: SelectItem[] = options.map((option, index) => ({
1019
+ value: String(index),
1020
+ label: option,
1021
+ }));
1022
+
1023
+ const selectedValue = await ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
1024
+ const container = new Container();
1025
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1026
+ container.addChild(new Text(theme.fg("accent", title), 1, 0));
1027
+ container.addChild(new Text("", 0, 0));
1028
+
1029
+ const selectList = new SelectList(items, Math.max(3, Math.min(maxVisible, items.length)), {
1030
+ selectedPrefix: (text: string) => theme.fg("accent", text),
1031
+ selectedText: (text: string) => theme.fg("accent", text),
1032
+ description: (text: string) => theme.fg("muted", text),
1033
+ scrollInfo: (text: string) => theme.fg("dim", text),
1034
+ noMatch: (text: string) => theme.fg("warning", text),
1035
+ });
1036
+
1037
+ selectList.onSelect = (item) => done(item.value);
1038
+ selectList.onCancel = () => done(undefined);
1039
+ container.addChild(selectList);
1040
+
1041
+ container.addChild(new Text("", 0, 0));
1042
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate • type to filter • enter select • esc back"), 1, 0));
1043
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1044
+
1045
+ return {
1046
+ render: (width: number) => container.render(width),
1047
+ invalidate: () => container.invalidate(),
1048
+ handleInput: (data: string) => {
1049
+ selectList.handleInput(data);
1050
+ tui.requestRender();
1051
+ },
1052
+ };
1053
+ });
1054
+
1055
+ if (selectedValue === undefined) return undefined;
1056
+ const selectedIndex = Number(selectedValue);
1057
+ if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length) return undefined;
1058
+ return options[selectedIndex];
1059
+ }
1060
+
932
1061
  export async function openSettingsTui(
933
1062
  ctx: ExtensionContext,
934
1063
  configRoot: string,
@@ -1120,26 +1249,36 @@ async function showSectionSettingsLoop(
1120
1249
 
1121
1250
  // Input fields: the submenu returned a sentinel — use ctx.ui.input() for actual editing
1122
1251
  if (result.rawValue === "__EDIT_REQUESTED__" && field.control === "input") {
1123
- const state = loadConfigState(configRoot, pointerConfigRoot);
1124
- const currentDisplay = getFieldDisplayValue(field, state.mergedConfig, state.prefs);
1125
- const currentClean = String(currentDisplay).replace(/\s+\((?:default|project|user)\)$/, "");
1126
- const placeholder = currentClean === "(not set)" || currentClean === "(inherit)" ? "" : currentClean;
1127
-
1128
- const newValue = await ctx.ui.input(
1129
- `${field.label}${field.description ? ` ${field.description}` : ""}`,
1130
- placeholder,
1131
- );
1132
-
1133
- if (newValue === null || newValue === undefined) continue; // Cancelled
1252
+ // Model fields: use interactive provider → model picker instead of free-text
1253
+ 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);
1258
+ if (selected === undefined) continue; // Cancelled
1259
+ result.rawValue = selected;
1260
+ } 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
+ const placeholder = currentClean === "(not set)" || currentClean === "(inherit)" ? "" : currentClean;
1265
+
1266
+ const newValue = await ctx.ui.input(
1267
+ `${field.label}${field.description ? ` — ${field.description}` : ""}`,
1268
+ placeholder,
1269
+ );
1270
+
1271
+ if (newValue === null || newValue === undefined) continue; // Cancelled
1272
+
1273
+ // Validate
1274
+ const validation = validateFieldInput(field, newValue);
1275
+ if (!validation.valid) {
1276
+ ctx.ui.notify(`❌ Invalid value: ${validation.error}`, "error");
1277
+ continue;
1278
+ }
1134
1279
 
1135
- // Validate
1136
- const validation = validateFieldInput(field, newValue);
1137
- if (!validation.valid) {
1138
- ctx.ui.notify(`❌ Invalid value: ${validation.error}`, "error");
1139
- continue;
1280
+ result.rawValue = newValue;
1140
1281
  }
1141
-
1142
- result.rawValue = newValue;
1143
1282
  }
1144
1283
 
1145
1284
  const typedValue = coerceValueForWrite(field, result.rawValue);
@@ -37,6 +37,8 @@ export interface OrchestratorConfig {
37
37
  merge: {
38
38
  model: string;
39
39
  tools: string;
40
+ /** Merge-agent thinking mode (empty = inherit session thinking) */
41
+ thinking: string;
40
42
  verify: string[];
41
43
  order: "fewest-files-first" | "sequential";
42
44
  /** Merge agent timeout in minutes. Default: 10. Increase for large batches. */
@@ -280,6 +282,7 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
280
282
  merge: {
281
283
  model: "",
282
284
  tools: "read,write,edit,bash,grep,find,ls",
285
+ thinking: "",
283
286
  verify: [],
284
287
  order: "fewest-files-first",
285
288
  timeout_minutes: 90,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.24.16",
3
+ "version": "0.24.18",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",