taskplane 0.24.16 → 0.24.17

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