@supatest/cli 0.0.17 → 0.0.19

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.
Files changed (2) hide show
  1. package/dist/index.js +474 -309
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -371,35 +371,21 @@ var init_planner = __esm({
371
371
  "src/prompts/planner.ts"() {
372
372
  "use strict";
373
373
  plannerPrompt = `<role>
374
- You are a Senior QA Engineer planning E2E tests called, Supatest AI. You think in terms of user value and business risk, not code coverage. Your job is to identify the minimum set of tests that provide maximum confidence.
374
+ You are Supatest AI, a Senior QA Engineer planning E2E tests. You think in user journeys and business risk, not code coverage. Your job: minimum tests for maximum confidence.
375
375
  </role>
376
376
 
377
- <context>
378
- E2E tests are expensive: slow to run, prone to flakiness, and costly to maintain. Every test you recommend must justify its existence. The goal is confidence with minimal overhead.
379
- </context>
380
-
381
377
  <core_principles>
382
- Before planning ANY test, ask yourself:
383
-
384
- 1. **"What user journey does this protect?"**
385
- Tests should map to real user workflows, not UI components.
386
- Bad: "Test that the submit button exists"
387
- Good: "Test that a user can complete checkout"
388
-
389
- 2. **"What's the risk if this breaks?"**
390
- Assess: Likelihood of breaking \xD7 Business impact if broken
391
- - High risk (auth, payments, core workflows) \u2192 Thorough coverage
392
- - Medium risk (secondary features) \u2192 Happy path only
393
- - Low risk (read-only, static, informational) \u2192 Smoke test or skip
394
-
395
- 3. **"Would a user notice if this breaks?"**
396
- If no user would notice or care, don't write the test.
397
-
398
- 4. **"Can one test cover this journey?"**
399
- Prefer ONE test that completes a full user journey over MANY tests that check individual elements. Tests that always pass/fail together should be one test.
400
-
401
- 5. **"What's the maintenance cost?"**
402
- Every selector is a potential break point. Every test is code to maintain. Minimize both.
378
+ Before planning ANY test, ask:
379
+ 1. "What user journey does this protect?" - Test workflows, not UI components
380
+ 2. "What's the risk if this breaks?" - High risk \u2192 thorough; Low risk \u2192 smoke test or skip
381
+ 3. "Would a user notice?" - If no, don't test it
382
+ 4. "Can one test cover this?" - Prefer ONE journey test over MANY element tests
383
+ 5. "What's the maintenance cost?" - Every selector is a break point
384
+
385
+ Risk levels:
386
+ - **High** (auth, payments, data mutations, core workflows) \u2192 Thorough coverage
387
+ - **Medium** (forms, navigation, search) \u2192 Happy path only
388
+ - **Low** (read-only dashboards, static pages) \u2192 Single smoke test or skip
403
389
  </core_principles>
404
390
 
405
391
  <code_first>
@@ -408,169 +394,38 @@ Before planning ANY test, ask yourself:
408
394
  2. Read the implementation
409
395
  3. Check conditionals, handlers, and data flow
410
396
 
411
- Only ask about undefined business logic or incomplete implementations (TODOs).
397
+ Only ask about undefined business logic or incomplete implementations.
412
398
  Never ask about routing, data scope, UI interactions, empty states, or error handling - these are in the code.
413
399
  </code_first>
414
400
 
415
- <risk_assessment>
416
- Categorize features before planning tests:
417
-
418
- **High Risk** (thorough testing):
419
- - Authentication and authorization
420
- - Payment processing
421
- - Data mutations (create, update, delete)
422
- - Business-critical workflows
423
- - Features with complex conditional logic
424
-
425
- **Medium Risk** (key paths only):
426
- - Forms with validation
427
- - Interactive features
428
- - Navigation flows
429
- - Search and filtering
430
-
431
- **Low Risk** (smoke test or skip):
432
- - Read-only dashboards
433
- - Static content pages
434
- - Informational displays
435
- - Admin-only features with low usage
436
- </risk_assessment>
437
-
438
- <planning_process>
439
- When analyzing a feature, think through:
440
-
441
- 1. What is this feature's purpose from the user's perspective?
442
- 2. What are the critical user journeys?
443
- 3. What's the risk level? (high/medium/low)
444
- 4. What's the minimum test set that catches meaningful regressions?
445
- 5. What should explicitly NOT be tested (and why)?
446
-
447
- Then provide your plan.
448
- </planning_process>
449
-
450
401
  <output_format>
451
- Structure your test plan as:
452
-
453
- **Summary**: One paragraph explaining what user flows are being tested and why they matter.
454
-
455
- **Risk Assessment**: Feature risk level (high/medium/low) with justification.
456
-
457
- **User Journeys**: List each critical user journey to test.
458
- Format: "User can [action] to [achieve goal]"
459
-
460
- **Test Cases**: For each test include:
461
- - Name (action-oriented, e.g., "completes checkout with valid payment")
462
- - User journey it protects
463
- - Key assertions (what user-visible outcomes to verify)
464
- - Test data needs
465
-
466
- **Not Testing**: What you're deliberately NOT testing and why. This demonstrates senior judgment.
467
-
468
- **Flakiness Risks**: Potential concerns and mitigation strategies.
402
+ **Risk Assessment**: [HIGH/MEDIUM/LOW] - one line justification
403
+ **User Journeys**: "User can [action] to [achieve goal]"
404
+ **Test Cases**: Name, assertions, test data needs
405
+ **Not Testing**: What you're skipping and why (shows judgment)
469
406
  </output_format>
470
407
 
471
- <examples>
472
- <example_good>
473
- <scenario>Read-only analytics dashboard showing charts and metrics</scenario>
474
- <analysis>
475
- This is a read-only dashboard. Risk level: LOW.
476
- - No data mutations
477
- - No user inputs
478
- - Breaking this wouldn't block any workflows
479
- - Users would notice if completely broken, but not minor visual issues
408
+ <example>
409
+ **Scenario**: Read-only analytics dashboard
480
410
 
481
- Minimum confidence needed: Page loads and shows data.
482
- </analysis>
483
- <plan>
484
- **Summary**: Single smoke test verifying the dashboard loads and displays its primary sections. This is a read-only view with no user interactions beyond viewing.
411
+ **Risk Assessment**: LOW - Read-only display, no mutations, no business-critical actions
485
412
 
486
- **Risk Assessment**: LOW - Read-only display, no business-critical actions, no data mutations.
413
+ **User Journeys**: User can view their analytics dashboard
487
414
 
488
415
  **Test Cases**:
489
- 1. "displays dashboard with analytics data"
490
- - Journey: User views their analytics
491
- - Assertions: Page loads, primary chart visible, at least one metric displayed
492
- - Data: Any user with historical data
493
-
494
- **Not Testing**:
495
- - Individual chart rendering details (implementation, not user value)
496
- - Specific metric calculations (unit test territory)
497
- - Tooltip interactions (low risk, visual detail)
498
- - Responsive layouts (unless specifically required)
499
- </plan>
500
- </example_good>
501
-
502
- <example_good>
503
- <scenario>E-commerce checkout flow</scenario>
504
- <analysis>
505
- This is the checkout flow. Risk level: HIGH.
506
- - Direct revenue impact if broken
507
- - Handles payment data
508
- - Multiple steps with validation
509
- - Users absolutely notice if this breaks
510
-
511
- This needs thorough coverage of the happy path and critical error states.
512
- </analysis>
513
- <plan>
514
- **Summary**: Comprehensive checkout flow testing covering the complete purchase journey and critical failure modes. This is the highest-risk flow in the application.
515
-
516
- **Risk Assessment**: HIGH - Revenue-critical, payment processing, user trust, multiple integration points.
517
-
518
- **User Journeys**:
519
- 1. User can complete a purchase with valid payment
520
- 2. User receives clear feedback when payment fails
521
- 3. User can modify cart during checkout
416
+ 1. "displays dashboard with data" - Page loads, chart visible, metrics shown
522
417
 
523
- **Test Cases**:
524
- 1. "completes purchase with valid credit card"
525
- - Journey: Full checkout happy path
526
- - Assertions: Order confirmation shown, order ID generated, confirmation email referenced
527
- - Data: Test user, test product, test card (4242...)
528
-
529
- 2. "shows clear error for declined card"
530
- - Journey: Payment failure recovery
531
- - Assertions: User-friendly error message, can retry, cart preserved
532
- - Data: Test user, decline test card
533
-
534
- 3. "preserves cart when returning to edit"
535
- - Journey: Cart modification mid-checkout
536
- - Assertions: Items retained, quantities correct, can proceed again
537
- - Data: Test user, multiple products
538
-
539
- **Not Testing**:
540
- - Every validation message (covered by unit tests)
541
- - Every payment provider error code (too many permutations)
542
- - Address autocomplete (third-party, low impact)
543
- </plan>
544
- </example_good>
545
-
546
- <example_bad>
547
- <scenario>Read-only dashboard - OVER-ENGINEERED</scenario>
548
- <what_went_wrong>
549
- This planner created 30 tests for a simple read-only dashboard:
550
- - 4 tests for "page load and layout"
551
- - 4 tests for "metric cards display" (one per card)
552
- - 5 tests for "chart interactions"
553
- - Separate tests for loading states, empty states, each tooltip
554
-
555
- Problems:
556
- 1. Tests implementation details, not user value
557
- 2. 30 tests = 30 maintenance points for a low-risk feature
558
- 3. Tests that always pass/fail together should be ONE test
559
- 4. No risk assessment was performed
560
- 5. "Loading skeleton displays" is not a user journey
561
- </what_went_wrong>
562
- </example_bad>
563
- </examples>
418
+ **Not Testing**: Individual chart details, tooltip interactions, loading skeletons (implementation details, not user value)
419
+ </example>
564
420
 
565
421
  <constraints>
566
- - You can ONLY use read-only tools: Read, Glob, Grep, Task
567
- - Do NOT write tests, modify files, or run commands
568
- - Focus on research and planning, not implementation
569
- - Present findings for user review before any test writing
422
+ - ONLY use read-only tools: Read, Glob, Grep, Task
423
+ - Do NOT write tests or modify files
424
+ - Present findings for user review before implementation
570
425
  </constraints>
571
426
 
572
427
  <golden_rule>
573
- The best test plan isn't the one with the most tests\u2014it's the one that catches meaningful regressions with the minimum maintenance burden.
428
+ The best test plan catches meaningful regressions with minimum maintenance burden. One good journey test beats ten shallow element tests.
574
429
  </golden_rule>`;
575
430
  }
576
431
  });
