takomi 2.1.33 → 2.1.34

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.
@@ -35,9 +35,6 @@ const SUBCOMMAND_COMPLETIONS: Record<string, TakomiCompletion[]> = {
35
35
  { value: "status", label: "status", description: "Show active subagents" },
36
36
  { value: "on", label: "on", description: "Allow subagent delegation" },
37
37
  { value: "off", label: "off", description: "Disable subagent delegation" },
38
- { value: "expand", label: "expand", description: "Expand native tool results" },
39
- { value: "collapse", label: "collapse", description: "Collapse native tool results" },
40
- { value: "toggle", label: "toggle", description: "Toggle native tool result expansion" },
41
38
  ],
42
39
  stats: [
43
40
  { value: "overview", label: "overview", description: "Show the full profile-card dashboard" },
@@ -98,7 +95,7 @@ export function commandHelp(): string {
98
95
  "/takomi plan [title]",
99
96
  "/takomi mode <direct|orchestrate|review>",
100
97
  "/takomi gate <auto|review>",
101
- "/takomi subagents <on|off|status|expand|collapse|toggle>",
98
+ "/takomi subagents <on|off|status>",
102
99
  "/takomi stats [overview|daily|models|projects|projects-full|sessions|sessions-full|tasks|tasks-full|tools|subagents|sources] [since 7d]",
103
100
  "/takomi routing [show|where]",
104
101
  "/takomi routing <policy text> # updates global policy",
@@ -134,6 +131,7 @@ export function statusText(state: TakomiRuntimeCommandState, subagentController:
134
131
  return [
135
132
  `Takomi: ${state.enabled ? "on" : "off"}`,
136
133
  `Mode: ${state.role}`,
134
+ `Source: ${state.modeSource ?? "idle"}${state.modeReason ? ` (${state.modeReason})` : ""}`,
137
135
  `Stage: ${state.stage ?? "-"}`,
138
136
  `Workflow: ${state.workflow ?? "-"}`,
139
137
  `Plan bias: ${state.planMode ? "on" : "off"}`,
@@ -20,6 +20,8 @@ export type TakomiRuntimeCommandState = {
20
20
  workflow?: TakomiWorkflowId;
21
21
  activeSessionId?: string;
22
22
  subagentsEnabled: boolean;
23
+ modeSource?: "idle" | "manual" | "model" | "board";
24
+ modeReason?: string;
23
25
  };
24
26
 
25
27
  type RegisterTakomiCommandOptions = {
@@ -36,6 +38,8 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
36
38
  async function handleStage(ctx: ExtensionCommandContext, stage: VibeLifecycleStage, prompt?: string): Promise<void> {
37
39
  await options.updateState(ctx, () => {
38
40
  options.getState().enabled = true;
41
+ options.getState().modeSource = "manual";
42
+ options.getState().modeReason = `/${stage}`;
39
43
  options.setStageAndWorkflow(stage, { preserveRole: stage === "genesis" && options.getState().role === "orchestrator" });
40
44
  options.getState().planMode = stage !== "build";
41
45
  }, workflowPrompt(stage, prompt));
@@ -54,17 +58,27 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
54
58
  state.autoOrch = false;
55
59
  state.planMode = false;
56
60
  state.role = "general";
61
+ state.stage = undefined;
62
+ state.workflow = undefined;
63
+ state.modeSource = "idle";
64
+ state.modeReason = undefined;
57
65
  } else if (mode === "orchestrate") {
58
66
  state.autoOrch = true;
59
67
  state.planMode = true;
60
68
  state.role = "orchestrator";
61
69
  state.stage = hasGenesis ? "build" : "genesis";
62
70
  state.workflow = hasGenesis ? "vibe-build" : "vibe-genesis";
71
+ state.modeSource = "manual";
72
+ state.modeReason = "/takomi mode orchestrate";
63
73
  } else {
64
74
  state.autoOrch = false;
65
75
  state.planMode = true;
66
76
  state.launchMode = "manual";
67
77
  state.role = "review";
78
+ state.stage = undefined;
79
+ state.workflow = undefined;
80
+ state.modeSource = "manual";
81
+ state.modeReason = "/takomi mode review";
68
82
  }
69
83
  }, () => `Takomi mode set to ${mode}`);
70
84
  }
@@ -198,27 +212,7 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
198
212
  ctx.ui.notify(statusText(options.getState(), controller), controller.hasRuns() ? "info" : "warning");
199
213
  return;
200
214
  }
201
- if (action === "expand" || action === "fullscreen") {
202
- ctx.ui.setToolsExpanded(true);
203
- ctx.ui.notify("Expanded native tool results for Takomi subagent output.", "info");
204
- return;
205
- }
206
- if (action === "collapse") {
207
- ctx.ui.setToolsExpanded(false);
208
- ctx.ui.notify("Collapsed native tool results.", "info");
209
- return;
210
- }
211
- if (action === "toggle") {
212
- const expanded = !ctx.ui.getToolsExpanded();
213
- ctx.ui.setToolsExpanded(expanded);
214
- ctx.ui.notify(`${expanded ? "Expanded" : "Collapsed"} native tool results.`, "info");
215
- return;
216
- }
217
- if (action === "next" || action === "prev") {
218
- ctx.ui.notify("Takomi now uses Pi's native inline result UI. Subagent navigation is handled by the transcript/tool results.", "info");
219
- return;
220
- }
221
- ctx.ui.notify("Usage: /takomi subagents <on|off|status|expand|collapse|toggle>", "warning");
215
+ ctx.ui.notify("Usage: /takomi subagents <on|off|status>", "warning");
222
216
  }
