@dieulc/workflow 0.8.1 → 0.9.0
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/index.ts +254 -171
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Plan↔Build mode with an independent **Review Mode** reviewer, questionnaire, s
|
|
|
11
11
|
- **Todos** — plan-faithful todo list (global step refs + statuses + phase groups) that persists across Plan/Build/Default modes. `workflow_todo` (`list/update/done/pending/add/toggle/sync/clear`) with industry-style wholesale `update` (send the COMPLETE list; omitted items are removed) plus idempotent `done`/`pending`, lenient `[DONE:<ref>]` markers, throttled Claude-Code-style reminders, a tool-result footer, and a stale ⚠ notice — but **no forced turns and no heuristic auto-completion** (reference: Claude Code / OpenCode / Codex practice).
|
|
12
12
|
- **Rewind** — shadow bare-repo checkpoints per cwd (`agent/checkpoints/checkpoint-<slug>.git`), `isDirty` + `createSafetySnapshot` + `restoreCode`
|
|
13
13
|
- **Plannotator bridge** — `plannotator:request/review-result/plan-approved` events, inline markdown handoff (`ScrollView` → inline scrollback), `getUtcDatePrefix` / `normalizePlanPath` for `.pi/plans/<UTC-date>-<slug>.md`
|
|
14
|
-
- **Model roles** — Planner/Explorer/Builder roles with one model + thinking level each (`roles.ts`, `~/.pi/agent/roles.json` v2). `/plan` switches to the planner model, `/build` to the builder model, `explore` subagents use the explorer model. `/role` opens a tabbed picker (scoped models first, `Ctrl+O` for all).
|
|
14
|
+
- **Model roles** — Planner/Explorer/Builder roles with one model + thinking level each (`roles.ts`, `~/.pi/agent/roles.json` v2). `/plan` switches to the planner model, `/build` to the builder model, `explore` subagents use the explorer model. `/role` opens a tabbed picker (scoped models first, `Ctrl+O` for all). The thinking list follows the selected model's `thinkingLevelMap`; unsupported stored levels are clamped on save/activation with a warning.
|
|
15
15
|
- **Write permission** — a user-controlled `allow`/`ask`/`never` permission for file writes in **Default** mode, the mode that is otherwise ungated. `/write-policy` sets it; see [Write permission](#write-permission) below.
|
|
16
16
|
- **Model profiles** — named combinations of role models (`profiles.ts`, `~/.pi/agent/profiles.json` v1). Create a profile per provider or per budget, switch with `/profile` — activation is session-scoped, so two concurrent sessions can run different profiles. With no profile active the roles.json flow applies unchanged. See [Model Profiles](#model-profiles).
|
|
17
17
|
|
package/index.ts
CHANGED
|
@@ -167,12 +167,21 @@ import {
|
|
|
167
167
|
roleIcon,
|
|
168
168
|
BUILT_IN_ROLES,
|
|
169
169
|
REVIEW_ROLE_NAME,
|
|
170
|
-
VALID_THINKING_LEVELS,
|
|
171
170
|
type RoleConfig,
|
|
172
171
|
type Role,
|
|
173
172
|
type RoleModel,
|
|
174
173
|
type ThinkingLevel,
|
|
175
174
|
} from "./roles.ts";
|
|
175
|
+
import {
|
|
176
|
+
clampThinkingForModel,
|
|
177
|
+
thinkingLevelsForModel,
|
|
178
|
+
thinkingLevelsForPair,
|
|
179
|
+
} from "./thinking-levels.ts";
|
|
180
|
+
import {
|
|
181
|
+
buildWorkflowStatusSegments,
|
|
182
|
+
setWorkflowStatus,
|
|
183
|
+
WORKFLOW_STATUS_KEYS,
|
|
184
|
+
} from "./workflow-status.ts";
|
|
176
185
|
import {
|
|
177
186
|
defaultProfileConfig,
|
|
178
187
|
loadProfilesDetailed,
|
|
@@ -1044,9 +1053,7 @@ export default function workflowExtension(
|
|
|
1044
1053
|
if (found) {
|
|
1045
1054
|
await withPersistSuppressed(async () => {
|
|
1046
1055
|
await (pi as any).setModel?.(found);
|
|
1047
|
-
|
|
1048
|
-
(pi as any).setThinkingLevel?.(m.thinking);
|
|
1049
|
-
}
|
|
1056
|
+
applyThinkingForModel(ctx, found, m.thinking, "Default mode");
|
|
1050
1057
|
});
|
|
1051
1058
|
if (!opts?.quiet) {
|
|
1052
1059
|
ctx.ui.notify(
|
|
@@ -1223,8 +1230,6 @@ export default function workflowExtension(
|
|
|
1223
1230
|
// Profile prefix (⚡ name) when one is active, then the role label.
|
|
1224
1231
|
const profileLabel = activeProfileLabel();
|
|
1225
1232
|
const roleLabel = activeRoleLabel();
|
|
1226
|
-
const statusParts = [profileLabel, roleLabel].filter(Boolean);
|
|
1227
|
-
const suffix = statusParts.length ? ` | ${statusParts.join(" | ")}` : "";
|
|
1228
1233
|
// Commit-policy surfacing (workflow.commitPolicy): always visible so the
|
|
1229
1234
|
// user can see at a glance what the agent may do to git history. Read
|
|
1230
1235
|
// live so /commit-policy changes show up without a reload.
|
|
@@ -1237,7 +1242,6 @@ export default function workflowExtension(
|
|
|
1237
1242
|
} catch {
|
|
1238
1243
|
commitMode = "never";
|
|
1239
1244
|
}
|
|
1240
|
-
const policy = ` · commits:${commitMode}`;
|
|
1241
1245
|
// Write-policy surfacing (workflow.writePolicy): the active file-write
|
|
1242
1246
|
// permission, visible at a glance like the commit policy. Read live so a
|
|
1243
1247
|
// /write-policy change shows up without a reload. It only applies in
|
|
@@ -1251,67 +1255,38 @@ export default function workflowExtension(
|
|
|
1251
1255
|
} catch {
|
|
1252
1256
|
writeMode = "allow";
|
|
1253
1257
|
}
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if (workflowMode === "build") {
|
|
1287
|
-
const label =
|
|
1288
|
-
total > 0
|
|
1289
|
-
? `▶ build ${done}/${total}${staleMarker}${suffix}${policy}${writePolicy}${reviewSegment}`
|
|
1290
|
-
: `▶ build${suffix}${policy}${writePolicy}${reviewSegment}`;
|
|
1291
|
-
ctx.ui.setStatus("workflow", ctx.ui.theme.fg("accent", label));
|
|
1292
|
-
} else if (workflowMode === "plan") {
|
|
1293
|
-
const base = plannotatorActive
|
|
1294
|
-
? "⏸ plan — reviewing in browser"
|
|
1295
|
-
: "⏸ plan";
|
|
1296
|
-
const label =
|
|
1297
|
-
total > 0
|
|
1298
|
-
? `${base} ${done}/${total}${staleMarker}${suffix}${policy}${writePolicy}${reviewSegment}`
|
|
1299
|
-
: `${base}${suffix}${policy}${writePolicy}${reviewSegment}`;
|
|
1300
|
-
ctx.ui.setStatus("workflow", ctx.ui.theme.fg("warning", label));
|
|
1301
|
-
} else if (total > 0) {
|
|
1302
|
-
ctx.ui.setStatus(
|
|
1303
|
-
"workflow",
|
|
1304
|
-
ctx.ui.theme.fg(
|
|
1305
|
-
"muted",
|
|
1306
|
-
`◦ ${done}/${total}${staleMarker}${suffix}${policy}${writePolicy}${reviewSegment}`,
|
|
1307
|
-
),
|
|
1308
|
-
);
|
|
1309
|
-
} else {
|
|
1310
|
-
ctx.ui.setStatus(
|
|
1311
|
-
"workflow",
|
|
1312
|
-
ctx.ui.theme.fg("muted", `◦ commits:${commitMode}${writePolicy}${reviewSegment}`),
|
|
1313
|
-
);
|
|
1314
|
-
}
|
|
1258
|
+
const segments = buildWorkflowStatusSegments({
|
|
1259
|
+
mode: workflowMode,
|
|
1260
|
+
plannotatorActive,
|
|
1261
|
+
done: todoItems.filter((t) => isDone(t)).length,
|
|
1262
|
+
total: todoItems.length,
|
|
1263
|
+
stale: todoStale,
|
|
1264
|
+
profileLabel,
|
|
1265
|
+
roleLabel,
|
|
1266
|
+
commitMode,
|
|
1267
|
+
writeMode: workflowMode === null ? writeMode : null,
|
|
1268
|
+
});
|
|
1269
|
+
const modeTone =
|
|
1270
|
+
workflowMode === "build"
|
|
1271
|
+
? "accent"
|
|
1272
|
+
: workflowMode === "plan"
|
|
1273
|
+
? "warning"
|
|
1274
|
+
: "muted";
|
|
1275
|
+
setWorkflowStatus(
|
|
1276
|
+
ctx.ui,
|
|
1277
|
+
WORKFLOW_STATUS_KEYS.mode,
|
|
1278
|
+
ctx.ui.theme.fg(modeTone, segments.mode),
|
|
1279
|
+
);
|
|
1280
|
+
setWorkflowStatus(
|
|
1281
|
+
ctx.ui,
|
|
1282
|
+
WORKFLOW_STATUS_KEYS.profile,
|
|
1283
|
+
segments.profile ? ctx.ui.theme.fg("muted", segments.profile) : "",
|
|
1284
|
+
);
|
|
1285
|
+
setWorkflowStatus(
|
|
1286
|
+
ctx.ui,
|
|
1287
|
+
WORKFLOW_STATUS_KEYS.policy,
|
|
1288
|
+
ctx.ui.theme.fg("dim", segments.policy),
|
|
1289
|
+
);
|
|
1315
1290
|
if (todoItems.length > 0) {
|
|
1316
1291
|
ctx.ui.setWidget("workflow-todos", (_tui, theme) => {
|
|
1317
1292
|
return {
|
|
@@ -1619,8 +1594,6 @@ export default function workflowExtension(
|
|
|
1619
1594
|
reviewTruncationNoted = false;
|
|
1620
1595
|
}
|
|
1621
1596
|
|
|
1622
|
-
const REVIEW_STATUS_WIDGET = "workflow-review-status";
|
|
1623
|
-
|
|
1624
1597
|
function reviewAgentDir(): string {
|
|
1625
1598
|
try {
|
|
1626
1599
|
return getAgentDir();
|
|
@@ -1844,26 +1817,23 @@ export default function workflowExtension(
|
|
|
1844
1817
|
roundActive,
|
|
1845
1818
|
paused,
|
|
1846
1819
|
});
|
|
1847
|
-
// The
|
|
1820
|
+
// The review status is a LIVE indicator, not a result holder: the last
|
|
1848
1821
|
// round's state stays for `/review-status` and the REVIEW SUMMARY band,
|
|
1849
|
-
// but the
|
|
1822
|
+
// but the footer segment must disappear as soon as the round ends. While
|
|
1850
1823
|
// paused it shows the paused form (never the terminal "aborted" summary).
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1824
|
+
const reviewText = visible ? reviewFooterSegment(reviewState, paused) : "";
|
|
1825
|
+
let lockText = "";
|
|
1826
|
+
try {
|
|
1827
|
+
lockText = reviewModeLockFooter(reviewModeLockKindNow());
|
|
1828
|
+
} catch (_e) {
|
|
1829
|
+
void _e;
|
|
1855
1830
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
),
|
|
1863
|
-
],
|
|
1864
|
-
invalidate: () => {},
|
|
1865
|
-
dispose: () => {},
|
|
1866
|
-
}));
|
|
1831
|
+
const text = [reviewText, lockText].filter(Boolean).join(" · ");
|
|
1832
|
+
setWorkflowStatus(
|
|
1833
|
+
ctx.ui,
|
|
1834
|
+
WORKFLOW_STATUS_KEYS.review,
|
|
1835
|
+
text ? ctx.ui.theme.fg("muted", text) : "",
|
|
1836
|
+
);
|
|
1867
1837
|
} catch (_e) {
|
|
1868
1838
|
void _e;
|
|
1869
1839
|
}
|
|
@@ -2630,7 +2600,7 @@ export default function workflowExtension(
|
|
|
2630
2600
|
resetReviewStream();
|
|
2631
2601
|
detachReviewEscListenerNow();
|
|
2632
2602
|
try {
|
|
2633
|
-
currentSessionCtx?.ui.
|
|
2603
|
+
currentSessionCtx?.ui.setStatus(WORKFLOW_STATUS_KEYS.review, undefined);
|
|
2634
2604
|
} catch (_e) {
|
|
2635
2605
|
void _e;
|
|
2636
2606
|
}
|
|
@@ -3497,9 +3467,12 @@ export default function workflowExtension(
|
|
|
3497
3467
|
) {
|
|
3498
3468
|
await withPersistSuppressed(async () => {
|
|
3499
3469
|
try {
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3470
|
+
applyThinkingForModel(
|
|
3471
|
+
ctx,
|
|
3472
|
+
current,
|
|
3473
|
+
m.thinking,
|
|
3474
|
+
`Workflow ${workflowMode ?? "mode"}`,
|
|
3475
|
+
);
|
|
3503
3476
|
} catch (_e) {
|
|
3504
3477
|
void _e;
|
|
3505
3478
|
}
|
|
@@ -3534,9 +3507,7 @@ export default function workflowExtension(
|
|
|
3534
3507
|
}
|
|
3535
3508
|
await withPersistSuppressed(async () => {
|
|
3536
3509
|
try {
|
|
3537
|
-
|
|
3538
|
-
(pi as any).setThinkingLevel?.(m.thinking);
|
|
3539
|
-
}
|
|
3510
|
+
applyThinkingForModel(ctx, found, m.thinking, `Workflow ${workflowMode ?? "mode"}`);
|
|
3540
3511
|
} catch (_e) {
|
|
3541
3512
|
void _e;
|
|
3542
3513
|
}
|
|
@@ -3598,12 +3569,12 @@ export default function workflowExtension(
|
|
|
3598
3569
|
current.id === defaultModel.id
|
|
3599
3570
|
) {
|
|
3600
3571
|
try {
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3572
|
+
applyThinkingForModel(
|
|
3573
|
+
ctx,
|
|
3574
|
+
current,
|
|
3575
|
+
defaultModel.thinking as ThinkingLevel | undefined,
|
|
3576
|
+
"Default mode",
|
|
3577
|
+
);
|
|
3607
3578
|
} catch (_e) {
|
|
3608
3579
|
void _e;
|
|
3609
3580
|
}
|
|
@@ -3619,12 +3590,12 @@ export default function workflowExtension(
|
|
|
3619
3590
|
if (!found) return; // registry no longer has it — keep current model
|
|
3620
3591
|
try {
|
|
3621
3592
|
await (pi as any).setModel?.(found);
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3593
|
+
applyThinkingForModel(
|
|
3594
|
+
ctx,
|
|
3595
|
+
found,
|
|
3596
|
+
defaultModel.thinking as ThinkingLevel | undefined,
|
|
3597
|
+
"Default mode",
|
|
3598
|
+
);
|
|
3628
3599
|
if (!opts?.quiet) {
|
|
3629
3600
|
ctx.ui.notify(
|
|
3630
3601
|
`Default mode: model → ${defaultModel.provider}/${defaultModel.id}${defaultModel.thinking ? ` (${defaultModel.thinking})` : ""}`,
|
|
@@ -3786,6 +3757,31 @@ export default function workflowExtension(
|
|
|
3786
3757
|
}
|
|
3787
3758
|
}
|
|
3788
3759
|
|
|
3760
|
+
/**
|
|
3761
|
+
* Apply a role/default thinking level, clamped to the model Pi will actually
|
|
3762
|
+
* use. The role config is shared with terminal Pi and may predate the model's
|
|
3763
|
+
* current `thinkingLevelMap`; clamping here keeps the live session valid.
|
|
3764
|
+
*
|
|
3765
|
+
* The clamp is **silent**: the host already shows the live thinking level, and
|
|
3766
|
+
* notifying on every mode switch buried the transcript and toasts in the same
|
|
3767
|
+
* advisory (2026-10-31 plan). Explicit `/role set` validation still warns
|
|
3768
|
+
* because that is a direct user edit.
|
|
3769
|
+
*/
|
|
3770
|
+
function applyThinkingForModel(
|
|
3771
|
+
_ctx: ExtensionContext | null,
|
|
3772
|
+
model: any,
|
|
3773
|
+
level: ThinkingLevel | undefined,
|
|
3774
|
+
_label: string,
|
|
3775
|
+
): void {
|
|
3776
|
+
if (!level || !isValidThinkingLevel(level)) return;
|
|
3777
|
+
const clamped = clampThinkingForModel(model, level);
|
|
3778
|
+
try {
|
|
3779
|
+
(pi as any).setThinkingLevel?.(clamped);
|
|
3780
|
+
} catch (_e) {
|
|
3781
|
+
void _e;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3789
3785
|
/**
|
|
3790
3786
|
* Read the persisted default from disk. Pure: it never mutates the in-memory
|
|
3791
3787
|
* `defaultModel`, because precedence is resolved in one place at session
|
|
@@ -5037,21 +5033,28 @@ export default function workflowExtension(
|
|
|
5037
5033
|
}
|
|
5038
5034
|
thinking = thinkingRaw.toLowerCase() as any;
|
|
5039
5035
|
}
|
|
5040
|
-
//
|
|
5036
|
+
// Validate against the registry. A missing model is warn-only (it may be
|
|
5037
|
+
// added later), but a known model clamps the stored level to its map.
|
|
5038
|
+
let found: any;
|
|
5041
5039
|
try {
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5040
|
+
found = (ctx as any)?.modelRegistry?.find?.(provider, id);
|
|
5041
|
+
} catch (_e) {
|
|
5042
|
+
void _e;
|
|
5043
|
+
}
|
|
5044
|
+
if (!found) {
|
|
5045
|
+
ctx.ui.notify(
|
|
5046
|
+
`Warning: ${provider}/${id} not in model registry — saved anyway.`,
|
|
5047
|
+
"warning",
|
|
5048
|
+
);
|
|
5049
|
+
} else {
|
|
5050
|
+
const pair = thinkingLevelsForPair(found, thinking);
|
|
5051
|
+
if (pair.level !== thinking) {
|
|
5048
5052
|
ctx.ui.notify(
|
|
5049
|
-
`
|
|
5053
|
+
`Thinking ${thinking} is not supported by ${provider}/${id}; using ${pair.level}.`,
|
|
5050
5054
|
"warning",
|
|
5051
5055
|
);
|
|
5052
5056
|
}
|
|
5053
|
-
|
|
5054
|
-
void _e;
|
|
5057
|
+
thinking = pair.level;
|
|
5055
5058
|
}
|
|
5056
5059
|
role.model = { provider, id, thinking };
|
|
5057
5060
|
const saved = saveAndSync(
|
|
@@ -5920,12 +5923,7 @@ export default function workflowExtension(
|
|
|
5920
5923
|
const buildModelItems = (): FilterableItem[] =>
|
|
5921
5924
|
modelPickerItems(showAll ? catalog.all : catalog.scoped);
|
|
5922
5925
|
const modelLists = new Map<string, FilterableList>();
|
|
5923
|
-
const
|
|
5924
|
-
items: VALID_THINKING_LEVELS.map((t) => ({ value: t, label: t })),
|
|
5925
|
-
maxVisible: Math.min(VALID_THINKING_LEVELS.length, 8),
|
|
5926
|
-
theme: themeList,
|
|
5927
|
-
noMatchText: "No matching thinking levels",
|
|
5928
|
-
});
|
|
5926
|
+
const thinkingLists = new Map<string, FilterableList>();
|
|
5929
5927
|
const getModelList = (roleName: string): FilterableList => {
|
|
5930
5928
|
let list = modelLists.get(roleName);
|
|
5931
5929
|
if (!list) {
|
|
@@ -5944,6 +5942,26 @@ export default function workflowExtension(
|
|
|
5944
5942
|
}
|
|
5945
5943
|
return list;
|
|
5946
5944
|
};
|
|
5945
|
+
/** One list per model: the supported set differs per role/default tab. */
|
|
5946
|
+
const getThinkingList = (roleName: string): FilterableList => {
|
|
5947
|
+
let list = thinkingLists.get(roleName);
|
|
5948
|
+
if (!list) {
|
|
5949
|
+
const st = staged.get(roleName)!;
|
|
5950
|
+
const levels = thinkingLevelsForModel(
|
|
5951
|
+
registryModel(ctx, st.provider, st.id),
|
|
5952
|
+
);
|
|
5953
|
+
const idx = levels.indexOf(st.thinking);
|
|
5954
|
+
list = new FilterableList({
|
|
5955
|
+
items: levels.map((t) => ({ value: t, label: t })),
|
|
5956
|
+
maxVisible: Math.min(levels.length, 8),
|
|
5957
|
+
theme: themeList,
|
|
5958
|
+
initialIndex: Math.max(0, idx),
|
|
5959
|
+
noMatchText: "No matching thinking levels",
|
|
5960
|
+
});
|
|
5961
|
+
thinkingLists.set(roleName, list);
|
|
5962
|
+
}
|
|
5963
|
+
return list;
|
|
5964
|
+
};
|
|
5947
5965
|
// Ctrl+O re-scopes the rows: keep each tab's query and re-filter instead
|
|
5948
5966
|
// of rebuilding the lists from scratch.
|
|
5949
5967
|
const rebuildModelLists = () => {
|
|
@@ -5952,11 +5970,14 @@ export default function workflowExtension(
|
|
|
5952
5970
|
};
|
|
5953
5971
|
const syncThinkingCursor = (roleName: string) => {
|
|
5954
5972
|
const st = staged.get(roleName)!;
|
|
5955
|
-
|
|
5956
|
-
//
|
|
5957
|
-
|
|
5958
|
-
const
|
|
5959
|
-
|
|
5973
|
+
const list = getThinkingList(roleName);
|
|
5974
|
+
// Clear first: the list may still be filtered from a previous visit.
|
|
5975
|
+
list.clearFilter();
|
|
5976
|
+
const levels = thinkingLevelsForModel(
|
|
5977
|
+
registryModel(ctx, st.provider, st.id),
|
|
5978
|
+
);
|
|
5979
|
+
const idx = levels.indexOf(st.thinking);
|
|
5980
|
+
list.setSelectedIndex(idx >= 0 ? idx : 0);
|
|
5960
5981
|
};
|
|
5961
5982
|
const commitModelSelection = (roleName: string) => {
|
|
5962
5983
|
const sel = getModelList(roleName).getSelectedItem();
|
|
@@ -5964,11 +5985,23 @@ export default function workflowExtension(
|
|
|
5964
5985
|
const { provider, id } = parseModelRef(sel.value);
|
|
5965
5986
|
if (!provider || !id) return false;
|
|
5966
5987
|
const st = staged.get(roleName)!;
|
|
5967
|
-
staged
|
|
5988
|
+
// A new model can drop the staged level (DeepSeek has no `medium`).
|
|
5989
|
+
const pair = thinkingLevelsForPair(
|
|
5990
|
+
registryModel(ctx, provider, id),
|
|
5991
|
+
st.thinking,
|
|
5992
|
+
);
|
|
5993
|
+
staged.set(roleName, {
|
|
5994
|
+
...st,
|
|
5995
|
+
provider,
|
|
5996
|
+
id,
|
|
5997
|
+
thinking: pair.level,
|
|
5998
|
+
});
|
|
5999
|
+
// The old list belongs to the old model; rebuild on the next visit.
|
|
6000
|
+
thinkingLists.delete(roleName);
|
|
5968
6001
|
return true;
|
|
5969
6002
|
};
|
|
5970
6003
|
const commitThinkingSelection = (roleName: string) => {
|
|
5971
|
-
const sel =
|
|
6004
|
+
const sel = getThinkingList(roleName).getSelectedItem();
|
|
5972
6005
|
if (!sel || !isValidThinkingLevel(sel.value)) return false;
|
|
5973
6006
|
const st = staged.get(roleName)!;
|
|
5974
6007
|
staged.set(roleName, { ...st, thinking: sel.value });
|
|
@@ -6026,7 +6059,7 @@ export default function workflowExtension(
|
|
|
6026
6059
|
// (thinking → model) or cancels the editor.
|
|
6027
6060
|
if (matchesKey(data, Key.escape)) {
|
|
6028
6061
|
const activeList =
|
|
6029
|
-
phase === "model" ? getModelList(roleName) :
|
|
6062
|
+
phase === "model" ? getModelList(roleName) : getThinkingList(roleName);
|
|
6030
6063
|
if (activeList.handleInput(data)) {
|
|
6031
6064
|
cached = undefined;
|
|
6032
6065
|
tui.requestRender();
|
|
@@ -6055,7 +6088,7 @@ export default function workflowExtension(
|
|
|
6055
6088
|
return;
|
|
6056
6089
|
}
|
|
6057
6090
|
const activeList =
|
|
6058
|
-
phase === "model" ? getModelList(roleName) :
|
|
6091
|
+
phase === "model" ? getModelList(roleName) : getThinkingList(roleName);
|
|
6059
6092
|
activeList.handleInput(data);
|
|
6060
6093
|
cached = undefined;
|
|
6061
6094
|
tui.requestRender();
|
|
@@ -6212,9 +6245,10 @@ export default function workflowExtension(
|
|
|
6212
6245
|
lines.push(
|
|
6213
6246
|
...wrap(` ${theme.fg("accent", "Thinking level:")}`),
|
|
6214
6247
|
);
|
|
6215
|
-
|
|
6248
|
+
const list = getThinkingList(roleName);
|
|
6249
|
+
for (const l of list.renderQuery(Math.max(1, W - 2)))
|
|
6216
6250
|
lines.push(` ${l}`);
|
|
6217
|
-
for (const l of
|
|
6251
|
+
for (const l of list.renderList(Math.max(1, W - 2)))
|
|
6218
6252
|
lines.push(` ${l}`);
|
|
6219
6253
|
}
|
|
6220
6254
|
}
|
|
@@ -6250,21 +6284,30 @@ export default function workflowExtension(
|
|
|
6250
6284
|
);
|
|
6251
6285
|
return "cancelled";
|
|
6252
6286
|
}
|
|
6253
|
-
// Validate against registry (warn-only, matching /role set).
|
|
6287
|
+
// Validate against registry (warn-only, matching /role set). A known model
|
|
6288
|
+
// also clamps its staged thinking level so this save cannot write a pair
|
|
6289
|
+
// the model does not support.
|
|
6254
6290
|
try {
|
|
6255
6291
|
const reg = (ctx as any)?.modelRegistry;
|
|
6256
6292
|
if (reg && typeof reg.find === "function") {
|
|
6257
6293
|
for (const [roleName, st] of saved.staged) {
|
|
6258
|
-
if (
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
typeof reg.find === "function" &&
|
|
6262
|
-
!reg.find(st.provider, st.id)
|
|
6263
|
-
)
|
|
6294
|
+
if (!st.provider || !st.id) continue;
|
|
6295
|
+
const model = reg.find(st.provider, st.id);
|
|
6296
|
+
if (!model) {
|
|
6264
6297
|
ctx.ui.notify(
|
|
6265
6298
|
`Warning: ${st.provider}/${st.id} (${roleName}) not in model registry — saved anyway.`,
|
|
6266
6299
|
"warning",
|
|
6267
6300
|
);
|
|
6301
|
+
continue;
|
|
6302
|
+
}
|
|
6303
|
+
const pair = thinkingLevelsForPair(model, st.thinking);
|
|
6304
|
+
if (pair.level !== st.thinking) {
|
|
6305
|
+
ctx.ui.notify(
|
|
6306
|
+
`Thinking ${st.thinking} → ${pair.level} for ${roleName} (${st.provider}/${st.id} does not support ${st.thinking}).`,
|
|
6307
|
+
"warning",
|
|
6308
|
+
);
|
|
6309
|
+
st.thinking = pair.level;
|
|
6310
|
+
}
|
|
6268
6311
|
}
|
|
6269
6312
|
}
|
|
6270
6313
|
} catch (_e) {
|
|
@@ -6798,15 +6841,7 @@ export default function workflowExtension(
|
|
|
6798
6841
|
const buildModelItems = (): FilterableItem[] =>
|
|
6799
6842
|
modelPickerItems(showAll ? catalog.all : catalog.scoped);
|
|
6800
6843
|
const modelLists = new Map<string, FilterableList>();
|
|
6801
|
-
const
|
|
6802
|
-
items: VALID_THINKING_LEVELS.map((t) => ({
|
|
6803
|
-
value: t,
|
|
6804
|
-
label: t,
|
|
6805
|
-
})),
|
|
6806
|
-
maxVisible: Math.min(VALID_THINKING_LEVELS.length, 8),
|
|
6807
|
-
theme: listTheme,
|
|
6808
|
-
noMatchText: "No matching thinking levels",
|
|
6809
|
-
});
|
|
6844
|
+
const thinkingLists = new Map<string, FilterableList>();
|
|
6810
6845
|
const getModelList = (role: Role): FilterableList => {
|
|
6811
6846
|
let list = modelLists.get(role.name);
|
|
6812
6847
|
if (!list) {
|
|
@@ -6825,6 +6860,26 @@ export default function workflowExtension(
|
|
|
6825
6860
|
}
|
|
6826
6861
|
return list;
|
|
6827
6862
|
};
|
|
6863
|
+
/** One list per role: the supported set follows that role's model. */
|
|
6864
|
+
const getThinkingList = (role: Role): FilterableList => {
|
|
6865
|
+
let list = thinkingLists.get(role.name);
|
|
6866
|
+
if (!list) {
|
|
6867
|
+
const st = staged.get(role.name)!;
|
|
6868
|
+
const levels = thinkingLevelsForModel(
|
|
6869
|
+
registryModel(ctx, st.provider, st.id),
|
|
6870
|
+
);
|
|
6871
|
+
const idx = levels.indexOf(st.thinking);
|
|
6872
|
+
list = new FilterableList({
|
|
6873
|
+
items: levels.map((t) => ({ value: t, label: t })),
|
|
6874
|
+
maxVisible: Math.min(levels.length, 8),
|
|
6875
|
+
theme: listTheme,
|
|
6876
|
+
initialIndex: Math.max(0, idx),
|
|
6877
|
+
noMatchText: "No matching thinking levels",
|
|
6878
|
+
});
|
|
6879
|
+
thinkingLists.set(role.name, list);
|
|
6880
|
+
}
|
|
6881
|
+
return list;
|
|
6882
|
+
};
|
|
6828
6883
|
// Ctrl+O re-scopes the rows: keep each tab's query and re-filter
|
|
6829
6884
|
// instead of rebuilding the lists from scratch.
|
|
6830
6885
|
const rebuildModelLists = () => {
|
|
@@ -6833,11 +6888,14 @@ export default function workflowExtension(
|
|
|
6833
6888
|
};
|
|
6834
6889
|
const syncThinkingCursor = (role: Role) => {
|
|
6835
6890
|
const st = staged.get(role.name)!;
|
|
6836
|
-
|
|
6837
|
-
//
|
|
6838
|
-
|
|
6839
|
-
const
|
|
6840
|
-
|
|
6891
|
+
const list = getThinkingList(role);
|
|
6892
|
+
// Clear first: the list may still be filtered from a previous visit.
|
|
6893
|
+
list.clearFilter();
|
|
6894
|
+
const levels = thinkingLevelsForModel(
|
|
6895
|
+
registryModel(ctx, st.provider, st.id),
|
|
6896
|
+
);
|
|
6897
|
+
const idx = levels.indexOf(st.thinking);
|
|
6898
|
+
list.setSelectedIndex(idx >= 0 ? idx : 0);
|
|
6841
6899
|
};
|
|
6842
6900
|
const isSubmitTab = () => currentTab === tabs.length;
|
|
6843
6901
|
|
|
@@ -6847,13 +6905,22 @@ export default function workflowExtension(
|
|
|
6847
6905
|
const { provider, id } = parseModelRef(sel.value);
|
|
6848
6906
|
if (!provider || !id) return false;
|
|
6849
6907
|
const st = staged.get(role.name)!;
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6908
|
+
const pair = thinkingLevelsForPair(
|
|
6909
|
+
registryModel(ctx, provider, id),
|
|
6910
|
+
st.thinking,
|
|
6911
|
+
);
|
|
6912
|
+
staged.set(role.name, {
|
|
6913
|
+
...st,
|
|
6914
|
+
provider,
|
|
6915
|
+
id,
|
|
6916
|
+
thinking: pair.level,
|
|
6917
|
+
});
|
|
6918
|
+
// The old list belongs to the old model; rebuild on the next visit.
|
|
6919
|
+
thinkingLists.delete(role.name);
|
|
6853
6920
|
return true;
|
|
6854
6921
|
};
|
|
6855
6922
|
const commitThinkingSelection = (role: Role) => {
|
|
6856
|
-
const sel =
|
|
6923
|
+
const sel = getThinkingList(role).getSelectedItem();
|
|
6857
6924
|
if (!sel || !isValidThinkingLevel(sel.value)) return false;
|
|
6858
6925
|
const st = staged.get(role.name)!;
|
|
6859
6926
|
staged.set(role.name, { ...st, thinking: sel.value });
|
|
@@ -6910,7 +6977,7 @@ export default function workflowExtension(
|
|
|
6910
6977
|
// (thinking → model) or cancels the picker.
|
|
6911
6978
|
if (matchesKey(data, Key.escape)) {
|
|
6912
6979
|
const activeList =
|
|
6913
|
-
phase === "model" ? getModelList(role) :
|
|
6980
|
+
phase === "model" ? getModelList(role) : getThinkingList(role);
|
|
6914
6981
|
if (activeList.handleInput(data)) {
|
|
6915
6982
|
cached = undefined;
|
|
6916
6983
|
tui.requestRender();
|
|
@@ -6939,7 +7006,7 @@ export default function workflowExtension(
|
|
|
6939
7006
|
return;
|
|
6940
7007
|
}
|
|
6941
7008
|
const activeList =
|
|
6942
|
-
phase === "model" ? getModelList(role) :
|
|
7009
|
+
phase === "model" ? getModelList(role) : getThinkingList(role);
|
|
6943
7010
|
activeList.handleInput(data);
|
|
6944
7011
|
cached = undefined;
|
|
6945
7012
|
tui.requestRender();
|
|
@@ -7082,9 +7149,10 @@ export default function workflowExtension(
|
|
|
7082
7149
|
lines.push(
|
|
7083
7150
|
...wrap(` ${theme.fg("accent", "Thinking level:")}`),
|
|
7084
7151
|
);
|
|
7085
|
-
|
|
7152
|
+
const list = getThinkingList(role);
|
|
7153
|
+
for (const l of list.renderQuery(Math.max(1, W - 2)))
|
|
7086
7154
|
lines.push(` ${l}`);
|
|
7087
|
-
for (const l of
|
|
7155
|
+
for (const l of list.renderList(Math.max(1, W - 2)))
|
|
7088
7156
|
lines.push(` ${l}`);
|
|
7089
7157
|
}
|
|
7090
7158
|
}
|
|
@@ -7118,16 +7186,28 @@ export default function workflowExtension(
|
|
|
7118
7186
|
ctx.ui.notify("Role picker cancelled.", "info");
|
|
7119
7187
|
return;
|
|
7120
7188
|
}
|
|
7121
|
-
// Validate against registry (warn-only, matching /role set)
|
|
7189
|
+
// Validate against registry (warn-only, matching /role set) and clamp each
|
|
7190
|
+
// staged thinking level to the model it will run on.
|
|
7122
7191
|
try {
|
|
7123
7192
|
const reg = (ctx as any)?.modelRegistry;
|
|
7124
7193
|
if (reg && typeof reg.find === "function") {
|
|
7125
7194
|
for (const [name, st] of saved) {
|
|
7126
|
-
|
|
7195
|
+
const model = reg.find(st.provider, st.id);
|
|
7196
|
+
if (!model) {
|
|
7127
7197
|
ctx.ui.notify(
|
|
7128
7198
|
`Warning: ${st.provider}/${st.id} (${name}) not in model registry — saved anyway.`,
|
|
7129
7199
|
"warning",
|
|
7130
7200
|
);
|
|
7201
|
+
continue;
|
|
7202
|
+
}
|
|
7203
|
+
const pair = thinkingLevelsForPair(model, st.thinking);
|
|
7204
|
+
if (pair.level !== st.thinking) {
|
|
7205
|
+
ctx.ui.notify(
|
|
7206
|
+
`Thinking ${st.thinking} → ${pair.level} for ${name} (${st.provider}/${st.id} does not support ${st.thinking}).`,
|
|
7207
|
+
"warning",
|
|
7208
|
+
);
|
|
7209
|
+
st.thinking = pair.level;
|
|
7210
|
+
}
|
|
7131
7211
|
}
|
|
7132
7212
|
}
|
|
7133
7213
|
} catch (_e) {
|
|
@@ -8235,12 +8315,12 @@ export default function workflowExtension(
|
|
|
8235
8315
|
await withPersistSuppressed(async () => {
|
|
8236
8316
|
const ok = await (pi as any)?.setModel?.(found);
|
|
8237
8317
|
if (ok === false) throw new Error("setModel returned false");
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8318
|
+
applyThinkingForModel(
|
|
8319
|
+
ctx as any,
|
|
8320
|
+
found,
|
|
8321
|
+
effectiveDefault.thinking,
|
|
8322
|
+
"Session start",
|
|
8323
|
+
);
|
|
8244
8324
|
});
|
|
8245
8325
|
} catch (e: any) {
|
|
8246
8326
|
// Auth missing or the provider rejected the switch: keep Pi's pick.
|
|
@@ -8371,7 +8451,10 @@ export default function workflowExtension(
|
|
|
8371
8451
|
void _e;
|
|
8372
8452
|
}
|
|
8373
8453
|
}
|
|
8374
|
-
ctx.ui.setStatus(
|
|
8454
|
+
ctx.ui.setStatus(WORKFLOW_STATUS_KEYS.mode, undefined);
|
|
8455
|
+
ctx.ui.setStatus(WORKFLOW_STATUS_KEYS.profile, undefined);
|
|
8456
|
+
ctx.ui.setStatus(WORKFLOW_STATUS_KEYS.policy, undefined);
|
|
8457
|
+
ctx.ui.setStatus(WORKFLOW_STATUS_KEYS.review, undefined);
|
|
8375
8458
|
ctx.ui.setWidget("workflow-todos", undefined);
|
|
8376
8459
|
// Drop any /commit authorization or flow: a reload/quit must never leave
|
|
8377
8460
|
// an exemption armed for whatever runs next.
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dieulc/workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"keywords": ["pi-package", "workflow", "plan-mode", "review-mode", "questionnaire", "rewind"],
|
|
7
7
|
"description": "Plan↔Build mode with an independent Review Mode reviewer, questionnaire, subagent explore, todos and shadow-checkpoint rewind for Pi.",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node --test utils.todo.test.ts permissions.test.ts roles.test.ts profiles.test.ts picker.test.ts questionnaire-layout.test.ts questionnaire-view.test.ts model-default.test.ts review.test.ts review-ui.test.ts extension.test.ts commit-policy.test.ts edit-policy.test.ts tab-policy.test.ts tab-editor.test.ts",
|
|
10
|
+
"test": "node --test utils.todo.test.ts permissions.test.ts roles.test.ts profiles.test.ts thinking-levels.test.ts workflow-status.test.ts picker.test.ts questionnaire-layout.test.ts questionnaire-view.test.ts model-default.test.ts review.test.ts review-ui.test.ts extension.test.ts commit-policy.test.ts edit-policy.test.ts tab-policy.test.ts tab-editor.test.ts",
|
|
11
11
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
12
12
|
"check:policy": "node --experimental-transform-types scripts/verify-write-policy.mjs"
|
|
13
13
|
},
|