@@ -4044,18 +3899,187 @@ var init_setup = __esm({
4044
3899
  }
4045
3900
  });
4046
3901
 
3902
+ // src/utils/command-discovery.ts
3903
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
3904
+ import { join, relative } from "path";
3905
+ function parseMarkdownFrontmatter(content) {
3906
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
3907
+ const match = content.match(frontmatterRegex);
3908
+ if (!match) {
3909
+ return { frontmatter: {}, body: content };
3910
+ }
3911
+ const [, frontmatterStr, body] = match;
3912
+ const frontmatter = {};
3913
+ for (const line of frontmatterStr.split("\n")) {
3914
+ const colonIndex = line.indexOf(":");
3915
+ if (colonIndex > 0) {
3916
+ const key = line.slice(0, colonIndex).trim();
3917
+ const value = line.slice(colonIndex + 1).trim();
3918
+ frontmatter[key] = value;
3919
+ }
3920
+ }
3921
+ return { frontmatter, body };
3922
+ }
3923
+ function discoverMarkdownFiles(dir, baseDir, files = []) {
3924
+ if (!existsSync(dir)) {
3925
+ return files;
3926
+ }
3927
+ const entries = readdirSync(dir);
3928
+ for (const entry of entries) {
3929
+ const fullPath = join(dir, entry);
3930
+ const stat = statSync(fullPath);
3931
+ if (stat.isDirectory()) {
3932
+ discoverMarkdownFiles(fullPath, baseDir, files);
3933
+ } else if (entry.endsWith(".md")) {
3934
+ files.push(fullPath);
3935
+ }
3936
+ }
3937
+ return files;
3938
+ }
3939
+ function discoverCommands(cwd) {
3940
+ const commandsDir = join(cwd, ".supatest", "commands");
3941
+ if (!existsSync(commandsDir)) {
3942
+ return [];
3943
+ }
3944
+ const files = discoverMarkdownFiles(commandsDir, commandsDir);
3945
+ const commands = [];
3946
+ for (const filePath of files) {
3947
+ try {
3948
+ const content = readFileSync(filePath, "utf-8");
3949
+ const { frontmatter } = parseMarkdownFrontmatter(content);
3950
+ const relativePath = relative(commandsDir, filePath);
3951
+ const name = relativePath.replace(/\.md$/, "").replace(/\//g, ".").replace(/\\/g, ".");
3952
+ commands.push({
3953
+ name,
3954
+ description: frontmatter.description,
3955
+ filePath
3956
+ });
3957
+ } catch {
3958
+ }
3959
+ }
3960
+ return commands.sort((a, b2) => a.name.localeCompare(b2.name));
3961
+ }
3962
+ function expandCommand(cwd, commandName, args) {
3963
+ const commandsDir = join(cwd, ".supatest", "commands");
3964
+ const relativePath = commandName.replace(/\./g, "/") + ".md";
3965
+ const filePath = join(commandsDir, relativePath);
3966
+ if (!existsSync(filePath)) {
3967
+ return null;
3968
+ }
3969
+ try {
3970
+ const content = readFileSync(filePath, "utf-8");
3971
+ const { body } = parseMarkdownFrontmatter(content);
3972
+ let expanded = body;
3973
+ if (args) {
3974
+ expanded = expanded.replace(/\$ARGUMENTS/g, args);
3975
+ const argParts = args.split(/\s+/);
3976
+ for (let i = 0; i < argParts.length; i++) {
3977
+ expanded = expanded.replace(new RegExp(`\\$${i + 1}`, "g"), argParts[i]);
3978
+ }
3979
+ } else {
3980
+ expanded = expanded.replace(/\$ARGUMENTS/g, "");
3981
+ }
3982
+ return expanded.trim();
3983
+ } catch {
3984
+ return null;
3985
+ }
3986
+ }
3987
+ function discoverAgents(cwd) {
3988
+ const agentsDir = join(cwd, ".supatest", "agents");
3989
+ if (!existsSync(agentsDir)) {
3990
+ return [];
3991
+ }
3992
+ const files = discoverMarkdownFiles(agentsDir, agentsDir);
3993
+ const agents = [];
3994
+ for (const filePath of files) {
3995
+ try {
3996
+ const content = readFileSync(filePath, "utf-8");
3997
+ const { frontmatter } = parseMarkdownFrontmatter(content);
3998
+ const relativePath = relative(agentsDir, filePath);
3999
+ const defaultName = relativePath.replace(/\.md$/, "").replace(/\//g, "-").replace(/\\/g, "-");
4000
+ agents.push({
4001
+ name: frontmatter.name || defaultName,
4002
+ description: frontmatter.description,
4003
+ model: frontmatter.model,
4004
+ filePath
4005
+ });
4006
+ } catch {
4007
+ }
4008
+ }
4009
+ return agents.sort((a, b2) => a.name.localeCompare(b2.name));
4010
+ }
4011
+ var init_command_discovery = __esm({
4012
+ "src/utils/command-discovery.ts"() {
4013
+ "use strict";
4014
+ }
4015
+ });
4016
+
4017
+ // src/utils/mcp-loader.ts
4018
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
4019
+ import { join as join2 } from "path";
4020
+ function expandEnvVar(value) {
4021
+ return value.replace(/\$\{([^}]+)\}/g, (_2, expr) => {
4022
+ const [varName, defaultValue] = expr.split(":-");
4023
+ return process.env[varName] ?? defaultValue ?? "";
4024
+ });
4025
+ }
4026
+ function expandServerConfig(config2) {
4027
+ const expanded = {
4028
+ command: expandEnvVar(config2.command)
4029
+ };
4030
+ if (config2.args) {
4031
+ expanded.args = config2.args.map(expandEnvVar);
4032
+ }
4033
+ if (config2.env) {
4034
+ expanded.env = {};
4035
+ for (const [key, value] of Object.entries(config2.env)) {
4036
+ expanded.env[key] = expandEnvVar(value);
4037
+ }
4038
+ }
4039
+ return expanded;
4040
+ }
4041
+ function loadMcpServers(cwd) {
4042
+ const mcpPath = join2(cwd, ".supatest", "mcp.json");
4043
+ if (!existsSync2(mcpPath)) {
4044
+ return {};
4045
+ }
4046
+ try {
4047
+ const content = readFileSync2(mcpPath, "utf-8");
4048
+ const config2 = JSON.parse(content);
4049
+ if (!config2.mcpServers) {
4050
+ return {};
4051
+ }
4052
+ const expanded = {};
4053
+ for (const [name, serverConfig] of Object.entries(config2.mcpServers)) {
4054
+ expanded[name] = expandServerConfig(serverConfig);
4055
+ }
4056
+ return expanded;
4057
+ } catch (error) {
4058
+ console.warn(
4059
+ `Warning: Failed to load MCP servers from ${mcpPath}:`,
4060
+ error instanceof Error ? error.message : String(error)
4061
+ );
4062
+ return {};
4063
+ }
4064
+ }
4065
+ var init_mcp_loader = __esm({
4066
+ "src/utils/mcp-loader.ts"() {
4067
+ "use strict";
4068
+ }
4069
+ });
4070
+
4047
4071
  // src/utils/project-instructions.ts
4048
- import { existsSync, readFileSync } from "fs";
4049
- import { join } from "path";
4072
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4073
+ import { join as join3 } from "path";
4050
4074
  function loadProjectInstructions(cwd) {
4051
4075
  const paths = [
4052
- join(cwd, "SUPATEST.md"),
4053
- join(cwd, ".supatest", "SUPATEST.md")
4076
+ join3(cwd, "SUPATEST.md"),
4077
+ join3(cwd, ".supatest", "SUPATEST.md")
4054
4078
  ];
4055
4079
  for (const path5 of paths) {
4056
- if (existsSync(path5)) {
4080
+ if (existsSync3(path5)) {
4057
4081
  try {
4058
- return readFileSync(path5, "utf-8");
4082
+ return readFileSync3(path5, "utf-8");
4059
4083
  } catch {
4060
4084
  }
4061
4085
  }
@@ -4070,14 +4094,15 @@ var init_project_instructions = __esm({
4070
4094
 
4071
4095
  // src/core/agent.ts
4072
4096
  import { createRequire } from "module";
4073
- import { homedir } from "os";
4074
- import { dirname, join as join2 } from "path";
4097
+ import { dirname, join as join4 } from "path";
4075
4098
  import { query } from "@anthropic-ai/claude-agent-sdk";
4076
4099
  var CoreAgent;
4077
4100
  var init_agent = __esm({
4078
4101
  "src/core/agent.ts"() {
4079
4102
  "use strict";
4080
4103
  init_config();
4104
+ init_command_discovery();
4105
+ init_mcp_loader();
4081
4106
  init_project_instructions();
4082
4107
  CoreAgent = class {
4083
4108
  presenter;
@@ -4110,13 +4135,29 @@ ${config2.logs}
4110
4135
  const isPlanMode = config2.mode === "plan";
4111
4136
  const cwd = config2.cwd || process.cwd();
4112
4137
  const projectInstructions = loadProjectInstructions(cwd);
4138
+ const customAgents = discoverAgents(cwd);
4139
+ let customAgentsPrompt;
4140
+ if (customAgents.length > 0) {
4141
+ const agentList = customAgents.map((agent) => {
4142
+ const modelInfo = agent.model ? ` (model: ${agent.model})` : "";
4143
+ return `- **${agent.name}**${modelInfo}: ${agent.description || "No description"}`;
4144
+ }).join("\n");
4145
+ customAgentsPrompt = `
4146
+
4147
+ # Custom Sub-Agents (from .supatest/agents/)
4148
+
4149
+ The following custom sub-agents are available via the Task tool. Use them by setting subagent_type to the agent name:
4150
+
4151
+ ${agentList}`;
4152
+ }
4113
4153
  const systemPromptAppend = [
4114
4154
  config2.systemPromptAppend,
4115
4155
  projectInstructions && `
4116
4156
 
4117
4157
  # Project Instructions (from SUPATEST.md)
4118
4158
 
4119
- ${projectInstructions}`
4159
+ ${projectInstructions}`,
4160
+ customAgentsPrompt
4120
4161
  ].filter(Boolean).join("\n") || void 0;
4121
4162
  const cleanEnv = {};
4122
4163
  const excludeKeys = /* @__PURE__ */ new Set([
@@ -4131,8 +4172,8 @@ ${projectInstructions}`
4131
4172
  cleanEnv[key] = value;
4132
4173
  }
4133
4174
  }
4134
- const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join2(homedir(), ".supatest", "claude-config");
4135
- cleanEnv.CLAUDE_CONFIG_DIR = claudeConfigDir;
4175
+ const projectConfigDir = join4(cwd, ".supatest");
4176
+ cleanEnv.CLAUDE_CONFIG_DIR = projectConfigDir;
4136
4177
  cleanEnv.ANTHROPIC_API_KEY = config2.supatestApiKey || "";
4137
4178
  cleanEnv.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || "";
4138
4179
  cleanEnv.ANTHROPIC_AUTH_TOKEN = "";
@@ -4150,12 +4191,21 @@ ${projectInstructions}`
4150
4191
  includePartialMessages: true,
4151
4192
  executable: "node",
4152
4193
  // MCP servers for enhanced capabilities
4153
- mcpServers: {
4154
- playwright: {
4155
- command: "npx",
4156
- args: ["-y", "@playwright/mcp@latest"]
4157
- }
4158
- },
4194
+ // User-defined servers from .supatest/mcp.json can override defaults
4195
+ mcpServers: (() => {
4196
+ const userServers = loadMcpServers(cwd);
4197
+ const allServers = {
4198
+ // Default Playwright MCP server for browser automation
4199
+ playwright: {
4200
+ command: "npx",
4201
+ args: ["-y", "@playwright/mcp@latest"]
4202
+ },
4203
+ // User-defined servers override defaults (spread after)
4204
+ ...userServers
4205
+ };
4206
+ this.presenter.onLog(`MCP servers loaded: ${Object.keys(allServers).join(", ")}`);
4207
+ return allServers;
4208
+ })(),
4159
4209
  // Resume from previous session if providerSessionId is provided
4160
4210
  // This allows the agent to continue conversations with full context
4161
4211
  // Note: Sessions expire after ~30 days due to Anthropic's data retention policy
@@ -4343,7 +4393,7 @@ ${projectInstructions}`
4343
4393
  async resolveClaudeCodePath() {
4344
4394
  const fs4 = await import("fs/promises");
4345
4395
  let claudeCodePath;
4346
- const bundledPath = join2(dirname(import.meta.url.replace("file://", "")), "claude-code-cli.js");
4396
+ const bundledPath = join4(dirname(import.meta.url.replace("file://", "")), "claude-code-cli.js");
4347
4397
  try {
4348
4398
  await fs4.access(bundledPath);
4349
4399
  claudeCodePath = bundledPath;
@@ -4351,7 +4401,7 @@ ${projectInstructions}`
4351
4401
  } catch {
4352
4402
  const require2 = createRequire(import.meta.url);
4353
4403
  const sdkPath = require2.resolve("@anthropic-ai/claude-agent-sdk/sdk.mjs");
4354
- claudeCodePath = join2(dirname(sdkPath), "cli.js");
4404
+ claudeCodePath = join4(dirname(sdkPath), "cli.js");
4355
4405
  this.presenter.onLog(`Development mode: ${claudeCodePath}`);
4356
4406
  }
4357
4407
  if (config.claudeCodeExecutablePath) {
@@ -4890,7 +4940,7 @@ var CLI_VERSION;
4890
4940
  var init_version = __esm({
4891
4941
  "src/version.ts"() {
4892
4942
  "use strict";
4893
- CLI_VERSION = "0.0.17";
4943
+ CLI_VERSION = "0.0.19";
4894
4944
  }
4895
4945
  });
4896
4946
 
@@ -4964,21 +5014,21 @@ var init_encryption = __esm({
4964
5014
  });
4965
5015
 
4966
5016
  // src/utils/token-storage.ts
4967
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
4968
- import { homedir as homedir2 } from "os";
4969
- import { join as join4 } from "path";
5017
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, unlinkSync, writeFileSync } from "fs";
5018
+ import { homedir } from "os";
5019
+ import { join as join6 } from "path";
4970
5020
  function getTokenFilePath() {
4971
5021
  const apiUrl = process.env.SUPATEST_API_URL || PRODUCTION_API_URL;
4972
5022
  if (apiUrl === PRODUCTION_API_URL) {
4973
- return join4(CONFIG_DIR, "token.json");
5023
+ return join6(CONFIG_DIR, "token.json");
4974
5024
  }
4975
- return join4(CONFIG_DIR, "token.local.json");
5025
+ return join6(CONFIG_DIR, "token.local.json");
4976
5026
  }
4977
5027
  function isV2Format(stored) {
4978
5028
  return "version" in stored && stored.version === 2;
4979
5029
  }
4980
5030
  function ensureConfigDir() {
4981
- if (!existsSync2(CONFIG_DIR)) {
5031
+ if (!existsSync4(CONFIG_DIR)) {
4982
5032
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
4983
5033
  }
4984
5034
  }
@@ -4998,11 +5048,11 @@ function saveToken(token, expiresAt) {
4998
5048
  }
4999
5049
  function loadToken() {
5000
5050
  const tokenFile = getTokenFilePath();
5001
- if (!existsSync2(tokenFile)) {
5051
+ if (!existsSync4(tokenFile)) {
5002
5052
  return null;
5003
5053
  }
5004
5054
  try {
5005
- const data = readFileSync2(tokenFile, "utf8");
5055
+ const data = readFileSync4(tokenFile, "utf8");
5006
5056
  const stored = JSON.parse(data);
5007
5057
  let payload;
5008
5058
  if (isV2Format(stored)) {
@@ -5031,7 +5081,7 @@ function loadToken() {
5031
5081
  }
5032
5082
  function removeToken() {
5033
5083
  const tokenFile = getTokenFilePath();
5034
- if (existsSync2(tokenFile)) {
5084
+ if (existsSync4(tokenFile)) {
5035
5085
  unlinkSync(tokenFile);
5036
5086
  }
5037
5087
  }
@@ -5040,10 +5090,10 @@ var init_token_storage = __esm({
5040
5090
  "src/utils/token-storage.ts"() {
5041
5091
  "use strict";
5042
5092
  init_encryption();
5043
- CONFIG_DIR = join4(homedir2(), ".supatest");
5093
+ CONFIG_DIR = join6(homedir(), ".supatest");
5044
5094
  PRODUCTION_API_URL = "https://code-api.supatest.ai";
5045
5095
  STORAGE_VERSION = 2;
5046
- TOKEN_FILE = join4(CONFIG_DIR, "token.json");
5096
+ TOKEN_FILE = join6(CONFIG_DIR, "token.json");
5047
5097
  }
5048
5098
  });
5049
5099
 
@@ -5201,6 +5251,10 @@ var init_react = __esm({
5201
5251
  todos
5202
5252
  });
5203
5253
  }
5254
+ } else if (tool === "ExitPlanMode") {
5255
+ this.callbacks.onExitPlanMode?.();
5256
+ } else if (tool === "EnterPlanMode") {
5257
+ this.callbacks.onEnterPlanMode?.();
5204
5258
  }
5205
5259
  const toolUseEvent = {
5206
5260
  type: "tool_use",
@@ -6707,13 +6761,20 @@ var init_FeedbackDialog = __esm({
6707
6761
 
6708
6762
  // src/ui/components/HelpMenu.tsx
6709
6763
  import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
6710
- import React5 from "react";
6764
+ import React5, { useEffect as useEffect4, useState as useState3 } from "react";
6711
6765
  var HelpMenu;
6712
6766
  var init_HelpMenu = __esm({
6713
6767
  "src/ui/components/HelpMenu.tsx"() {
6714
6768
  "use strict";
6769
+ init_command_discovery();
6715
6770
  init_theme();
6716
- HelpMenu = ({ isAuthenticated, onClose }) => {
6771
+ HelpMenu = ({ isAuthenticated, onClose, cwd }) => {
6772
+ const [customCommands, setCustomCommands] = useState3([]);
6773
+ useEffect4(() => {
6774
+ const projectDir = cwd || process.cwd();
6775
+ const commands = discoverCommands(projectDir);
6776
+ setCustomCommands(commands);
6777
+ }, [cwd]);
6717
6778
  useInput2((input, key) => {
6718
6779
  if (key.escape || input === "q" || input === "?" || key.ctrl && input === "h") {
6719
6780
  onClose();
@@ -6732,15 +6793,16 @@ var init_HelpMenu = __esm({
6732
6793
  /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }),
6733
6794
  /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: theme.text.secondary }, "Slash Commands:"),
6734
6795
  /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/help"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " or "), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/?"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Toggle this help menu")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/resume"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Resume a previous session")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/clear"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Clear message history")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/model"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Cycle through available models")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/setup"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Initial setup for Supatest CLI")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/feedback"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Report an issue or request a feature")), isAuthenticated ? /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/logout"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Log out of Supatest")) : /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/login"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Authenticate with Supatest")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/exit"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Exit the CLI"))),
6796
+ customCommands.length > 0 && /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }), /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: theme.text.secondary }, "Project Commands:"), /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, customCommands.slice(0, 5).map((cmd) => /* @__PURE__ */ React5.createElement(Text4, { key: cmd.name }, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "/", cmd.name), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, cmd.description ? ` - ${cmd.description}` : ""))), customCommands.length > 5 && /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "...and ", customCommands.length - 5, " more (use Tab to autocomplete)"))),
6735
6797
  /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }),
6736
6798
  /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: theme.text.secondary }, "Keyboard Shortcuts:"),
6737
- /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "?"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " ", "- Toggle help (when input is empty)")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+H"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Toggle help")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+C"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " ", "- Exit (or clear input if not empty)")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+D"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Exit immediately")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+L"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Clear terminal screen")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+U"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Clear current input line")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "ESC"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Interrupt running agent")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Shift+Up/Down"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Scroll through messages")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Shift+Enter"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Add new line in input")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "ctrl+o"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Toggle tool outputs"))),
6799
+ /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "?"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " ", "- Toggle help (when input is empty)")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+H"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Toggle help")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+C"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " ", "- Exit (or clear input if not empty)")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+D"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Exit immediately")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+L"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Clear terminal screen")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+U"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Clear current input line")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "ESC"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Interrupt running agent")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Shift+Enter"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Add new line in input")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "ctrl+o"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Toggle tool outputs")), /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "Ctrl+M"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " - Cycle through models"))),
6738
6800
  /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }),
6739
6801
  /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: theme.text.secondary }, "File References:"),
6740
6802
  /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, /* @__PURE__ */ React5.createElement(Text4, null, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.accent }, "@filename"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, " ", "- Reference a file (autocomplete with Tab)")), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, 'Example: "Fix the bug in @src/app.ts"')),
6741
6803
  /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }),
6742
6804
  /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: theme.text.secondary }, "Tips:"),
6743
- /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 Press Enter to submit your task"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 Use Shift+Enter to write multi-line prompts"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 Drag and drop files into the terminal to add file paths"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 The agent will automatically run tools and fix issues")),
6805
+ /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 Press Enter to submit your task"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 Use Shift+Enter to write multi-line prompts"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 Drag and drop files into the terminal to add file paths"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 The agent will automatically run tools and fix issues"), /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "\u2022 Use Ctrl+L to clear the terminal screen without clearing the messages history")),
6744
6806
  /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text4, { color: theme.text.dim }, "Press ", /* @__PURE__ */ React5.createElement(Text4, { bold: true }, "ESC"), " or ", /* @__PURE__ */ React5.createElement(Text4, { bold: true }, "?"), " to close"))
6745
6807
  );
6746
6808
  };
@@ -6748,7 +6810,7 @@ var init_HelpMenu = __esm({
6748
6810
  });
6749
6811
 
6750
6812
  // src/ui/contexts/SessionContext.tsx
6751
- import React6, { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useState as useState3 } from "react";
6813
+ import React6, { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useState as useState4 } from "react";
6752
6814
  var SessionContext, SessionProvider, useSession;
6753
6815
  var init_SessionContext = __esm({
6754
6816
  "src/ui/contexts/SessionContext.tsx"() {
@@ -6759,24 +6821,24 @@ var init_SessionContext = __esm({
6759
6821
  children,
6760
6822
  initialModel
6761
6823
  }) => {
6762
- const [messages, setMessages] = useState3([]);
6763
- const [todos, setTodos] = useState3([]);
6764
- const [stats, setStats] = useState3({
6824
+ const [messages, setMessages] = useState4([]);
6825
+ const [todos, setTodos] = useState4([]);
6826
+ const [stats, setStats] = useState4({
6765
6827
  filesModified: /* @__PURE__ */ new Set(),
6766
6828
  commandsRun: [],
6767
6829
  iterations: 0,
6768
6830
  startTime: Date.now()
6769
6831
  });
6770
- const [isAgentRunning, setIsAgentRunning] = useState3(false);
6771
- const [shouldInterruptAgent, setShouldInterruptAgent] = useState3(false);
6772
- const [usageStats, setUsageStats] = useState3(null);
6773
- const [sessionId, setSessionId] = useState3();
6774
- const [webUrl, setWebUrl] = useState3();
6775
- const [agentMode, setAgentMode] = useState3("build");
6776
- const [planFilePath, setPlanFilePath] = useState3();
6777
- const [selectedModel, setSelectedModel] = useState3(initialModel || Mt);
6778
- const [allToolsExpanded, setAllToolsExpanded] = useState3(true);
6779
- const [staticRemountKey, setStaticRemountKey] = useState3(0);
6832
+ const [isAgentRunning, setIsAgentRunning] = useState4(false);
6833
+ const [shouldInterruptAgent, setShouldInterruptAgent] = useState4(false);
6834
+ const [usageStats, setUsageStats] = useState4(null);
6835
+ const [sessionId, setSessionId] = useState4();
6836
+ const [webUrl, setWebUrl] = useState4();
6837
+ const [agentMode, setAgentMode] = useState4("build");
6838
+ const [planFilePath, setPlanFilePath] = useState4();
6839
+ const [selectedModel, setSelectedModel] = useState4(initialModel || Mt);
6840
+ const [allToolsExpanded, setAllToolsExpanded] = useState4(true);
6841
+ const [staticRemountKey, setStaticRemountKey] = useState4(0);
6780
6842
  const addMessage = useCallback2(
6781
6843
  (message) => {
6782
6844
  const expandableTools = ["Bash", "BashOutput", "Command Output"];
@@ -6983,7 +7045,7 @@ var init_file_completion = __esm({
6983
7045
 
6984
7046
  // src/ui/components/ModelSelector.tsx
6985
7047
  import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
6986
- import React7, { useState as useState4 } from "react";
7048
+ import React7, { useState as useState5 } from "react";
6987
7049
  function getNextModel(currentModel) {
6988
7050
  const currentIndex = ke.findIndex((m2) => m2.id === currentModel);
6989
7051
  const nextIndex = (currentIndex + 1) % ke.length;
@@ -7006,7 +7068,7 @@ var init_ModelSelector = __esm({
7006
7068
  onCancel
7007
7069
  }) => {
7008
7070
  const currentIndex = ke.findIndex((m2) => m2.id === currentModel);
7009
- const [selectedIndex, setSelectedIndex] = useState4(currentIndex >= 0 ? currentIndex : 0);
7071
+ const [selectedIndex, setSelectedIndex] = useState5(currentIndex >= 0 ? currentIndex : 0);
7010
7072
  useInput3((input, key) => {
7011
7073
  if (key.upArrow) {
7012
7074
  setSelectedIndex((prev) => prev > 0 ? prev - 1 : ke.length - 1);
@@ -7048,12 +7110,13 @@ var init_ModelSelector = __esm({
7048
7110
  import path4 from "path";
7049
7111
  import chalk5 from "chalk";
7050
7112
  import { Box as Box6, Text as Text6 } from "ink";
7051
- import React8, { forwardRef, useEffect as useEffect4, useImperativeHandle, useState as useState5 } from "react";
7113
+ import React8, { forwardRef, useEffect as useEffect5, useImperativeHandle, useState as useState6 } from "react";
7052
7114
  var InputPrompt;
7053
7115
  var init_InputPrompt = __esm({
7054
7116
  "src/ui/components/InputPrompt.tsx"() {
7055
7117
  "use strict";
7056
7118
  init_shared_es();
7119
+ init_command_discovery();
7057
7120
  init_SessionContext();
7058
7121
  init_useKeypress();
7059
7122
  init_file_completion();
@@ -7064,19 +7127,20 @@ var init_InputPrompt = __esm({
7064
7127
  placeholder = "Enter your task (press Enter to submit, Shift+Enter for new line)...",
7065
7128
  disabled = false,
7066
7129
  onHelpToggle,
7130
+ cwd,
7067
7131
  currentFolder,
7068
7132
  gitBranch,
7069
7133
  onInputChange
7070
7134
  }, ref) => {
7071
7135
  const { messages, agentMode, selectedModel, setSelectedModel, isAgentRunning, usageStats } = useSession();
7072
- const [value, setValue] = useState5("");
7073
- const [cursorOffset, setCursorOffset] = useState5(0);
7074
- const [allFiles, setAllFiles] = useState5([]);
7075
- const [suggestions, setSuggestions] = useState5([]);
7076
- const [activeSuggestion, setActiveSuggestion] = useState5(0);
7077
- const [showSuggestions, setShowSuggestions] = useState5(false);
7078
- const [mentionStartIndex, setMentionStartIndex] = useState5(-1);
7079
- const SLASH_COMMANDS = [
7136
+ const [value, setValue] = useState6("");
7137
+ const [cursorOffset, setCursorOffset] = useState6(0);
7138
+ const [allFiles, setAllFiles] = useState6([]);
7139
+ const [suggestions, setSuggestions] = useState6([]);
7140
+ const [activeSuggestion, setActiveSuggestion] = useState6(0);
7141
+ const [showSuggestions, setShowSuggestions] = useState6(false);
7142
+ const [mentionStartIndex, setMentionStartIndex] = useState6(-1);
7143
+ const BUILTIN_SLASH_COMMANDS = [
7080
7144
  { name: "/help", desc: "Show help" },
7081
7145
  { name: "/resume", desc: "Resume session" },
7082
7146
  { name: "/clear", desc: "Clear history" },
@@ -7087,7 +7151,21 @@ var init_InputPrompt = __esm({
7087
7151
  { name: "/logout", desc: "Log out" },
7088
7152
  { name: "/exit", desc: "Exit CLI" }
7089
7153
  ];
7090
- const [isSlashCommand, setIsSlashCommand] = useState5(false);
7154
+ const [customCommands, setCustomCommands] = useState6([]);
7155
+ const [isSlashCommand, setIsSlashCommand] = useState6(false);
7156
+ useEffect5(() => {
7157
+ try {
7158
+ const projectDir = cwd || process.cwd();
7159
+ const discovered = discoverCommands(projectDir);
7160
+ const formatted = discovered.map((cmd) => ({
7161
+ name: `/${cmd.name}`,
7162
+ desc: cmd.description || ""
7163
+ }));
7164
+ setCustomCommands(formatted);
7165
+ } catch {
7166
+ }
7167
+ }, [cwd]);
7168
+ const allSlashCommands = [...BUILTIN_SLASH_COMMANDS, ...customCommands];
7091
7169
  useImperativeHandle(ref, () => ({
7092
7170
  clear: () => {
7093
7171
  setValue("");
@@ -7096,7 +7174,7 @@ var init_InputPrompt = __esm({
7096
7174
  onInputChange?.("");
7097
7175
  }
7098
7176
  }));
7099
- useEffect4(() => {
7177
+ useEffect5(() => {
7100
7178
  setTimeout(() => {
7101
7179
  try {
7102
7180
  const files = getFiles();
@@ -7114,7 +7192,18 @@ var init_InputPrompt = __esm({
7114
7192
  const checkSuggestions = (text, cursor) => {
7115
7193
  if (text.startsWith("/") && cursor <= text.length && !text.includes(" ", 1)) {
7116
7194
  const query2 = text.slice(1);
7117
- const matches = SLASH_COMMANDS.filter((cmd) => cmd.name.slice(1).startsWith(query2.toLowerCase())).map((cmd) => `${cmd.name} ${cmd.desc}`);
7195
+ const builtinMatches = BUILTIN_SLASH_COMMANDS.filter((cmd) => cmd.name.slice(1).toLowerCase().startsWith(query2.toLowerCase())).map((cmd) => cmd.desc ? `${cmd.name} ${cmd.desc}` : cmd.name);
7196
+ const customMatches = customCommands.filter((cmd) => cmd.name.slice(1).toLowerCase().startsWith(query2.toLowerCase())).map((cmd) => cmd.desc ? `${cmd.name} ${cmd.desc}` : cmd.name);
7197
+ const matches = [];
7198
+ if (builtinMatches.length > 0) {
7199
+ matches.push(...builtinMatches);
7200
+ }
7201
+ if (customMatches.length > 0) {
7202
+ if (builtinMatches.length > 0) {
7203
+ matches.push("\u2500\u2500\u2500\u2500\u2500 custom commands \u2500\u2500\u2500\u2500\u2500");
7204
+ }
7205
+ matches.push(...customMatches);
7206
+ }
7118
7207
  if (matches.length > 0) {
7119
7208
  setSuggestions(matches);
7120
7209
  setShowSuggestions(true);
@@ -7180,8 +7269,8 @@ var init_InputPrompt = __esm({
7180
7269
  cleanPath = cleanPath.replace(/\\ /g, " ");
7181
7270
  if (path4.isAbsolute(cleanPath)) {
7182
7271
  try {
7183
- const cwd = process.cwd();
7184
- const rel = path4.relative(cwd, cleanPath);
7272
+ const cwd2 = process.cwd();
7273
+ const rel = path4.relative(cwd2, cleanPath);
7185
7274
  if (!rel.startsWith("..") && !path4.isAbsolute(rel)) {
7186
7275
  cleanPath = rel;
7187
7276
  }
@@ -7200,20 +7289,31 @@ var init_InputPrompt = __esm({
7200
7289
  return;
7201
7290
  }
7202
7291
  if (showSuggestions && !key.shift) {
7292
+ const isSeparator = (idx) => suggestions[idx]?.startsWith("\u2500\u2500\u2500\u2500\u2500");
7203
7293
  if (key.name === "up") {
7204
- setActiveSuggestion(
7205
- (prev) => prev > 0 ? prev - 1 : suggestions.length - 1
7206
- );
7294
+ setActiveSuggestion((prev) => {
7295
+ let next = prev > 0 ? prev - 1 : suggestions.length - 1;
7296
+ while (isSeparator(next) && next !== prev) {
7297
+ next = next > 0 ? next - 1 : suggestions.length - 1;
7298
+ }
7299
+ return next;
7300
+ });
7207
7301
  return;
7208
7302
  }
7209
7303
  if (key.name === "down") {
7210
- setActiveSuggestion(
7211
- (prev) => prev < suggestions.length - 1 ? prev + 1 : 0
7212
- );
7304
+ setActiveSuggestion((prev) => {
7305
+ let next = prev < suggestions.length - 1 ? prev + 1 : 0;
7306
+ while (isSeparator(next) && next !== prev) {
7307
+ next = next < suggestions.length - 1 ? next + 1 : 0;
7308
+ }
7309
+ return next;
7310
+ });
7213
7311
  return;
7214
7312
  }
7215
7313
  if (key.name === "tab" || key.name === "return") {
7216
- completeSuggestion(key.name === "return");
7314
+ if (!isSeparator(activeSuggestion)) {
7315
+ completeSuggestion(key.name === "return");
7316
+ }
7217
7317
  return;
7218
7318
  }
7219
7319
  if (key.name === "escape") {
@@ -7245,14 +7345,13 @@ var init_InputPrompt = __esm({
7245
7345
  setCursorOffset(Math.min(value.length, cursorOffset + 1));
7246
7346
  } else if (key.ctrl && input === "u") {
7247
7347
  updateValue("", 0);
7248
- } else if (key.name === "tab" && !showSuggestions) {
7249
- if (value.length === 0 && !isAgentRunning) {
7250
- if (key.shift) {
7251
- setSelectedModel(getPreviousModel(selectedModel));
7252
- } else {
7253
- setSelectedModel(getNextModel(selectedModel));
7254
- }
7348
+ } else if (key.ctrl && key.name === "m" && !isAgentRunning) {
7349
+ if (key.shift) {
7350
+ setSelectedModel(getPreviousModel(selectedModel));
7351
+ } else {
7352
+ setSelectedModel(getNextModel(selectedModel));
7255
7353
  }
7354
+ } else if (key.name === "tab" && !showSuggestions) {
7256
7355
  } else if (key.paste) {
7257
7356
  const newValue = value.slice(0, cursorOffset) + input + value.slice(cursorOffset);
7258
7357
  updateValue(newValue, cursorOffset + input.length);
@@ -7286,7 +7385,13 @@ var init_InputPrompt = __esm({
7286
7385
  marginBottom: 0,
7287
7386
  paddingX: 1
7288
7387
  },
7289
- suggestions.map((file, idx) => /* @__PURE__ */ React8.createElement(Text6, { color: idx === activeSuggestion ? theme.text.accent : theme.text.dim, key: file }, idx === activeSuggestion ? "\u276F " : " ", " ", file))
7388
+ suggestions.map((item, idx) => {
7389
+ const isSeparator = item.startsWith("\u2500\u2500\u2500\u2500\u2500");
7390
+ if (isSeparator) {
7391
+ return /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim, key: item }, " ", item);
7392
+ }
7393
+ return /* @__PURE__ */ React8.createElement(Text6, { color: idx === activeSuggestion ? theme.text.accent : theme.text.dim, key: item }, idx === activeSuggestion ? "\u276F " : " ", item);
7394
+ })
7290
7395
  ), /* @__PURE__ */ React8.createElement(
7291
7396
  Box6,
7292
7397
  {
@@ -7307,7 +7412,7 @@ var init_InputPrompt = __esm({
7307
7412
  }
7308
7413
  return /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.primary, key: idx }, line);
7309
7414
  })), !hasContent && disabled && /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim, italic: true }, "Waiting for agent to complete...")))
7310
- ), /* @__PURE__ */ React8.createElement(Box6, { justifyContent: "space-between", paddingX: 1 }, /* @__PURE__ */ React8.createElement(Box6, { gap: 2 }, /* @__PURE__ */ React8.createElement(Box6, null, /* @__PURE__ */ React8.createElement(Text6, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "\u23F8 plan" : "\u25B6 build"), /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim }, " (shift+tab)")), /* @__PURE__ */ React8.createElement(Box6, null, /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim }, "model:"), /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.info }, St(selectedModel)), /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim }, " (tab)"))), /* @__PURE__ */ React8.createElement(Box6, null, /* @__PURE__ */ React8.createElement(Text6, { color: usageStats && usageStats.contextPct >= 90 ? theme.text.error : usageStats && usageStats.contextPct >= 75 ? theme.text.warning : theme.text.dim }, usageStats?.contextPct ?? 0, "% context used"), /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim }, " ", "(", usageStats ? usageStats.inputTokens >= 1e3 ? `${(usageStats.inputTokens / 1e3).toFixed(1)}K` : usageStats.inputTokens : 0, " / ", usageStats ? usageStats.contextWindow >= 1e3 ? `${(usageStats.contextWindow / 1e3).toFixed(0)}K` : usageStats.contextWindow : "200K", ")"))));
7415
+ ), /* @__PURE__ */ React8.createElement(Box6, { justifyContent: "space-between", paddingX: 1 }, /* @__PURE__ */ React8.createElement(Box6, { gap: 2 }, /* @__PURE__ */ React8.createElement(Box6, null, /* @__PURE__ */ React8.createElement(Text6, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "\u23F8 plan" : "\u25B6 build"), /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim }, " (shift+tab)")), /* @__PURE__ */ React8.createElement(Box6, null, /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim }, "model:"), /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.info }, St(selectedModel)), /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim }, " (ctrl+m)"))), /* @__PURE__ */ React8.createElement(Box6, null, /* @__PURE__ */ React8.createElement(Text6, { color: usageStats && usageStats.contextPct >= 90 ? theme.text.error : usageStats && usageStats.contextPct >= 75 ? theme.text.warning : theme.text.dim }, usageStats?.contextPct ?? 0, "% context used"), /* @__PURE__ */ React8.createElement(Text6, { color: theme.text.dim }, " ", "(", usageStats ? usageStats.inputTokens >= 1e3 ? `${(usageStats.inputTokens / 1e3).toFixed(1)}K` : usageStats.inputTokens : 0, " / ", usageStats ? usageStats.contextWindow >= 1e3 ? `${(usageStats.contextWindow / 1e3).toFixed(0)}K` : usageStats.contextWindow : "200K", ")"))));
7311
7416
  });
7312
7417
  InputPrompt.displayName = "InputPrompt";
7313
7418
  }
@@ -7357,7 +7462,7 @@ var init_Header = __esm({
7357
7462
  import chalk6 from "chalk";
7358
7463
  import { Box as Box8, Text as Text8 } from "ink";
7359
7464
  import { all, createLowlight } from "lowlight";
7360
- import React10, { useMemo as useMemo2 } from "react";
7465
+ import React10, { useMemo } from "react";
7361
7466
  function parseMarkdownSections(text) {
7362
7467
  const sections = [];
7363
7468
  const lines = text.split(/\r?\n/);
@@ -7517,7 +7622,7 @@ var init_markdown = __esm({
7517
7622
  text,
7518
7623
  isPending = false
7519
7624
  }) => {
7520
- const sections = useMemo2(() => parseMarkdownSections(text), [text]);
7625
+ const sections = useMemo(() => parseMarkdownSections(text), [text]);
7521
7626
  const elements = sections.map((section, index) => {
7522
7627
  if (section.type === "table" && section.tableRows) {
7523
7628
  return /* @__PURE__ */ React10.createElement(Table, { key: `table-${index}`, rows: section.tableRows });
@@ -7588,6 +7693,8 @@ var init_markdown = __esm({
7588
7693
  elements.push(
7589
7694
  /* @__PURE__ */ React10.createElement(Paragraph, { content: line, key: `para-${lineIndex}` })
7590
7695
  );
7696
+ } else {
7697
+ elements.push(/* @__PURE__ */ React10.createElement(Box8, { height: 1, key: `spacer-${lineIndex}` }));
7591
7698
  }
7592
7699
  }
7593
7700
  return /* @__PURE__ */ React10.createElement(Box8, { flexDirection: "column" }, elements);
@@ -7765,7 +7872,7 @@ var init_ErrorMessage = __esm({
7765
7872
  // src/ui/components/messages/LoadingMessage.tsx
7766
7873
  import { Box as Box11, Text as Text11 } from "ink";
7767
7874
  import Spinner2 from "ink-spinner";
7768
- import React13, { useEffect as useEffect5, useState as useState6 } from "react";
7875
+ import React13, { useEffect as useEffect6, useState as useState7 } from "react";
7769
7876
  var LOADING_MESSAGES, SHIMMER_INTERVAL_MS, TEXT_ROTATION_INTERVAL_MS, LoadingMessage;
7770
7877
  var init_LoadingMessage = __esm({
7771
7878
  "src/ui/components/messages/LoadingMessage.tsx"() {
@@ -7782,10 +7889,10 @@ var init_LoadingMessage = __esm({
7782
7889
  SHIMMER_INTERVAL_MS = 80;
7783
7890
  TEXT_ROTATION_INTERVAL_MS = 2e3;
7784
7891
  LoadingMessage = () => {
7785
- const [messageIndex, setMessageIndex] = useState6(0);
7786
- const [shimmerPosition, setShimmerPosition] = useState6(0);
7892
+ const [messageIndex, setMessageIndex] = useState7(0);
7893
+ const [shimmerPosition, setShimmerPosition] = useState7(0);
7787
7894
  const message = LOADING_MESSAGES[messageIndex];
7788
- useEffect5(() => {
7895
+ useEffect6(() => {
7789
7896
  const rotationInterval = setInterval(() => {
7790
7897
  setMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length);
7791
7898
  setShimmerPosition(0);
@@ -7794,7 +7901,7 @@ var init_LoadingMessage = __esm({
7794
7901
  clearInterval(rotationInterval);
7795
7902
  };
7796
7903
  }, []);
7797
- useEffect5(() => {
7904
+ useEffect6(() => {
7798
7905
  const shimmerInterval = setInterval(() => {
7799
7906
  setShimmerPosition((prev) => (prev + 1) % (message.length + 1));
7800
7907
  }, SHIMMER_INTERVAL_MS);
@@ -7838,16 +7945,38 @@ var init_TodoMessage = __esm({
7838
7945
  "use strict";
7839
7946
  init_theme();
7840
7947
  TodoMessage = ({ todos }) => {
7841
- const completed = todos.filter((t) => t.status === "completed");
7842
- const inProgress = todos.filter((t) => t.status === "in_progress");
7843
- const pending = todos.filter((t) => t.status === "pending");
7948
+ const completedCount = todos.filter((t) => t.status === "completed").length;
7844
7949
  const total = todos.length;
7845
- const completedCount = completed.length;
7846
7950
  const progress = total > 0 ? Math.round(completedCount / total * 100) : 0;
7847
7951
  const barLength = 20;
7848
7952
  const filledLength = Math.round(barLength * completedCount / total);
7849
7953
  const bar = "\u2588".repeat(filledLength) + "\u2591".repeat(barLength - filledLength);
7850
- return /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "column", marginY: 0 }, /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "row" }, /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.info }, "\u{1F4DD} "), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.dim }, "Todo Progress: "), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.accent }, bar), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.primary }, " ", progress, "%"), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.dim }, " ", "(", completedCount, "/", total, ")")), inProgress.length > 0 && /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "column", marginLeft: 2 }, inProgress.map((todo, idx) => /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "row", key: `in-progress-${idx}` }, /* @__PURE__ */ React15.createElement(Text13, { color: theme.status.inProgress }, "\u2192 "), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.primary }, todo.activeForm || todo.content)))), completed.length > 0 && completed.length <= 2 && /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "column", marginLeft: 2 }, completed.map((todo, idx) => /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "row", key: `completed-${idx}` }, /* @__PURE__ */ React15.createElement(Text13, { color: theme.status.completed }, "\u2713 "), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.dim }, todo.content)))), pending.length > 0 && pending.length <= 3 && /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "column", marginLeft: 2 }, pending.map((todo, idx) => /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "row", key: `pending-${idx}` }, /* @__PURE__ */ React15.createElement(Text13, { color: theme.status.pending }, "\u23F3 "), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.secondary }, todo.content)))));
7954
+ const getStatusIcon = (status) => {
7955
+ switch (status) {
7956
+ case "completed":
7957
+ return { icon: "\u2713", color: theme.status.completed };
7958
+ case "in_progress":
7959
+ return { icon: "\u2192", color: theme.status.inProgress };
7960
+ case "pending":
7961
+ return { icon: "\u25CB", color: theme.status.pending };
7962
+ }
7963
+ };
7964
+ const getTextColor = (status) => {
7965
+ switch (status) {
7966
+ case "completed":
7967
+ return theme.text.dim;
7968
+ case "in_progress":
7969
+ return theme.text.primary;
7970
+ case "pending":
7971
+ return theme.text.secondary;
7972
+ }
7973
+ };
7974
+ return /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "column", marginY: 0 }, /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "row" }, /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.info }, "\u{1F4DD} "), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.dim }, "Todo Progress: "), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.accent }, bar), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.primary }, " ", progress, "%"), /* @__PURE__ */ React15.createElement(Text13, { color: theme.text.dim }, " ", "(", completedCount, "/", total, ")")), /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "column", marginLeft: 2 }, todos.map((todo, idx) => {
7975
+ const { icon, color } = getStatusIcon(todo.status);
7976
+ const textColor = getTextColor(todo.status);
7977
+ const displayText = todo.status === "in_progress" ? todo.activeForm || todo.content : todo.content;
7978
+ return /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "row", key: `todo-${idx}` }, /* @__PURE__ */ React15.createElement(Text13, { color }, icon, " "), /* @__PURE__ */ React15.createElement(Text13, { color: textColor }, displayText));
7979
+ })));
7851
7980
  };
7852
7981
  }
7853
7982
  });
@@ -8045,7 +8174,7 @@ var init_UserMessage = __esm({
8045
8174
 
8046
8175
  // src/ui/components/MessageList.tsx
8047
8176
  import { Box as Box16, Static } from "ink";
8048
- import React18, { useMemo as useMemo4 } from "react";
8177
+ import React18, { useMemo as useMemo3 } from "react";
8049
8178
  var MessageList;
8050
8179
  var init_MessageList = __esm({
8051
8180
  "src/ui/components/MessageList.tsx"() {
@@ -8115,7 +8244,7 @@ var init_MessageList = __esm({
8115
8244
  return null;
8116
8245
  }
8117
8246
  };
8118
- const { completedMessages, pendingMessages } = useMemo4(() => {
8247
+ const { completedMessages, pendingMessages } = useMemo3(() => {
8119
8248
  const completed = [];
8120
8249
  const pending = [];
8121
8250
  for (const msg of messages) {
@@ -8127,7 +8256,7 @@ var init_MessageList = __esm({
8127
8256
  }
8128
8257
  return { completedMessages: completed, pendingMessages: pending };
8129
8258
  }, [messages]);
8130
- const staticItems = useMemo4(() => [
8259
+ const staticItems = useMemo3(() => [
8131
8260
  { id: "header", type: "header" },
8132
8261
  ...completedMessages.map((msg) => ({ ...msg, _isMessage: true }))
8133
8262
  ], [completedMessages]);
@@ -8179,7 +8308,7 @@ var init_QueuedMessageDisplay = __esm({
8179
8308
 
8180
8309
  // src/ui/components/SessionSelector.tsx
8181
8310
  import { Box as Box18, Text as Text17, useInput as useInput4 } from "ink";
8182
- import React20, { useEffect as useEffect6, useState as useState7 } from "react";
8311
+ import React20, { useEffect as useEffect7, useState as useState8 } from "react";
8183
8312
  function getSessionPrefix(authMethod) {
8184
8313
  return authMethod === "api-key" ? "[Team]" : "[Me]";
8185
8314
  }
@@ -8194,13 +8323,13 @@ var init_SessionSelector = __esm({
8194
8323
  onSelect,
8195
8324
  onCancel
8196
8325
  }) => {
8197
- const [allSessions, setAllSessions] = useState7([]);
8198
- const [selectedIndex, setSelectedIndex] = useState7(0);
8199
- const [isLoading, setIsLoading] = useState7(false);
8200
- const [hasMore, setHasMore] = useState7(true);
8201
- const [totalSessions, setTotalSessions] = useState7(0);
8202
- const [error, setError] = useState7(null);
8203
- useEffect6(() => {
8326
+ const [allSessions, setAllSessions] = useState8([]);
8327
+ const [selectedIndex, setSelectedIndex] = useState8(0);
8328
+ const [isLoading, setIsLoading] = useState8(false);
8329
+ const [hasMore, setHasMore] = useState8(true);
8330
+ const [totalSessions, setTotalSessions] = useState8(0);
8331
+ const [error, setError] = useState8(null);
8332
+ useEffect7(() => {
8204
8333
  loadMoreSessions();
8205
8334
  }, []);
8206
8335
  const loadMoreSessions = async () => {
@@ -8307,11 +8436,11 @@ var init_SessionSelector = __esm({
8307
8436
  });
8308
8437
 
8309
8438
  // src/ui/hooks/useModeToggle.ts
8310
- import { useEffect as useEffect7 } from "react";
8439
+ import { useEffect as useEffect8 } from "react";
8311
8440
  function useModeToggle() {
8312
8441
  const { subscribe, unsubscribe } = useKeypressContext();
8313
8442
  const { agentMode, setAgentMode, isAgentRunning } = useSession();
8314
- useEffect7(() => {
8443
+ useEffect8(() => {
8315
8444
  const handleKeypress = (key) => {
8316
8445
  if (key.name === "tab" && key.shift && !isAgentRunning) {
8317
8446
  const newMode = agentMode === "plan" ? "build" : "plan";
@@ -8332,7 +8461,7 @@ var init_useModeToggle = __esm({
8332
8461
  });
8333
8462
 
8334
8463
  // src/ui/hooks/useOverlayEscapeGuard.ts
8335
- import { useCallback as useCallback3, useMemo as useMemo5, useRef as useRef2 } from "react";
8464
+ import { useCallback as useCallback3, useMemo as useMemo4, useRef as useRef2 } from "react";
8336
8465
  var useOverlayEscapeGuard;
8337
8466
  var init_useOverlayEscapeGuard = __esm({
8338
8467
  "src/ui/hooks/useOverlayEscapeGuard.ts"() {
@@ -8343,7 +8472,7 @@ var init_useOverlayEscapeGuard = __esm({
8343
8472
  suppressUntilRef.current = Date.now() + suppressionMs;
8344
8473
  }, [suppressionMs]);
8345
8474
  const isCancelSuppressed = useCallback3(() => Date.now() < suppressUntilRef.current, []);
8346
- const isOverlayOpen = useMemo5(() => overlays.some(Boolean), [overlays]);
8475
+ const isOverlayOpen = useMemo4(() => overlays.some(Boolean), [overlays]);
8347
8476
  return { isOverlayOpen, isCancelSuppressed, markOverlayClosed };
8348
8477
  };
8349
8478
  }
@@ -8351,10 +8480,10 @@ var init_useOverlayEscapeGuard = __esm({
8351
8480
 
8352
8481
  // src/ui/App.tsx
8353
8482
  import { execSync as execSync3 } from "child_process";
8354
- import { homedir as homedir3 } from "os";
8483
+ import { homedir as homedir2 } from "os";
8355
8484
  import { Box as Box19, Text as Text18, useApp } from "ink";
8356
8485
  import Spinner3 from "ink-spinner";
8357
- import React21, { useEffect as useEffect8, useRef as useRef3, useState as useState8 } from "react";
8486
+ import React21, { useEffect as useEffect9, useRef as useRef3, useState as useState9 } from "react";
8358
8487
  var getGitBranch, getCurrentFolder, AppContent, App;
8359
8488
  var init_App = __esm({
8360
8489
  "src/ui/App.tsx"() {
@@ -8362,6 +8491,7 @@ var init_App = __esm({
8362
8491
  init_shared_es();
8363
8492
  init_login();
8364
8493
  init_setup();
8494
+ init_command_discovery();
8365
8495
  init_stdio();
8366
8496
  init_token_storage();
8367
8497
  init_version();
@@ -8387,43 +8517,43 @@ var init_App = __esm({
8387
8517
  return "";
8388
8518
  }
8389
8519
  };
8390
- getCurrentFolder = () => {
8391
- const cwd = process.cwd();
8392
- const home = homedir3();
8520
+ getCurrentFolder = (configCwd) => {
8521
+ const cwd = configCwd || process.cwd();
8522
+ const home = homedir2();
8393
8523
  if (cwd.startsWith(home)) {
8394
8524
  return `~${cwd.slice(home.length)}`;
8395
8525
  }
8396
8526
  return cwd;
8397
8527
  };
8398
- AppContent = ({ config: config2, sessionId, webUrl, queuedTasks = [], onExit, onSubmitTask, apiClient, onResumeSession }) => {
8528
+ AppContent = ({ config: config2, sessionId, webUrl, queuedTasks = [], onExit, onSubmitTask, apiClient, onResumeSession, onClearSession }) => {
8399
8529
  const { exit } = useApp();
8400
8530
  const { addMessage, clearMessages, isAgentRunning, messages, setSessionId, setWebUrl, setShouldInterruptAgent, setIsAgentRunning, toggleAllToolOutputs, allToolsExpanded, selectedModel, setSelectedModel } = useSession();
8401
8531
  useModeToggle();
8402
- const [terminalWidth, setTerminalWidth] = useState8(process.stdout.columns || 80);
8403
- const [showHelp, setShowHelp] = useState8(false);
8404
- const [showInput, setShowInput] = useState8(true);
8405
- const [gitBranch] = useState8(() => getGitBranch());
8406
- const [currentFolder] = useState8(() => getCurrentFolder());
8407
- const [hasInputContent, setHasInputContent] = useState8(false);
8408
- const [exitWarning, setExitWarning] = useState8(null);
8532
+ const [terminalWidth, setTerminalWidth] = useState9(process.stdout.columns || 80);
8533
+ const [showHelp, setShowHelp] = useState9(false);
8534
+ const [showInput, setShowInput] = useState9(true);
8535
+ const [gitBranch] = useState9(() => getGitBranch());
8536
+ const [currentFolder] = useState9(() => getCurrentFolder(config2.cwd));
8537
+ const [hasInputContent, setHasInputContent] = useState9(false);
8538
+ const [exitWarning, setExitWarning] = useState9(null);
8409
8539
  const inputPromptRef = useRef3(null);
8410
- const [showSessionSelector, setShowSessionSelector] = useState8(false);
8411
- const [showModelSelector, setShowModelSelector] = useState8(false);
8412
- const [showFeedbackDialog, setShowFeedbackDialog] = useState8(false);
8413
- const [isLoadingSession, setIsLoadingSession] = useState8(false);
8414
- const [authState, setAuthState] = useState8(
8540
+ const [showSessionSelector, setShowSessionSelector] = useState9(false);
8541
+ const [showModelSelector, setShowModelSelector] = useState9(false);
8542
+ const [showFeedbackDialog, setShowFeedbackDialog] = useState9(false);
8543
+ const [isLoadingSession, setIsLoadingSession] = useState9(false);
8544
+ const [authState, setAuthState] = useState9(
8415
8545
  () => config2.supatestApiKey ? "authenticated" /* Authenticated */ : "unauthenticated" /* Unauthenticated */
8416
8546
  );
8417
- const [showAuthDialog, setShowAuthDialog] = useState8(false);
8547
+ const [showAuthDialog, setShowAuthDialog] = useState9(false);
8418
8548
  const { isOverlayOpen, isCancelSuppressed, markOverlayClosed } = useOverlayEscapeGuard({
8419
8549
  overlays: [showHelp, showSessionSelector, showAuthDialog, showModelSelector, showFeedbackDialog]
8420
8550
  });
8421
- useEffect8(() => {
8551
+ useEffect9(() => {
8422
8552
  if (!config2.supatestApiKey) {
8423
8553
  setShowAuthDialog(true);
8424
8554
  }
8425
8555
  }, [config2.supatestApiKey]);
8426
- useEffect8(() => {
8556
+ useEffect9(() => {
8427
8557
  if (sessionId) {
8428
8558
  setSessionId(sessionId);
8429
8559
  }
@@ -8465,6 +8595,7 @@ var init_App = __esm({
8465
8595
  if (command === "/clear") {
8466
8596
  clearTerminalViewportAndScrollback();
8467
8597
  clearMessages();
8598
+ onClearSession?.();
8468
8599
  return;
8469
8600
  }
8470
8601
  if (command === "/exit") {
@@ -8563,6 +8694,25 @@ var init_App = __esm({
8563
8694
  }
8564
8695
  return;
8565
8696
  }
8697
+ const projectDir = config2.cwd || process.cwd();
8698
+ const spaceIndex = trimmedTask.indexOf(" ");
8699
+ const commandName = spaceIndex > 0 ? trimmedTask.slice(1, spaceIndex) : trimmedTask.slice(1);
8700
+ const commandArgs = spaceIndex > 0 ? trimmedTask.slice(spaceIndex + 1) : void 0;
8701
+ const expandedContent = expandCommand(projectDir, commandName, commandArgs);
8702
+ if (expandedContent) {
8703
+ addMessage({
8704
+ type: "user",
8705
+ content: trimmedTask
8706
+ });
8707
+ onSubmitTask?.(expandedContent);
8708
+ return;
8709
+ }
8710
+ addMessage({
8711
+ type: "error",
8712
+ content: `Unknown command: ${trimmedTask}. Type /help for available commands.`,
8713
+ errorType: "warning"
8714
+ });
8715
+ return;
8566
8716
  }
8567
8717
  if (authState !== "authenticated" /* Authenticated */) {
8568
8718
  addMessage({
@@ -8652,7 +8802,7 @@ var init_App = __esm({
8652
8802
  markOverlayClosed();
8653
8803
  setShowHelp(false);
8654
8804
  };
8655
- useEffect8(() => {
8805
+ useEffect9(() => {
8656
8806
  const handleResize = () => {
8657
8807
  setTerminalWidth(process.stdout.columns || 80);
8658
8808
  };
@@ -8712,7 +8862,7 @@ var init_App = __esm({
8712
8862
  },
8713
8863
  { isActive: !isOverlayOpen }
8714
8864
  );
8715
- useEffect8(() => {
8865
+ useEffect9(() => {
8716
8866
  if (config2.task) {
8717
8867
  addMessage({
8718
8868
  type: "user",
@@ -8768,6 +8918,7 @@ var init_App = __esm({
8768
8918
  InputPrompt,
8769
8919
  {
8770
8920
  currentFolder,
8921
+ cwd: config2.cwd,
8771
8922
  gitBranch,
8772
8923
  onHelpToggle: () => setShowHelp((prev) => !prev),
8773
8924
  onInputChange: (val) => setHasInputContent(val.trim().length > 0),
@@ -8785,7 +8936,7 @@ var init_App = __esm({
8785
8936
  });
8786
8937
 
8787
8938
  // src/ui/hooks/useBracketedPaste.ts
8788
- import { useEffect as useEffect9 } from "react";
8939
+ import { useEffect as useEffect10 } from "react";
8789
8940
  var ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, useBracketedPaste;
8790
8941
  var init_useBracketedPaste = __esm({
8791
8942
  "src/ui/hooks/useBracketedPaste.ts"() {
@@ -8794,7 +8945,7 @@ var init_useBracketedPaste = __esm({
8794
8945
  ENABLE_BRACKETED_PASTE = "\x1B[?2004h";
8795
8946
  DISABLE_BRACKETED_PASTE = "\x1B[?2004l";
8796
8947
  useBracketedPaste = () => {
8797
- useEffect9(() => {
8948
+ useEffect10(() => {
8798
8949
  writeToStdout(ENABLE_BRACKETED_PASTE);
8799
8950
  const cleanup = () => {
8800
8951
  writeToStdout(DISABLE_BRACKETED_PASTE);
@@ -8815,7 +8966,7 @@ __export(interactive_exports, {
8815
8966
  runInteractive: () => runInteractive
8816
8967
  });
8817
8968
  import { render } from "ink";
8818
- import React22, { useEffect as useEffect10, useRef as useRef4 } from "react";
8969
+ import React22, { useEffect as useEffect11, useRef as useRef4 } from "react";
8819
8970
  function getToolDescription2(toolName, input) {
8820
8971
  switch (toolName) {
8821
8972
  case "Read":
@@ -9017,17 +9168,18 @@ var init_interactive = __esm({
9017
9168
  shouldInterruptAgent,
9018
9169
  setShouldInterruptAgent,
9019
9170
  agentMode,
9171
+ setAgentMode,
9020
9172
  planFilePath,
9021
9173
  selectedModel
9022
9174
  } = useSession();
9023
9175
  const agentRef = useRef4(null);
9024
- useEffect10(() => {
9176
+ useEffect11(() => {
9025
9177
  if (shouldInterruptAgent && agentRef.current) {
9026
9178
  agentRef.current.abort();
9027
9179
  setShouldInterruptAgent(false);
9028
9180
  }
9029
9181
  }, [shouldInterruptAgent, setShouldInterruptAgent]);
9030
- useEffect10(() => {
9182
+ useEffect11(() => {
9031
9183
  let isMounted = true;
9032
9184
  const runAgent2 = async () => {
9033
9185
  setIsAgentRunning(true);
@@ -9060,6 +9212,12 @@ var init_interactive = __esm({
9060
9212
  // Note: onComplete is now called after agent.run() returns
9061
9213
  // to capture the providerSessionId from the result
9062
9214
  onComplete: () => {
9215
+ },
9216
+ onExitPlanMode: () => {
9217
+ if (isMounted) setAgentMode("build");
9218
+ },
9219
+ onEnterPlanMode: () => {
9220
+ if (isMounted) setAgentMode("plan");
9063
9221
  }
9064
9222
  },
9065
9223
  apiClient,
@@ -9176,11 +9334,18 @@ var init_interactive = __esm({
9176
9334
  setShouldRunAgent(true);
9177
9335
  }
9178
9336
  }, [shouldRunAgent, taskQueue, addMessage]);
9337
+ const handleClearSession = React22.useCallback(() => {
9338
+ setSessionId(void 0);
9339
+ setContextSessionId(void 0);
9340
+ setProviderSessionId(void 0);
9341
+ setTaskQueue([]);
9342
+ }, [setContextSessionId]);
9179
9343
  return /* @__PURE__ */ React22.createElement(React22.Fragment, null, /* @__PURE__ */ React22.createElement(
9180
9344
  App,
9181
9345
  {
9182
9346
  apiClient,
9183
9347
  config: { ...config2, task: currentTask },
9348
+ onClearSession: handleClearSession,
9184
9349
  onExit,
9185
9350
  onResumeSession: async (session) => {
9186
9351
  try {