223
217
 
224
218
  pi.registerCommand("takomi", {
@@ -58,6 +58,8 @@ import {
58
58
  } from "./profile";
59
59
  import { installTakomiRoutingPolicy, previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy } from "./routing-policy";
60
60
 
61
+ type TakomiModeSource = "idle" | "manual" | "model" | "board";
62
+
61
63
  type TakomiState = {
62
64
  enabled: boolean;
63
65
  autoOrch: boolean;
@@ -69,15 +71,18 @@ type TakomiState = {
69
71
  activeSessionId?: string;
70
72
  subagentsEnabled: boolean;
71
73
  lastFullPromptKey?: string;
74
+ modeSource?: TakomiModeSource;
75
+ modeReason?: string;
72
76
  };
73
77
 
74
78
  const DEFAULT_STATE: TakomiState = {
75
79
  enabled: true,
76
- autoOrch: true,
80
+ autoOrch: false,
77
81
  launchMode: "auto",
78
82
  planMode: false,
79
83
  role: "general",
80
84
  subagentsEnabled: true,
85
+ modeSource: "idle",
81
86
  };
82
87
 
83
88
  const STATE_ENTRY = "takomi-runtime-state";
@@ -97,6 +102,8 @@ const ThinkingSchema = Type.Union([
97
102
  Type.Literal("xhigh"),
98
103
  ]);
99
104
 
105
+ const TakomiModeSchema = StringEnum(["idle", "code", "orchestrate", "review", "genesis", "design", "build"] as const);
106
+
100
107
  function cloneState(state: TakomiState): TakomiState {
101
108
  return { ...state };
102
109
  }
@@ -111,6 +118,8 @@ function formatState(state: TakomiState): string {
111
118
  `launch=${state.launchMode}`,
112
119
  `plan=${state.planMode ? "on" : "off"}`,
113
120
  `subagents=${state.subagentsEnabled ? "on" : "off"}`,
121
+ `source=${state.modeSource ?? "idle"}`,
122
+ state.modeReason ? `reason=${state.modeReason}` : "",
114
123
  state.activeSessionId ? `session=${state.activeSessionId}` : "",
115
124
  ].filter(Boolean).join(" | ");
116
125
  }
@@ -766,6 +775,8 @@ export default function takomiRuntime(pi: ExtensionAPI) {
766
775
  state.stage = "genesis";
767
776
  state.workflow = "vibe-genesis";
768
777
  state.role = "orchestrator";
778
+ state.modeSource = "manual";
779
+ state.modeReason = "/takomi plan";
769
780
  });
770
781
  return `Takomi plan created session ${session.sessionId}\nMaster plan: ${paths.masterPlan}`;
771
782
  },
@@ -783,34 +794,82 @@ export default function takomiRuntime(pi: ExtensionAPI) {
783
794
  },
784
795
  });
785
796
 
786
- pi.registerShortcut("alt+t", {
787
- description: "Toggle native tool result expansion",
788
- handler: async (ctx) => {
789
- const expanded = !ctx.ui.getToolsExpanded();
790
- ctx.ui.setToolsExpanded(expanded);
791
- ctx.ui.notify(`${expanded ? "Expanded" : "Collapsed"} native tool results.`, "info");
792
- },
793
- });
794
797
 
795
- pi.registerShortcut("alt+shift+t", {
796
- description: "Expand native tool results",
797
- handler: async (ctx) => {
798
- ctx.ui.setToolsExpanded(true);
799
- ctx.ui.notify("Expanded native tool results for Takomi subagent output.", "info");
800
- },
801
- });
798
+ async function applyTakomiMode(ctx: ExtensionContext, mode: string, source: TakomiModeSource, reason?: string): Promise<string> {
799
+ const hasGenesis = await hasGenesisArtifacts(ctx.cwd);
800
+ state.enabled = true;
801
+ state.modeSource = source;
802
+ state.modeReason = reason?.trim() || undefined;
803
+
804
+ switch (mode) {
805
+ case "idle":
806
+ state.modeSource = "idle";
807
+ state.modeReason = undefined;
808
+ state.autoOrch = false;
809
+ state.planMode = false;
810
+ state.role = "general";
811
+ state.stage = undefined;
812
+ state.workflow = undefined;
813
+ break;
814
+ case "code":
815
+ state.autoOrch = false;
816
+ state.planMode = false;
817
+ state.role = "code";
818
+ state.stage = undefined;
819
+ state.workflow = undefined;
820
+ break;
821
+ case "review":
822
+ state.autoOrch = false;
823
+ state.planMode = true;
824
+ state.launchMode = "manual";
825
+ state.role = "review";
826
+ state.stage = undefined;
827
+ state.workflow = undefined;
828
+ break;
829
+ case "orchestrate":
830
+ state.autoOrch = true;
831
+ state.planMode = true;
832
+ state.role = "orchestrator";
833
+ state.stage = hasGenesis ? "build" : "genesis";
834
+ state.workflow = hasGenesis ? "vibe-build" : "vibe-genesis";
835
+ break;
836
+ case "genesis":
837
+ case "design":
838
+ case "build":
839
+ setStageAndWorkflow(state, mode);
840
+ state.autoOrch = mode === "build";
841
+ state.planMode = mode !== "build";
842
+ break;
843
+ }
802
844
 
803
- pi.registerShortcut("alt+n", {
804
- description: "Show native subagent navigation hint",
805
- handler: async (ctx) => {
806
- ctx.ui.notify("Native subagent results are shown inline by Pi; use the transcript/tool expansion instead of Takomi focus cycling.", "info");
807
- },
808
- });
845
+ persistState();
846
+ syncContextPanelState();
847
+ await refreshUi(ctx, state);
848
+ const label = state.modeSource === "idle" ? "idle" : `${state.modeSource}:${state.stage ?? state.role}`;
849
+ return `Takomi mode set to ${label}${state.modeReason ? ` (${state.modeReason})` : ""}.`;
850
+ }
809
851
 
