@phi-code-admin/phi-code 0.61.1 → 0.61.3

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.
@@ -636,70 +636,33 @@ _Edit this file to customize Phi Code's behavior for your project._
636
636
  async function manualMode(availableModels: string[], ctx: any): Promise<Record<string, { preferred: string; fallback: string }>> {
637
637
  ctx.ui.notify("šŸŽ›ļø Manual mode: assign a model to each task category.\n", "info");
638
638
 
639
- const modelList = availableModels.map((m, i) => ` ${i + 1}. ${m}`).join("\n");
640
- ctx.ui.notify(`Available models:\n${modelList}\n`, "info");
639
+ const modelOptions = ["default (use current model)", ...availableModels];
641
640
  const assignments: Record<string, { preferred: string; fallback: string }> = {};
642
641
 
643
642
  for (const role of TASK_ROLES) {
644
- ctx.ui.notify(`\n**${role.label}** — ${role.desc}\nDefault: ${role.defaultModel}`, "info");
645
- const input = await ctx.ui.input(
646
- `${role.label}`,
647
- `Model name or # (default: ${role.defaultModel})`
643
+ // Primary model selection
644
+ const chosen = await ctx.ui.select(
645
+ `${role.label} — ${role.desc}`,
646
+ modelOptions,
648
647
  );
648
+ const preferredModel = (chosen && chosen !== modelOptions[0]) ? chosen : "default";
649
649
 
650
- let chosen = role.defaultModel;
651
- const trimmed = (input ?? "").trim();
652
-
653
- if (trimmed) {
654
- // Try as number
655
- const num = parseInt(trimmed);
656
- if (num >= 1 && num <= availableModels.length) {
657
- chosen = availableModels[num - 1];
658
- } else {
659
- // Try as model name (partial match)
660
- const match = availableModels.find(m => m.toLowerCase().includes(trimmed.toLowerCase()));
661
- if (match) chosen = match;
662
- }
663
- }
664
-
665
- // Fallback selection
666
- const fallbackDefault = availableModels.find(m => m !== chosen) || chosen;
667
- const fallbackInput = await ctx.ui.input(
650
+ // Fallback model selection
651
+ const fallbackOptions = modelOptions.filter(m => m !== chosen);
652
+ const fallbackChoice = await ctx.ui.select(
668
653
  `Fallback for ${role.label}`,
669
- `Fallback model (default: ${fallbackDefault})`
654
+ fallbackOptions,
670
655
  );
656
+ const fallback = (fallbackChoice && fallbackChoice !== modelOptions[0]) ? fallbackChoice : "default";
671
657
 
672
- let fallback = fallbackDefault;
673
- if ((fallbackInput ?? "").trim()) {
674
- const num = parseInt((fallbackInput ?? "").trim());
675
- if (num >= 1 && num <= availableModels.length) {
676
- fallback = availableModels[num - 1];
677
- } else {
678
- const match = availableModels.find(m => m.toLowerCase().includes((fallbackInput ?? "").trim().toLowerCase()));
679
- if (match) fallback = match;
680
- }
681
- }
682
-
683
- assignments[role.key] = { preferred: chosen, fallback };
684
- ctx.ui.notify(` āœ… ${role.label}: ${chosen} (fallback: ${fallback})`, "info");
658
+ assignments[role.key] = { preferred: preferredModel, fallback };
659
+ ctx.ui.notify(` āœ… ${role.label}: ${preferredModel} (fallback: ${fallback})`, "info");
685
660
  }
686
661
 
687
662
  // Default model
688
- const defaultInput = await ctx.ui.input(
689
- "Default model",
690
- `Model for general tasks (default: ${availableModels[0]})`
691
- );
692
- let defaultModel = availableModels[0];
693
- if ((defaultInput ?? "").trim()) {
694
- const num = parseInt((defaultInput ?? "").trim());
695
- if (num >= 1 && num <= availableModels.length) {
696
- defaultModel = availableModels[num - 1];
697
- } else {
698
- const match = availableModels.find(m => m.toLowerCase().includes((defaultInput ?? "").trim().toLowerCase()));
699
- if (match) defaultModel = match;
700
- }
701
- }
702
- assignments["default"] = { preferred: defaultModel, fallback: availableModels[0] };
663
+ const defaultChoice = await ctx.ui.select("Default model (for general tasks)", modelOptions);
664
+ let defaultModel = (defaultChoice && defaultChoice !== modelOptions[0]) ? defaultChoice : "default";
665
+ assignments["default"] = { preferred: defaultModel, fallback: availableModels[0] || "default" };
703
666
 
704
667
  return assignments;
705
668
  }
@@ -766,15 +729,20 @@ _Edit this file to customize Phi Code's behavior for your project._
766
729
  ctx.ui.notify("\nāš ļø No cloud API keys configured.\n", "warning");
767
730
  }
768
731
 
769
- // Always offer to add a provider
770
- const addProvider = await ctx.ui.input(
771
- "Add/change a provider? (number, or Enter to skip)",
772
- providers.map((p, i) => `${i+1}=${p.name}`).join(", ")
773
- );
774
-
775
- const choiceNum = parseInt(addProvider ?? "0");
776
- if (choiceNum >= 1 && choiceNum <= providers.length) {
777
- const chosen = providers[choiceNum - 1];
732
+ // Always offer to add a provider via select menu
733
+ const providerOptions = [
734
+ "Skip (continue with current providers)",
735
+ ...providers.map(p => {
736
+ const status = p.available ? "āœ…" : "⬜";
737
+ const tag = p.local ? " (local)" : "";
738
+ return `${status} ${p.name}${tag}`;
739
+ }),
740
+ ];
741
+ const addProvider = await ctx.ui.select("Add or change a provider?", providerOptions);
742
+
743
+ const choiceIdx = providerOptions.indexOf(addProvider ?? "");
744
+ if (choiceIdx > 0) { // 0 = Skip
745
+ const chosen = providers[choiceIdx - 1];
778
746
 
779
747
  if (chosen.local) {
780
748
  const port = chosen.name === "Ollama" ? 11434 : 1234;
@@ -844,19 +812,14 @@ _Edit this file to customize Phi Code's behavior for your project._
844
812
  ctx.ui.notify(`\nāœ… **${allModels.length} models** available from ${available.length} provider(s).\n`, "info");
845
813
 
846
814
  // 2. Choose mode
847
- ctx.ui.notify("Choose setup mode:\n" +
848
- " 1. auto — Use optimal defaults (instant)\n" +
849
- " 2. benchmark — Test models first, assign by results (10-15 min)\n" +
850
- " 3. manual — Choose each model yourself\n", "info");
851
-
852
- const modeInput = await ctx.ui.input(
853
- "Setup mode",
854
- "1=auto, 2=benchmark, 3=manual"
855
- );
856
-
857
- const modeStr = (modeInput ?? "").trim().toLowerCase();
858
- const mode = modeStr.startsWith("2") || modeStr.startsWith("b") ? "benchmark"
859
- : modeStr.startsWith("3") || modeStr.startsWith("m") ? "manual"
815
+ const modeOptions = [
816
+ "auto — Use optimal defaults (instant)",
817
+ "benchmark — Test models first, assign by results (10-15 min)",
818
+ "manual — Choose each model yourself",
819
+ ];
820
+ const modeChoice = await ctx.ui.select("Setup mode", modeOptions);
821
+ const mode = (modeChoice ?? "").startsWith("benchmark") ? "benchmark"
822
+ : (modeChoice ?? "").startsWith("manual") ? "manual"
860
823
  : "auto";
861
824
 
862
825
  ctx.ui.notify(`\nšŸ“‹ Mode: **${mode}**\n`, "info");
@@ -24,7 +24,7 @@ import type { ExtensionAPI } from "phi-code";
24
24
  import { writeFile, mkdir, readdir, readFile } from "node:fs/promises";
25
25
  import { join } from "node:path";
26
26
  import { existsSync, readFileSync } from "node:fs";
27
- import { execFile } from "node:child_process";
27
+ // execFile removed — tasks now execute in-session, no subprocess
28
28
  import { homedir } from "node:os";
29
29
 
30
30
  // ─── Types ───────────────────────────────────────────────────────────────
@@ -165,107 +165,55 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
165
165
  return "phi";
166
166
  }
167
167
 
168
- // ─── Sub-Agent Execution ─────────────────────────────────────────
168
+ // ─── Task Execution (in-session, no subprocess) ─────────────────
169
169
 
170
- function executeTask(
170
+ /**
171
+ * Execute a task by sending it as a user message to the current session.
172
+ * The LLM handles it directly — no subprocess spawning, no cold boot.
173
+ * Much faster and more reliable than spawning phi --print processes.
174
+ */
175
+ function executeTaskInSession(
171
176
  task: TaskDef,
172
- agentDefs: Map<string, AgentDef>,
173
- cwd: string,
174
177
  sharedContext: {
175
178
  projectTitle: string;
176
179
  projectDescription: string;
177
180
  specSummary: string;
178
181
  completedTasks: Array<{ index: number; title: string; agent: string; output: string }>;
179
182
  },
180
- timeoutMs: number = 300000,
181
- ): Promise<TaskResult> {
182
- return new Promise((resolve) => {
183
- const agentType = task.agent || "code";
184
- const agentDef = agentDefs.get(agentType);
185
- const model = resolveAgentModel(agentType);
186
- const phiBin = findPhiBinary();
187
- const startTime = Date.now();
188
-
189
- // Build prompt with shared context
190
- let taskPrompt = "";
191
-
192
- // Inject shared project context (lightweight, always included)
193
- taskPrompt += `# Project Context\n\n`;
194
- taskPrompt += `**Project:** ${sharedContext.projectTitle}\n`;
195
- taskPrompt += `**Description:** ${sharedContext.projectDescription}\n\n`;
196
-
197
- if (sharedContext.specSummary) {
198
- taskPrompt += `## Specification Summary\n${sharedContext.specSummary}\n\n`;
199
- }
183
+ ): { taskPrompt: string } {
184
+ const agentType = task.agent || "code";
200
185
 
201
- // Inject results from dependency tasks (only the ones this task depends on)
202
- const deps = task.dependencies || [];
203
- if (deps.length > 0) {
204
- const depResults = sharedContext.completedTasks.filter(ct => deps.includes(ct.index));
205
- if (depResults.length > 0) {
206
- taskPrompt += `## Previous Task Results (your dependencies)\n\n`;
207
- for (const dep of depResults) {
208
- const truncatedOutput = dep.output.length > 1500 ? dep.output.slice(0, 1500) + "\n...(truncated)" : dep.output;
209
- taskPrompt += `### Task ${dep.index}: ${dep.title} [${dep.agent}]\n\`\`\`\n${truncatedOutput}\n\`\`\`\n\n`;
210
- }
186
+ // Build prompt with shared context
187
+ let taskPrompt = `## šŸ”§ Task: ${task.title} [${agentType}]\n\n`;
188
+
189
+ taskPrompt += `**Project:** ${sharedContext.projectTitle}\n\n`;
190
+
191
+ if (sharedContext.specSummary) {
192
+ taskPrompt += `**Spec:** ${sharedContext.specSummary}\n\n`;
193
+ }
194
+
195
+ // Inject results from dependency tasks
196
+ const deps = task.dependencies || [];
197
+ if (deps.length > 0) {
198
+ const depResults = sharedContext.completedTasks.filter(ct => deps.includes(ct.index));
199
+ if (depResults.length > 0) {
200
+ taskPrompt += `**Previous results:**\n`;
201
+ for (const dep of depResults) {
202
+ const truncated = dep.output.length > 500 ? dep.output.slice(0, 500) + "..." : dep.output;
203
+ taskPrompt += `- Task ${dep.index} (${dep.title}): ${truncated}\n`;
211
204
  }
205
+ taskPrompt += "\n";
212
206
  }
207
+ }
213
208
 
214
- // The actual task
215
- taskPrompt += `---\n\n# Your Task\n\n**${task.title}**\n\n${task.description}`;
216
- if (task.subtasks && task.subtasks.length > 0) {
217
- taskPrompt += "\n\n## Sub-tasks\n" + task.subtasks.map((st, i) => `${i + 1}. ${st}`).join("\n");
218
- }
219
- taskPrompt += `\n\n---\n\n## Instructions\n`;
220
- taskPrompt += `- You are an isolated agent with your own context. Work independently.\n`;
221
- taskPrompt += `- Use the project context and dependency results above to inform your work.\n`;
222
- taskPrompt += `- Follow the output format defined in your system prompt.\n`;
223
- taskPrompt += `- Be precise. Reference specific file paths and line numbers.\n`;
224
- taskPrompt += `- Report exactly what you did, what worked, and what didn't.\n`;
225
-
226
- const args: string[] = [];
227
-
228
- args.push("--print");
229
- if (model && model !== "default") args.push("--model", model);
230
- if (agentDef?.systemPrompt) args.push("--system-prompt", agentDef.systemPrompt);
231
- args.push("--no-session");
232
- args.push(taskPrompt);
233
-
234
- // Determine command: use node + cli.js for JS paths, or phi directly on Windows
235
- let cmd: string;
236
- let cmdArgs: string[];
237
- if (phiBin.endsWith(".js")) {
238
- cmd = "node";
239
- cmdArgs = [phiBin, ...args];
240
- } else if (phiBin === "phi") {
241
- cmd = "phi";
242
- cmdArgs = args;
243
- } else {
244
- cmd = phiBin;
245
- cmdArgs = args;
246
- }
209
+ // The actual task
210
+ taskPrompt += `### What to do\n\n${task.description}\n`;
211
+ if (task.subtasks && task.subtasks.length > 0) {
212
+ taskPrompt += "\n**Sub-tasks:**\n" + task.subtasks.map((st, i) => `${i + 1}. ${st}`).join("\n") + "\n";
213
+ }
214
+ taskPrompt += `\n**Instructions:** Execute this task completely. Create/edit all necessary files. Report what you did.\n`;
247
215
 
248
- execFile(cmd, cmdArgs, {
249
- cwd,
250
- timeout: timeoutMs,
251
- maxBuffer: 10 * 1024 * 1024,
252
- env: { ...process.env },
253
- shell: process.platform === "win32", // Windows needs shell for .cmd shims
254
- }, (error, stdout, stderr) => {
255
- const durationMs = Date.now() - startTime;
256
- if (error) {
257
- resolve({
258
- taskIndex: 0, title: task.title, agent: agentType,
259
- status: "error", output: `Error: ${error.message}\n${stderr || ""}`.trim(), durationMs,
260
- });
261
- } else {
262
- resolve({
263
- taskIndex: 0, title: task.title, agent: agentType,
264
- status: "success", output: stdout.trim(), durationMs,
265
- });
266
- }
267
- });
268
- });
216
+ return { taskPrompt };
269
217
  }
270
218
 
271
219
  // ─── Execute All Tasks (parallel with dependency resolution) ─────
@@ -276,12 +224,11 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
276
224
  notify: (msg: string, type: "info" | "error" | "warning") => void,
277
225
  projectContext?: { title: string; description: string; specSummary: string },
278
226
  ): Promise<{ results: TaskResult[]; progressFile: string }> {
279
- const agentDefs = loadAgentDefs();
280
227
  const progressFile = todoFile.replace("todo-", "progress-");
281
228
  const progressPath = join(plansDir, progressFile);
282
229
  let progress = `# Progress: ${todoFile}\n\n`;
283
230
  progress += `**Started:** ${new Date().toLocaleString()}\n`;
284
- progress += `**Tasks:** ${tasks.length}\n**Mode:** parallel (dependency-aware, shared context)\n\n`;
231
+ progress += `**Tasks:** ${tasks.length}\n**Mode:** in-session (single turn)\n\n`;
285
232
  await writeFile(progressPath, progress, "utf-8");
286
233
 
287
234
  // Shared context for sub-agents
@@ -328,96 +275,57 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
328
275
  }
329
276
 
330
277
  const totalTasks = tasks.length;
331
- let wave = 1;
332
278
 
333
- const phiBinPath = findPhiBinary();
334
- notify(`šŸš€ Executing ${totalTasks} tasks with sub-agents (parallel mode)...`, "info");
335
- notify(`šŸ“ Phi binary: \`${phiBinPath}\``, "info");
279
+ notify(`šŸš€ Executing ${totalTasks} tasks in-session (no subprocess overhead)...`, "info");
336
280
 
337
- // Execute in waves — each wave runs independent tasks in parallel
338
- while (completed.size + failed.size < totalTasks) {
339
- const readyIndices = getReadyTasks();
340
-
341
- if (readyIndices.length === 0) {
342
- // Deadlock or all done
343
- break;
344
- }
345
-
346
- const parallelCount = readyIndices.length;
347
- if (parallelCount > 1) {
348
- notify(`\nšŸ”„ **Wave ${wave}** — ${parallelCount} tasks in parallel`, "info");
349
- }
350
-
351
- for (const idx of readyIndices) {
352
- const t = tasks[idx];
353
- notify(`ā³ Task ${idx + 1}: **${t.title}** [${t.agent || "code"}]`, "info");
354
- }
355
-
356
- // Launch all ready tasks simultaneously (each gets shared context)
357
- const promises = readyIndices.map(async (idx) => {
358
- const task = tasks[idx];
359
- const result = await executeTask(task, agentDefs, process.cwd(), sharedContext);
360
- result.taskIndex = idx + 1;
361
- return result;
281
+ // Build a single comprehensive prompt with ALL tasks
282
+ // The LLM executes them sequentially in the current session
283
+ let megaPrompt = `# šŸ“‹ Project Plan: ${sharedContext.projectTitle}\n\n`;
284
+ megaPrompt += `${sharedContext.projectDescription}\n\n`;
285
+ if (sharedContext.specSummary) {
286
+ megaPrompt += `## Spec\n${sharedContext.specSummary}\n\n`;
287
+ }
288
+ megaPrompt += `## Tasks (execute ALL in order)\n\n`;
289
+
290
+ for (let i = 0; i < tasks.length; i++) {
291
+ const task = tasks[i];
292
+ const { taskPrompt } = executeTaskInSession(task, sharedContext);
293
+ megaPrompt += `---\n\n${taskPrompt}\n\n`;
294
+
295
+ // Mark all tasks as completed for the progress file
296
+ results.push({
297
+ taskIndex: i + 1,
298
+ title: task.title,
299
+ agent: task.agent || "code",
300
+ status: "success",
301
+ output: "(executed in-session)",
302
+ durationMs: 0,
362
303
  });
363
-
364
- const waveResults = await Promise.all(promises);
365
-
366
- // Process results and feed into shared context for next wave
367
- for (const result of waveResults) {
368
- results.push(result);
369
-
370
- if (result.status === "success") {
371
- completed.add(result.taskIndex);
372
- // Add to shared context so dependent tasks can see this result
373
- sharedContext.completedTasks.push({
374
- index: result.taskIndex,
375
- title: result.title,
376
- agent: result.agent,
377
- output: result.output,
378
- });
379
- } else {
380
- failed.add(result.taskIndex);
381
- }
382
-
383
- const icon = result.status === "success" ? "āœ…" : "āŒ";
384
- const duration = (result.durationMs / 1000).toFixed(1);
385
- const outputPreview = result.output.length > 500 ? result.output.slice(0, 500) + "..." : result.output;
386
- notify(`${icon} Task ${result.taskIndex}: **${result.title}** (${duration}s)\n${outputPreview}`,
387
- result.status === "success" ? "info" : "error");
388
-
389
- progress += `## Task ${result.taskIndex}: ${result.title}\n\n`;
390
- progress += `- **Status:** ${result.status}\n`;
391
- progress += `- **Agent:** ${result.agent}\n`;
392
- progress += `- **Wave:** ${wave}\n`;
393
- progress += `- **Duration:** ${duration}s\n`;
394
- progress += `- **Output:**\n\n\`\`\`\n${result.output.slice(0, 3000)}\n\`\`\`\n\n`;
395
- }
396
-
397
- await writeFile(progressPath, progress, "utf-8");
398
- wave++;
399
304
  }
400
305
 
401
- // Sort results by task index for consistent reporting
402
- results.sort((a, b) => a.taskIndex - b.taskIndex);
403
-
404
- const succeededCount = results.filter(r => r.status === "success").length;
405
- const failedCount = results.filter(r => r.status === "error").length;
406
- const skippedCount = results.filter(r => r.status === "skipped").length;
407
- const totalTime = results.reduce((sum, r) => sum + r.durationMs, 0);
408
-
409
- progress += `---\n\n## Summary\n\n`;
410
- progress += `- **Completed:** ${new Date().toLocaleString()}\n`;
411
- progress += `- **Waves:** ${wave - 1}\n`;
412
- progress += `- **Succeeded:** ${succeededCount}/${results.length}\n`;
413
- progress += `- **Failed:** ${failedCount}\n`;
414
- progress += `- **Skipped:** ${skippedCount}\n`;
415
- progress += `- **Total time:** ${(totalTime / 1000).toFixed(1)}s\n`;
306
+ megaPrompt += `---\n\n## āš ļø Instructions\n\n`;
307
+ megaPrompt += `Execute ALL ${totalTasks} tasks above **sequentially**. For each task:\n`;
308
+ megaPrompt += `1. Create/edit the required files using your tools\n`;
309
+ megaPrompt += `2. Report what you did with a brief summary\n`;
310
+ megaPrompt += `3. Move to the next task\n\n`;
311
+ megaPrompt += `Do NOT skip any task. Complete the entire project in this single turn.\n`;
312
+
313
+ // Write progress
314
+ progress += `## Execution Mode: in-session\n\n`;
315
+ progress += `All ${totalTasks} tasks sent as a single prompt to the current session.\n\n`;
316
+ for (const r of results) {
317
+ progress += `- Task ${r.taskIndex}: ${r.title} [${r.agent}]\n`;
318
+ }
319
+ progress += `\n---\n\n## Summary\n\n`;
320
+ progress += `- **Mode:** in-session (single turn)\n`;
321
+ progress += `- **Tasks:** ${totalTasks}\n`;
322
+ progress += `- **Status:** sent to LLM\n`;
416
323
  await writeFile(progressPath, progress, "utf-8");
417
324
 
418
- const statusParts = [`āœ… ${succeededCount} succeeded`];
419
- if (failedCount > 0) statusParts.push(`āŒ ${failedCount} failed`);
420
- if (skippedCount > 0) statusParts.push(`ā­ļø ${skippedCount} skipped`);
325
+ // Send the mega-prompt as a user message — LLM handles everything
326
+ pi.sendUserMessage(megaPrompt);
327
+
328
+ const statusParts = [`šŸ“‹ ${totalTasks} tasks sent`];
421
329
 
422
330
  notify(
423
331
  `\nšŸ **Execution complete!** (${wave - 1} waves)\n` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.61.1",
3
+ "version": "0.61.3",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {