@dieulc/workflow 0.9.0 → 0.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dieulc/workflow",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "keywords": ["pi-package", "workflow", "plan-mode", "review-mode", "questionnaire", "rewind"],
@@ -9,7 +9,8 @@
9
9
  "scripts": {
10
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
- "check:policy": "node --experimental-transform-types scripts/verify-write-policy.mjs"
12
+ "check:policy": "node --experimental-transform-types scripts/verify-write-policy.mjs",
13
+ "prepublishOnly": "node ../../../scripts/verify-packages.mjs --pack-only --packages workflow"
13
14
  },
14
15
  "repository": {
15
16
  "type": "git",
@@ -34,6 +35,8 @@
34
35
  "picker.ts",
35
36
  "tab-policy.ts",
36
37
  "tab-editor.ts",
38
+ "thinking-levels.ts",
39
+ "workflow-status.ts",
37
40
  "questionnaire-layout.ts",
38
41
  "questionnaire-view.ts",
39
42
  "model-default.ts",
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Model-aware thinking levels for the role/profile editors (2026-10-31 UI plan).
3
+ *
4
+ * Pi's `getSupportedThinkingLevels` is the authority. The editors used to list
5
+ * every level (`VALID_THINKING_LEVELS`) for every model, so a profile could
6
+ * store `medium` for a model whose map makes it unsupported; Pi then silently
7
+ * clamped it on activation. These helpers keep the staged value inside the
8
+ * model's set and fall back to the full list only when the model is unknown
9
+ * (offline editing, hand-edited files) so a missing registry entry never
10
+ * blocks the picker.
11
+ */
12
+
13
+ import {
14
+ clampThinkingLevel,
15
+ getSupportedThinkingLevels,
16
+ } from "@earendil-works/pi-ai";
17
+
18
+ import { VALID_THINKING_LEVELS, type ThinkingLevel } from "./roles.ts";
19
+
20
+ function isKnownModel(model: unknown): model is Record<string, unknown> {
21
+ if (!model || typeof model !== "object") return false;
22
+ const record = model as Record<string, unknown>;
23
+ return typeof record.reasoning === "boolean";
24
+ }
25
+
26
+ function asThinkingLevel(value: string): ThinkingLevel | null {
27
+ return (VALID_THINKING_LEVELS as readonly string[]).includes(value)
28
+ ? (value as ThinkingLevel)
29
+ : null;
30
+ }
31
+
32
+ /** The model's supported levels, or all levels when the model is unknown. */
33
+ export function thinkingLevelsForModel(model: unknown): ThinkingLevel[] {
34
+ if (!isKnownModel(model)) return [...VALID_THINKING_LEVELS];
35
+ try {
36
+ const levels = getSupportedThinkingLevels(model as never)
37
+ .map((level) => asThinkingLevel(String(level)))
38
+ .filter((level): level is ThinkingLevel => level !== null);
39
+ return levels.length > 0 ? levels : [...VALID_THINKING_LEVELS];
40
+ } catch {
41
+ return [...VALID_THINKING_LEVELS];
42
+ }
43
+ }
44
+
45
+ /** Clamp a stored level to the model's set (no-op for an unknown model). */
46
+ export function clampThinkingForModel(model: unknown, level: ThinkingLevel): ThinkingLevel {
47
+ if (!isKnownModel(model)) return level;
48
+ try {
49
+ return asThinkingLevel(String(clampThinkingLevel(model as never, level))) ?? level;
50
+ } catch {
51
+ return level;
52
+ }
53
+ }
54
+
55
+ /** The pair an editor should stage after a model change / before a save. */
56
+ export function thinkingLevelsForPair(
57
+ model: unknown,
58
+ level: ThinkingLevel,
59
+ ): { levels: ThinkingLevel[]; level: ThinkingLevel } {
60
+ const levels = thinkingLevelsForModel(model);
61
+ if (levels.includes(level)) return { levels, level };
62
+ const clamped = clampThinkingForModel(model, level);
63
+ return {
64
+ levels,
65
+ level: levels.includes(clamped) ? clamped : (levels[0] ?? level),
66
+ };
67
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Workflow footer segments (2026-10-31 UI plan, step 10).
3
+ *
4
+ * The extension used to publish one long `workflow` status containing the mode,
5
+ * progress, profile/role, commit/write policy and review text. Desktop hosts can
6
+ * only lay an arbitrary status string out as one column, and terminal Pi wraps
7
+ * the same line less efficiently. This module builds short, stable segments so
8
+ * each can be its own footer column.
9
+ *
10
+ * `workflow` stays the mode/progress string: it is what the Desktop parser
11
+ * (`src/shared/workflow-mode.ts`) and existing terminal Pi users already know.
12
+ * The other keys are additive.
13
+ */
14
+
15
+ export const WORKFLOW_STATUS_KEYS = {
16
+ mode: "workflow",
17
+ profile: "workflow-profile",
18
+ policy: "workflow-policy",
19
+ review: "workflow-review",
20
+ } as const;
21
+
22
+ export interface WorkflowStatusInput {
23
+ mode: "build" | "plan" | null;
24
+ /** Plannotator's Plan sub-state, shown inside the mode segment. */
25
+ plannotatorActive?: boolean;
26
+ done: number;
27
+ total: number;
28
+ stale?: boolean;
29
+ profileLabel?: string;
30
+ roleLabel?: string;
31
+ commitMode: string;
32
+ /** Only Default mode has a write policy; null/undefined omits it. */
33
+ writeMode?: string | null;
34
+ }
35
+
36
+ export interface WorkflowStatusSegments {
37
+ mode: string;
38
+ profile: string;
39
+ policy: string;
40
+ }
41
+
42
+ export function buildWorkflowStatusSegments(
43
+ input: WorkflowStatusInput,
44
+ ): WorkflowStatusSegments {
45
+ const done = Number.isFinite(input.done) ? Math.max(0, input.done) : 0;
46
+ const total = Number.isFinite(input.total) ? Math.max(0, input.total) : 0;
47
+ const stale = input.stale ? " ⚠" : "";
48
+ const profile = [input.profileLabel ?? "", input.roleLabel ?? ""]
49
+ .map((part) => part.trim())
50
+ .filter(Boolean)
51
+ .join(" | ");
52
+ const policy =
53
+ `commits:${input.commitMode}` +
54
+ (input.writeMode ? ` · writes:${input.writeMode}` : "");
55
+
56
+ let mode: string;
57
+ if (input.mode === "build") {
58
+ mode = total > 0 ? `▶ build ${done}/${total}${stale}` : `▶ build${stale}`;
59
+ } else if (input.mode === "plan") {
60
+ const base = input.plannotatorActive
61
+ ? "⏸ plan — reviewing in browser"
62
+ : "⏸ plan";
63
+ mode = total > 0 ? `${base} ${done}/${total}${stale}` : `${base}${stale}`;
64
+ } else if (total > 0) {
65
+ // The default mode must stay parseable by the Desktop badge decoder.
66
+ mode = `◦ ${done}/${total}${stale}`;
67
+ } else {
68
+ mode = `◦ commits:${input.commitMode}${stale}`;
69
+ }
70
+
71
+ return { mode, profile, policy };
72
+ }
73
+
74
+ /** Sets a status only while it has text; `undefined` removes a stale key. */
75
+ export function setWorkflowStatus(
76
+ ui: { setStatus?: (key: string, text: string | undefined) => void },
77
+ key: string,
78
+ text: string,
79
+ ): void {
80
+ ui.setStatus?.(key, text.length > 0 ? text : undefined);
81
+ }