810
- pi.registerShortcut("alt+p", {
811
- description: "Show native subagent navigation hint",
812
- handler: async (ctx) => {
813
- ctx.ui.notify("Native subagent results are shown inline by Pi; use the transcript/tool expansion instead of Takomi focus cycling.", "info");
852
+ pi.registerTool({
853
+ name: "takomi_mode",
854
+ label: "Takomi Mode",
855
+ description: "Set or clear the visible Takomi runtime mode after the active model decides a task benefits from code, review, lifecycle, or orchestration handling.",
856
+ promptSnippet: "Optional: call takomi_mode when you decide the current request should visibly enter Takomi code/review/orchestration/lifecycle mode. Do not call it for normal creative/general conversation. Use mode=idle to clear Takomi mode.",
857
+ promptGuidelines: [
858
+ "Let the user's request drive the choice; do not switch modes just because a vague word like code/review/build appears alone.",
859
+ "Prefer mode=code for direct coding in the current chat, mode=orchestrate only for broad/multi-step durable work, and mode=review for critique/audit/QA.",
860
+ "Use mode=idle when the user asks normal non-coding/non-Takomi questions and Takomi should get out of the way.",
861
+ ],
862
+ parameters: Type.Object({
863
+ mode: TakomiModeSchema,
864
+ reason: Type.Optional(Type.String({ description: "Short human-readable reason for the switch" })),
865
+ }),
866
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
867
+ const text = await applyTakomiMode(ctx, params.mode, "model", params.reason);
868
+ ctx.ui.notify(text, "info");
869
+ return {
870
+ content: [{ type: "text", text }],
871
+ details: { mode: params.mode, source: state.modeSource, reason: state.modeReason, role: state.role, stage: state.stage, workflow: state.workflow },
872
+ };
814
873
  },
815
874
  });
816
875
 
@@ -1007,6 +1066,12 @@ ${stateJson}`
1007
1066
  }
1008
1067
  }
1009
1068
  sessionState.tasks[idx] = nextTask;
1069
+ state.activeSessionId = params.sessionId;
1070
+ state.modeSource = state.modeSource === "idle" ? "board" : state.modeSource;
1071
+ state.modeReason = state.modeReason ?? "board task update";
1072
+ persistState();
1073
+ syncContextPanelState();
1074
+ await refreshUi(ctx, state);
1010
1075
  const nextState = buildSessionState(
1011
1076
  sessionState.sessionId,
1012
1077
  sessionState.title,
@@ -1062,8 +1127,11 @@ ${stateJson}`
1062
1127
  }
1063
1128
  }
1064
1129
  state.activeSessionId = nextState.sessionId;
1130
+ state.modeSource = "board";
1131
+ state.modeReason = `expanded ${params.stage} stage`;
1065
1132
  persistState();
1066
1133
  syncContextPanelState();
1134
+ await refreshUi(ctx, state);
1067
1135
 
1068
1136
  return {
1069
1137
  content: [{ type: "text", text: `Expanded ${params.stage} stage in session ${nextState.sessionId}.\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks)}` }],
@@ -1104,8 +1172,11 @@ ${stateJson}`
1104
1172
  state.role = "orchestrator";
1105
1173
  state.stage = nextState.lifecycle.genesis.status === "completed" ? "build" : "genesis";
1106
1174
  state.workflow = state.stage === "genesis" ? "vibe-genesis" : "vibe-build";
1175
+ state.modeSource = "board";
1176
+ state.modeReason = "orchestrator session";
1107
1177
  persistState();
1108
1178
  syncContextPanelState();
1179
+ await refreshUi(ctx, state);
1109
1180
 
1110
1181
  return {
1111
1182
  content: [{ type: "text", text: `Created Takomi orchestrator session ${nextState.sessionId} in hybrid mode\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks) || "No tasks provided."}` }],
@@ -1152,11 +1223,15 @@ ${stateJson}`
1152
1223
 
1153
1224
  if (lowered === "use takomi") {
1154
1225
  state.enabled = true;
1226
+ state.modeSource = "manual";
1227
+ state.modeReason = "explicit user request";
1155
1228
  return { action: "transform", text: "Use the Takomi runtime, identify the correct lifecycle stage, and proceed accordingly." };
1156
1229
  }
1157
1230
 
