@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.
- package/dist/index.js +474 -309
- 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
|
|
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
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
**
|
|
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
|
-
<
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
413
|
+
**User Journeys**: User can view their analytics dashboard
|
|
487
414
|
|
|
488
415
|
**Test Cases**:
|
|
489
|
-
1. "displays dashboard with
|
|
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
|
-
**
|
|
524
|
-
|
|
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
|
-
-
|
|
567
|
-
- Do NOT write tests
|
|
568
|
-
-
|
|
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
|
|
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
|
-
|
|
4053
|
-
|
|
4076
|
+
join3(cwd, "SUPATEST.md"),
|
|
4077
|
+
join3(cwd, ".supatest", "SUPATEST.md")
|
|
4054
4078
|
];
|
|
4055
4079
|
for (const path5 of paths) {
|
|
4056
|
-
if (
|
|
4080
|
+
if (existsSync3(path5)) {
|
|
4057
4081
|
try {
|
|
4058
|
-
return
|
|
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 {
|
|
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
|
|
4135
|
-
cleanEnv.CLAUDE_CONFIG_DIR =
|
|
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
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
|
4968
|
-
import { homedir
|
|
4969
|
-
import { join as
|
|
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
|
|
5023
|
+
return join6(CONFIG_DIR, "token.json");
|
|
4974
5024
|
}
|
|
4975
|
-
return
|
|
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 (!
|
|
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 (!
|
|
5051
|
+
if (!existsSync4(tokenFile)) {
|
|
5002
5052
|
return null;
|
|
5003
5053
|
}
|
|
5004
5054
|
try {
|
|
5005
|
-
const data =
|
|
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 (
|
|
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 =
|
|
5093
|
+
CONFIG_DIR = join6(homedir(), ".supatest");
|
|
5044
5094
|
PRODUCTION_API_URL = "https://code-api.supatest.ai";
|
|
5045
5095
|
STORAGE_VERSION = 2;
|
|
5046
|
-
TOKEN_FILE =
|
|
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+
|
|
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
|
|
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] =
|
|
6763
|
-
const [todos, setTodos] =
|
|
6764
|
-
const [stats, setStats] =
|
|
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] =
|
|
6771
|
-
const [shouldInterruptAgent, setShouldInterruptAgent] =
|
|
6772
|
-
const [usageStats, setUsageStats] =
|
|
6773
|
-
const [sessionId, setSessionId] =
|
|
6774
|
-
const [webUrl, setWebUrl] =
|
|
6775
|
-
const [agentMode, setAgentMode] =
|
|
6776
|
-
const [planFilePath, setPlanFilePath] =
|
|
6777
|
-
const [selectedModel, setSelectedModel] =
|
|
6778
|
-
const [allToolsExpanded, setAllToolsExpanded] =
|
|
6779
|
-
const [staticRemountKey, setStaticRemountKey] =
|
|
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
|
|
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] =
|
|
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
|
|
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] =
|
|
7073
|
-
const [cursorOffset, setCursorOffset] =
|
|
7074
|
-
const [allFiles, setAllFiles] =
|
|
7075
|
-
const [suggestions, setSuggestions] =
|
|
7076
|
-
const [activeSuggestion, setActiveSuggestion] =
|
|
7077
|
-
const [showSuggestions, setShowSuggestions] =
|
|
7078
|
-
const [mentionStartIndex, setMentionStartIndex] =
|
|
7079
|
-
const
|
|
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 [
|
|
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
|
-
|
|
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
|
|
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
|
|
7184
|
-
const rel = path4.relative(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
7249
|
-
if (
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
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((
|
|
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 }, " (
|
|
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
|
|
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 =
|
|
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
|
|
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] =
|
|
7786
|
-
const [shimmerPosition, setShimmerPosition] =
|
|
7892
|
+
const [messageIndex, setMessageIndex] = useState7(0);
|
|
7893
|
+
const [shimmerPosition, setShimmerPosition] = useState7(0);
|
|
7787
7894
|
const message = LOADING_MESSAGES[messageIndex];
|
|
7788
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 } =
|
|
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 =
|
|
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
|
|
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] =
|
|
8198
|
-
const [selectedIndex, setSelectedIndex] =
|
|
8199
|
-
const [isLoading, setIsLoading] =
|
|
8200
|
-
const [hasMore, setHasMore] =
|
|
8201
|
-
const [totalSessions, setTotalSessions] =
|
|
8202
|
-
const [error, setError] =
|
|
8203
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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] =
|
|
8403
|
-
const [showHelp, setShowHelp] =
|
|
8404
|
-
const [showInput, setShowInput] =
|
|
8405
|
-
const [gitBranch] =
|
|
8406
|
-
const [currentFolder] =
|
|
8407
|
-
const [hasInputContent, setHasInputContent] =
|
|
8408
|
-
const [exitWarning, setExitWarning] =
|
|
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] =
|
|
8411
|
-
const [showModelSelector, setShowModelSelector] =
|
|
8412
|
-
const [showFeedbackDialog, setShowFeedbackDialog] =
|
|
8413
|
-
const [isLoadingSession, setIsLoadingSession] =
|
|
8414
|
-
const [authState, setAuthState] =
|
|
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] =
|
|
8547
|
+
const [showAuthDialog, setShowAuthDialog] = useState9(false);
|
|
8418
8548
|
const { isOverlayOpen, isCancelSuppressed, markOverlayClosed } = useOverlayEscapeGuard({
|
|
8419
8549
|
overlays: [showHelp, showSessionSelector, showAuthDialog, showModelSelector, showFeedbackDialog]
|
|
8420
8550
|
});
|
|
8421
|
-
|
|
8551
|
+
useEffect9(() => {
|
|
8422
8552
|
if (!config2.supatestApiKey) {
|
|
8423
8553
|
setShowAuthDialog(true);
|
|
8424
8554
|
}
|
|
8425
8555
|
}, [config2.supatestApiKey]);
|
|
8426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9176
|
+
useEffect11(() => {
|
|
9025
9177
|
if (shouldInterruptAgent && agentRef.current) {
|
|
9026
9178
|
agentRef.current.abort();
|
|
9027
9179
|
setShouldInterruptAgent(false);
|
|
9028
9180
|
}
|
|
9029
9181
|
}, [shouldInterruptAgent, setShouldInterruptAgent]);
|
|
9030
|
-
|
|
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 {
|