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.
- package/extensions/taskplane/settings-tui.ts +160 -22
- package/package.json +1 -1
|
@@ -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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
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);
|