1158
1231
  if (lowered.startsWith("use takomi ")) {
1159
1232
  state.enabled = true;
1233
+ state.modeSource = "manual";
1234
+ state.modeReason = "explicit user request";
1160
1235
  const route = decideRoute(text.slice("use takomi ".length));
1161
1236
  if (route.stage) setStageAndWorkflow(state, route.stage, { preserveRole: state.role === "orchestrator" && route.stage === "genesis" });
1162
1237
  else if (route.role !== "general") state.role = route.role;
@@ -1164,14 +1239,20 @@ ${stateJson}`
1164
1239
  }
1165
1240
 
1166
1241
  if (/\bvibe genesis\b/i.test(text)) {
1242
+ state.modeSource = "manual";
1243
+ state.modeReason = "vibe genesis";
1167
1244
  setStageAndWorkflow(state, "genesis", { preserveRole: state.role === "orchestrator" });
1168
1245
  return { action: "transform", text };
1169
1246
  }
1170
1247
  if (/\bvibe design\b/i.test(text)) {
1248
+ state.modeSource = "manual";
1249
+ state.modeReason = "vibe design";
1171
1250
  setStageAndWorkflow(state, "design");
1172
1251
  return { action: "transform", text };
1173
1252
  }
1174
1253
  if (/\bvibe build\b/i.test(text)) {
1254
+ state.modeSource = "manual";
1255
+ state.modeReason = "vibe build";
1175
1256
  setStageAndWorkflow(state, "build");
1176
1257
  return { action: "transform", text };
1177
1258
  }
@@ -1180,43 +1261,36 @@ ${stateJson}`
1180
1261
  });
1181
1262
 
1182
1263
  pi.on("before_agent_start", async (event, ctx) => {
1183
- if (!state.enabled) return;
1264
+ if (!state.enabled || (state.modeSource ?? "idle") === "idle") return;
1184
1265
 
1185
1266
  let effectiveState = cloneState(state);
1186
1267
  const runtimeCwd = ctx.cwd;
1187
1268
  const genesisExists = await hasGenesisArtifacts(runtimeCwd);
1188
1269
  const route = decideRoute(event.prompt);
1189
- if (state.autoOrch && shouldAutoRoute(event.prompt)) {
1190
- effectiveState.role = "orchestrator";
1191
- effectiveState.stage = genesisExists ? "build" : "genesis";
1192
- effectiveState.workflow = genesisExists ? "vibe-build" : "vibe-genesis";
1193
- }
1194
-
1195
- const shouldHonorRoute = route.stage || route.role !== "general" || route.sessionRecommendation !== "none";
1196
- if (shouldHonorRoute && route.stage) {
1197
- effectiveState.stage = route.stage;
1198
- effectiveState.workflow = route.workflow;
1199
- effectiveState.role = effectiveState.role === "orchestrator" && route.stage === "genesis" ? "orchestrator" : route.role;
1200
- } else if (shouldHonorRoute && route.role !== "general") {
1201
- effectiveState.role = route.role;
1202
- }
1203
-
1204
- let routingNote = route.reason;
1270
+ let routingNote = state.modeReason
1271
+ ? `Takomi mode selected by ${state.modeSource}: ${state.modeReason}.`
1272
+ : `Takomi mode selected by ${state.modeSource ?? "runtime"}.`;
1205
1273
  const explicitLifecycleWaiver = /skip genesis|waive genesis|genesis complete|already have (a )?(prd|requirements)|design complete|jump straight to build/i.test(event.prompt);
1206
- const orchestrationActive = effectiveState.role === "orchestrator" || route.executionMode === "orchestrate";
1274
+ const orchestrationActive = effectiveState.role === "orchestrator";
1207
1275
  if (!genesisExists && orchestrationActive && !explicitLifecycleWaiver) {
1208
1276
  effectiveState.stage = "genesis";
1209
1277
  effectiveState.workflow = "vibe-genesis";
1210
1278
  routingNote = "Blank project detected; orchestrator remains in control and must honor Genesis → Design → Build.";
1211
1279
  }
1280
+ if (effectiveState.stage !== state.stage || effectiveState.workflow !== state.workflow || effectiveState.role !== state.role) {
1281
+ state.role = effectiveState.role;
1282
+ state.stage = effectiveState.stage;
1283
+ state.workflow = effectiveState.workflow;
1284
+ }
1212
1285
 
1213
1286
  const promptKey = `${effectiveState.role}:${effectiveState.workflow ?? "none"}`;
1214
1287
  const includeFullWorkflow = Boolean(effectiveState.workflow && effectiveState.lastFullPromptKey !== promptKey);
1215
1288
  if (includeFullWorkflow) {
1216
1289
  state.lastFullPromptKey = promptKey;
1217
- persistState();
1218
- syncContextPanelState();
1219
1290
  }
1291
+ persistState();
1292
+ syncContextPanelState();
1293
+ await refreshUi(ctx, state);
1220
1294
 
1221
1295
  const routingPolicy = await resolveTakomiRoutingPolicy(runtimeCwd);
1222
1296
  const optionalFeatureContext = (() => {
@@ -1263,14 +1337,14 @@ ${stateJson}`
1263
1337
  `Execution mode: ${route.executionMode}. Session recommendation: ${route.sessionRecommendation}.`,
1264
1338
  `Takomi execution gate: ${effectiveState.launchMode === "manual" ? "review" : "auto"}. In review gate mode, show the delegation plan before launching and return to the user after each task with results, verification guidance, and the recommended next step.`,
1265
1339
  !effectiveState.subagentsEnabled ? "Takomi subagents are disabled for this session. Do not call takomi_subagent or subagent until the user enables subagents." : "",
1266
- !genesisExists ? "Project foundation is missing or incomplete. Do not skip Genesis unless the user explicitly waives it." : "",
1267
- "Takomi is the default orchestration mindset here. Do not wait for the literal phrase 'use Takomi' before applying lifecycle judgment.",
1268
- "Task fan-out is flexible. Do not force exactly three tasks; decompose Genesis, Design, and Build work to fit the actual scope.",
1269
- "A new orchestration session should usually begin with one Genesis foundation task that creates or updates the required markdown artifacts, then expand later stages only when the scope justifies it.",
1270
- "If a follow-up request is small, one-shot it. If it is multi-part or large, create or expand an orchestration session instead of pretending it is a single task.",
1340
+ orchestrationActive && !genesisExists ? "Project foundation is missing or incomplete. Do not skip Genesis unless the user explicitly waives it." : "",
1341
+ "Do not escalate to orchestration or review just because this is coding-related; stay in the selected Takomi mode unless the user asks or a durable board/session is genuinely needed.",
1342
+ orchestrationActive ? "Task fan-out is flexible. Do not force exactly three tasks; decompose Genesis, Design, and Build work to fit the actual scope." : "",
1343
+ orchestrationActive ? "A new orchestration session should usually begin with one Genesis foundation task that creates or updates the required markdown artifacts, then expand later stages only when the scope justifies it." : "",
1344
+ orchestrationActive ? "If a follow-up request is small, one-shot it. If it is multi-part or large, create or expand an orchestration session instead of pretending it is a single task." : "",
1271
1345
  "Before any Takomi subagent dispatch or model override, use the injected Pi model-registry context and project routing policy. Prefer provider-qualified model IDs. Do not run `pi --list-models` unless the registry context is missing or the user asks for a visible diagnostic.",
1272
- "When useful, state the current Takomi stage and the recommended next stage.",
1273
- effectiveState.stage === "build"
1346
+ "When useful, state the current Takomi mode/stage and the recommended next step.",
1347
+ orchestrationActive && effectiveState.stage === "build"
1274
1348
  ? "For build orchestration, use takomi_subagent to dispatch work to specialist subagents, then record the result on takomi_board; reuse the same conversation id when sending fixes back to the agent."
1275
1349
  : "",
1276
1350
  ].filter(Boolean);
@@ -27,6 +27,8 @@ export type RuntimeHudState = {
27
27
  activeSessionId?: string;
28
28
  launchMode?: string;
29
29
  subagentsEnabled?: boolean;
30
+ modeSource?: "idle" | "manual" | "model" | "board";
31
+ modeReason?: string;
30
32
  };
31
33
 
32
34
  type Tone = "accent" | "warning" | "success" | "error" | "muted" | "dim" | "thinkingMinimal";
@@ -70,24 +72,30 @@ function badge(theme: Theme, label: string, tone: Tone): string {
70
72
  }
71
73
 
72
74
  export function renderRuntimeStatus(theme: Theme, state: RuntimeHudState): string {
75
+ const source = state.modeSource ?? "idle";
76
+ if (!state.enabled) return [theme.fg("accent", "Takomi"), theme.fg("dim", "off")].join(" ");
77
+ if (source === "idle") return [theme.fg("accent", "Takomi"), theme.fg("dim", "idle")].join(" ");
78
+
73
79
  const primary = state.stage ?? state.role;
74
80
  const stageBadge = badge(theme, primary, stageTone(state.stage));
75
- const auto = state.autoOrch ? theme.fg("accent", "auto") : theme.fg("dim", "manual");
76
- const gate = state.launchMode === "manual" ? theme.fg("warning", "review-gate") : theme.fg("accent", "auto-gate");
81
+ const sourceLabel = source === "manual" ? theme.fg("warning", "manual") : theme.fg("success", source);
82
+ const role = state.stage && state.role !== state.stage ? theme.fg("dim", state.role) : "";
77
83
  const plan = state.planMode ? theme.fg("warning", "plan") : theme.fg("dim", "direct");
78
- const subagents = state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : theme.fg("dim", "subagents:on");
79
- return [theme.fg("accent", "Takomi"), stageBadge, theme.fg("dim", `role:${state.role}`), auto, gate, plan, subagents].join(" ");
84
+ const gate = state.launchMode === "manual" ? theme.fg("warning", "review-gate") : "";
85
+ const subagents = state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : "";
86
+ return [theme.fg("accent", "Takomi"), sourceLabel, stageBadge, role, gate || plan, subagents].filter(Boolean).join(" ");
80
87
  }
81
88
 
82
89
  export function renderRuntimeWidget(theme: Theme, state: RuntimeHudState): string[] {
83
- if (!state.enabled) return [];
90
+ if (!state.enabled || (state.modeSource ?? "idle") === "idle") return [];
84
91
  const primary = state.stage ?? state.role;
92
+ const sourceLabel = state.modeSource === "manual" ? theme.fg("warning", "manual") : theme.fg("success", state.modeSource ?? "model");
85
93
  const parts = [
86
94
  theme.fg("accent", "Takomi"),
95
+ sourceLabel,
87
96
  badge(theme, primary, stageTone(state.stage)),
88
- theme.fg("dim", `role:${state.role}`),
89
- state.autoOrch ? theme.fg("accent", "auto") : theme.fg("dim", "manual"),
90
- state.launchMode === "manual" ? theme.fg("warning", "review-gate") : theme.fg("accent", "auto-gate"),
97
+ state.stage && state.role !== state.stage ? theme.fg("dim", `role:${state.role}`) : "",
98
+ state.launchMode === "manual" ? theme.fg("warning", "review-gate") : "",
91
99
  state.planMode ? theme.fg("warning", "plan") : theme.fg("dim", "direct"),
92
100
  state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : theme.fg("dim", "subagents:on"),
93
101
  state.workflow ? theme.fg("dim", `wf:${state.workflow}`) : "",
@@ -115,7 +123,7 @@ export class TakomiFooterComponent implements Component {
115
123
  this.unsubscribeBranchChange();
116
124
  }
117
125
 
118
- invalidate(): void {}
126
+ invalidate(): void { }
119
127
 
120
128
  render(width: number): string[] {
121
129
  const state = this.getState();
@@ -141,8 +149,7 @@ export class TakomiFooterComponent implements Component {
141
149
  .filter(([key]) => key !== "takomi-runtime")
142
150
  .map(([, value]) => value)
143
151
  .filter(Boolean);
144
- const runtimeStatus = renderRuntimeStatus(this.theme, state);
145
- const left = [runtimeStatus, ...extensionStatuses].join(this.theme.fg("dim", " | "));
152
+ const left = extensionStatuses.join(this.theme.fg("dim", " | "));
146
153
  const branch = this.footerData.getGitBranch();
147
154
  const rightText = [this.ctx.model?.id || "no-model", branch ? `git:${branch}` : ""].filter(Boolean).join(" | ");
148
155
  const right = this.theme.fg("dim", rightText);
@@ -44,6 +44,7 @@ const SubagentParameters = Type.Object({
44
44
  tasks: Type.Optional(Type.Array(TaskSchema, { description: "Parallel subagent tasks" })),
45
45
  confirmLaunch: Type.Optional(Type.Boolean({ description: "Required to launch immediately in manual Takomi launch mode" })),
46
46
  previewOnly: Type.Optional(Type.Boolean({ description: "Return the delegation plan without launching" })),
47
+ clarify: Type.Optional(Type.Boolean({ description: "Show the native pi-subagents TUI to preview/edit before execution. Especially useful for chains; requires an interactive Pi TUI." })),
47
48
  chain: Type.Optional(Type.Array(TaskSchema, { description: "Sequential chain of subagent tasks" })),
48
49
  agentScope: Type.Optional(Type.Union([Type.Literal("user"), Type.Literal("project"), Type.Literal("both")])),
49
50
  confirmProjectAgents: Type.Optional(Type.Boolean({ description: "Prompt before running project-local agents. Default: true." })),
@@ -59,6 +60,7 @@ function registerSubagentTool(pi: ExtensionAPI): void {
59
60
  promptGuidelines: [
60
61
  "Use this tool during orchestration when a specialist should handle a task.",
61
62
  "Use tasks for independent parallel work and chain for dependent handoffs with {previous}.",
63
+ "Set clarify=true when the user asks to preview/edit a subagent run in the native Pi TUI before launch.",
62
64
  "Use model, fallbackModels, and thinking only when deliberate; otherwise let the agent/profile defaults apply.",
63
65
  "If review sends work back to the same agent, reuse the same conversationId for continuity.",
64
66
  "If a launch is blocked, cancelled, paused, or review-gated, do not retry automatically; wait for the user's next prompt. Use overrideUserBlock only after explicit user approval.",
@@ -30,15 +30,37 @@ export function renderTakomiSubagentCall(params: TakomiSubagentToolParams, theme
30
30
  );
31
31
  }
32
32
 
33
+ function resultText(result: ToolResult): string {
34
+ return typeof (result as any)?.content === "string"
35
+ ? (result as any).content
36
+ : Array.isArray((result as any)?.content)
37
+ ? (result as any).content.map((part: any) => part?.text ?? "").filter(Boolean).join("\n")
38
+ : JSON.stringify((result as any)?.details ?? {}, null, 2);
39
+ }
40
+
41
+ function summarizeCollapsedResult(text: string, status: string, theme: Theme): string {
42
+ const policyMatch = text.match(/Required policies:\n((?:- .+\n?)+)/);
43
+ const policies = policyMatch?.[1]
44
+ ?.split("\n")
45
+ .map((line) => line.replace(/^[-\s]+/, "").trim())
46
+ .filter(Boolean) ?? [];
47
+ const lineCount = text ? text.split(/\r?\n/).length : 0;
48
+ const label = policies.length > 0
49
+ ? `policy context loaded: ${policies.join(", ")}`
50
+ : `${lineCount} result line${lineCount === 1 ? "" : "s"} hidden`;
51
+ const icon = status === "failed" ? "⚠" : "āœ“";
52
+ const color = status === "failed" ? "warning" : "success";
53
+ return `${theme.fg(color, `${icon} takomi_subagent ${status}`)}${theme.fg("dim", ` Ā· ${label} (ctrl+o to expand)`)}`;
54
+ }
55
+
33
56
  export function renderTakomiSubagentResult(result: ToolResult, options: { expanded?: boolean; isPartial?: boolean }, theme: Theme, context: any): any {
34
57
  const native = renderNativeSubagentResult(result, options, theme, context);
35
58
  if (native) return native;
36
59
 
37
60
  const status = (result as any)?.isError ? "failed" : "completed";
38
- const text = typeof (result as any)?.content === "string"
39
- ? (result as any).content
40
- : Array.isArray((result as any)?.content)
41
- ? (result as any).content.map((part: any) => part?.text ?? "").filter(Boolean).join("\n")
42
- : JSON.stringify((result as any)?.details ?? {}, null, 2);
61
+ const text = resultText(result);
62
+ if (!options.expanded && !options.isPartial) {
63
+ return new Text(summarizeCollapsedResult(text, status, theme), 0, 0);
64
+ }
43
65
  return new Text(`${theme.fg("toolTitle", theme.bold(`takomi_subagent ${status}`))}\n${text || "No result content."}`, 0, 0);
44
66
  }
@@ -186,7 +186,7 @@ function toSubagentParams(params: TakomiSubagentToolParams, rootCwd: string, dis
186
186
  cwd: rootCwd,
187
187
  context: "fresh" as const,
188
188
  async: false,
189
- clarify: false,
189
+ clarify: params.clarify === true,
190
190
  includeProgress: true,
191
191
  sessionDir: stableConversationSessionDir(rootCwd, tasks),
192
192
  };
@@ -28,6 +28,7 @@ export type TakomiSubagentToolParams = Partial<TakomiSubagentToolTask> & {
28
28
  chain?: TakomiSubagentToolTask[];
29
29
  confirmLaunch?: boolean;
30
30
  previewOnly?: boolean;
31
+ clarify?: boolean;
31
32
  agentScope?: TakomiAgentScope;
32
33
  confirmProjectAgents?: boolean;
33
34
  overrideUserBlock?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takomi",
3
- "version": "2.1.33",
3
+ "version": "2.1.34",
4
4
  "description": "šŸŽÆ Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  isStoreInitialized,
44
44
  } from './store.js';
45
45
  import { runDoctor } from './doctor.js';
46
- import { ensurePiInstalled, ensurePiSubagentsInstalled, launchTakomiHarness, printPiInstallResult, printPiSubagentsInstallResult, updatePiManagedPackages, printPiManagedPackageUpdateResult } from './pi-harness.js';
46
+ import { ensurePiInstalled, ensurePiSubagentsInstalled, launchTakomiHarness, printPiInstallResult, printPiSubagentsInstallResult, updatePiCliPackage, updatePiManagedPackages, printPiManagedPackageUpdateResult } from './pi-harness.js';
47
47
  import { installPiHarnessAssets, printPiInstallSummary, syncPiHarnessAssets, validatePiHarnessInstall } from './pi-installer.js';
48
48
  import { offerPiOptionalFeatures } from './pi-optional-features.js';
49
49
  import {
@@ -459,7 +459,7 @@ async function installPiTarget(options = {}) {
459
459
  console.log(pc.cyan('\n🧩 Optional Takomi Pi feature packs...\n'));
460
460
  await offerPiOptionalFeatures({ interactive: true });
461
461
  }
462
- console.log(pc.cyan('\nšŸ“¦ Checking Pi-managed package updates...\n'));
462
+ console.log(pc.cyan('\nšŸ“¦ Checking Pi-managed extension package updates...\n'));
463
463
  printPiManagedPackageUpdateResult(await updatePiManagedPackages());
464
464
  console.log(pc.dim('\nNext: cd <project> && takomi\n'));
465
465
  } catch (error) {
@@ -474,7 +474,7 @@ async function installPiOptionalFeaturesTarget() {
474
474
  printPiInstallResult(pi);
475
475
  if (!pi.ok) return;
476
476
  await offerPiOptionalFeatures({ interactive: true });
477
- console.log(pc.cyan('\nšŸ“¦ Checking Pi-managed package updates...\n'));
477
+ console.log(pc.cyan('\nšŸ“¦ Checking Pi-managed extension package updates...\n'));
478
478
  printPiManagedPackageUpdateResult(await updatePiManagedPackages());
479
479
  }
480
480
 
@@ -484,7 +484,7 @@ async function syncPiTarget() {
484
484
  const result = await syncPiHarnessAssets(program.version());
485
485
  const validation = await validatePiHarnessInstall();
486
486
  printPiInstallSummary(result, validation);
487
- console.log(pc.cyan('\nšŸ“¦ Checking Pi-managed package updates...\n'));
487
+ console.log(pc.cyan('\nšŸ“¦ Checking Pi-managed extension package updates...\n'));
488
488
  printPiManagedPackageUpdateResult(await updatePiManagedPackages());
489
489
  } catch (error) {
490
490
  console.log(pc.red('\nPi sync failed.'));
@@ -558,6 +558,8 @@ async function upgrade(target = 'all') {
558
558
  }
559
559
 
560
560
  if (normalizedTarget === 'pi') {
561
+ console.log(pc.cyan('\nšŸ“¦ Updating Pi CLI...\n'));
562
+ printPiInstallResult(await updatePiCliPackage());
561
563
  await installPiTarget({ offerOptionalFeatures: false });
562
564
  return;
563
565
  }
@@ -567,6 +569,8 @@ async function upgrade(target = 'all') {
567
569
  return;
568
570
  }
569
571
 
572
+ console.log(pc.cyan('\nšŸ“¦ Updating Pi CLI...\n'));
573
+ printPiInstallResult(await updatePiCliPackage());
570
574
  await installAllTargets({ offerOptionalFeatures: false });
571
575
  console.log(pc.magenta('\n✨ Fully upgraded. Next: run `takomi` from your project.\n'));
572
576
  }
package/src/pi-harness.js CHANGED
@@ -275,9 +275,9 @@ function runCommandWithTimeout(command, args, timeoutMs) {
275
275
  });
276
276
  }
277
277
 
278
- export async function ensurePiInstalled() {
278
+ async function installOrUpdatePiCli({ force = false } = {}) {
279
279
  const before = await detectPiCommand();
280
- if (before.installed) {
280
+ if (before.installed && !force) {
281
281
  return { ok: true, changed: false, report: 'Pi already available.' };
282
282
  }
283
283
 
@@ -295,7 +295,9 @@ export async function ensurePiInstalled() {
295
295
  return {
296
296
  ok: true,
297
297
  changed: true,
298
- report: (result.stdout || result.stderr || 'Installed Pi.').trim(),
298
+ report: (result.stdout || result.stderr || (force ? 'Updated Pi.' : 'Installed Pi.')).trim(),
299
+ version: after.version,
300
+ action: force ? 'Updated' : 'Installed',
299
301
  };
300
302
  }
301
303
  lastError = 'npm install reported success, but pi was still not detected.';
@@ -307,9 +309,17 @@ export async function ensurePiInstalled() {
307
309
  return { ok: false, changed: false, report: lastError };
308
310
  }
309
311
 
312
+ export async function ensurePiInstalled() {
313
+ return installOrUpdatePiCli({ force: false });
314
+ }
315
+
316
+ export async function updatePiCliPackage() {
317
+ return installOrUpdatePiCli({ force: true });
318
+ }
319
+
310
320
  export function printPiInstallResult(result) {
311
321
  if (result.ok) {
312
- console.log(pc.green(`āœ” ${result.changed ? 'Installed' : 'Validated'} Pi`));
322
+ console.log(pc.green(`āœ” ${result.action || (result.changed ? 'Installed' : 'Validated')} Pi`));
313
323
  if (result.report) console.log(pc.dim(result.report.split(/\r?\n/).slice(-4).join('\n')));
314
324
  return;
315
325
  }
@@ -359,42 +369,54 @@ export function printPiSubagentsInstallResult(result) {
359
369
  if (result.report) console.log(pc.dim(result.report.split(/\r?\n/).slice(-8).join('\n')));
360
370
  }
361
371
 
362
- export async function updatePiManagedPackages({ timeoutMs = 20_000 } = {}) {
372
+ function getPiPackageUpdateTimeoutMs(timeoutMs) {
373
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) return timeoutMs;
374
+ const envValue = Number(process.env.TAKOMI_PI_PACKAGE_UPDATE_TIMEOUT_MS || '');
375
+ if (Number.isFinite(envValue) && envValue > 0) return envValue;
376
+ return 180_000;
377
+ }
378
+
379
+ export async function updatePiManagedPackages({ timeoutMs } = {}) {
363
380
  if (process.env.TAKOMI_SKIP_PI_PACKAGE_UPDATE === '1') {
364
- return { ok: true, changed: false, report: 'Skipped Pi package update because TAKOMI_SKIP_PI_PACKAGE_UPDATE=1.' };
381
+ return { ok: true, changed: false, report: 'Skipped Pi extension package update because TAKOMI_SKIP_PI_PACKAGE_UPDATE=1.' };
365
382
  }
366
383
 
367
384
  const pi = await detectPiCommand();
368
385
  if (!pi.installed) return { ok: false, changed: false, report: 'Pi is not installed.' };
369
386
 
370
- // `pi update` reconciles packages from Pi settings, including user-installed
371
- // npm/git/local packages. Treat this as a best-effort follow-up: third-party
372
- // packages or manual extension entries must never hang Takomi installation.
387
+ // Only reconcile Pi-managed extension/package entries here. Plain `pi update`
388
+ // also self-updates the Pi CLI (`npm install -g @earendil-works/pi-coding-agent`).
389
+ // If Takomi times out and kills that process, npm can leave the global `pi`
390
+ // shim temporarily removed on Windows. `--extensions` avoids touching the Pi
391
+ // executable while still updating optional/user package packs.
392
+ const effectiveTimeoutMs = getPiPackageUpdateTimeoutMs(timeoutMs);
373
393
  const command = pi.path || 'pi';
374
- const result = await runCommandWithTimeout(command, ['update'], timeoutMs);
394
+ const updateArgs = ['update', '--extensions'];
395
+ const updateCommandText = `pi ${updateArgs.join(' ')}`;
396
+ const result = await runCommandWithTimeout(command, updateArgs, effectiveTimeoutMs);
375
397
  const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
376
398
  if (result.timedOut) {
377
399
  return {
378
400
  ok: false,
379
401
  changed: false,
380
- report: `Timed out after ${Math.round(timeoutMs / 1000)}s while running \`pi update\`. Takomi setup is complete; retry package updates later with \`pi update\` or skip this step with TAKOMI_SKIP_PI_PACKAGE_UPDATE=1. Last output:\n${output}`.trim(),
402
+ report: `Timed out after ${Math.round(effectiveTimeoutMs / 1000)}s while running \`${updateCommandText}\`. Takomi setup is complete; retry package updates later with \`${updateCommandText}\` or skip this step with TAKOMI_SKIP_PI_PACKAGE_UPDATE=1. Increase the timeout with TAKOMI_PI_PACKAGE_UPDATE_TIMEOUT_MS=<ms>. Last output:\n${output}`.trim(),
381
403
  };
382
404
  }
383
405
 
384
406
  return {
385
407
  ok: result.status === 0,
386
408
  changed: result.status === 0,
387
- report: output || (result.status === 0 ? 'All Pi-managed packages are up to date.' : 'pi update failed.'),
409
+ report: output || (result.status === 0 ? 'All Pi-managed extension packages are up to date.' : `${updateCommandText} failed.`),
388
410
  };
389
411
  }
390
412
 
391
413
  export function printPiManagedPackageUpdateResult(result) {
392
414
  if (result.ok) {
393
- console.log(pc.green('āœ” Updated all Pi-managed packages'));
415
+ console.log(pc.green('āœ” Updated all Pi-managed extension packages'));
394
416
  if (result.report) console.log(pc.dim(result.report.split(/\r?\n/).slice(-8).join('\n')));
395
417
  return;
396
418
  }
397
- console.log(pc.yellow('⚠ Could not update Pi-managed packages'));
419
+ console.log(pc.yellow('⚠ Could not update Pi-managed extension packages'));
398
420
  if (result.report) console.log(pc.dim(result.report.split(/\r?\n/).slice(-8).join('\n')));
399
421
  }
400
422