@supatest/cli 0.0.39 → 0.0.41

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 +2459 -555
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -233,7 +233,7 @@ You are the Supatest CLI Help Assistant. You help users understand and use the S
233
233
  - MCP (Model Context Protocol) server management
234
234
  - npm package information and npm link setup
235
235
 
236
- You provide clear, friendly, and accurate answers based on the comprehensive CLI documentation below. You can maintain conversation context across multiple messages, answer follow-up questions, and guide users toward the right features for their needs.
236
+ You provide clear, friendly, and accurate answers based on the comprehensive CLI documentation below. You can maintain session context across multiple messages, answer follow-up questions, and guide users toward the right features for their needs.
237
237
  </role>
238
238
 
239
239
  <capabilities>
@@ -358,7 +358,7 @@ These work in interactive mode:
358
358
  - **ESC or Q** - Close overlays (help, menus, dialogs)
359
359
 
360
360
  ### Navigation
361
- - **Ctrl+M** - Cycle through available models
361
+ - **Ctrl+Shift+M** - Cycle through available models
362
362
  - **Ctrl+P** - Cycle through LLM providers
363
363
 
364
364
  ### Input & Text Editing
@@ -585,7 +585,7 @@ Just ask: "Can you check the npm docs?" or "What's the latest version?"
585
585
  - The agent will handle framework-specific syntax
586
586
 
587
587
  ### Session Persistence
588
- - Your conversations are saved and can be resumed
588
+ - Your sessions are saved and can be resumed
589
589
  - Use /resume to continue work from where you left off
590
590
  - Useful for long-running projects
591
591
 
@@ -596,7 +596,7 @@ Just ask: "Can you check the npm docs?" or "What's the latest version?"
596
596
 
597
597
  </cli-documentation>
598
598
 
599
- <conversation-guidelines>
599
+ <session-guidelines>
600
600
  When answering questions:
601
601
 
602
602
  1. **Be direct**: Answer the question asked first, then offer related information
@@ -614,7 +614,7 @@ When users mix help with actual test building:
614
614
  - Answer their help question
615
615
  - Then ask if they want to start building tests or need more information
616
616
 
617
- </conversation-guidelines>
617
+ </session-guidelines>
618
618
 
619
619
  <action-guide>
620
620
  ## How to Help Users with Actions
@@ -1155,7 +1155,7 @@ function getToolGroupCounts(tools) {
1155
1155
  );
1156
1156
  return Object.entries(groups).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
1157
1157
  }
1158
- var AVAILABLE_MODELS, DATE_SUFFIX_REGEX, SMALL_COST_MULTIPLIER, MEDIUM_COST_MULTIPLIER, PREMIUM_COST_MULTIPLIER, SMALL_COST_LABEL, MEDIUM_COST_LABEL, PREMIUM_COST_LABEL, MODEL_TIERS, CONTEXT_WINDOWS, util, objectUtil, ZodParsedType, getParsedType, ZodIssueCode, ZodError, errorMap, overrideErrorMap, makeIssue, ParseStatus, INVALID, DIRTY, OK, isAborted, isDirty, isValid, isAsync, errorUtil, ParseInputLazyPath, handleResult, ZodType, cuidRegex, cuid2Regex, ulidRegex, uuidRegex, nanoidRegex, jwtRegex, durationRegex, emailRegex, _emojiRegex, emojiRegex, ipv4Regex, ipv4CidrRegex, ipv6Regex, ipv6CidrRegex, base64Regex, base64urlRegex, dateRegexSource, dateRegex, ZodString, ZodNumber, ZodBigInt, ZodBoolean, ZodDate, ZodSymbol, ZodUndefined, ZodNull, ZodAny, ZodUnknown, ZodNever, ZodVoid, ZodArray, ZodObject, ZodUnion, getDiscriminator, ZodDiscriminatedUnion, ZodIntersection, ZodTuple, ZodRecord, ZodMap, ZodSet, ZodFunction, ZodLazy, ZodLiteral, ZodEnum, ZodNativeEnum, ZodPromise, ZodEffects, ZodOptional, ZodNullable, ZodDefault, ZodCatch, ZodNaN, ZodBranded, ZodPipeline, ZodReadonly, ZodFirstPartyTypeKind, stringType, numberType, booleanType, dateType, unknownType, arrayType, objectType, unionType, discriminatedUnionType, recordType, functionType, lazyType, literalType, enumType, promiseType, coerce, MAX_API_KEY_NAME_LENGTH, apiKeySchema, apiKeyUsageSchema, createApiKeyRequestSchema, apiKeyResponseSchema, apiKeyUsageSummarySchema, genericErrorSchema, validationErrorSchema, feedbackCategorySchema, FEEDBACK_CATEGORIES, createFeedbackSchema, feedbackResponseSchema, listFeedbackQuerySchema, feedbackListResponseSchema, healthMetricSchema, healthMetricDailyItemSchema, healthMetricsWithDailySchema, healthAnalyticsPeriodSchema, healthAnalyticsDailyItemSchema, healthAnalyticsResponseSchema, MAX_TIMEZONE_CHAR_LENGTH, organizationSchema, organizationSettingsSchema, textBlockSchema, toolUseBlockSchema, toolResultBlockSchema, thinkingBlockSchema, imageBlockSchema, contentBlockSchema, sessionSchema, createSessionRequestSchema, updateSessionRequestSchema, messageSchema, createMessageRequestSchema, cliEventSchema, createCLISessionRequestSchema, queryResultSchema, queryTurnSchema, queryContentSchema, queryUsageSchema, querySchema, runStatusSchema, testResultStatusSchema, testOutcomeSchema, attachmentKindSchema, stepCategorySchema, runSummarySchema, ciMetadataSchema, gitMetadataSchema, playwrightConfigSchema, errorInfoSchema, locationSchema, sourceSnippetSchema, runSchema, annotationSchema, testSchema, testResultSchema, baseStepSchema, stepSchema, attachmentSchema, listRunsQuerySchema, listTestsQuerySchema, runsListResponseSchema, runDetailResponseSchema, testsListResponseSchema, testDetailResponseSchema, testHistoryItemSchema, testHistoryResponseSchema, topOffenderSchema, topOffendersResponseSchema, trendPointSchema, trendsResponseSchema, errorCategorySchema, failureClusterSchema, newFailureSchema, runInsightsResponseSchema, FailureCategoryEnum, SelectorTypeEnum, FailureCategoryStatsSchema, FailureCategoriesResponseSchema, FailingSelectorStatsSchema, FailingSelectorsResponseSchema, newFailureItemSchema, newFailuresResponseSchema, flakyTestItemSchema, flakyTestsResponseSchema, slowestTestItemSchema, slowestTestsResponseSchema;
1158
+ var AVAILABLE_MODELS, DATE_SUFFIX_REGEX, SMALL_COST_MULTIPLIER, MEDIUM_COST_MULTIPLIER, PREMIUM_COST_MULTIPLIER, SMALL_COST_LABEL, MEDIUM_COST_LABEL, PREMIUM_COST_LABEL, MODEL_TIERS, CONTEXT_WINDOWS, util, objectUtil, ZodParsedType, getParsedType, ZodIssueCode, ZodError, errorMap, overrideErrorMap, makeIssue, ParseStatus, INVALID, DIRTY, OK, isAborted, isDirty, isValid, isAsync, errorUtil, ParseInputLazyPath, handleResult, ZodType, cuidRegex, cuid2Regex, ulidRegex, uuidRegex, nanoidRegex, jwtRegex, durationRegex, emailRegex, _emojiRegex, emojiRegex, ipv4Regex, ipv4CidrRegex, ipv6Regex, ipv6CidrRegex, base64Regex, base64urlRegex, dateRegexSource, dateRegex, ZodString, ZodNumber, ZodBigInt, ZodBoolean, ZodDate, ZodSymbol, ZodUndefined, ZodNull, ZodAny, ZodUnknown, ZodNever, ZodVoid, ZodArray, ZodObject, ZodUnion, getDiscriminator, ZodDiscriminatedUnion, ZodIntersection, ZodTuple, ZodRecord, ZodMap, ZodSet, ZodFunction, ZodLazy, ZodLiteral, ZodEnum, ZodNativeEnum, ZodPromise, ZodEffects, ZodOptional, ZodNullable, ZodDefault, ZodCatch, ZodNaN, ZodBranded, ZodPipeline, ZodReadonly, ZodFirstPartyTypeKind, stringType, numberType, booleanType, dateType, unknownType, arrayType, objectType, unionType, discriminatedUnionType, recordType, functionType, lazyType, literalType, enumType, promiseType, coerce, MAX_API_KEY_NAME_LENGTH, apiKeySchema, apiKeyUsageSchema, createApiKeyRequestSchema, apiKeyResponseSchema, apiKeyUsageSummarySchema, genericErrorSchema, validationErrorSchema, feedbackCategorySchema, FEEDBACK_CATEGORIES, createFeedbackSchema, feedbackResponseSchema, listFeedbackQuerySchema, feedbackListResponseSchema, healthMetricSchema, healthMetricDailyItemSchema, healthMetricsWithDailySchema, healthAnalyticsPeriodSchema, healthAnalyticsDailyItemSchema, healthAnalyticsResponseSchema, MAX_TIMEZONE_CHAR_LENGTH, organizationSchema, organizationSettingsSchema, textBlockSchema, toolUseBlockSchema, toolResultBlockSchema, thinkingBlockSchema, imageBlockSchema, contentBlockSchema, sessionSchema, createSessionRequestSchema, updateSessionRequestSchema, messageSchema, createMessageRequestSchema, cliEventSchema, createCLISessionRequestSchema, queryResultSchema, queryTurnSchema, queryContentSchema, queryUsageSchema, querySchema, runStatusSchema, testResultStatusSchema, testOutcomeSchema, attachmentKindSchema, stepCategorySchema, runSummarySchema, ciMetadataSchema, gitMetadataSchema, playwrightConfigSchema, errorInfoSchema, locationSchema, sourceSnippetSchema, runSchema, annotationSchema, testSchema, testResultSchema, baseStepSchema, stepSchema, attachmentSchema, listRunsQuerySchema, listTestsQuerySchema, runsListResponseSchema, runDetailResponseSchema, testsListResponseSchema, testDetailResponseSchema, testHistoryItemSchema, testHistoryResponseSchema, topOffenderSchema, topOffendersResponseSchema, trendPointSchema, trendsResponseSchema, errorCategorySchema, failureClusterSchema, newFailureSchema, runInsightsResponseSchema, FailureCategoryEnum, SelectorTypeEnum, FailureCategoryStatsSchema, FailureCategoriesResponseSchema, FailingSelectorStatsSchema, FailingSelectorsResponseSchema, newFailureItemSchema, newFailuresResponseSchema, flakyTestItemSchema, flakyTestsResponseSchema, slowestTestItemSchema, slowestTestsResponseSchema, runSummaryEmailFailureSchema, runSummaryEmailReportSchema, sendRunReportRequestSchema, metricWithTrendSchema, weekOverWeekMetricsSchema, ciComputeTimeSchema, investigationCandidateSchema, failureCategoryBreakdownSchema, stabilityTrendSchema, folderStabilitySchema, managerReportSchema, managerReportQuerySchema, sendManagerReportRequestSchema, reportAttachmentLinkSchema, developerReportRegressionSchema, developerReportFlakyTestSchema, developerReportSlowTestSchema, developerRunSummaryReportSchema, executiveReportStatusSchema, executiveMetricSchema, executiveKeyMetricsSchema, executiveTrendPointSchema, executiveTrendsSchema, executiveFlakyOffenderSchema, executiveSlowestOffenderSchema, executiveTopOffendersSchema, executiveReportSchema, executiveReportQuerySchema, sendExecutiveReportRequestSchema, fixAssignmentStatusSchema, fixAssignmentSchema, testWithAssignmentSchema, listAssignmentsQuerySchema, listTestsWithAssignmentsQuerySchema, assignmentsListResponseSchema, assigneeInfoSchema, assigneesListResponseSchema, assignTestsRequestSchema, assignTestRequestSchema, reassignTestRequestSchema, completeAssignmentsRequestSchema, completeAssignmentRequestSchema, bulkReassignRequestSchema, assignmentResponseSchema, assignTestsResponseSchema, testsWithAssignmentsResponseSchema, bulkReassignResponseSchema, userAssignmentStatsSchema, SECONDS_PER_MINUTE, SECONDS_PER_HOUR, SECONDS_PER_DAY, SECONDS_PER_WEEK, SECONDS_PER_MONTH, SECONDS_PER_YEAR;
1159
1159
  var init_shared_es = __esm({
1160
1160
  "../shared/dist/shared.es.mjs"() {
1161
1161
  "use strict";
@@ -5103,7 +5103,7 @@ var init_shared_es = __esm({
5103
5103
  originMetadata: recordType(unknownType()).nullable().optional(),
5104
5104
  authMethod: enumType(["clerk", "cli-token", "api-key"]).default("clerk"),
5105
5105
  authId: stringType().nullable().optional(),
5106
- // Claude Agent SDK session ID for resuming conversations
5106
+ // Claude Agent SDK session ID for resuming sessions
5107
5107
  // Sessions expire after ~30 days due to Anthropic's data retention policy
5108
5108
  providerSessionId: stringType().nullable().optional(),
5109
5109
  createdAt: dateType(),
@@ -5120,7 +5120,7 @@ var init_shared_es = __esm({
5120
5120
  });
5121
5121
  messageSchema = objectType({
5122
5122
  id: stringType().uuid(),
5123
- conversationId: stringType().uuid(),
5123
+ sessionId: stringType().uuid(),
5124
5124
  role: enumType(["user", "assistant"]),
5125
5125
  content: arrayType(contentBlockSchema),
5126
5126
  createdAt: dateType()
@@ -5414,6 +5414,7 @@ var init_shared_es = __esm({
5414
5414
  limit: coerce.number().min(1).max(100).default(20),
5415
5415
  status: runStatusSchema.optional(),
5416
5416
  branch: stringType().optional(),
5417
+ search: stringType().optional(),
5417
5418
  startDate: stringType().optional(),
5418
5419
  endDate: stringType().optional()
5419
5420
  });
@@ -5503,6 +5504,7 @@ var init_shared_es = __esm({
5503
5504
  "assertion",
5504
5505
  "element_not_found",
5505
5506
  "server_error",
5507
+ "navigation",
5506
5508
  "other"
5507
5509
  ]);
5508
5510
  failureClusterSchema = objectType({
@@ -5524,14 +5526,7 @@ var init_shared_es = __esm({
5524
5526
  newFailures: arrayType(newFailureSchema),
5525
5527
  clusters: arrayType(failureClusterSchema)
5526
5528
  });
5527
- FailureCategoryEnum = enumType([
5528
- "timeout",
5529
- "element_not_found",
5530
- "assertion",
5531
- "network",
5532
- "navigation",
5533
- "other"
5534
- ]);
5529
+ FailureCategoryEnum = errorCategorySchema;
5535
5530
  SelectorTypeEnum = enumType([
5536
5531
  "testid",
5537
5532
  "class",
@@ -5629,12 +5624,1021 @@ var init_shared_es = __esm({
5629
5624
  days: numberType()
5630
5625
  })
5631
5626
  });
5627
+ runSummaryEmailFailureSchema = objectType({
5628
+ testRunId: stringType(),
5629
+ testId: stringType(),
5630
+ title: stringType(),
5631
+ file: stringType(),
5632
+ errorMessage: stringType().nullable(),
5633
+ errorStack: stringType().nullable()
5634
+ });
5635
+ runSummaryEmailReportSchema = objectType({
5636
+ runId: stringType(),
5637
+ readableId: stringType().optional(),
5638
+ runDetailsUrl: stringType(),
5639
+ startedAt: stringType(),
5640
+ endedAt: stringType().optional(),
5641
+ durationMs: numberType(),
5642
+ branch: stringType().optional(),
5643
+ commit: stringType().optional(),
5644
+ commitMessage: stringType().optional(),
5645
+ totalTests: numberType(),
5646
+ passedTests: numberType(),
5647
+ failedTests: numberType(),
5648
+ flakyTests: numberType(),
5649
+ skippedTests: numberType(),
5650
+ passRate: numberType(),
5651
+ topFailures: arrayType(runSummaryEmailFailureSchema)
5652
+ });
5653
+ sendRunReportRequestSchema = objectType({
5654
+ runId: stringType(),
5655
+ emails: arrayType(stringType().email())
5656
+ });
5657
+ metricWithTrendSchema = objectType({
5658
+ current: numberType(),
5659
+ previous: numberType(),
5660
+ change: numberType(),
5661
+ percentChange: numberType().nullable()
5662
+ });
5663
+ weekOverWeekMetricsSchema = objectType({
5664
+ passRate: metricWithTrendSchema,
5665
+ flakyTestCount: metricWithTrendSchema,
5666
+ newFailures: metricWithTrendSchema,
5667
+ totalRuns: metricWithTrendSchema
5668
+ });
5669
+ ciComputeTimeSchema = objectType({
5670
+ failedRunsMs: numberType(),
5671
+ retriedRunsMs: numberType(),
5672
+ totalWastedMs: numberType(),
5673
+ failedRunsHours: numberType(),
5674
+ retriedRunsHours: numberType(),
5675
+ totalWastedHours: numberType()
5676
+ });
5677
+ investigationCandidateSchema = objectType({
5678
+ testId: stringType(),
5679
+ testRunId: stringType(),
5680
+ runId: stringType(),
5681
+ file: stringType(),
5682
+ title: stringType(),
5683
+ flakeCount: numberType(),
5684
+ passRate: numberType(),
5685
+ avgDurationMs: numberType(),
5686
+ ciTimeImpactMs: numberType(),
5687
+ ciTimeImpactHours: numberType(),
5688
+ category: FailureCategoryEnum.nullable(),
5689
+ firstFlakyAt: stringType().nullable()
5690
+ });
5691
+ failureCategoryBreakdownSchema = objectType({
5692
+ category: FailureCategoryEnum,
5693
+ count: numberType(),
5694
+ percentage: numberType()
5695
+ });
5696
+ stabilityTrendSchema = enumType([
5697
+ "improving",
5698
+ "degrading",
5699
+ "stable"
5700
+ ]);
5701
+ folderStabilitySchema = objectType({
5702
+ folder: stringType(),
5703
+ passRate: numberType(),
5704
+ previousPassRate: numberType(),
5705
+ trend: stabilityTrendSchema,
5706
+ testCount: numberType(),
5707
+ failureCount: numberType()
5708
+ });
5709
+ managerReportSchema = objectType({
5710
+ period: objectType({
5711
+ start: stringType(),
5712
+ end: stringType(),
5713
+ days: numberType()
5714
+ }),
5715
+ weekOverWeek: weekOverWeekMetricsSchema,
5716
+ ciComputeTime: ciComputeTimeSchema,
5717
+ investigationCandidates: arrayType(investigationCandidateSchema),
5718
+ failureCategories: arrayType(failureCategoryBreakdownSchema),
5719
+ folderStability: arrayType(folderStabilitySchema)
5720
+ });
5721
+ managerReportQuerySchema = objectType({
5722
+ startDate: stringType().regex(
5723
+ /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/,
5724
+ "startDate must be in format YYYY-MM-DD or ISO datetime"
5725
+ ),
5726
+ endDate: stringType().regex(
5727
+ /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/,
5728
+ "endDate must be in format YYYY-MM-DD or ISO datetime"
5729
+ )
5730
+ });
5731
+ sendManagerReportRequestSchema = objectType({
5732
+ startDate: stringType(),
5733
+ endDate: stringType(),
5734
+ emails: arrayType(stringType().email())
5735
+ });
5736
+ reportAttachmentLinkSchema = objectType({
5737
+ name: stringType(),
5738
+ kind: attachmentKindSchema,
5739
+ url: stringType().optional()
5740
+ });
5741
+ developerReportRegressionSchema = objectType({
5742
+ testRunId: stringType(),
5743
+ testId: stringType(),
5744
+ title: stringType(),
5745
+ file: stringType(),
5746
+ errorMessage: stringType().nullable(),
5747
+ errorStack: stringType().nullable(),
5748
+ aiSummary: stringType().nullable(),
5749
+ attachments: arrayType(reportAttachmentLinkSchema),
5750
+ lastStableRun: objectType({
5751
+ runId: stringType(),
5752
+ date: stringType(),
5753
+ branch: stringType().optional()
5754
+ }).nullable()
5755
+ });
5756
+ developerReportFlakyTestSchema = objectType({
5757
+ testRunId: stringType(),
5758
+ testId: stringType(),
5759
+ title: stringType(),
5760
+ file: stringType(),
5761
+ passRate: numberType(),
5762
+ category: errorCategorySchema.nullable(),
5763
+ lastPassedRun: objectType({
5764
+ runId: stringType(),
5765
+ date: stringType()
5766
+ }).nullable()
5767
+ });
5768
+ developerReportSlowTestSchema = objectType({
5769
+ testRunId: stringType(),
5770
+ testId: stringType(),
5771
+ title: stringType(),
5772
+ file: stringType(),
5773
+ durationMs: numberType(),
5774
+ historicalAvgMs: numberType(),
5775
+ slowdownFactor: numberType()
5776
+ // how many times slower than average
5777
+ });
5778
+ developerRunSummaryReportSchema = objectType({
5779
+ runId: stringType(),
5780
+ readableId: stringType().optional(),
5781
+ runDetailsUrl: stringType(),
5782
+ startedAt: stringType(),
5783
+ endedAt: stringType().optional(),
5784
+ durationMs: numberType(),
5785
+ branch: stringType().optional(),
5786
+ commit: stringType().optional(),
5787
+ commitMessage: stringType().optional(),
5788
+ // Summary counts
5789
+ totalTests: numberType(),
5790
+ passedTests: numberType(),
5791
+ failedTests: numberType(),
5792
+ flakyTests: numberType(),
5793
+ skippedTests: numberType(),
5794
+ passRate: numberType(),
5795
+ // Sections
5796
+ newRegressions: arrayType(developerReportRegressionSchema),
5797
+ knownFlaky: arrayType(developerReportFlakyTestSchema),
5798
+ slowTests: arrayType(developerReportSlowTestSchema)
5799
+ });
5800
+ executiveReportStatusSchema = enumType([
5801
+ "healthy",
5802
+ "needs_attention",
5803
+ "critical"
5804
+ ]);
5805
+ executiveMetricSchema = objectType({
5806
+ /** Current month's value */
5807
+ value: numberType(),
5808
+ /** Previous month's value */
5809
+ previousValue: numberType(),
5810
+ /** Percentage change from previous month (null if previous was 0) */
5811
+ change: numberType().nullable()
5812
+ });
5813
+ executiveKeyMetricsSchema = objectType({
5814
+ /** CI First-Try Pass Rate: COUNT(runs WHERE failed=0 AND flaky=0) / COUNT(runs) */
5815
+ ciFirstTryPassRate: executiveMetricSchema,
5816
+ /** Total Test Pass Rate from health analytics */
5817
+ testPassRate: executiveMetricSchema,
5818
+ /** Count of distinct tests with flaky outcomes */
5819
+ flakyTestCount: executiveMetricSchema,
5820
+ /** CI compute time lost on failed/retried runs in hours */
5821
+ ciComputeLostHours: executiveMetricSchema
5822
+ });
5823
+ executiveTrendPointSchema = objectType({
5824
+ date: stringType(),
5825
+ value: numberType()
5826
+ });
5827
+ executiveTrendsSchema = objectType({
5828
+ /** Daily pass rate values for sparkline */
5829
+ dailyPassRate: arrayType(executiveTrendPointSchema),
5830
+ /** Daily flaky count values for sparkline */
5831
+ dailyFlakyCount: arrayType(executiveTrendPointSchema)
5832
+ });
5833
+ executiveFlakyOffenderSchema = objectType({
5834
+ testId: stringType(),
5835
+ title: stringType(),
5836
+ file: stringType(),
5837
+ /** Number of flaky occurrences */
5838
+ flakyCount: numberType(),
5839
+ /** CI time impact in hours */
5840
+ ciTimeImpactHours: numberType()
5841
+ });
5842
+ executiveSlowestOffenderSchema = objectType({
5843
+ testId: stringType(),
5844
+ title: stringType(),
5845
+ file: stringType(),
5846
+ /** Average duration in milliseconds */
5847
+ avgDurationMs: numberType(),
5848
+ /** Trend percentage change from previous period (null if no previous data) */
5849
+ trend: numberType().nullable()
5850
+ });
5851
+ executiveTopOffendersSchema = objectType({
5852
+ /** Top 3 most flaky tests with CI time impact */
5853
+ mostFlaky: arrayType(executiveFlakyOffenderSchema),
5854
+ /** Top 3 slowest tests with trend */
5855
+ slowest: arrayType(executiveSlowestOffenderSchema)
5856
+ });
5857
+ executiveReportSchema = objectType({
5858
+ /** Month in format YYYY-MM */
5859
+ month: stringType(),
5860
+ /** When the report was generated */
5861
+ generatedAt: stringType(),
5862
+ /** URL to view full report in dashboard */
5863
+ reportUrl: stringType(),
5864
+ /** Overall status: healthy, needs_attention, or critical */
5865
+ status: executiveReportStatusSchema,
5866
+ /** 4 key metrics with month-over-month comparison */
5867
+ keyMetrics: executiveKeyMetricsSchema,
5868
+ /** 30-day trend data for sparklines */
5869
+ trends: executiveTrendsSchema,
5870
+ /** Top offenders (most flaky and slowest tests) */
5871
+ topOffenders: executiveTopOffendersSchema,
5872
+ /** Auto-generated signal messages based on thresholds */
5873
+ signals: arrayType(stringType())
5874
+ });
5875
+ executiveReportQuerySchema = objectType({
5876
+ month: stringType().regex(/^\d{4}-\d{2}$/, "month must be in format YYYY-MM")
5877
+ });
5878
+ sendExecutiveReportRequestSchema = objectType({
5879
+ month: stringType().regex(/^\d{4}-\d{2}$/, "month must be in format YYYY-MM"),
5880
+ emails: arrayType(stringType().email())
5881
+ });
5882
+ fixAssignmentStatusSchema = enumType([
5883
+ "assigned",
5884
+ // Currently being worked on
5885
+ "completed",
5886
+ // Successfully fixed
5887
+ "failed"
5888
+ // Fix attempt failed
5889
+ ]);
5890
+ fixAssignmentSchema = objectType({
5891
+ id: stringType(),
5892
+ testRunId: stringType(),
5893
+ // Reference to test_run table
5894
+ assignedTo: stringType().optional(),
5895
+ // User ID (Clerk user ID)
5896
+ assignedAt: stringType().optional(),
5897
+ // ISO timestamp
5898
+ status: fixAssignmentStatusSchema.optional(),
5899
+ completedAt: stringType().optional(),
5900
+ reason: stringType().optional(),
5901
+ createdAt: stringType(),
5902
+ updatedAt: stringType()
5903
+ });
5904
+ testWithAssignmentSchema = objectType({
5905
+ // Core test fields from Test schema
5906
+ id: stringType(),
5907
+ readableId: stringType().optional(),
5908
+ runId: stringType(),
5909
+ file: stringType(),
5910
+ title: stringType(),
5911
+ location: objectType({
5912
+ file: stringType(),
5913
+ line: numberType(),
5914
+ column: numberType().optional()
5915
+ }).optional(),
5916
+ status: enumType(["passed", "failed", "timedOut", "skipped", "interrupted"]),
5917
+ durationMs: numberType(),
5918
+ retryCount: numberType(),
5919
+ isFlaky: booleanType().optional(),
5920
+ // Assignment fields
5921
+ assignmentId: stringType().optional(),
5922
+ assignedTo: stringType().optional(),
5923
+ assignedAt: stringType().optional(),
5924
+ assignmentStatus: fixAssignmentStatusSchema.optional(),
5925
+ assignmentCompletedAt: stringType().optional(),
5926
+ assignmentReason: stringType().optional()
5927
+ });
5928
+ listAssignmentsQuerySchema = objectType({
5929
+ runId: stringType().optional(),
5930
+ assignedTo: stringType().optional(),
5931
+ status: fixAssignmentStatusSchema.optional(),
5932
+ file: stringType().optional(),
5933
+ search: stringType().optional(),
5934
+ page: coerce.number().min(1).default(1),
5935
+ limit: coerce.number().min(1).max(100).default(50)
5936
+ });
5937
+ listTestsWithAssignmentsQuerySchema = objectType({
5938
+ assignedTo: stringType().optional(),
5939
+ status: fixAssignmentStatusSchema.optional(),
5940
+ testStatus: enumType(["passed", "failed", "timedOut", "skipped", "interrupted"]).optional(),
5941
+ file: stringType().optional(),
5942
+ search: stringType().optional(),
5943
+ page: coerce.number().min(1).default(1),
5944
+ limit: coerce.number().min(1).max(100).default(50)
5945
+ });
5946
+ assignmentsListResponseSchema = objectType({
5947
+ assignments: arrayType(testWithAssignmentSchema),
5948
+ total: numberType(),
5949
+ page: numberType(),
5950
+ limit: numberType(),
5951
+ // Summary stats
5952
+ stats: objectType({
5953
+ total: numberType(),
5954
+ assigned: numberType(),
5955
+ completed: numberType(),
5956
+ failed: numberType(),
5957
+ free: numberType()
5958
+ })
5959
+ });
5960
+ assigneeInfoSchema = objectType({
5961
+ id: stringType(),
5962
+ name: stringType(),
5963
+ email: stringType().optional(),
5964
+ activeTests: numberType().default(0)
5965
+ // Number of tests currently assigned
5966
+ });
5967
+ assigneesListResponseSchema = objectType({
5968
+ assignees: arrayType(assigneeInfoSchema)
5969
+ });
5970
+ assignTestsRequestSchema = objectType({
5971
+ testRunIds: arrayType(stringType()).min(1),
5972
+ assignedTo: stringType().min(1),
5973
+ notes: stringType().optional()
5974
+ });
5975
+ assignTestRequestSchema = objectType({
5976
+ testRunId: stringType(),
5977
+ assignedTo: stringType(),
5978
+ reason: stringType().optional()
5979
+ });
5980
+ reassignTestRequestSchema = objectType({
5981
+ assignedTo: stringType().min(1),
5982
+ notes: stringType().optional()
5983
+ });
5984
+ completeAssignmentsRequestSchema = objectType({
5985
+ assignmentIds: arrayType(stringType()).min(1),
5986
+ status: fixAssignmentStatusSchema,
5987
+ notes: stringType().optional(),
5988
+ fixSessionId: stringType().optional()
5989
+ });
5990
+ completeAssignmentRequestSchema = objectType({
5991
+ assignmentId: stringType(),
5992
+ success: booleanType()
5993
+ });
5994
+ bulkReassignRequestSchema = objectType({
5995
+ fromUser: stringType().min(1),
5996
+ toUser: stringType().min(1),
5997
+ status: fixAssignmentStatusSchema.optional(),
5998
+ notes: stringType().optional()
5999
+ });
6000
+ assignmentResponseSchema = objectType({
6001
+ assignment: fixAssignmentSchema
6002
+ });
6003
+ assignTestsResponseSchema = objectType({
6004
+ assigned: arrayType(
6005
+ objectType({
6006
+ id: stringType(),
6007
+ testRunId: stringType()
6008
+ })
6009
+ ),
6010
+ conflicts: arrayType(
6011
+ objectType({
6012
+ testRunId: stringType(),
6013
+ file: stringType(),
6014
+ title: stringType(),
6015
+ currentAssignee: stringType()
6016
+ })
6017
+ )
6018
+ });
6019
+ testsWithAssignmentsResponseSchema = objectType({
6020
+ tests: arrayType(testWithAssignmentSchema),
6021
+ total: numberType(),
6022
+ page: numberType(),
6023
+ limit: numberType(),
6024
+ stats: objectType({
6025
+ total: numberType(),
6026
+ assigned: numberType(),
6027
+ completed: numberType(),
6028
+ failed: numberType(),
6029
+ free: numberType()
6030
+ })
6031
+ });
6032
+ bulkReassignResponseSchema = objectType({
6033
+ reassigned: numberType(),
6034
+ conflicts: arrayType(
6035
+ objectType({
6036
+ testRunId: stringType(),
6037
+ file: stringType(),
6038
+ title: stringType(),
6039
+ reason: stringType()
6040
+ })
6041
+ )
6042
+ });
6043
+ userAssignmentStatsSchema = objectType({
6044
+ assigned: numberType(),
6045
+ completed: numberType(),
6046
+ failed: numberType()
6047
+ });
6048
+ SECONDS_PER_MINUTE = 60;
6049
+ SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
6050
+ SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
6051
+ SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY;
6052
+ SECONDS_PER_MONTH = 30 * SECONDS_PER_DAY;
6053
+ SECONDS_PER_YEAR = 365 * SECONDS_PER_DAY;
6054
+ }
6055
+ });
6056
+
6057
+ // src/utils/claude-oauth.ts
6058
+ var claude_oauth_exports = {};
6059
+ __export(claude_oauth_exports, {
6060
+ ClaudeOAuthService: () => ClaudeOAuthService
6061
+ });
6062
+ import { spawn } from "child_process";
6063
+ import { createHash, randomBytes } from "crypto";
6064
+ import http from "http";
6065
+ import { platform } from "os";
6066
+ var OAUTH_CONFIG, CALLBACK_PORT, CALLBACK_TIMEOUT_MS, ClaudeOAuthService;
6067
+ var init_claude_oauth = __esm({
6068
+ "src/utils/claude-oauth.ts"() {
6069
+ "use strict";
6070
+ OAUTH_CONFIG = {
6071
+ clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
6072
+ // Claude Code's client ID
6073
+ authorizationEndpoint: "https://claude.ai/oauth/authorize",
6074
+ tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
6075
+ redirectUri: "http://localhost:8421/callback",
6076
+ // Local callback for CLI
6077
+ scopes: ["user:inference", "user:profile", "org:create_api_key"]
6078
+ };
6079
+ CALLBACK_PORT = 8421;
6080
+ CALLBACK_TIMEOUT_MS = 3e5;
6081
+ ClaudeOAuthService = class _ClaudeOAuthService {
6082
+ secretStorage;
6083
+ static TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
6084
+ // 5 minutes
6085
+ pendingCodeVerifier = null;
6086
+ // Store code verifier for PKCE
6087
+ constructor(secretStorage) {
6088
+ this.secretStorage = secretStorage;
6089
+ }
6090
+ /**
6091
+ * Starts the OAuth authorization flow
6092
+ * Opens the default browser for user authentication
6093
+ * Returns after successful authentication
6094
+ */
6095
+ async authorize() {
6096
+ try {
6097
+ const state = this.generateRandomState();
6098
+ const pkce = this.generatePKCEChallenge();
6099
+ this.pendingCodeVerifier = pkce.codeVerifier;
6100
+ const authUrl = this.buildAuthorizationUrl(state, pkce.codeChallenge);
6101
+ console.log("\nAuthenticating with Claude...\n");
6102
+ console.log(`Opening browser to: ${authUrl}
6103
+ `);
6104
+ const tokenPromise = this.startCallbackServer(CALLBACK_PORT, state);
6105
+ try {
6106
+ this.openBrowser(authUrl);
6107
+ } catch (error) {
6108
+ console.warn("Failed to open browser automatically:", error);
6109
+ console.log(`
6110
+ Please manually open this URL in your browser:
6111
+ ${authUrl}
6112
+ `);
6113
+ }
6114
+ await tokenPromise;
6115
+ console.log("\n\u2705 Successfully authenticated with Claude!\n");
6116
+ return { success: true };
6117
+ } catch (error) {
6118
+ this.pendingCodeVerifier = null;
6119
+ return {
6120
+ success: false,
6121
+ error: error instanceof Error ? error.message : "Authentication failed"
6122
+ };
6123
+ }
6124
+ }
6125
+ /**
6126
+ * Start local HTTP server to receive OAuth callback
6127
+ */
6128
+ startCallbackServer(port, expectedState) {
6129
+ return new Promise((resolve2, reject) => {
6130
+ const server = http.createServer(async (req, res) => {
6131
+ if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
6132
+ res.writeHead(404);
6133
+ res.end("Not Found");
6134
+ return;
6135
+ }
6136
+ const url = new URL(req.url, `http://localhost:${port}`);
6137
+ const code = url.searchParams.get("code");
6138
+ const returnedState = url.searchParams.get("state");
6139
+ const error = url.searchParams.get("error");
6140
+ if (error) {
6141
+ res.writeHead(200, { "Content-Type": "text/html" });
6142
+ res.end(this.buildErrorPage(error));
6143
+ server.close();
6144
+ reject(new Error(`OAuth error: ${error}`));
6145
+ return;
6146
+ }
6147
+ if (returnedState !== expectedState) {
6148
+ const errorMsg = "Security error: state parameter mismatch";
6149
+ res.writeHead(200, { "Content-Type": "text/html" });
6150
+ res.end(this.buildErrorPage(errorMsg));
6151
+ server.close();
6152
+ reject(new Error(errorMsg));
6153
+ return;
6154
+ }
6155
+ if (!code) {
6156
+ const errorMsg = "No authorization code received";
6157
+ res.writeHead(400, { "Content-Type": "text/html" });
6158
+ res.end(this.buildErrorPage(errorMsg));
6159
+ server.close();
6160
+ reject(new Error(errorMsg));
6161
+ return;
6162
+ }
6163
+ try {
6164
+ await this.submitAuthCode(code, returnedState);
6165
+ res.writeHead(200, { "Content-Type": "text/html" });
6166
+ res.end(this.buildSuccessPage());
6167
+ server.close();
6168
+ resolve2();
6169
+ } catch (err) {
6170
+ const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
6171
+ res.writeHead(200, { "Content-Type": "text/html" });
6172
+ res.end(this.buildErrorPage(errorMsg));
6173
+ server.close();
6174
+ reject(err);
6175
+ }
6176
+ });
6177
+ server.on("error", (error) => {
6178
+ if (error.code === "EADDRINUSE") {
6179
+ reject(new Error("Port already in use. Please try again."));
6180
+ } else {
6181
+ reject(error);
6182
+ }
6183
+ });
6184
+ const timeout = setTimeout(() => {
6185
+ server.close();
6186
+ reject(new Error("Authentication timeout - no response received after 5 minutes"));
6187
+ }, CALLBACK_TIMEOUT_MS);
6188
+ server.on("close", () => {
6189
+ clearTimeout(timeout);
6190
+ });
6191
+ server.listen(port, "127.0.0.1", () => {
6192
+ console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
6193
+ });
6194
+ });
6195
+ }
6196
+ /**
6197
+ * Submits the authorization code and exchanges it for tokens
6198
+ */
6199
+ async submitAuthCode(code, state) {
6200
+ const tokens = await this.exchangeCodeForTokens(code, state);
6201
+ await this.saveTokens(tokens);
6202
+ return tokens;
6203
+ }
6204
+ /**
6205
+ * Exchanges authorization code for access and refresh tokens
6206
+ */
6207
+ async exchangeCodeForTokens(code, state) {
6208
+ if (!this.pendingCodeVerifier) {
6209
+ throw new Error("No PKCE code verifier found. Please start the auth flow first.");
6210
+ }
6211
+ const body = {
6212
+ grant_type: "authorization_code",
6213
+ code,
6214
+ state,
6215
+ // Non-standard: state in body
6216
+ redirect_uri: OAUTH_CONFIG.redirectUri,
6217
+ client_id: OAUTH_CONFIG.clientId,
6218
+ code_verifier: this.pendingCodeVerifier
6219
+ // PKCE verifier
6220
+ };
6221
+ const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
6222
+ method: "POST",
6223
+ headers: {
6224
+ "Content-Type": "application/json"
6225
+ // Non-standard: JSON instead of form-encoded
6226
+ },
6227
+ body: JSON.stringify(body)
6228
+ });
6229
+ this.pendingCodeVerifier = null;
6230
+ if (!response.ok) {
6231
+ const error = await response.text();
6232
+ throw new Error(`Token exchange failed: ${error}`);
6233
+ }
6234
+ const data = await response.json();
6235
+ return {
6236
+ accessToken: data.access_token,
6237
+ refreshToken: data.refresh_token,
6238
+ expiresAt: Date.now() + data.expires_in * 1e3
6239
+ };
6240
+ }
6241
+ /**
6242
+ * Refreshes the access token using the refresh token
6243
+ */
6244
+ async refreshTokens() {
6245
+ const tokens = await this.getTokens();
6246
+ if (!tokens) {
6247
+ throw new Error("No tokens found to refresh");
6248
+ }
6249
+ const body = {
6250
+ grant_type: "refresh_token",
6251
+ refresh_token: tokens.refreshToken,
6252
+ client_id: OAUTH_CONFIG.clientId
6253
+ };
6254
+ const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
6255
+ method: "POST",
6256
+ headers: {
6257
+ "Content-Type": "application/json"
6258
+ },
6259
+ body: JSON.stringify(body)
6260
+ });
6261
+ if (!response.ok) {
6262
+ const error = await response.text();
6263
+ throw new Error(`Token refresh failed: ${error}`);
6264
+ }
6265
+ const data = await response.json();
6266
+ const newTokens = {
6267
+ accessToken: data.access_token,
6268
+ refreshToken: data.refresh_token,
6269
+ expiresAt: Date.now() + data.expires_in * 1e3
6270
+ };
6271
+ await this.saveTokens(newTokens);
6272
+ return newTokens;
6273
+ }
6274
+ /**
6275
+ * Gets the current access token, refreshing if necessary
6276
+ */
6277
+ async getAccessToken() {
6278
+ const tokens = await this.getTokens();
6279
+ if (!tokens) {
6280
+ return null;
6281
+ }
6282
+ if (Date.now() > tokens.expiresAt - _ClaudeOAuthService.TOKEN_REFRESH_BUFFER_MS) {
6283
+ try {
6284
+ const refreshedTokens = await this.refreshTokens();
6285
+ return refreshedTokens.accessToken;
6286
+ } catch (error) {
6287
+ console.warn("Token refresh failed:", error);
6288
+ return null;
6289
+ }
6290
+ }
6291
+ return tokens.accessToken;
6292
+ }
6293
+ /**
6294
+ * Gets the stored tokens
6295
+ */
6296
+ async getTokens() {
6297
+ try {
6298
+ const accessToken = await this.secretStorage.getSecret("claude_oauth_access_token");
6299
+ const refreshToken = await this.secretStorage.getSecret("claude_oauth_refresh_token");
6300
+ const expiresAt = await this.secretStorage.getSecret("claude_oauth_expires_at");
6301
+ if (!accessToken || !refreshToken || !expiresAt) {
6302
+ return null;
6303
+ }
6304
+ return {
6305
+ accessToken,
6306
+ refreshToken,
6307
+ expiresAt: parseInt(expiresAt, 10)
6308
+ };
6309
+ } catch (error) {
6310
+ console.error("Failed to get OAuth tokens:", error);
6311
+ return null;
6312
+ }
6313
+ }
6314
+ /**
6315
+ * Saves OAuth tokens to secure storage
6316
+ */
6317
+ async saveTokens(tokens) {
6318
+ await this.secretStorage.setSecret("claude_oauth_access_token", tokens.accessToken);
6319
+ await this.secretStorage.setSecret("claude_oauth_refresh_token", tokens.refreshToken);
6320
+ await this.secretStorage.setSecret("claude_oauth_expires_at", tokens.expiresAt.toString());
6321
+ }
6322
+ /**
6323
+ * Deletes stored OAuth tokens
6324
+ */
6325
+ async deleteTokens() {
6326
+ await this.secretStorage.deleteSecret("claude_oauth_access_token");
6327
+ await this.secretStorage.deleteSecret("claude_oauth_refresh_token");
6328
+ await this.secretStorage.deleteSecret("claude_oauth_expires_at");
6329
+ }
6330
+ /**
6331
+ * Checks if user is authenticated via OAuth
6332
+ */
6333
+ async isAuthenticated() {
6334
+ const tokens = await this.getTokens();
6335
+ return tokens !== null;
6336
+ }
6337
+ /**
6338
+ * Gets the current OAuth authentication status
6339
+ */
6340
+ async getStatus() {
6341
+ try {
6342
+ const tokens = await this.getTokens();
6343
+ if (!tokens) {
6344
+ return { isAuthenticated: false };
6345
+ }
6346
+ return {
6347
+ isAuthenticated: true,
6348
+ expiresAt: tokens.expiresAt
6349
+ };
6350
+ } catch (error) {
6351
+ return {
6352
+ isAuthenticated: false,
6353
+ error: error instanceof Error ? error.message : "Failed to get OAuth status"
6354
+ };
6355
+ }
6356
+ }
6357
+ /**
6358
+ * Builds the authorization URL with all required parameters
6359
+ */
6360
+ buildAuthorizationUrl(state, codeChallenge) {
6361
+ const params = new URLSearchParams({
6362
+ response_type: "code",
6363
+ client_id: OAUTH_CONFIG.clientId,
6364
+ redirect_uri: OAUTH_CONFIG.redirectUri,
6365
+ scope: OAUTH_CONFIG.scopes.join(" "),
6366
+ state
6367
+ });
6368
+ if (codeChallenge) {
6369
+ params.set("code_challenge", codeChallenge);
6370
+ params.set("code_challenge_method", "S256");
6371
+ }
6372
+ return `${OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
6373
+ }
6374
+ /**
6375
+ * Generates a random state string for CSRF protection
6376
+ */
6377
+ generateRandomState() {
6378
+ return Array.from(crypto.getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
6379
+ }
6380
+ /**
6381
+ * Generates PKCE code verifier and challenge
6382
+ * PKCE (Proof Key for Code Exchange) adds security to OAuth for public clients
6383
+ */
6384
+ generatePKCEChallenge() {
6385
+ const codeVerifier = randomBytes(32).toString("base64url");
6386
+ const hash = createHash("sha256").update(codeVerifier).digest("base64url");
6387
+ return {
6388
+ codeVerifier,
6389
+ codeChallenge: hash
6390
+ };
6391
+ }
6392
+ /**
6393
+ * Open a URL in the default browser cross-platform
6394
+ */
6395
+ openBrowser(url) {
6396
+ const os3 = platform();
6397
+ let command;
6398
+ let args;
6399
+ switch (os3) {
6400
+ case "darwin":
6401
+ command = "open";
6402
+ args = [url];
6403
+ break;
6404
+ case "win32":
6405
+ command = "start";
6406
+ args = ["", url];
6407
+ break;
6408
+ default:
6409
+ command = "xdg-open";
6410
+ args = [url];
6411
+ break;
6412
+ }
6413
+ const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
6414
+ spawn(command, args, options).unref();
6415
+ }
6416
+ /**
6417
+ * Build success HTML page
6418
+ */
6419
+ buildSuccessPage() {
6420
+ return `
6421
+ <!DOCTYPE html>
6422
+ <html lang="en">
6423
+ <head>
6424
+ <meta charset="UTF-8" />
6425
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6426
+ <title>Authentication Successful - Supatest CLI</title>
6427
+ <style>
6428
+ body {
6429
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6430
+ display: flex;
6431
+ align-items: center;
6432
+ justify-content: center;
6433
+ height: 100vh;
6434
+ margin: 0;
6435
+ background: #fefefe;
6436
+ }
6437
+ .container {
6438
+ background: white;
6439
+ padding: 3rem 2rem;
6440
+ border-radius: 12px;
6441
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
6442
+ border: 1px solid #e5e7eb;
6443
+ text-align: center;
6444
+ max-width: 400px;
6445
+ }
6446
+ .success-icon {
6447
+ font-size: 48px;
6448
+ margin-bottom: 1rem;
6449
+ }
6450
+ h1 {
6451
+ color: #10b981;
6452
+ margin: 0 0 1rem 0;
6453
+ font-size: 24px;
6454
+ }
6455
+ p {
6456
+ color: #666;
6457
+ margin: 0;
6458
+ line-height: 1.5;
6459
+ }
6460
+ </style>
6461
+ </head>
6462
+ <body>
6463
+ <div class="container">
6464
+ <div class="success-icon">\u2705</div>
6465
+ <h1>Authentication Successful!</h1>
6466
+ <p>You're now authenticated with Claude.</p>
6467
+ <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
6468
+ </div>
6469
+ </body>
6470
+ </html>
6471
+ `;
6472
+ }
6473
+ /**
6474
+ * Build error HTML page
6475
+ */
6476
+ buildErrorPage(errorMessage) {
6477
+ const escapedError = errorMessage.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6478
+ return `
6479
+ <!DOCTYPE html>
6480
+ <html lang="en">
6481
+ <head>
6482
+ <meta charset="UTF-8" />
6483
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6484
+ <title>Authentication Failed - Supatest CLI</title>
6485
+ <style>
6486
+ body {
6487
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6488
+ display: flex;
6489
+ align-items: center;
6490
+ justify-content: center;
6491
+ height: 100vh;
6492
+ margin: 0;
6493
+ background: #fefefe;
6494
+ }
6495
+ .container {
6496
+ background: white;
6497
+ padding: 3rem 2rem;
6498
+ border-radius: 12px;
6499
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
6500
+ border: 1px solid #e5e7eb;
6501
+ text-align: center;
6502
+ max-width: 400px;
6503
+ }
6504
+ .error-icon {
6505
+ font-size: 48px;
6506
+ margin-bottom: 1rem;
6507
+ }
6508
+ h1 {
6509
+ color: #dc2626;
6510
+ margin: 0 0 1rem 0;
6511
+ font-size: 24px;
6512
+ }
6513
+ p {
6514
+ color: #666;
6515
+ margin: 0;
6516
+ line-height: 1.5;
6517
+ }
6518
+ </style>
6519
+ </head>
6520
+ <body>
6521
+ <div class="container">
6522
+ <div class="error-icon">\u274C</div>
6523
+ <h1>Authentication Failed</h1>
6524
+ <p>${escapedError}</p>
6525
+ <p style="margin-top: 1rem;">You can close this window and try again.</p>
6526
+ </div>
6527
+ </body>
6528
+ </html>
6529
+ `;
6530
+ }
6531
+ };
6532
+ }
6533
+ });
6534
+
6535
+ // src/utils/secret-storage.ts
6536
+ var secret_storage_exports = {};
6537
+ __export(secret_storage_exports, {
6538
+ deleteSecret: () => deleteSecret,
6539
+ getSecret: () => getSecret,
6540
+ getSecretStorage: () => getSecretStorage,
6541
+ listSecrets: () => listSecrets,
6542
+ setSecret: () => setSecret
6543
+ });
6544
+ import { promises as fs } from "fs";
6545
+ import { homedir } from "os";
6546
+ import { dirname, join } from "path";
6547
+ async function getSecret(key) {
6548
+ return storage.getSecret(key);
6549
+ }
6550
+ async function setSecret(key, value) {
6551
+ await storage.setSecret(key, value);
6552
+ }
6553
+ async function deleteSecret(key) {
6554
+ return storage.deleteSecret(key);
6555
+ }
6556
+ async function listSecrets() {
6557
+ return storage.listSecrets();
6558
+ }
6559
+ function getSecretStorage() {
6560
+ return storage;
6561
+ }
6562
+ var SECRET_FILE_NAME, FileSecretStorage, storage;
6563
+ var init_secret_storage = __esm({
6564
+ "src/utils/secret-storage.ts"() {
6565
+ "use strict";
6566
+ SECRET_FILE_NAME = "secrets.json";
6567
+ FileSecretStorage = class {
6568
+ secretFilePath;
6569
+ constructor() {
6570
+ const rootDirName = process.env.NODE_ENV === "development" ? ".supatest-dev" : ".supatest";
6571
+ const secretsDir = join(homedir(), rootDirName, "claude-auth");
6572
+ this.secretFilePath = join(secretsDir, SECRET_FILE_NAME);
6573
+ }
6574
+ async ensureDirectoryExists() {
6575
+ const dir = dirname(this.secretFilePath);
6576
+ await fs.mkdir(dir, { recursive: true, mode: 448 });
6577
+ }
6578
+ async loadSecrets() {
6579
+ try {
6580
+ const data = await fs.readFile(this.secretFilePath, "utf-8");
6581
+ const secrets = JSON.parse(data);
6582
+ return new Map(Object.entries(secrets));
6583
+ } catch (error) {
6584
+ const err = error;
6585
+ if (err.code === "ENOENT") {
6586
+ return /* @__PURE__ */ new Map();
6587
+ }
6588
+ try {
6589
+ await fs.unlink(this.secretFilePath);
6590
+ } catch {
6591
+ }
6592
+ return /* @__PURE__ */ new Map();
6593
+ }
6594
+ }
6595
+ async saveSecrets(secrets) {
6596
+ await this.ensureDirectoryExists();
6597
+ const data = Object.fromEntries(secrets);
6598
+ const json = JSON.stringify(data, null, 2);
6599
+ await fs.writeFile(this.secretFilePath, json, { mode: 384 });
6600
+ }
6601
+ async getSecret(key) {
6602
+ const secrets = await this.loadSecrets();
6603
+ return secrets.get(key) ?? null;
6604
+ }
6605
+ async setSecret(key, value) {
6606
+ const secrets = await this.loadSecrets();
6607
+ secrets.set(key, value);
6608
+ await this.saveSecrets(secrets);
6609
+ }
6610
+ async deleteSecret(key) {
6611
+ const secrets = await this.loadSecrets();
6612
+ if (!secrets.has(key)) {
6613
+ return false;
6614
+ }
6615
+ secrets.delete(key);
6616
+ if (secrets.size === 0) {
6617
+ try {
6618
+ await fs.unlink(this.secretFilePath);
6619
+ } catch (error) {
6620
+ const err = error;
6621
+ if (err.code !== "ENOENT") {
6622
+ throw error;
6623
+ }
6624
+ }
6625
+ } else {
6626
+ await this.saveSecrets(secrets);
6627
+ }
6628
+ return true;
6629
+ }
6630
+ async listSecrets() {
6631
+ const secrets = await this.loadSecrets();
6632
+ return Array.from(secrets.keys());
6633
+ }
6634
+ };
6635
+ storage = new FileSecretStorage();
5632
6636
  }
5633
6637
  });
5634
6638
 
5635
6639
  // src/commands/setup.ts
5636
- import { execSync, spawnSync } from "child_process";
5637
- import fs from "fs";
6640
+ import { execSync, spawn as spawn2, spawnSync } from "child_process";
6641
+ import fs2 from "fs";
5638
6642
  import os from "os";
5639
6643
  import path from "path";
5640
6644
  function parseVersion(versionString) {
@@ -5688,7 +6692,7 @@ function getPlaywrightCachePath() {
5688
6692
  // Windows
5689
6693
  ];
5690
6694
  for (const cachePath of cachePaths) {
5691
- if (fs.existsSync(cachePath)) {
6695
+ if (fs2.existsSync(cachePath)) {
5692
6696
  return cachePath;
5693
6697
  }
5694
6698
  }
@@ -5698,7 +6702,7 @@ function getInstalledChromiumVersion() {
5698
6702
  const cachePath = getPlaywrightCachePath();
5699
6703
  if (!cachePath) return null;
5700
6704
  try {
5701
- const entries = fs.readdirSync(cachePath);
6705
+ const entries = fs2.readdirSync(cachePath);
5702
6706
  const chromiumVersions = entries.filter((entry) => entry.startsWith("chromium-") && !entry.includes("headless")).map((entry) => entry.replace("chromium-", "")).sort((a, b) => Number(b) - Number(a));
5703
6707
  return chromiumVersions[0] || null;
5704
6708
  } catch {
@@ -5730,47 +6734,64 @@ function checkNodeVersion() {
5730
6734
  version: nodeVersion.raw
5731
6735
  };
5732
6736
  }
5733
- function installChromium() {
5734
- console.log(" Running: npx playwright install chromium");
5735
- console.log("");
5736
- try {
5737
- const result = spawnSync("npx", ["playwright", "install", "chromium"], {
6737
+ async function installChromium() {
6738
+ return new Promise((resolve2) => {
6739
+ const child = spawn2("npx", ["playwright", "install", "chromium"], {
5738
6740
  stdio: "inherit",
5739
6741
  shell: true
5740
6742
  // Required for Windows where npx is npx.cmd
5741
6743
  });
5742
- if (result.status === 0) {
5743
- return {
5744
- ok: true,
5745
- message: "Chromium browser installed successfully."
5746
- };
5747
- }
5748
- return {
5749
- ok: false,
5750
- message: `Playwright install exited with code ${result.status}`
5751
- };
5752
- } catch (error) {
5753
- return {
5754
- ok: false,
5755
- message: `Failed to install Chromium: ${error instanceof Error ? error.message : String(error)}`
5756
- };
5757
- }
6744
+ child.on("close", (code) => {
6745
+ if (code === 0) {
6746
+ resolve2({
6747
+ ok: true,
6748
+ message: "Chromium browser installed successfully."
6749
+ });
6750
+ } else {
6751
+ resolve2({
6752
+ ok: false,
6753
+ message: `Playwright install exited with code ${code}`
6754
+ });
6755
+ }
6756
+ });
6757
+ child.on("error", (error) => {
6758
+ resolve2({
6759
+ ok: false,
6760
+ message: `Failed to install Chromium: ${error.message}`
6761
+ });
6762
+ });
6763
+ });
5758
6764
  }
5759
6765
  function createSupatestConfig(cwd) {
5760
6766
  const supatestDir = path.join(cwd, ".supatest");
5761
6767
  const mcpJsonPath = path.join(supatestDir, "mcp.json");
5762
6768
  try {
5763
- if (fs.existsSync(mcpJsonPath)) {
6769
+ if (!fs2.existsSync(supatestDir)) {
6770
+ fs2.mkdirSync(supatestDir, { recursive: true });
6771
+ }
6772
+ let config2;
6773
+ let fileExisted = false;
6774
+ if (fs2.existsSync(mcpJsonPath)) {
6775
+ fileExisted = true;
6776
+ const existingContent = fs2.readFileSync(mcpJsonPath, "utf-8");
6777
+ config2 = JSON.parse(existingContent);
6778
+ } else {
6779
+ config2 = {};
6780
+ }
6781
+ if (!config2.mcpServers || typeof config2.mcpServers !== "object") {
6782
+ config2.mcpServers = {};
6783
+ }
6784
+ if (!config2.mcpServers.playwright) {
6785
+ config2.mcpServers.playwright = DEFAULT_MCP_CONFIG.mcpServers.playwright;
6786
+ }
6787
+ fs2.writeFileSync(mcpJsonPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
6788
+ if (fileExisted) {
5764
6789
  return {
5765
6790
  ok: true,
5766
- message: "mcp.json already exists",
6791
+ message: "Updated .supatest/mcp.json with Playwright MCP server configuration",
5767
6792
  created: false
5768
6793
  };
5769
6794
  }
5770
- if (!fs.existsSync(supatestDir)) {
5771
- fs.mkdirSync(supatestDir, { recursive: true });
5772
- }
5773
- fs.writeFileSync(mcpJsonPath, JSON.stringify(DEFAULT_MCP_CONFIG, null, 2) + "\n", "utf-8");
5774
6795
  return {
5775
6796
  ok: true,
5776
6797
  message: "Created .supatest/mcp.json with Playwright MCP server configuration",
@@ -5799,7 +6820,6 @@ async function setupCommand(options) {
5799
6820
  const output = [];
5800
6821
  const log = (msg) => {
5801
6822
  output.push(msg);
5802
- console.log(msg);
5803
6823
  };
5804
6824
  const result = {
5805
6825
  nodeVersionOk: false,
@@ -5833,7 +6853,7 @@ async function setupCommand(options) {
5833
6853
  result.playwrightInstalled = true;
5834
6854
  } else {
5835
6855
  log(" \u{1F4E6} Chromium browser not found. Installing...\n");
5836
- const chromiumResult = installChromium();
6856
+ const chromiumResult = await installChromium();
5837
6857
  result.playwrightInstalled = chromiumResult.ok;
5838
6858
  log("");
5839
6859
  if (chromiumResult.ok) {
@@ -5891,18 +6911,18 @@ var CLI_VERSION;
5891
6911
  var init_version = __esm({
5892
6912
  "src/version.ts"() {
5893
6913
  "use strict";
5894
- CLI_VERSION = "0.0.39";
6914
+ CLI_VERSION = "0.0.41";
5895
6915
  }
5896
6916
  });
5897
6917
 
5898
6918
  // src/utils/error-logger.ts
5899
- import * as fs2 from "fs";
6919
+ import * as fs3 from "fs";
5900
6920
  import * as os2 from "os";
5901
6921
  import * as path2 from "path";
5902
6922
  function ensureLogDir() {
5903
6923
  try {
5904
- if (!fs2.existsSync(LOGS_DIR)) {
5905
- fs2.mkdirSync(LOGS_DIR, { recursive: true });
6924
+ if (!fs3.existsSync(LOGS_DIR)) {
6925
+ fs3.mkdirSync(LOGS_DIR, { recursive: true });
5906
6926
  }
5907
6927
  return true;
5908
6928
  } catch {
@@ -5911,14 +6931,14 @@ function ensureLogDir() {
5911
6931
  }
5912
6932
  function rotateLogIfNeeded() {
5913
6933
  try {
5914
- if (!fs2.existsSync(ERROR_LOG_FILE)) return;
5915
- const stats = fs2.statSync(ERROR_LOG_FILE);
6934
+ if (!fs3.existsSync(ERROR_LOG_FILE)) return;
6935
+ const stats = fs3.statSync(ERROR_LOG_FILE);
5916
6936
  if (stats.size > MAX_LOG_SIZE) {
5917
6937
  const oldLogFile = `${ERROR_LOG_FILE}.old`;
5918
- if (fs2.existsSync(oldLogFile)) {
5919
- fs2.unlinkSync(oldLogFile);
6938
+ if (fs3.existsSync(oldLogFile)) {
6939
+ fs3.unlinkSync(oldLogFile);
5920
6940
  }
5921
- fs2.renameSync(ERROR_LOG_FILE, oldLogFile);
6941
+ fs3.renameSync(ERROR_LOG_FILE, oldLogFile);
5922
6942
  }
5923
6943
  } catch {
5924
6944
  }
@@ -5954,7 +6974,7 @@ function logError(error, context) {
5954
6974
  const logLine = `${JSON.stringify(entry)}
5955
6975
  `;
5956
6976
  try {
5957
- fs2.appendFileSync(ERROR_LOG_FILE, logLine);
6977
+ fs3.appendFileSync(ERROR_LOG_FILE, logLine);
5958
6978
  } catch {
5959
6979
  }
5960
6980
  }
@@ -5971,7 +6991,7 @@ var init_error_logger = __esm({
5971
6991
  });
5972
6992
 
5973
6993
  // src/utils/logger.ts
5974
- import * as fs3 from "fs";
6994
+ import * as fs4 from "fs";
5975
6995
  import * as path3 from "path";
5976
6996
  import chalk from "chalk";
5977
6997
  var Logger, logger;
@@ -6005,7 +7025,7 @@ ${"=".repeat(80)}
6005
7025
  ${"=".repeat(80)}
6006
7026
  `;
6007
7027
  try {
6008
- fs3.appendFileSync(this.logFile, separator);
7028
+ fs4.appendFileSync(this.logFile, separator);
6009
7029
  } catch (error) {
6010
7030
  }
6011
7031
  }
@@ -6019,7 +7039,7 @@ ${"=".repeat(80)}
6019
7039
  ` : `[${timestamp}] [${level}] ${message}
6020
7040
  `;
6021
7041
  try {
6022
- fs3.appendFileSync(this.logFile, logEntry);
7042
+ fs4.appendFileSync(this.logFile, logEntry);
6023
7043
  } catch (error) {
6024
7044
  }
6025
7045
  }
@@ -6626,13 +7646,146 @@ var init_api_client = __esm({
6626
7646
  logger.debug(`Fetched ${data.history.length} history items for test ${testId}`);
6627
7647
  return data;
6628
7648
  }
7649
+ /**
7650
+ * Assign tests to yourself (or someone else)
7651
+ * @param params - Assignment parameters
7652
+ * @returns Assignment result with assigned tests and conflicts
7653
+ */
7654
+ async assignTests(params) {
7655
+ const url = `${this.apiUrl}/v1/fix-assignments/assign`;
7656
+ logger.debug(`Assigning tests`, {
7657
+ runId: params.runId,
7658
+ count: params.testRunIds.length
7659
+ });
7660
+ const response = await fetch(url, {
7661
+ method: "POST",
7662
+ headers: {
7663
+ "Content-Type": "application/json",
7664
+ Authorization: `Bearer ${this.apiKey}`
7665
+ },
7666
+ body: JSON.stringify({
7667
+ testRunIds: params.testRunIds,
7668
+ assignedTo: params.assignedTo
7669
+ })
7670
+ });
7671
+ if (!response.ok) {
7672
+ const errorText = await response.text();
7673
+ throw new ApiError(response.status, response.statusText, errorText);
7674
+ }
7675
+ const data = await response.json();
7676
+ logger.debug(
7677
+ `Assigned ${data.assigned.length} tests, ${data.conflicts.length} conflicts`
7678
+ );
7679
+ return data;
7680
+ }
7681
+ /**
7682
+ * Mark assignment as complete
7683
+ * @param params - Completion parameters
7684
+ * @returns Success status
7685
+ */
7686
+ async completeAssignment(params) {
7687
+ const url = `${this.apiUrl}/v1/fix-assignments/${params.assignmentId}/status`;
7688
+ logger.debug(`Completing assignment`, {
7689
+ assignmentId: params.assignmentId,
7690
+ status: params.status
7691
+ });
7692
+ const response = await fetch(url, {
7693
+ method: "PATCH",
7694
+ headers: {
7695
+ "Content-Type": "application/json",
7696
+ Authorization: `Bearer ${this.apiKey}`
7697
+ },
7698
+ body: JSON.stringify({
7699
+ status: params.status,
7700
+ fixSessionId: params.fixSessionId
7701
+ })
7702
+ });
7703
+ if (!response.ok) {
7704
+ const errorText = await response.text();
7705
+ throw new ApiError(response.status, response.statusText, errorText);
7706
+ }
7707
+ const data = await response.json();
7708
+ logger.debug(`Assignment completed: ${params.assignmentId}`);
7709
+ return data;
7710
+ }
7711
+ /**
7712
+ * Release claim (delete assignment)
7713
+ * @param assignmentId - The assignment ID to release
7714
+ * @returns Success status
7715
+ */
7716
+ async releaseAssignment(assignmentId) {
7717
+ const url = `${this.apiUrl}/v1/fix-assignments/${assignmentId}`;
7718
+ logger.debug(`Releasing assignment: ${assignmentId}`);
7719
+ const response = await fetch(url, {
7720
+ method: "DELETE",
7721
+ headers: {
7722
+ Authorization: `Bearer ${this.apiKey}`
7723
+ }
7724
+ });
7725
+ if (!response.ok) {
7726
+ const errorText = await response.text();
7727
+ throw new ApiError(response.status, response.statusText, errorText);
7728
+ }
7729
+ const data = await response.json();
7730
+ logger.debug(`Assignment released: ${assignmentId}`);
7731
+ return data;
7732
+ }
7733
+ /**
7734
+ * Get my current assignments
7735
+ * @returns List of current assignments
7736
+ */
7737
+ async getMyAssignments() {
7738
+ const url = `${this.apiUrl}/v1/fix-assignments?status=assigned`;
7739
+ logger.debug(`Fetching my assignments`);
7740
+ const response = await fetch(url, {
7741
+ method: "GET",
7742
+ headers: {
7743
+ Authorization: `Bearer ${this.apiKey}`
7744
+ }
7745
+ });
7746
+ if (!response.ok) {
7747
+ const errorText = await response.text();
7748
+ throw new ApiError(response.status, response.statusText, errorText);
7749
+ }
7750
+ const data = await response.json();
7751
+ logger.debug(`Fetched ${data.assignments.length} assignments`);
7752
+ return { assignments: data.assignments };
7753
+ }
7754
+ /**
7755
+ * Get assignments for a specific run (to show what others are working on)
7756
+ * @param runId - The run ID
7757
+ * @returns List of assignments for the run
7758
+ */
7759
+ async getRunAssignments(runId) {
7760
+ const url = `${this.apiUrl}/v1/fix-assignments?runId=${runId}&status=assigned`;
7761
+ logger.debug(`Fetching assignments for run: ${runId}`);
7762
+ const response = await fetch(url, {
7763
+ method: "GET",
7764
+ headers: {
7765
+ Authorization: `Bearer ${this.apiKey}`
7766
+ }
7767
+ });
7768
+ if (!response.ok) {
7769
+ const errorText = await response.text();
7770
+ throw new ApiError(response.status, response.statusText, errorText);
7771
+ }
7772
+ const data = await response.json();
7773
+ logger.debug(`Fetched ${data.assignments.length} assignments for run ${runId}`);
7774
+ return {
7775
+ assignments: data.assignments.map((a) => ({
7776
+ testRunId: a.testRunId,
7777
+ assignedTo: a.assignedTo,
7778
+ assignedAt: a.assignedAt
7779
+ }))
7780
+ };
7781
+ }
6629
7782
  };
6630
7783
  }
6631
7784
  });
6632
7785
 
6633
7786
  // src/utils/command-discovery.ts
6634
7787
  import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
6635
- import { join as join3, relative } from "path";
7788
+ import { join as join4, relative } from "path";
6636
7789
  function parseMarkdownFrontmatter(content) {
6637
7790
  const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
6638
7791
  const match = content.match(frontmatterRegex);
@@ -6657,7 +7810,7 @@ function discoverMarkdownFiles(dir, baseDir, files = []) {
6657
7810
  }
6658
7811
  const entries = readdirSync(dir);
6659
7812
  for (const entry of entries) {
6660
- const fullPath = join3(dir, entry);
7813
+ const fullPath = join4(dir, entry);
6661
7814
  const stat = statSync2(fullPath);
6662
7815
  if (stat.isDirectory()) {
6663
7816
  discoverMarkdownFiles(fullPath, baseDir, files);
@@ -6668,7 +7821,7 @@ function discoverMarkdownFiles(dir, baseDir, files = []) {
6668
7821
  return files;
6669
7822
  }
6670
7823
  function discoverCommands(cwd) {
6671
- const commandsDir = join3(cwd, ".supatest", "commands");
7824
+ const commandsDir = join4(cwd, ".supatest", "commands");
6672
7825
  if (!existsSync2(commandsDir)) {
6673
7826
  return [];
6674
7827
  }
@@ -6691,9 +7844,9 @@ function discoverCommands(cwd) {
6691
7844
  return commands.sort((a, b) => a.name.localeCompare(b.name));
6692
7845
  }
6693
7846
  function expandCommand(cwd, commandName, args) {
6694
- const commandsDir = join3(cwd, ".supatest", "commands");
7847
+ const commandsDir = join4(cwd, ".supatest", "commands");
6695
7848
  const relativePath = commandName.replace(/\./g, "/") + ".md";
6696
- const filePath = join3(commandsDir, relativePath);
7849
+ const filePath = join4(commandsDir, relativePath);
6697
7850
  if (!existsSync2(filePath)) {
6698
7851
  return null;
6699
7852
  }
@@ -6716,7 +7869,7 @@ function expandCommand(cwd, commandName, args) {
6716
7869
  }
6717
7870
  }
6718
7871
  function discoverAgents(cwd) {
6719
- const agentsDir = join3(cwd, ".supatest", "agents");
7872
+ const agentsDir = join4(cwd, ".supatest", "agents");
6720
7873
  if (!existsSync2(agentsDir)) {
6721
7874
  return [];
6722
7875
  }
@@ -6747,8 +7900,8 @@ var init_command_discovery = __esm({
6747
7900
 
6748
7901
  // src/utils/mcp-loader.ts
6749
7902
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
6750
- import { homedir as homedir2 } from "os";
6751
- import { join as join4 } from "path";
7903
+ import { homedir as homedir3 } from "os";
7904
+ import { join as join5 } from "path";
6752
7905
  function expandEnvVar(value) {
6753
7906
  return value.replace(/\$\{([^}]+)\}/g, (_, expr) => {
6754
7907
  const [varName, defaultValue] = expr.split(":-");
@@ -6797,9 +7950,9 @@ function loadMcpServersFromFile(mcpPath) {
6797
7950
  }
6798
7951
  }
6799
7952
  function loadMcpServers(cwd) {
6800
- const globalMcpPath = join4(homedir2(), ".supatest", "mcp.json");
7953
+ const globalMcpPath = join5(homedir3(), ".supatest", "mcp.json");
6801
7954
  const globalServers = loadMcpServersFromFile(globalMcpPath);
6802
- const projectMcpPath = join4(cwd, ".supatest", "mcp.json");
7955
+ const projectMcpPath = join5(cwd, ".supatest", "mcp.json");
6803
7956
  const projectServers = loadMcpServersFromFile(projectMcpPath);
6804
7957
  return { ...globalServers, ...projectServers };
6805
7958
  }
@@ -6811,11 +7964,11 @@ var init_mcp_loader = __esm({
6811
7964
 
6812
7965
  // src/utils/project-instructions.ts
6813
7966
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
6814
- import { join as join5 } from "path";
7967
+ import { join as join6 } from "path";
6815
7968
  function loadProjectInstructions(cwd) {
6816
7969
  const paths = [
6817
- join5(cwd, "SUPATEST.md"),
6818
- join5(cwd, ".supatest", "SUPATEST.md")
7970
+ join6(cwd, "SUPATEST.md"),
7971
+ join6(cwd, ".supatest", "SUPATEST.md")
6819
7972
  ];
6820
7973
  for (const path6 of paths) {
6821
7974
  if (existsSync4(path6)) {
@@ -6835,8 +7988,8 @@ var init_project_instructions = __esm({
6835
7988
 
6836
7989
  // src/core/agent.ts
6837
7990
  import { createRequire } from "module";
6838
- import { homedir as homedir3 } from "os";
6839
- import { dirname, join as join6 } from "path";
7991
+ import { homedir as homedir4 } from "os";
7992
+ import { dirname as dirname2, join as join7 } from "path";
6840
7993
  import { query } from "@anthropic-ai/claude-agent-sdk";
6841
7994
  var CoreAgent;
6842
7995
  var init_agent = __esm({
@@ -6853,6 +8006,8 @@ var init_agent = __esm({
6853
8006
  presenter;
6854
8007
  abortController = null;
6855
8008
  messageBridge = null;
8009
+ // Map of pending questions waiting for user answers
8010
+ pendingQuestionResolvers = /* @__PURE__ */ new Map();
6856
8011
  constructor(presenter, messageBridge) {
6857
8012
  this.presenter = presenter;
6858
8013
  this.messageBridge = messageBridge ?? null;
@@ -6866,9 +8021,37 @@ var init_agent = __esm({
6866
8021
  this.abortController.abort();
6867
8022
  }
6868
8023
  }
8024
+ /**
8025
+ * Create a promise that waits for user to answer a question.
8026
+ * Used by canUseTool callback to pause agent execution until user responds.
8027
+ */
8028
+ createQuestionPromise(questionId) {
8029
+ return new Promise((resolve2) => {
8030
+ this.pendingQuestionResolvers.set(questionId, resolve2);
8031
+ });
8032
+ }
8033
+ /**
8034
+ * Submit an answer for a pending question.
8035
+ * Called from the UI when user responds to AskUserQuestion.
8036
+ * @param questionId - The unique ID of the question
8037
+ * @param answers - The user's answers, or null if dismissed
8038
+ * @returns true if question was found and resolved, false otherwise
8039
+ */
8040
+ submitAnswer(questionId, answers) {
8041
+ const resolver = this.pendingQuestionResolvers.get(questionId);
8042
+ if (resolver) {
8043
+ resolver(answers);
8044
+ this.pendingQuestionResolvers.delete(questionId);
8045
+ return true;
8046
+ }
8047
+ return false;
8048
+ }
6869
8049
  async run(config2) {
6870
8050
  this.abortController = new AbortController();
6871
8051
  this.presenter.onStart(config2);
8052
+ if (config2.providerSessionId) {
8053
+ this.presenter.onLog(`Resuming with providerSessionId: ${config2.providerSessionId}`);
8054
+ }
6872
8055
  const claudeCodePath = await this.resolveClaudeCodePath();
6873
8056
  let prompt = config2.task;
6874
8057
  if (config2.logs) {
@@ -6936,7 +8119,7 @@ ${projectInstructions}`,
6936
8119
  this.presenter.onLog(`Auth: Using Claude Max (default Claude Code credentials)`);
6937
8120
  logger.debug("[agent] Claude Max mode: Using default ~/.claude/ config, cleared provider credentials");
6938
8121
  } else {
6939
- const internalConfigDir = join6(homedir3(), ".supatest", "claude-internal");
8122
+ const internalConfigDir = join7(homedir4(), ".supatest", "claude-internal");
6940
8123
  cleanEnv.CLAUDE_CONFIG_DIR = internalConfigDir;
6941
8124
  cleanEnv.ANTHROPIC_API_KEY = config2.supatestApiKey || "";
6942
8125
  cleanEnv.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || "";
@@ -6951,6 +8134,9 @@ ${projectInstructions}`,
6951
8134
  CLAUDE_CONFIG_DIR: cleanEnv.CLAUDE_CONFIG_DIR || "(using default ~/.claude/)"
6952
8135
  });
6953
8136
  cleanEnv.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "1";
8137
+ cleanEnv.CLAUDE_CODE_ENABLE_TELEMETRY = "0";
8138
+ cleanEnv.DISABLE_TELEMETRY = "1";
8139
+ cleanEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
6954
8140
  const selectedModel = config2.selectedModel || "premium";
6955
8141
  const defaultModel = resolveToAnthropicModel(selectedModel);
6956
8142
  const queryOptions = {
@@ -6982,7 +8168,7 @@ ${projectInstructions}`,
6982
8168
  return servers;
6983
8169
  })(),
6984
8170
  // Resume from previous session if providerSessionId is provided
6985
- // This allows the agent to continue conversations with full context
8171
+ // This allows the agent to continue sessions with full context
6986
8172
  // Note: Sessions expire after ~30 days due to Anthropic's data retention policy
6987
8173
  ...config2.providerSessionId && {
6988
8174
  resume: config2.providerSessionId
@@ -6999,6 +8185,29 @@ ${projectInstructions}`,
6999
8185
  env: cleanEnv,
7000
8186
  stderr: (msg) => {
7001
8187
  this.presenter.onLog(`[Agent Failure] ${msg}`);
8188
+ },
8189
+ // Intercept AskUserQuestion tool to wait for user response
8190
+ canUseTool: async (toolName, input) => {
8191
+ if (toolName !== "AskUserQuestion") {
8192
+ return { behavior: "allow", updatedInput: input };
8193
+ }
8194
+ const questions = input.questions || [];
8195
+ if (questions.length === 0) {
8196
+ logger.warn("[AskUserQuestion] No questions provided");
8197
+ return { behavior: "deny", message: "No questions provided" };
8198
+ }
8199
+ const questionId = `ask-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
8200
+ this.presenter.onToolUse?.(toolName, input, questionId);
8201
+ const answers = await this.createQuestionPromise(questionId);
8202
+ if (answers === null) {
8203
+ this.presenter.onToolResult?.(questionId, JSON.stringify({ dismissed: true }));
8204
+ return { behavior: "deny", message: "User dismissed the question" };
8205
+ }
8206
+ this.presenter.onToolResult?.(questionId, JSON.stringify({ answers }));
8207
+ return {
8208
+ behavior: "allow",
8209
+ updatedInput: { questions, answers }
8210
+ };
7002
8211
  }
7003
8212
  };
7004
8213
  let resultText = "";
@@ -7023,7 +8232,7 @@ ${projectInstructions}`,
7023
8232
  };
7024
8233
  const isSessionExpiredError = (errorMsg) => {
7025
8234
  const expiredPatterns = [
7026
- "no conversation found",
8235
+ "no session found",
7027
8236
  "session not found",
7028
8237
  "session expired",
7029
8238
  "invalid session"
@@ -7031,6 +8240,20 @@ ${projectInstructions}`,
7031
8240
  const lowerError = errorMsg.toLowerCase();
7032
8241
  return expiredPatterns.some((pattern) => lowerError.includes(pattern));
7033
8242
  };
8243
+ const isContextWindowExceededError = (errorMsg) => {
8244
+ const contextPatterns = [
8245
+ "exceed context limit",
8246
+ "exceeds context limit",
8247
+ "context length",
8248
+ "maximum context",
8249
+ "prompt is too long",
8250
+ "input_length and max_tokens",
8251
+ "requested token count exceeds",
8252
+ "context window"
8253
+ ];
8254
+ const lowerError = errorMsg.toLowerCase();
8255
+ return contextPatterns.some((pattern) => lowerError.includes(pattern));
8256
+ };
7034
8257
  const runQuery = async (options) => {
7035
8258
  logger.debug("[agent] Starting query", {
7036
8259
  model: options.model,
@@ -7181,11 +8404,17 @@ ${projectInstructions}`,
7181
8404
  wasInterrupted = true;
7182
8405
  logger.debug("[agent] Query was aborted by user");
7183
8406
  } else if (config2.providerSessionId && isSessionExpiredError(errorMessage)) {
7184
- const expiredMessage = "Can't continue conversation older than 30 days. Please start a new session.";
8407
+ const expiredMessage = "Can't continue session older than 30 days. Please start a new session.";
7185
8408
  this.presenter.onError(expiredMessage, true);
7186
8409
  hasError = true;
7187
8410
  errors.push(expiredMessage);
7188
8411
  logger.debug("[agent] Session expired error");
8412
+ } else if (isContextWindowExceededError(errorMessage)) {
8413
+ const contextMessage = "Context window exceeded. The session has grown too large.\n\nSuggestions:\n\u2022 Start a new session to begin fresh\n\u2022 Use /clear to reset the current session\n\u2022 Reduce file sizes or number of files in your request\n\u2022 Be more specific with your prompts to reduce context";
8414
+ this.presenter.onError(contextMessage, true);
8415
+ hasError = true;
8416
+ errors.push(contextMessage);
8417
+ logger.debug("[agent] Context window exceeded error");
7189
8418
  } else {
7190
8419
  logError(error, {
7191
8420
  source: "CoreAgent.run",
@@ -7220,7 +8449,7 @@ ${projectInstructions}`,
7220
8449
  let claudeCodePath;
7221
8450
  const require2 = createRequire(import.meta.url);
7222
8451
  const sdkPath = require2.resolve("@anthropic-ai/claude-agent-sdk/sdk.mjs");
7223
- claudeCodePath = join6(dirname(sdkPath), "cli.js");
8452
+ claudeCodePath = join7(dirname2(sdkPath), "cli.js");
7224
8453
  this.presenter.onLog(`Using SDK CLI: ${claudeCodePath}`);
7225
8454
  if (config.claudeCodeExecutablePath) {
7226
8455
  claudeCodePath = config.claudeCodeExecutablePath;
@@ -7270,6 +8499,10 @@ function getToolDescription(toolName, input) {
7270
8499
  case "BashOutput":
7271
8500
  case "Command Output":
7272
8501
  return input?.bash_id || "shell output";
8502
+ case "AskUserQuestion": {
8503
+ const question = input?.questions?.[0]?.question || input?.question || "Question";
8504
+ return question.length > 60 ? `${question.substring(0, 60)}...` : question;
8505
+ }
7273
8506
  default:
7274
8507
  return toolName;
7275
8508
  }
@@ -7402,6 +8635,15 @@ var init_react = __esm({
7402
8635
  this.callbacks.onExitPlanMode?.();
7403
8636
  } else if (tool === "EnterPlanMode") {
7404
8637
  this.callbacks.onEnterPlanMode?.();
8638
+ } else if (tool === "AskUserQuestion") {
8639
+ const questions = input?.questions || [];
8640
+ if (questions.length > 0) {
8641
+ const firstQuestion = questions[0];
8642
+ const question = firstQuestion.question || "";
8643
+ const options = firstQuestion.options || [];
8644
+ const multiSelect = firstQuestion.multiSelect || false;
8645
+ this.callbacks.onAskUserQuestion?.(toolId, question, options, multiSelect);
8646
+ }
7405
8647
  }
7406
8648
  streamEventAsync(this.apiClient, this.sessionId, {
7407
8649
  type: "tool_use",
@@ -7528,10 +8770,11 @@ var init_react = __esm({
7528
8770
 
7529
8771
  // src/ui/contexts/SessionContext.tsx
7530
8772
  import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
7531
- var SessionContext, SessionProvider, useSession;
8773
+ var UsageStatsContext, SessionContext, SessionProvider, useSession, useUsageStats;
7532
8774
  var init_SessionContext = __esm({
7533
8775
  "src/ui/contexts/SessionContext.tsx"() {
7534
8776
  "use strict";
8777
+ UsageStatsContext = createContext(null);
7535
8778
  SessionContext = createContext(null);
7536
8779
  SessionProvider = ({
7537
8780
  children,
@@ -7558,6 +8801,7 @@ var init_SessionContext = __esm({
7558
8801
  const [allToolsExpanded, setAllToolsExpanded] = useState(true);
7559
8802
  const [toolGroupsExpanded, setToolGroupsExpanded] = useState(false);
7560
8803
  const [staticRemountKey, setStaticRemountKey] = useState(0);
8804
+ const [pendingQuestion, setPendingQuestion] = useState(null);
7561
8805
  const addMessage = useCallback(
7562
8806
  (message) => {
7563
8807
  const expandableTools = ["Bash", "BashOutput", "Command Output"];
@@ -7671,8 +8915,6 @@ var init_SessionContext = __esm({
7671
8915
  setTodos,
7672
8916
  stats,
7673
8917
  updateStats,
7674
- usageStats,
7675
- setUsageStats,
7676
8918
  isAgentRunning,
7677
8919
  setIsAgentRunning,
7678
8920
  shouldInterruptAgent,
@@ -7690,7 +8932,9 @@ var init_SessionContext = __esm({
7690
8932
  llmProvider,
7691
8933
  setLlmProvider,
7692
8934
  staticRemountKey,
7693
- refreshStatic
8935
+ refreshStatic,
8936
+ pendingQuestion,
8937
+ setPendingQuestion
7694
8938
  }), [
7695
8939
  messages,
7696
8940
  addMessage,
@@ -7706,7 +8950,6 @@ var init_SessionContext = __esm({
7706
8950
  todos,
7707
8951
  stats,
7708
8952
  updateStats,
7709
- usageStats,
7710
8953
  isAgentRunning,
7711
8954
  shouldInterruptAgent,
7712
8955
  sessionId,
@@ -7716,9 +8959,14 @@ var init_SessionContext = __esm({
7716
8959
  selectedModel,
7717
8960
  llmProvider,
7718
8961
  staticRemountKey,
7719
- refreshStatic
8962
+ refreshStatic,
8963
+ pendingQuestion
7720
8964
  ]);
7721
- return /* @__PURE__ */ React.createElement(SessionContext.Provider, { value }, children);
8965
+ const usageStatsValue = useMemo(() => ({
8966
+ usageStats,
8967
+ setUsageStats
8968
+ }), [usageStats]);
8969
+ return /* @__PURE__ */ React.createElement(SessionContext.Provider, { value }, /* @__PURE__ */ React.createElement(UsageStatsContext.Provider, { value: usageStatsValue }, children));
7722
8970
  };
7723
8971
  useSession = () => {
7724
8972
  const context = useContext(SessionContext);
@@ -7727,6 +8975,13 @@ var init_SessionContext = __esm({
7727
8975
  }
7728
8976
  return context;
7729
8977
  };
8978
+ useUsageStats = () => {
8979
+ const context = useContext(UsageStatsContext);
8980
+ if (!context) {
8981
+ throw new Error("useUsageStats must be used within SessionProvider");
8982
+ }
8983
+ return context;
8984
+ };
7730
8985
  }
7731
8986
  });
7732
8987
 
@@ -8436,6 +9691,14 @@ function getCommandDisplay(toolName, input) {
8436
9691
  return input.pattern ? [`"${input.pattern}"${input.path ? ` in ${input.path}` : ""}`] : null;
8437
9692
  case "Glob":
8438
9693
  return input.pattern ? [input.pattern] : null;
9694
+ case "AskUserQuestion": {
9695
+ const questions = input.questions || [];
9696
+ if (questions.length > 0) {
9697
+ const question = questions[0].question || "Question";
9698
+ return [question.length > 80 ? `${question.substring(0, 80)}...` : question];
9699
+ }
9700
+ return null;
9701
+ }
8439
9702
  default:
8440
9703
  return null;
8441
9704
  }
@@ -8451,7 +9714,8 @@ function getToolStyle(toolName) {
8451
9714
  Glob: { icon: "\u{1F50D}", color: theme.tool.search },
8452
9715
  Grep: { icon: "\u{1F50D}", color: theme.tool.search },
8453
9716
  Task: { icon: "\u{1F916}", color: theme.tool.agent },
8454
- TodoWrite: { icon: "\u{1F4DD}", color: theme.text.info }
9717
+ TodoWrite: { icon: "\u{1F4DD}", color: theme.text.info },
9718
+ AskUserQuestion: { icon: "\u2753", color: theme.text.info }
8455
9719
  };
8456
9720
  return styles[toolName] || { icon: "\u{1F527}", color: theme.text.secondary };
8457
9721
  }
@@ -8485,6 +9749,9 @@ function getResultSummary(toolName, result) {
8485
9749
  const lines = result.split("\n").filter((line) => line.trim());
8486
9750
  return lines.length > 0 ? `${lines.length} lines of output` : "No output";
8487
9751
  }
9752
+ case "AskUserQuestion": {
9753
+ return result ? `Answered: ${result.substring(0, 50)}${result.length > 50 ? "..." : ""}` : "Waiting for response...";
9754
+ }
8488
9755
  default:
8489
9756
  return null;
8490
9757
  }
@@ -8569,16 +9836,7 @@ var init_UserMessage = __esm({
8569
9836
  "use strict";
8570
9837
  init_theme();
8571
9838
  UserMessage = ({ text }) => {
8572
- return /* @__PURE__ */ React11.createElement(Box10, { alignItems: "center", flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, { color: theme.text.info }, "\u{1F464} "), /* @__PURE__ */ React11.createElement(
8573
- Box10,
8574
- {
8575
- borderColor: theme.text.dim,
8576
- borderStyle: "round",
8577
- paddingLeft: 1,
8578
- paddingRight: 1
8579
- },
8580
- /* @__PURE__ */ React11.createElement(Text10, { color: theme.text.secondary }, text)
8581
- ));
9839
+ return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, { color: theme.text.info }, "\u{1F464} "), /* @__PURE__ */ React11.createElement(Text10, { color: theme.text.secondary }, text));
8582
9840
  };
8583
9841
  }
8584
9842
  });
@@ -8621,8 +9879,8 @@ var init_QueuedMessageDisplay = __esm({
8621
9879
 
8622
9880
  // src/ui/components/MessageList.tsx
8623
9881
  import { Box as Box12, Static } from "ink";
8624
- import React13, { memo as memo2, useCallback as useCallback2, useMemo as useMemo3, useRef as useRef3 } from "react";
8625
- var MessageList;
9882
+ import React13, { memo as memo2, useCallback as useCallback2, useMemo as useMemo3 } from "react";
9883
+ var StaticHeader, MessageList;
8626
9884
  var init_MessageList = __esm({
8627
9885
  "src/ui/components/MessageList.tsx"() {
8628
9886
  "use strict";
@@ -8637,6 +9895,11 @@ var init_MessageList = __esm({
8637
9895
  init_ToolMessage();
8638
9896
  init_UserMessage();
8639
9897
  init_QueuedMessageDisplay();
9898
+ StaticHeader = memo2(({ currentFolder, gitBranch, headless, staticRemountKey }) => {
9899
+ const headerItems = useMemo3(() => [{ id: "header" }], []);
9900
+ return /* @__PURE__ */ React13.createElement(Static, { items: headerItems, key: `header-${staticRemountKey}` }, () => /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column", key: "header" }, /* @__PURE__ */ React13.createElement(Header, { currentFolder, gitBranch, headless })));
9901
+ });
9902
+ StaticHeader.displayName = "StaticHeader";
8640
9903
  MessageList = memo2(({ terminalWidth, currentFolder, gitBranch, headless = false, queuedTasks = [] }) => {
8641
9904
  const { messages, updateMessageById, isAgentRunning, staticRemountKey, toolGroupsExpanded, toggleToolGroups } = useSession();
8642
9905
  const handleToggle = useCallback2((id, currentExpanded) => {
@@ -8738,31 +10001,29 @@ var init_MessageList = __esm({
8738
10001
  }
8739
10002
  return renderMessage(group.messages[0]);
8740
10003
  }, [toolGroupsExpanded, toggleToolGroups, handleToggle, renderMessage]);
8741
- const lastUserMessageIndex = useMemo3(() => {
8742
- for (let i = messages.length - 1; i >= 0; i--) {
8743
- if (messages[i].type === "user") {
8744
- return i;
8745
- }
8746
- }
8747
- return -1;
8748
- }, [messages]);
8749
10004
  const hasPendingAssistant = useMemo3(
8750
10005
  () => messages.some((m) => m.type === "assistant" && m.isPending),
8751
10006
  [messages]
8752
10007
  );
8753
- const completedBoundaryRef = useRef3(-1);
8754
- const completedBoundaryKey = useMemo3(() => {
8755
- const currentBoundary = lastUserMessageIndex;
8756
- if (currentBoundary !== completedBoundaryRef.current) {
8757
- completedBoundaryRef.current = currentBoundary;
8758
- return `boundary-${currentBoundary}`;
8759
- }
8760
- return `boundary-${completedBoundaryRef.current}`;
8761
- }, [lastUserMessageIndex]);
8762
10008
  const { completedGroups, currentTurnGroups } = useMemo3(() => {
8763
10009
  const completed = [];
8764
10010
  const currentTurn = [];
8765
- const processTurn = (turnMessages2, targetArray) => {
10011
+ const isMessageComplete = (msg) => {
10012
+ switch (msg.type) {
10013
+ case "user":
10014
+ case "error":
10015
+ case "todo":
10016
+ case "thinking":
10017
+ return true;
10018
+ case "tool":
10019
+ return msg.toolResult !== void 0;
10020
+ case "assistant":
10021
+ return !msg.isPending;
10022
+ default:
10023
+ return true;
10024
+ }
10025
+ };
10026
+ const processTurn = (turnMessages, targetArray) => {
8766
10027
  let currentToolGroup = [];
8767
10028
  const flushToolGroup = () => {
8768
10029
  if (currentToolGroup.length === 0) return;
@@ -8773,7 +10034,7 @@ var init_MessageList = __esm({
8773
10034
  }
8774
10035
  currentToolGroup = [];
8775
10036
  };
8776
- for (const msg of turnMessages2) {
10037
+ for (const msg of turnMessages) {
8777
10038
  if (msg.type === "tool") {
8778
10039
  currentToolGroup.push(msg);
8779
10040
  } else {
@@ -8783,38 +10044,46 @@ var init_MessageList = __esm({
8783
10044
  }
8784
10045
  flushToolGroup();
8785
10046
  };
8786
- let turnMessages = [];
8787
- for (let i = 0; i < lastUserMessageIndex; i++) {
8788
- const msg = messages[i];
8789
- if (msg.type === "user") {
8790
- processTurn(turnMessages, completed);
8791
- turnMessages = [];
8792
- completed.push({ type: "single", messages: [msg] });
10047
+ const completeMessages = [];
10048
+ const inProgressMessages = [];
10049
+ for (const msg of messages) {
10050
+ if (isMessageComplete(msg)) {
10051
+ completeMessages.push(msg);
8793
10052
  } else {
8794
- turnMessages.push(msg);
10053
+ inProgressMessages.push(msg);
8795
10054
  }
8796
10055
  }
8797
- processTurn(turnMessages, completed);
8798
- if (lastUserMessageIndex >= 0) {
8799
- completed.push({ type: "single", messages: [messages[lastUserMessageIndex]] });
10056
+ let splitIndex = messages.length;
10057
+ for (let i = messages.length - 1; i >= 0; i--) {
10058
+ if (isMessageComplete(messages[i])) {
10059
+ splitIndex = i + 1;
10060
+ break;
10061
+ }
8800
10062
  }
8801
- const currentTurnMessages = lastUserMessageIndex >= 0 ? messages.slice(lastUserMessageIndex + 1) : messages;
8802
- processTurn(currentTurnMessages, currentTurn);
10063
+ const finalCompleteMessages = messages.slice(0, splitIndex);
10064
+ const finalInProgressMessages = messages.slice(splitIndex);
10065
+ processTurn(finalCompleteMessages, completed);
10066
+ processTurn(finalInProgressMessages, currentTurn);
8803
10067
  return { completedGroups: completed, currentTurnGroups: currentTurn };
8804
- }, [messages, lastUserMessageIndex, completedBoundaryKey]);
8805
- const staticItems = useMemo3(() => [
8806
- { id: "header", type: "header" },
8807
- ...completedGroups.map((group, idx) => {
10068
+ }, [messages]);
10069
+ const staticItems = useMemo3(
10070
+ () => completedGroups.map((group, idx) => {
8808
10071
  if (group.type === "group") {
8809
- return { ...group, _isGroup: true, id: `group-${idx}` };
10072
+ return { ...group, _isGroup: true, id: `group-${group.messages[0]?.id || idx}` };
8810
10073
  }
8811
10074
  return { ...group.messages[0], _isMessage: true };
8812
- })
8813
- ], [completedGroups]);
8814
- return /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React13.createElement(Static, { items: staticItems, key: staticRemountKey }, (item) => {
8815
- if (item.type === "header") {
8816
- return /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column", key: "header" }, /* @__PURE__ */ React13.createElement(Header, { currentFolder, gitBranch, headless }));
10075
+ }),
10076
+ [completedGroups]
10077
+ );
10078
+ return /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React13.createElement(
10079
+ StaticHeader,
10080
+ {
10081
+ currentFolder,
10082
+ gitBranch,
10083
+ headless,
10084
+ staticRemountKey
8817
10085
  }
10086
+ ), staticItems.length > 0 && /* @__PURE__ */ React13.createElement(Static, { items: staticItems, key: `messages-${staticRemountKey}-${toolGroupsExpanded}` }, (item) => {
8818
10087
  if (item._isGroup) {
8819
10088
  const content2 = renderGroupedMessage(item);
8820
10089
  if (!content2) {
@@ -8908,7 +10177,7 @@ var init_stdio = __esm({
8908
10177
  });
8909
10178
 
8910
10179
  // src/utils/encryption.ts
8911
- import crypto from "crypto";
10180
+ import crypto2 from "crypto";
8912
10181
  import { hostname, userInfo } from "os";
8913
10182
  function deriveEncryptionKey() {
8914
10183
  const host = hostname();
@@ -8917,7 +10186,7 @@ function deriveEncryptionKey() {
8917
10186
  if (process.env.DEBUG_ENCRYPTION) {
8918
10187
  console.error(`[encryption] hostname=${host}, username=${user}, salt=${salt}`);
8919
10188
  }
8920
- return crypto.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
10189
+ return crypto2.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
8921
10190
  }
8922
10191
  function getEncryptionKey() {
8923
10192
  if (!cachedKey) {
@@ -8927,8 +10196,8 @@ function getEncryptionKey() {
8927
10196
  }
8928
10197
  function encrypt(plaintext) {
8929
10198
  const key = getEncryptionKey();
8930
- const iv = crypto.randomBytes(IV_LENGTH);
8931
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
10199
+ const iv = crypto2.randomBytes(IV_LENGTH);
10200
+ const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
8932
10201
  let encrypted = cipher.update(plaintext, "utf8", "hex");
8933
10202
  encrypted += cipher.final("hex");
8934
10203
  const authTag = cipher.getAuthTag();
@@ -8943,7 +10212,7 @@ function decrypt(encryptedData) {
8943
10212
  const iv = Buffer.from(ivHex, "hex");
8944
10213
  const authTag = Buffer.from(authTagHex, "hex");
8945
10214
  const key = getEncryptionKey();
8946
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
10215
+ const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
8947
10216
  decipher.setAuthTag(authTag);
8948
10217
  let decrypted = decipher.update(encrypted, "hex", "utf8");
8949
10218
  decrypted += decipher.final("utf8");
@@ -8962,14 +10231,14 @@ var init_encryption = __esm({
8962
10231
 
8963
10232
  // src/utils/token-storage.ts
8964
10233
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync } from "fs";
8965
- import { homedir as homedir5 } from "os";
8966
- import { join as join7 } from "path";
10234
+ import { homedir as homedir6 } from "os";
10235
+ import { join as join8 } from "path";
8967
10236
  function getTokenFilePath() {
8968
10237
  const apiUrl = process.env.SUPATEST_API_URL || PRODUCTION_API_URL;
8969
10238
  if (apiUrl === PRODUCTION_API_URL) {
8970
- return join7(CONFIG_DIR, "token.json");
10239
+ return join8(CONFIG_DIR, "token.json");
8971
10240
  }
8972
- return join7(CONFIG_DIR, "token.local.json");
10241
+ return join8(CONFIG_DIR, "token.local.json");
8973
10242
  }
8974
10243
  function isV2Format(stored) {
8975
10244
  return "version" in stored && stored.version === 2;
@@ -9037,10 +10306,10 @@ var init_token_storage = __esm({
9037
10306
  "src/utils/token-storage.ts"() {
9038
10307
  "use strict";
9039
10308
  init_encryption();
9040
- CONFIG_DIR = join7(homedir5(), ".supatest");
10309
+ CONFIG_DIR = join8(homedir6(), ".supatest");
9041
10310
  PRODUCTION_API_URL = "https://code-api.supatest.ai";
9042
10311
  STORAGE_VERSION = 2;
9043
- TOKEN_FILE = join7(CONFIG_DIR, "token.json");
10312
+ TOKEN_FILE = join8(CONFIG_DIR, "token.json");
9044
10313
  }
9045
10314
  });
9046
10315
 
@@ -9064,7 +10333,7 @@ var init_message_bridge = __esm({
9064
10333
  this.sessionId = sessionId;
9065
10334
  }
9066
10335
  /**
9067
- * Push a user message to be injected into the conversation.
10336
+ * Push a user message to be injected into the session.
9068
10337
  * Call this from the UI when user submits a message during agent execution.
9069
10338
  */
9070
10339
  push(text) {
@@ -9136,15 +10405,15 @@ var init_message_bridge = __esm({
9136
10405
  });
9137
10406
 
9138
10407
  // src/commands/login.ts
9139
- import { spawn as spawn2 } from "child_process";
9140
- import crypto2 from "crypto";
9141
- import http from "http";
9142
- import { platform } from "os";
10408
+ import { spawn as spawn4 } from "child_process";
10409
+ import crypto3 from "crypto";
10410
+ import http2 from "http";
10411
+ import { platform as platform2 } from "os";
9143
10412
  function escapeHtml(text) {
9144
10413
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9145
10414
  }
9146
10415
  function generateState() {
9147
- return crypto2.randomBytes(STATE_LENGTH).toString("hex");
10416
+ return crypto3.randomBytes(STATE_LENGTH).toString("hex");
9148
10417
  }
9149
10418
  async function exchangeCodeForToken(code, state) {
9150
10419
  const response = await fetch(`${API_URL}/web/auth/cli-token-exchange`, {
@@ -9160,7 +10429,7 @@ async function exchangeCodeForToken(code, state) {
9160
10429
  return { token: data.token, expiresAt: data.expiresAt };
9161
10430
  }
9162
10431
  function openBrowser(url) {
9163
- const os3 = platform();
10432
+ const os3 = platform2();
9164
10433
  let command;
9165
10434
  let args;
9166
10435
  switch (os3) {
@@ -9177,11 +10446,11 @@ function openBrowser(url) {
9177
10446
  args = [url];
9178
10447
  }
9179
10448
  const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
9180
- spawn2(command, args, options).unref();
10449
+ spawn4(command, args, options).unref();
9181
10450
  }
9182
10451
  function startCallbackServer(port, expectedState) {
9183
10452
  return new Promise((resolve2, reject) => {
9184
- const server = http.createServer(async (req, res) => {
10453
+ const server = http2.createServer(async (req, res) => {
9185
10454
  if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
9186
10455
  res.writeHead(404);
9187
10456
  res.end("Not Found");
@@ -9242,7 +10511,7 @@ function startCallbackServer(port, expectedState) {
9242
10511
  const timeout = setTimeout(() => {
9243
10512
  server.close();
9244
10513
  reject(new Error("Login timeout - no response received after 5 minutes"));
9245
- }, CALLBACK_TIMEOUT_MS);
10514
+ }, CALLBACK_TIMEOUT_MS2);
9246
10515
  server.on("close", () => {
9247
10516
  clearTimeout(timeout);
9248
10517
  });
@@ -9501,74 +10770,33 @@ ${loginUrl}
9501
10770
  throw error;
9502
10771
  }
9503
10772
  }
9504
- var CLI_LOGIN_PORT, FRONTEND_URL, API_URL, CALLBACK_TIMEOUT_MS, STATE_LENGTH;
10773
+ var CLI_LOGIN_PORT, FRONTEND_URL, API_URL, CALLBACK_TIMEOUT_MS2, STATE_LENGTH;
9505
10774
  var init_login = __esm({
9506
10775
  "src/commands/login.ts"() {
9507
10776
  "use strict";
9508
10777
  CLI_LOGIN_PORT = 8420;
9509
10778
  FRONTEND_URL = process.env.SUPATEST_FRONTEND_URL || "https://code.supatest.ai";
9510
10779
  API_URL = process.env.SUPATEST_API_URL || "https://code-api.supatest.ai";
9511
- CALLBACK_TIMEOUT_MS = 3e5;
10780
+ CALLBACK_TIMEOUT_MS2 = 3e5;
9512
10781
  STATE_LENGTH = 32;
9513
10782
  }
9514
10783
  });
9515
10784
 
9516
10785
  // src/utils/claude-max.ts
9517
- import { execSync as execSync5 } from "child_process";
9518
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
9519
- import { homedir as homedir6 } from "os";
9520
- import { join as join8 } from "path";
9521
- function isClaudeMaxAvailable() {
9522
- const platform2 = process.platform;
9523
- logger.debug("[claude-max] Checking Claude Code credentials", { platform: platform2 });
9524
- if (platform2 === "darwin") {
9525
- return checkMacOSKeychain();
9526
- } else {
9527
- return checkCredentialsFile();
9528
- }
9529
- }
9530
- function checkMacOSKeychain() {
10786
+ async function isClaudeMaxAvailable() {
10787
+ logger.debug("[claude-max] Checking Supatest OAuth credentials");
9531
10788
  try {
9532
- const credentialsJson = execSync5(
9533
- 'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
9534
- { encoding: "utf-8" }
9535
- ).trim();
9536
- if (!credentialsJson) {
9537
- logger.debug("[claude-max] No credentials found in macOS keychain");
9538
- return false;
10789
+ const secretStorage = getSecretStorage();
10790
+ const oauthService = new ClaudeOAuthService(secretStorage);
10791
+ const hasSupatestOAuth = await oauthService.isAuthenticated();
10792
+ if (hasSupatestOAuth) {
10793
+ logger.debug("[claude-max] Found Supatest OAuth credentials");
10794
+ return true;
9539
10795
  }
9540
- const credentials = JSON.parse(credentialsJson);
9541
- const hasOauth = !!credentials.claudeAiOauth?.accessToken;
9542
- logger.debug("[claude-max] macOS keychain credentials check", {
9543
- hasOauth,
9544
- hasRefreshToken: !!credentials.claudeAiOauth?.refreshToken
9545
- });
9546
- return hasOauth;
9547
- } catch (error) {
9548
- logger.debug("[claude-max] Error checking macOS keychain", {
9549
- error: error instanceof Error ? error.message : String(error)
9550
- });
10796
+ logger.debug("[claude-max] No Supatest OAuth credentials found");
9551
10797
  return false;
9552
- }
9553
- }
9554
- function checkCredentialsFile() {
9555
- try {
9556
- const credentialsPath = join8(homedir6(), ".claude", ".credentials.json");
9557
- if (!existsSync6(credentialsPath)) {
9558
- logger.debug("[claude-max] Credentials file not found", { path: credentialsPath });
9559
- return false;
9560
- }
9561
- const credentialsJson = readFileSync5(credentialsPath, "utf-8");
9562
- const credentials = JSON.parse(credentialsJson);
9563
- const hasOauth = !!credentials.claudeAiOauth?.accessToken;
9564
- logger.debug("[claude-max] Credentials file check", {
9565
- path: credentialsPath,
9566
- hasOauth,
9567
- hasRefreshToken: !!credentials.claudeAiOauth?.refreshToken
9568
- });
9569
- return hasOauth;
9570
10798
  } catch (error) {
9571
- logger.debug("[claude-max] Error checking credentials file", {
10799
+ logger.debug("[claude-max] Error checking Supatest OAuth storage", {
9572
10800
  error: error instanceof Error ? error.message : String(error)
9573
10801
  });
9574
10802
  return false;
@@ -9577,12 +10805,14 @@ function checkCredentialsFile() {
9577
10805
  var init_claude_max = __esm({
9578
10806
  "src/utils/claude-max.ts"() {
9579
10807
  "use strict";
10808
+ init_claude_oauth();
9580
10809
  init_logger();
10810
+ init_secret_storage();
9581
10811
  }
9582
10812
  });
9583
10813
 
9584
10814
  // src/utils/mcp-manager.ts
9585
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
10815
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
9586
10816
  import { homedir as homedir7 } from "os";
9587
10817
  import { join as join9 } from "path";
9588
10818
  function getGlobalMcpPath() {
@@ -9592,11 +10822,11 @@ function getProjectMcpPath(cwd) {
9592
10822
  return join9(cwd, ".supatest", "mcp.json");
9593
10823
  }
9594
10824
  function loadMcpConfigFromFile(mcpPath, scope) {
9595
- if (!existsSync7(mcpPath)) {
10825
+ if (!existsSync6(mcpPath)) {
9596
10826
  return {};
9597
10827
  }
9598
10828
  try {
9599
- const content = readFileSync6(mcpPath, "utf-8");
10829
+ const content = readFileSync5(mcpPath, "utf-8");
9600
10830
  const config2 = JSON.parse(content);
9601
10831
  if (!config2.mcpServers) {
9602
10832
  return {};
@@ -9630,7 +10860,7 @@ function loadMcpConfig(cwd) {
9630
10860
  }
9631
10861
  function saveMcpConfigToFile(mcpPath, servers) {
9632
10862
  const mcpDir = join9(mcpPath, "..");
9633
- if (!existsSync7(mcpDir)) {
10863
+ if (!existsSync6(mcpDir)) {
9634
10864
  mkdirSync3(mcpDir, { recursive: true });
9635
10865
  }
9636
10866
  const config2 = {
@@ -9814,15 +11044,15 @@ var init_mcp_manager = __esm({
9814
11044
  });
9815
11045
 
9816
11046
  // src/utils/settings-loader.ts
9817
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
11047
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
9818
11048
  import { join as join10 } from "path";
9819
11049
  function loadSupatestSettings(cwd) {
9820
11050
  const settingsPath = join10(cwd, ".supatest", "settings.json");
9821
- if (!existsSync8(settingsPath)) {
11051
+ if (!existsSync7(settingsPath)) {
9822
11052
  return {};
9823
11053
  }
9824
11054
  try {
9825
- const content = readFileSync7(settingsPath, "utf-8");
11055
+ const content = readFileSync6(settingsPath, "utf-8");
9826
11056
  return JSON.parse(content);
9827
11057
  } catch (error) {
9828
11058
  console.warn(
@@ -9836,7 +11066,7 @@ function saveSupatestSettings(cwd, settings) {
9836
11066
  const settingsDir = join10(cwd, ".supatest");
9837
11067
  const settingsPath = join10(settingsDir, "settings.json");
9838
11068
  try {
9839
- if (!existsSync8(settingsDir)) {
11069
+ if (!existsSync7(settingsDir)) {
9840
11070
  mkdirSync4(settingsDir, { recursive: true });
9841
11071
  }
9842
11072
  const existingSettings = loadSupatestSettings(cwd);
@@ -11015,18 +12245,37 @@ var init_TestSelector = __esm({
11015
12245
  apiClient,
11016
12246
  run,
11017
12247
  onSelect,
11018
- onCancel
12248
+ onCancel,
12249
+ assignments = []
11019
12250
  }) => {
11020
12251
  const [allTests, setAllTests] = useState7([]);
11021
12252
  const [selectedTests, setSelectedTests] = useState7(/* @__PURE__ */ new Set());
11022
12253
  const [cursorIndex, setCursorIndex] = useState7(0);
11023
12254
  const [isLoading, setIsLoading] = useState7(false);
12255
+ const [isLoadingAssignments, setIsLoadingAssignments] = useState7(true);
11024
12256
  const [hasMore, setHasMore] = useState7(true);
11025
12257
  const [totalTests, setTotalTests] = useState7(0);
11026
12258
  const [error, setError] = useState7(null);
12259
+ const [showAvailableOnly, setShowAvailableOnly] = useState7(true);
12260
+ const [groupByFile, setGroupByFile] = useState7(false);
12261
+ const assignedTestMap = new Map(assignments.map((a) => [a.testRunId, a]));
12262
+ const assignedTestIds = new Set(assignments.map((a) => a.testRunId));
11027
12263
  useEffect7(() => {
11028
12264
  loadMoreTests();
11029
12265
  }, []);
12266
+ useEffect7(() => {
12267
+ fetchAssignments();
12268
+ }, [run.id]);
12269
+ const fetchAssignments = async () => {
12270
+ setIsLoadingAssignments(true);
12271
+ try {
12272
+ const result = await apiClient.getRunAssignments(run.id);
12273
+ } catch (err) {
12274
+ console.error("Failed to load assignments:", err);
12275
+ } finally {
12276
+ setIsLoadingAssignments(false);
12277
+ }
12278
+ };
11030
12279
  const loadMoreTests = async () => {
11031
12280
  if (isLoading || !hasMore) {
11032
12281
  return;
@@ -11042,7 +12291,8 @@ var init_TestSelector = __esm({
11042
12291
  // Only fetch failed tests
11043
12292
  });
11044
12293
  setTotalTests(result.total);
11045
- setHasMore(allTests.length + result.tests.length < result.total);
12294
+ const loadedCount = allTests.length + result.tests.length;
12295
+ setHasMore(result.tests.length === PAGE_SIZE2 && loadedCount < result.total);
11046
12296
  setAllTests((prev) => [...prev, ...result.tests]);
11047
12297
  } catch (err) {
11048
12298
  setError(err instanceof Error ? err.message : String(err));
@@ -11051,8 +12301,34 @@ var init_TestSelector = __esm({
11051
12301
  setIsLoading(false);
11052
12302
  }
11053
12303
  };
11054
- const totalItems = allTests.length + 1;
11055
- const isOnFixAll = cursorIndex === 0;
12304
+ const getFilteredTests = () => {
12305
+ if (showAvailableOnly) {
12306
+ return allTests.filter((t) => !assignedTestIds.has(t.id));
12307
+ }
12308
+ return allTests;
12309
+ };
12310
+ const filteredTests = getFilteredTests();
12311
+ const availableCount = allTests.filter((t) => !assignedTestIds.has(t.id)).length;
12312
+ const assignedCount = assignedTestIds.size;
12313
+ const getTestsByFile = () => {
12314
+ const groups = /* @__PURE__ */ new Map();
12315
+ for (const test of filteredTests) {
12316
+ const file = test.file;
12317
+ if (!groups.has(file)) {
12318
+ groups.set(file, []);
12319
+ }
12320
+ groups.get(file).push(test);
12321
+ }
12322
+ return Array.from(groups.entries()).map(([file, tests]) => ({
12323
+ file,
12324
+ tests,
12325
+ availableCount: tests.filter((t) => !assignedTestIds.has(t.id)).length,
12326
+ assignedCount: tests.filter((t) => assignedTestIds.has(t.id)).length
12327
+ }));
12328
+ };
12329
+ const fileGroups = getTestsByFile();
12330
+ const totalItems = groupByFile ? fileGroups.length + 1 : filteredTests.length + 1;
12331
+ const isOnFixNext10 = cursorIndex === 0;
11056
12332
  const toggleTest = (testId) => {
11057
12333
  setSelectedTests((prev) => {
11058
12334
  const next = new Set(prev);
@@ -11064,50 +12340,92 @@ var init_TestSelector = __esm({
11064
12340
  return next;
11065
12341
  });
11066
12342
  };
11067
- const selectAll = () => {
11068
- setSelectedTests(new Set(allTests.map((t) => t.id)));
11069
- };
11070
12343
  const selectNone = () => {
11071
12344
  setSelectedTests(/* @__PURE__ */ new Set());
11072
12345
  };
12346
+ const selectAvailable = (tests) => {
12347
+ const availableTests = tests.filter((t) => !assignedTestIds.has(t.id));
12348
+ setSelectedTests(new Set(availableTests.map((t) => t.id)));
12349
+ };
12350
+ const selectFileTests = (fileTests) => {
12351
+ const available = fileTests.filter((t) => !assignedTestIds.has(t.id));
12352
+ const newSelected = new Set(selectedTests);
12353
+ const allSelected = available.every((t) => newSelected.has(t.id));
12354
+ if (allSelected) {
12355
+ for (const test of available) {
12356
+ newSelected.delete(test.id);
12357
+ }
12358
+ } else {
12359
+ for (const test of available) {
12360
+ newSelected.add(test.id);
12361
+ }
12362
+ }
12363
+ setSelectedTests(newSelected);
12364
+ };
11073
12365
  useInput3((input, key) => {
11074
- if (allTests.length === 0 && !isLoading) {
12366
+ if (allTests.length === 0 && !isLoading && !isLoadingAssignments) {
11075
12367
  if (key.escape || input === "q") {
11076
12368
  onCancel();
11077
12369
  }
11078
12370
  return;
11079
12371
  }
12372
+ if (input === "f") {
12373
+ setGroupByFile((prev) => !prev);
12374
+ setCursorIndex(0);
12375
+ return;
12376
+ }
12377
+ if (input === "t") {
12378
+ setShowAvailableOnly((prev) => !prev);
12379
+ setCursorIndex(0);
12380
+ return;
12381
+ }
11080
12382
  if (key.upArrow) {
11081
12383
  setCursorIndex((prev) => prev > 0 ? prev - 1 : totalItems - 1);
11082
12384
  } else if (key.downArrow) {
11083
12385
  const newIndex = cursorIndex < totalItems - 1 ? cursorIndex + 1 : 0;
11084
12386
  setCursorIndex(newIndex);
11085
- if (newIndex >= allTests.length - 2 && hasMore && !isLoading) {
12387
+ if (!groupByFile && newIndex >= allTests.length - 2 && hasMore && !isLoading) {
11086
12388
  loadMoreTests();
11087
12389
  }
11088
12390
  } else if (input === " ") {
11089
- if (isOnFixAll) {
11090
- selectAll();
12391
+ if (isOnFixNext10) {
12392
+ const availableTests = getFilteredTests();
12393
+ const next10 = availableTests.slice(0, Math.min(10, availableTests.length));
12394
+ selectAvailable(next10);
12395
+ } else if (groupByFile) {
12396
+ const fileGroups2 = getTestsByFile();
12397
+ const fileIndex = cursorIndex - 1;
12398
+ if (fileGroups2[fileIndex]) {
12399
+ selectFileTests(fileGroups2[fileIndex].tests);
12400
+ }
11091
12401
  } else {
11092
12402
  const testIndex = cursorIndex - 1;
11093
- if (allTests[testIndex]) {
11094
- toggleTest(allTests[testIndex].id);
12403
+ const filteredTests2 = getFilteredTests();
12404
+ if (filteredTests2[testIndex]) {
12405
+ const test = filteredTests2[testIndex];
12406
+ if (!assignedTestIds.has(test.id)) {
12407
+ toggleTest(test.id);
12408
+ }
11095
12409
  }
11096
12410
  }
11097
- } else if (input === "a") {
11098
- selectAll();
11099
12411
  } else if (input === "n") {
11100
12412
  selectNone();
12413
+ } else if (input === "s") {
12414
+ const availableTests = getFilteredTests();
12415
+ const next10 = availableTests.slice(0, Math.min(10, availableTests.length));
12416
+ selectAvailable(next10);
11101
12417
  } else if (key.return) {
11102
- if (isOnFixAll) {
11103
- onSelect(allTests);
12418
+ const availableTests = getFilteredTests();
12419
+ if (isOnFixNext10) {
12420
+ const next10 = availableTests.slice(0, Math.min(10, availableTests.length));
12421
+ onSelect(next10);
11104
12422
  } else if (selectedTests.size > 0) {
11105
- const testsToFix = allTests.filter((t) => selectedTests.has(t.id));
12423
+ const testsToFix = availableTests.filter((t) => selectedTests.has(t.id));
11106
12424
  onSelect(testsToFix);
11107
- } else {
12425
+ } else if (!groupByFile) {
11108
12426
  const testIndex = cursorIndex - 1;
11109
- if (allTests[testIndex]) {
11110
- onSelect([allTests[testIndex]]);
12427
+ if (availableTests[testIndex] && !assignedTestIds.has(availableTests[testIndex].id)) {
12428
+ onSelect([availableTests[testIndex]]);
11111
12429
  }
11112
12430
  }
11113
12431
  } else if (key.escape || input === "q") {
@@ -11124,44 +12442,66 @@ var init_TestSelector = __esm({
11124
12442
  return /* @__PURE__ */ React21.createElement(Box18, { borderColor: "green", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React21.createElement(Text16, { bold: true, color: "green" }, "No Failed Tests"), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, "All tests passed in this run. Nothing to fix!"), /* @__PURE__ */ React21.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, "Press ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "ESC"), " to go back")));
11125
12443
  }
11126
12444
  const testStartIndex = Math.max(0, cursorIndex - 1);
11127
- const adjustedStart = isOnFixAll ? 0 : Math.max(0, testStartIndex - Math.floor(VISIBLE_ITEMS2 / 2));
11128
- const adjustedEnd = Math.min(allTests.length, adjustedStart + VISIBLE_ITEMS2);
11129
- const visibleTests = allTests.slice(adjustedStart, adjustedEnd);
12445
+ const adjustedStart = isOnFixNext10 ? 0 : Math.max(0, testStartIndex - Math.floor(VISIBLE_ITEMS2 / 2));
12446
+ const adjustedEnd = Math.min(filteredTests.length, adjustedStart + VISIBLE_ITEMS2);
12447
+ const visibleTests = filteredTests.slice(adjustedStart, adjustedEnd);
11130
12448
  const branch = run.git?.branch || "unknown";
11131
12449
  const commit = run.git?.commit?.slice(0, 7) || "";
11132
- return /* @__PURE__ */ React21.createElement(Box18, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text16, { bold: true, color: "cyan" }, "Run: ", branch, commit && /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " @ ", commit), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 "), /* @__PURE__ */ React21.createElement(Text16, { color: "red" }, allTests.length, " failed test", allTests.length !== 1 ? "s" : ""))), /* @__PURE__ */ React21.createElement(Box18, { flexDirection: "column" }, /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(
12450
+ return /* @__PURE__ */ React21.createElement(Box18, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text16, { bold: true, color: "cyan" }, "Run: ", branch, commit && /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " @ ", commit), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 "), /* @__PURE__ */ React21.createElement(Text16, { color: "red" }, allTests.length, " failed"), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 "), /* @__PURE__ */ React21.createElement(Text16, { color: "green" }, availableCount, " avail"), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 "), /* @__PURE__ */ React21.createElement(Text16, { color: "yellow" }, assignedCount, " working"))), /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, "[", showAvailableOnly ? "x" : " ", "] ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "t"), " avail only", " ", "[", groupByFile ? "x" : " ", "] ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "f"), " group files")), /* @__PURE__ */ React21.createElement(Box18, { flexDirection: "column" }, /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(
11133
12451
  Text16,
11134
12452
  {
11135
- backgroundColor: isOnFixAll ? theme.text.accent : void 0,
11136
- bold: isOnFixAll,
11137
- color: isOnFixAll ? "black" : theme.text.primary
12453
+ backgroundColor: isOnFixNext10 ? theme.text.accent : void 0,
12454
+ bold: isOnFixNext10,
12455
+ color: isOnFixNext10 ? "black" : theme.text.primary
11138
12456
  },
11139
- isOnFixAll ? "\u25B6 " : " ",
11140
- "[Fix All ",
11141
- allTests.length,
11142
- " Failed Test",
11143
- allTests.length !== 1 ? "s" : "",
12457
+ isOnFixNext10 ? "\u25B6 " : " ",
12458
+ "[Fix Next 10 Available Test",
12459
+ Math.min(10, filteredTests.length) !== 1 ? "s" : "",
11144
12460
  "]"
11145
- )), /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), visibleTests.map((test, index) => {
11146
- const actualIndex = adjustedStart + index;
11147
- const itemIndex = actualIndex + 1;
11148
- const isSelected = itemIndex === cursorIndex;
11149
- const isChecked = selectedTests.has(test.id);
11150
- const checkbox = isChecked ? "[x]" : "[ ]";
11151
- const file = test.file.split("/").pop() || test.file;
11152
- const line = test.location?.line || "";
11153
- const title = test.title;
11154
- const indicator = isSelected ? "\u25B6 " : " ";
11155
- const bgColor = isSelected ? theme.text.accent : void 0;
11156
- return /* @__PURE__ */ React21.createElement(Box18, { key: test.id, marginBottom: 0 }, /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, indicator), /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, color: isChecked ? "green" : isSelected ? "black" : theme.text.dim }, checkbox), /* @__PURE__ */ React21.createElement(Text16, null, " "), /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, file, line && /* @__PURE__ */ React21.createElement(Text16, { color: isSelected ? "black" : theme.text.dim }, ":", line), /* @__PURE__ */ React21.createElement(Text16, { color: isSelected ? "black" : theme.text.dim }, " - "), title));
11157
- })), /* @__PURE__ */ React21.createElement(Box18, { flexDirection: "column", marginTop: 1 }, allTests.length > VISIBLE_ITEMS2 && /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: "yellow" }, "Showing ", adjustedStart + 1, "-", adjustedEnd, " of ", totalTests || allTests.length, " failed tests", hasMore && !isLoading && /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 Scroll for more"))), /* @__PURE__ */ React21.createElement(Box18, null, /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "\u2191\u2193"), " navigate \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "Space"), " toggle \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "a"), " all \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "n"), " none \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "Enter"), " fix selected")), selectedTests.size > 0 && /* @__PURE__ */ React21.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: "green" }, selectedTests.size, " test", selectedTests.size !== 1 ? "s" : "", " selected")), isLoading && /* @__PURE__ */ React21.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: "cyan" }, "Loading more tests..."))));
12461
+ )), /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), groupByFile ? (
12462
+ // File Grouping Mode
12463
+ fileGroups.map((group, index) => {
12464
+ const itemIndex = index + 1;
12465
+ const isSelected = itemIndex === cursorIndex;
12466
+ const bgColor = isSelected ? theme.text.accent : void 0;
12467
+ const indicator = isSelected ? "\u25B6 " : " ";
12468
+ const fileName = group.file.split("/").pop() || group.file;
12469
+ const allSelected = group.availableCount > 0 && group.tests.filter((t) => !assignedTestIds.has(t.id)).every((t) => selectedTests.has(t.id));
12470
+ const someSelected = group.tests.filter((t) => !assignedTestIds.has(t.id)).some((t) => selectedTests.has(t.id));
12471
+ return /* @__PURE__ */ React21.createElement(Box18, { flexDirection: "column", key: group.file, marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Box18, null, /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, indicator), /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, color: allSelected ? "green" : someSelected ? "yellow" : isSelected ? "black" : theme.text.dim }, "[", allSelected ? "\u2713" : someSelected ? "\u2297" : " ", "]"), /* @__PURE__ */ React21.createElement(Text16, null, " "), /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, fileName), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 "), /* @__PURE__ */ React21.createElement(Text16, { color: "green" }, group.availableCount, " available"), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 "), /* @__PURE__ */ React21.createElement(Text16, { color: "yellow" }, group.assignedCount, " working"), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 "), /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, group.tests.length, " total")), isSelected && group.tests.length <= 5 && /* @__PURE__ */ React21.createElement(Box18, { marginLeft: 2 }, group.tests.map((test) => {
12472
+ const line = test.location?.line || "";
12473
+ const title = test.title;
12474
+ const isAssigned = assignedTestIds.has(test.id);
12475
+ return /* @__PURE__ */ React21.createElement(Box18, { key: test.id }, /* @__PURE__ */ React21.createElement(Text16, { color: isAssigned ? theme.text.dim : theme.text.primary }, isAssigned ? "\u{1F504} " : " ", line && /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, line, ": "), title));
12476
+ })));
12477
+ })
12478
+ ) : (
12479
+ // Individual Tests Mode
12480
+ visibleTests.map((test, index) => {
12481
+ const actualIndex = adjustedStart + index;
12482
+ const itemIndex = actualIndex + 1;
12483
+ const isSelected = itemIndex === cursorIndex;
12484
+ const isChecked = selectedTests.has(test.id);
12485
+ const isAssigned = assignedTestIds.has(test.id);
12486
+ const assignment = assignedTestMap.get(test.id);
12487
+ const checkbox = isChecked ? "[x]" : "[ ]";
12488
+ const file = test.file.split("/").pop() || test.file;
12489
+ const line = test.location?.line || "";
12490
+ const title = test.title;
12491
+ const indicator = isSelected ? "\u25B6 " : " ";
12492
+ const bgColor = isSelected ? theme.text.accent : void 0;
12493
+ const assignee = assignment?.assignedTo || "";
12494
+ const displayAssignee = assignee.startsWith("cli:") ? assignee.slice(4) : assignee;
12495
+ return /* @__PURE__ */ React21.createElement(Box18, { key: test.id, marginBottom: 0 }, /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : isAssigned ? theme.text.dim : theme.text.primary }, indicator), /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, color: isAssigned ? theme.text.dim : isChecked ? "green" : isSelected ? "black" : theme.text.dim }, isAssigned ? "\u{1F504}" : checkbox), /* @__PURE__ */ React21.createElement(Text16, null, " "), /* @__PURE__ */ React21.createElement(Text16, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : isAssigned ? theme.text.dim : theme.text.primary }, file, line && /* @__PURE__ */ React21.createElement(Text16, { color: isSelected ? "black" : theme.text.dim }, ":", line), isAssigned && /* @__PURE__ */ React21.createElement(React21.Fragment, null, /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " \u2022 "), /* @__PURE__ */ React21.createElement(Text16, { color: "yellow" }, displayAssignee)), /* @__PURE__ */ React21.createElement(Text16, { color: isSelected ? "black" : theme.text.dim }, " - "), title));
12496
+ })
12497
+ )), /* @__PURE__ */ React21.createElement(Box18, { flexDirection: "column", marginTop: 1 }, !groupByFile && filteredTests.length > VISIBLE_ITEMS2 && /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: "yellow" }, "Showing ", adjustedStart + 1, "-", adjustedEnd, " of ", filteredTests.length, " ", showAvailableOnly ? "available" : "", " tests", hasMore && !isLoading && /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " (scroll for more)"))), groupByFile && /* @__PURE__ */ React21.createElement(Box18, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: "yellow" }, "Showing ", fileGroups.length, " file", fileGroups.length !== 1 ? "s" : "", " \u2022 ", filteredTests.length, " total test", filteredTests.length !== 1 ? "s" : "")), /* @__PURE__ */ React21.createElement(Box18, null, /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "\u2191\u2193"), " navigate \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "Space"), " toggle \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "n"), " none \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "s"), " next 10 \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "f"), " group files \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "t"), " toggle filter \u2022 ", /* @__PURE__ */ React21.createElement(Text16, { bold: true }, "Enter"), " fix selected")), selectedTests.size > 0 && /* @__PURE__ */ React21.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: "green" }, selectedTests.size, " test", selectedTests.size !== 1 ? "s" : "", " selected")), (isLoading || isLoadingAssignments) && /* @__PURE__ */ React21.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(Text16, { color: "cyan" }, isLoading ? "Loading more tests..." : "Loading assignments..."))));
11158
12498
  };
11159
12499
  }
11160
12500
  });
11161
12501
 
11162
12502
  // src/ui/components/FixFlow.tsx
11163
12503
  import { Box as Box19, Text as Text17, useInput as useInput4 } from "ink";
11164
- import React22, { useState as useState8 } from "react";
12504
+ import React22, { useEffect as useEffect8, useState as useState8 } from "react";
11165
12505
  var FixFlow;
11166
12506
  var init_FixFlow = __esm({
11167
12507
  "src/ui/components/FixFlow.tsx"() {
@@ -11180,6 +12520,8 @@ var init_FixFlow = __esm({
11180
12520
  const [step, setStep] = useState8(initialRunId ? "select-run" : "select-run");
11181
12521
  const [selectedRun, setSelectedRun] = useState8(null);
11182
12522
  const [selectedTests, setSelectedTests] = useState8([]);
12523
+ const [assignments, setAssignments] = useState8([]);
12524
+ const [assignmentIds, setAssignmentIds] = useState8(/* @__PURE__ */ new Map());
11183
12525
  const [loadingProgress, setLoadingProgress] = useState8({ current: 0, total: 0 });
11184
12526
  const [loadError, setLoadError] = useState8(null);
11185
12527
  useInput4((input, key) => {
@@ -11189,7 +12531,7 @@ var init_FixFlow = __esm({
11189
12531
  setStep("select-run");
11190
12532
  } else if (step === "select-run") {
11191
12533
  onCancel();
11192
- } else if (step === "loading-details" && loadError) {
12534
+ } else if ((step === "loading-details" || step === "claiming-tests" || step === "error") && loadError) {
11193
12535
  setLoadError(null);
11194
12536
  setStep("select-tests");
11195
12537
  }
@@ -11198,13 +12540,47 @@ var init_FixFlow = __esm({
11198
12540
  const handleRunSelect = (run) => {
11199
12541
  setSelectedRun(run);
11200
12542
  setStep("select-tests");
12543
+ fetchAssignments(run.id);
12544
+ };
12545
+ const fetchAssignments = async (runId) => {
12546
+ try {
12547
+ const result = await apiClient.getRunAssignments(runId);
12548
+ setAssignments(result.assignments);
12549
+ } catch (err) {
12550
+ console.error("Failed to load assignments:", err);
12551
+ }
11201
12552
  };
11202
12553
  const handleTestsSelect = async (tests) => {
11203
12554
  setSelectedTests(tests);
11204
- setStep("loading-details");
11205
- setLoadingProgress({ current: 0, total: tests.length });
12555
+ setStep("claiming-tests");
11206
12556
  setLoadError(null);
11207
12557
  try {
12558
+ if (selectedRun) {
12559
+ const claimResult = await apiClient.assignTests({
12560
+ runId: selectedRun.id,
12561
+ testRunIds: tests.map((t) => t.id)
12562
+ });
12563
+ if (claimResult.conflicts.length > 0) {
12564
+ const conflictList = claimResult.conflicts.slice(0, 5).map((c) => `${c.file}: ${c.title} (claimed by ${c.currentAssignee})`).join("\n\u2022 ");
12565
+ const moreCount = claimResult.conflicts.length - 5;
12566
+ setLoadError(
12567
+ `${claimResult.conflicts.length} test${claimResult.conflicts.length > 1 ? "s were" : " was"} already claimed by others:
12568
+ \u2022 ${conflictList}${moreCount > 0 ? `
12569
+ \u2022 ... and ${moreCount} more` : ""}
12570
+
12571
+ Please select different tests.`
12572
+ );
12573
+ setStep("error");
12574
+ return;
12575
+ }
12576
+ const assignmentMap = /* @__PURE__ */ new Map();
12577
+ claimResult.assigned.forEach((assignment) => {
12578
+ assignmentMap.set(assignment.testRunId, assignment.id);
12579
+ });
12580
+ setAssignmentIds(assignmentMap);
12581
+ }
12582
+ setStep("loading-details");
12583
+ setLoadingProgress({ current: 0, total: tests.length });
11208
12584
  const testDetails = [];
11209
12585
  const batchSize = 5;
11210
12586
  for (let i = 0; i < tests.length; i += batchSize) {
@@ -11220,7 +12596,25 @@ var init_FixFlow = __esm({
11220
12596
  setStep("fixing");
11221
12597
  onStartFix(prompt, tests);
11222
12598
  } catch (err) {
11223
- setLoadError(err instanceof Error ? err.message : String(err));
12599
+ const errorMessage = err instanceof Error ? err.message : String(err);
12600
+ if (errorMessage.includes("network") || errorMessage.includes("fetch")) {
12601
+ setLoadError(`Network error: Unable to connect to the server.
12602
+
12603
+ Please check your internet connection and try again.
12604
+
12605
+ Error: ${errorMessage}`);
12606
+ } else if (errorMessage.includes("auth") || errorMessage.includes("unauthorized")) {
12607
+ setLoadError(`Authentication error: Please run 'supatest auth' to log in.
12608
+
12609
+ Error: ${errorMessage}`);
12610
+ } else {
12611
+ setLoadError(`Failed to load test details:
12612
+
12613
+ ${errorMessage}
12614
+
12615
+ Press ESC to go back and try again.`);
12616
+ }
12617
+ setStep("error");
11224
12618
  }
11225
12619
  };
11226
12620
  const handleRunCancel = () => {
@@ -11228,8 +12622,47 @@ var init_FixFlow = __esm({
11228
12622
  };
11229
12623
  const handleTestCancel = () => {
11230
12624
  setSelectedRun(null);
12625
+ setAssignments([]);
11231
12626
  setStep("select-run");
11232
12627
  };
12628
+ const markAssignmentsComplete = async (fixSessionId) => {
12629
+ if (assignmentIds.size === 0) {
12630
+ return;
12631
+ }
12632
+ try {
12633
+ await Promise.all(
12634
+ Array.from(assignmentIds.values()).map(
12635
+ (assignmentId) => apiClient.completeAssignment({
12636
+ assignmentId,
12637
+ status: "completed",
12638
+ fixSessionId
12639
+ })
12640
+ )
12641
+ );
12642
+ } catch (err) {
12643
+ console.error("Failed to mark assignments as complete:", err);
12644
+ }
12645
+ };
12646
+ const releaseAssignments = async () => {
12647
+ if (assignmentIds.size === 0) {
12648
+ return;
12649
+ }
12650
+ try {
12651
+ await Promise.all(
12652
+ Array.from(assignmentIds.values()).map(
12653
+ (assignmentId) => apiClient.releaseAssignment(assignmentId)
12654
+ )
12655
+ );
12656
+ setAssignmentIds(/* @__PURE__ */ new Map());
12657
+ } catch (err) {
12658
+ console.error("Failed to release assignments:", err);
12659
+ }
12660
+ };
12661
+ useEffect8(() => {
12662
+ if (step === "complete") {
12663
+ markAssignmentsComplete();
12664
+ }
12665
+ }, [step]);
11233
12666
  switch (step) {
11234
12667
  case "select-run":
11235
12668
  return /* @__PURE__ */ React22.createElement(
@@ -11249,20 +12682,25 @@ var init_FixFlow = __esm({
11249
12682
  TestSelector,
11250
12683
  {
11251
12684
  apiClient,
12685
+ assignments,
11252
12686
  onCancel: handleTestCancel,
11253
12687
  onSelect: handleTestsSelect,
11254
12688
  run: selectedRun
11255
12689
  }
11256
12690
  );
12691
+ case "claiming-tests":
12692
+ return /* @__PURE__ */ React22.createElement(Box19, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React22.createElement(Text17, { bold: true, color: "cyan" }, "Claiming Tests..."), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, "Assigning ", selectedTests.length, " test", selectedTests.length !== 1 ? "s" : "", " to you...")));
11257
12693
  case "loading-details":
11258
12694
  if (loadError) {
11259
12695
  return /* @__PURE__ */ React22.createElement(Box19, { borderColor: "red", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React22.createElement(Text17, { bold: true, color: "red" }, "Error Loading Test Details"), /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, loadError), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, "Press ", /* @__PURE__ */ React22.createElement(Text17, { bold: true }, "ESC"), " to go back")));
11260
12696
  }
11261
12697
  return /* @__PURE__ */ React22.createElement(Box19, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React22.createElement(Text17, { bold: true, color: "cyan" }, "Loading Test Details..."), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, "Fetching error messages, stack traces, and execution steps...")), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: "yellow" }, loadingProgress.current, " / ", loadingProgress.total, " tests loaded")));
12698
+ case "error":
12699
+ return /* @__PURE__ */ React22.createElement(Box19, { borderColor: "red", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React22.createElement(Text17, { bold: true, color: "red" }, "Error"), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, loadError)), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, "Press ", /* @__PURE__ */ React22.createElement(Text17, { bold: true }, "ESC"), " to go back")));
11262
12700
  case "fixing":
11263
12701
  return /* @__PURE__ */ React22.createElement(Box19, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React22.createElement(Text17, { bold: true, color: "cyan" }, "Fixing ", selectedTests.length, " Test", selectedTests.length !== 1 ? "s" : "", "..."), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, selectedTests.map((test, index) => /* @__PURE__ */ React22.createElement(Box19, { key: test.id }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, index + 1, ". ", test.file.split("/").pop(), ":", test.location?.line || "", " - ", test.title)))), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, "The agent will now analyze and fix each test...")));
11264
12702
  case "complete":
11265
- return /* @__PURE__ */ React22.createElement(Box19, { borderColor: "green", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React22.createElement(Text17, { bold: true, color: "green" }, "Fix Complete"), /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, "Press any key to continue..."));
12703
+ return /* @__PURE__ */ React22.createElement(Box19, { borderColor: "green", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React22.createElement(Text17, { bold: true, color: "green" }, "Tests Marked as Complete"), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, "\u2713 ", selectedTests.length, " test", selectedTests.length !== 1 ? "s have" : " has", " been marked as fixed")), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text17, { color: theme.text.dim }, "Press any key to continue...")));
11266
12704
  default:
11267
12705
  return null;
11268
12706
  }
@@ -11270,91 +12708,144 @@ var init_FixFlow = __esm({
11270
12708
  }
11271
12709
  });
11272
12710
 
11273
- // src/ui/utils/file-completion.ts
11274
- import fs4 from "fs";
12711
+ // src/ui/utils/file-search.ts
11275
12712
  import path4 from "path";
11276
- function shouldIgnore(name, isDir) {
11277
- if (name.startsWith(".")) return true;
11278
- if (isDir) return IGNORED_DIRS.has(name);
11279
- const ext = path4.extname(name).toLowerCase();
11280
- return IGNORED_EXTENSIONS.has(ext);
11281
- }
11282
- function getFiles(rootDir = process.cwd(), maxDepth = 3) {
11283
- const files = [];
11284
- function scan(dir, depth) {
11285
- if (depth > maxDepth) return;
11286
- try {
11287
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
11288
- for (const entry of entries) {
11289
- if (shouldIgnore(entry.name, entry.isDirectory())) continue;
11290
- const fullPath = path4.join(dir, entry.name);
11291
- const relativePath = path4.relative(rootDir, fullPath);
11292
- if (entry.isDirectory()) {
11293
- scan(fullPath, depth + 1);
11294
- } else {
11295
- files.push(relativePath);
11296
- }
11297
- }
11298
- } catch (error) {
12713
+ import { glob } from "glob";
12714
+ function fuzzyMatch(text, query2) {
12715
+ const textLower = text.toLowerCase();
12716
+ const queryLower = query2.toLowerCase();
12717
+ if (!queryLower) return 1;
12718
+ let queryIdx = 0;
12719
+ let score = 0;
12720
+ let consecutiveMatches = 0;
12721
+ for (let i = 0; i < textLower.length && queryIdx < queryLower.length; i++) {
12722
+ if (textLower[i] === queryLower[queryIdx]) {
12723
+ queryIdx++;
12724
+ score += 1 + consecutiveMatches * 0.5;
12725
+ consecutiveMatches++;
12726
+ } else {
12727
+ consecutiveMatches = 0;
11299
12728
  }
11300
12729
  }
11301
- scan(rootDir, 0);
11302
- return files;
12730
+ if (queryIdx < queryLower.length) {
12731
+ return 0;
12732
+ }
12733
+ const segments = textLower.split(path4.sep);
12734
+ for (const segment of segments) {
12735
+ if (segment.startsWith(queryLower[0])) {
12736
+ score += 0.5;
12737
+ }
12738
+ }
12739
+ if (textLower === queryLower) {
12740
+ score += 2;
12741
+ }
12742
+ return score;
12743
+ }
12744
+ async function searchFiles(query2, options = {}) {
12745
+ const { signal, maxResults = 20, maxDepth = 5, cwd = process.cwd() } = options;
12746
+ const pattern = "**/*";
12747
+ const files = await glob(pattern, {
12748
+ cwd,
12749
+ dot: false,
12750
+ // Don't include dotfiles by default
12751
+ signal,
12752
+ nodir: true,
12753
+ maxDepth,
12754
+ absolute: false
12755
+ });
12756
+ const safeFiles = files || [];
12757
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
12758
+ if (!query2) {
12759
+ return safeFiles.slice(0, maxResults);
12760
+ }
12761
+ const results = [];
12762
+ for (const file of safeFiles) {
12763
+ const score = fuzzyMatch(file, query2);
12764
+ if (score > 0) {
12765
+ results.push({ file, score });
12766
+ }
12767
+ }
12768
+ results.sort((a, b) => b.score - a.score);
12769
+ return results.slice(0, maxResults).map((r) => r.file);
11303
12770
  }
11304
- var IGNORED_DIRS, IGNORED_EXTENSIONS;
11305
- var init_file_completion = __esm({
11306
- "src/ui/utils/file-completion.ts"() {
12771
+ var init_file_search = __esm({
12772
+ "src/ui/utils/file-search.ts"() {
11307
12773
  "use strict";
11308
- IGNORED_DIRS = /* @__PURE__ */ new Set([
11309
- "node_modules",
11310
- ".git",
11311
- ".turbo",
11312
- "dist",
11313
- "build",
11314
- "coverage",
11315
- ".next",
11316
- ".cache"
11317
- ]);
11318
- IGNORED_EXTENSIONS = /* @__PURE__ */ new Set([
11319
- ".png",
11320
- ".jpg",
11321
- ".jpeg",
11322
- ".gif",
11323
- ".ico",
11324
- ".svg",
11325
- ".woff",
11326
- ".woff2",
11327
- ".ttf",
11328
- ".eot",
11329
- ".mp4",
11330
- ".webm",
11331
- ".mp3",
11332
- ".wav",
11333
- ".zip",
11334
- ".tar",
11335
- ".gz",
11336
- ".7z",
11337
- ".rar",
11338
- ".pdf",
11339
- ".doc",
11340
- ".docx",
11341
- ".xls",
11342
- ".xlsx",
11343
- ".exe",
11344
- ".dll",
11345
- ".so",
11346
- ".dylib",
11347
- ".bin",
11348
- ".map",
11349
- ".lock",
11350
- ".tsbuildinfo"
11351
- ]);
12774
+ }
12775
+ });
12776
+
12777
+ // src/ui/hooks/useFileCompletion.ts
12778
+ import { useEffect as useEffect9, useRef as useRef6, useState as useState9 } from "react";
12779
+ function useFileCompletion(enabled, query2, cwd, maxResults = 20) {
12780
+ const [suggestions, setSuggestions] = useState9([]);
12781
+ const [isLoading, setIsLoading] = useState9(false);
12782
+ const abortControllerRef = useRef6(null);
12783
+ const debounceTimerRef = useRef6(null);
12784
+ const loadingTimerRef = useRef6(null);
12785
+ useEffect9(() => {
12786
+ if (abortControllerRef.current) {
12787
+ abortControllerRef.current.abort();
12788
+ }
12789
+ if (debounceTimerRef.current) {
12790
+ clearTimeout(debounceTimerRef.current);
12791
+ }
12792
+ if (loadingTimerRef.current) {
12793
+ clearTimeout(loadingTimerRef.current);
12794
+ }
12795
+ if (!enabled || !query2 || query2.length === 0) {
12796
+ setSuggestions([]);
12797
+ setIsLoading(false);
12798
+ return;
12799
+ }
12800
+ loadingTimerRef.current = setTimeout(() => {
12801
+ setIsLoading(true);
12802
+ }, 200);
12803
+ debounceTimerRef.current = setTimeout(async () => {
12804
+ const controller = new AbortController();
12805
+ abortControllerRef.current = controller;
12806
+ try {
12807
+ const results = await searchFiles(query2, {
12808
+ signal: controller.signal,
12809
+ maxResults,
12810
+ cwd
12811
+ });
12812
+ if (!controller.signal.aborted) {
12813
+ setSuggestions(Array.isArray(results) ? results : []);
12814
+ }
12815
+ } catch (error) {
12816
+ if (error instanceof Error && error.name !== "AbortError") {
12817
+ console.error("File search failed:", error);
12818
+ }
12819
+ setSuggestions([]);
12820
+ } finally {
12821
+ if (loadingTimerRef.current) {
12822
+ clearTimeout(loadingTimerRef.current);
12823
+ }
12824
+ setIsLoading(false);
12825
+ }
12826
+ }, 150);
12827
+ return () => {
12828
+ abortControllerRef.current?.abort();
12829
+ if (debounceTimerRef.current) {
12830
+ clearTimeout(debounceTimerRef.current);
12831
+ }
12832
+ if (loadingTimerRef.current) {
12833
+ clearTimeout(loadingTimerRef.current);
12834
+ }
12835
+ };
12836
+ }, [enabled, query2, cwd, maxResults]);
12837
+ return { suggestions, isLoading };
12838
+ }
12839
+ var init_useFileCompletion = __esm({
12840
+ "src/ui/hooks/useFileCompletion.ts"() {
12841
+ "use strict";
12842
+ init_file_search();
11352
12843
  }
11353
12844
  });
11354
12845
 
11355
12846
  // src/ui/components/ModelSelector.tsx
11356
12847
  import { Box as Box20, Text as Text18, useInput as useInput5 } from "ink";
11357
- import React23, { useState as useState9 } from "react";
12848
+ import React23, { useState as useState10 } from "react";
11358
12849
  function getAvailableModels(isClaudeMax) {
11359
12850
  if (isClaudeMax) {
11360
12851
  return AVAILABLE_MODELS.map((m) => ({
@@ -11389,7 +12880,7 @@ var init_ModelSelector = __esm({
11389
12880
  }) => {
11390
12881
  const models = getAvailableModels(isClaudeMax);
11391
12882
  const currentIndex = models.findIndex((m) => m.id === currentModel);
11392
- const [selectedIndex, setSelectedIndex] = useState9(currentIndex >= 0 ? currentIndex : 0);
12883
+ const [selectedIndex, setSelectedIndex] = useState10(currentIndex >= 0 ? currentIndex : 0);
11393
12884
  useInput5((input, key) => {
11394
12885
  if (key.upArrow) {
11395
12886
  setSelectedIndex((prev) => prev > 0 ? prev - 1 : models.length - 1);
@@ -11431,7 +12922,7 @@ var init_ModelSelector = __esm({
11431
12922
  import path5 from "path";
11432
12923
  import chalk4 from "chalk";
11433
12924
  import { Box as Box21, Text as Text19 } from "ink";
11434
- import React24, { forwardRef, memo as memo3, useEffect as useEffect9, useImperativeHandle, useState as useState10 } from "react";
12925
+ import React24, { forwardRef, memo as memo3, useEffect as useEffect10, useImperativeHandle, useState as useState11 } from "react";
11435
12926
  var InputPromptInner, InputPrompt;
11436
12927
  var init_InputPrompt = __esm({
11437
12928
  "src/ui/components/InputPrompt.tsx"() {
@@ -11439,13 +12930,13 @@ var init_InputPrompt = __esm({
11439
12930
  init_shared_es();
11440
12931
  init_command_discovery();
11441
12932
  init_SessionContext();
12933
+ init_useFileCompletion();
11442
12934
  init_useKeypress();
11443
- init_file_completion();
11444
12935
  init_theme();
11445
12936
  init_ModelSelector();
11446
12937
  InputPromptInner = forwardRef(({
11447
12938
  onSubmit,
11448
- placeholder = "Enter your task (press Enter to submit, Shift+Enter for new line)...",
12939
+ placeholder = "Enter your task... (Ctrl+W=del word, Alt+\u2190/\u2192=word nav, Ctrl+U/K=clear)",
11449
12940
  disabled = false,
11450
12941
  onHelpToggle,
11451
12942
  cwd,
@@ -11454,14 +12945,40 @@ var init_InputPrompt = __esm({
11454
12945
  onInputChange,
11455
12946
  isClaudeMax = false
11456
12947
  }, ref) => {
11457
- const { agentMode, selectedModel, setSelectedModel, isAgentRunning, usageStats } = useSession();
11458
- const [value, setValue] = useState10("");
11459
- const [cursorOffset, setCursorOffset] = useState10(0);
11460
- const [allFiles, setAllFiles] = useState10([]);
11461
- const [suggestions, setSuggestions] = useState10([]);
11462
- const [activeSuggestion, setActiveSuggestion] = useState10(0);
11463
- const [showSuggestions, setShowSuggestions] = useState10(false);
11464
- const [mentionStartIndex, setMentionStartIndex] = useState10(-1);
12948
+ const { agentMode, selectedModel, setSelectedModel, isAgentRunning } = useSession();
12949
+ const { usageStats } = useUsageStats();
12950
+ const [value, setValue] = useState11("");
12951
+ const [cursorPos, setCursorPos] = useState11([0, 0]);
12952
+ const getCursorOffset = () => {
12953
+ const lines2 = value.split("\n");
12954
+ let offset = 0;
12955
+ for (let i = 0; i < cursorPos[0]; i++) {
12956
+ offset += lines2[i]?.length ?? 0;
12957
+ offset += 1;
12958
+ }
12959
+ offset += cursorPos[1];
12960
+ return offset;
12961
+ };
12962
+ const offsetToCursorPos = (offset, text) => {
12963
+ const lines2 = text.split("\n");
12964
+ let currentOffset = 0;
12965
+ for (let i = 0; i < lines2.length; i++) {
12966
+ const lineLength = lines2[i]?.length ?? 0;
12967
+ if (currentOffset + lineLength >= offset) {
12968
+ return [i, offset - currentOffset];
12969
+ }
12970
+ currentOffset += lineLength + 1;
12971
+ }
12972
+ return [lines2.length - 1, lines2[lines2.length - 1]?.length ?? 0];
12973
+ };
12974
+ const setCursorFromOffset = (offset, text) => {
12975
+ setCursorPos(offsetToCursorPos(offset, text ?? value));
12976
+ };
12977
+ const [suggestions, setSuggestions] = useState11([]);
12978
+ const [activeSuggestion, setActiveSuggestion] = useState11(0);
12979
+ const [showSuggestions, setShowSuggestions] = useState11(false);
12980
+ const [mentionStartIndex, setMentionStartIndex] = useState11(null);
12981
+ const [mentionQuery, setMentionQuery] = useState11("");
11465
12982
  const BUILTIN_SLASH_COMMANDS = [
11466
12983
  { name: "/help", desc: "Show help" },
11467
12984
  { name: "/resume", desc: "Resume session" },
@@ -11477,9 +12994,9 @@ var init_InputPrompt = __esm({
11477
12994
  { name: "/logout", desc: "Log out" },
11478
12995
  { name: "/exit", desc: "Exit CLI" }
11479
12996
  ];
11480
- const [customCommands, setCustomCommands] = useState10([]);
11481
- const [isSlashCommand, setIsSlashCommand] = useState10(false);
11482
- useEffect9(() => {
12997
+ const [customCommands, setCustomCommands] = useState11([]);
12998
+ const [isSlashCommand, setIsSlashCommand] = useState11(false);
12999
+ useEffect10(() => {
11483
13000
  try {
11484
13001
  const projectDir = cwd || process.cwd();
11485
13002
  const discovered = discoverCommands(projectDir);
@@ -11495,29 +13012,55 @@ var init_InputPrompt = __esm({
11495
13012
  useImperativeHandle(ref, () => ({
11496
13013
  clear: () => {
11497
13014
  setValue("");
11498
- setCursorOffset(0);
13015
+ setCursorPos([0, 0]);
11499
13016
  setShowSuggestions(false);
11500
13017
  onInputChange?.("");
11501
13018
  }
11502
13019
  }));
11503
- useEffect9(() => {
11504
- setTimeout(() => {
11505
- try {
11506
- const files = getFiles();
11507
- setAllFiles(files);
11508
- } catch (e) {
11509
- }
11510
- }, 100);
11511
- }, []);
13020
+ const { suggestions: fileSuggestions, isLoading: isLoadingFiles } = useFileCompletion(
13021
+ showSuggestions && !isSlashCommand && mentionQuery.length > 0,
13022
+ mentionQuery,
13023
+ cwd || process.cwd(),
13024
+ 20
13025
+ // maxResults
13026
+ );
13027
+ useEffect10(() => {
13028
+ if (!isSlashCommand && showSuggestions) {
13029
+ setSuggestions(fileSuggestions);
13030
+ }
13031
+ }, [fileSuggestions, isSlashCommand, showSuggestions]);
11512
13032
  const updateValue = (newValue, newCursor) => {
11513
13033
  setValue(newValue);
11514
- setCursorOffset(newCursor);
13034
+ if (typeof newCursor === "number") {
13035
+ setCursorFromOffset(newCursor, newValue);
13036
+ } else {
13037
+ setCursorPos(newCursor);
13038
+ }
13039
+ const newOffset = typeof newCursor === "number" ? newCursor : (() => {
13040
+ const lines2 = newValue.split("\n");
13041
+ let offset = 0;
13042
+ for (let i = 0; i < newCursor[0]; i++) {
13043
+ offset += lines2[i]?.length ?? 0;
13044
+ offset += 1;
13045
+ }
13046
+ offset += newCursor[1];
13047
+ return offset;
13048
+ })();
11515
13049
  checkSuggestions(newValue, newCursor);
11516
13050
  onInputChange?.(newValue);
11517
13051
  };
11518
13052
  const checkSuggestions = (text, cursor) => {
11519
- if (text.startsWith("/") && cursor <= text.length && !text.includes(" ", 1)) {
11520
- const query2 = text.slice(1);
13053
+ let cursorRow;
13054
+ let cursorCol2;
13055
+ if (typeof cursor === "number") {
13056
+ [cursorRow, cursorCol2] = offsetToCursorPos(cursor, text);
13057
+ } else {
13058
+ [cursorRow, cursorCol2] = cursor;
13059
+ }
13060
+ const lines2 = text.split("\n");
13061
+ const currentLine = lines2[cursorRow] || "";
13062
+ if (cursorRow === 0 && text.startsWith("/") && cursorCol2 <= currentLine.length && !currentLine.includes(" ", 1)) {
13063
+ const query2 = currentLine.slice(1);
11521
13064
  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);
11522
13065
  const customMatches = customCommands.filter((cmd) => cmd.name.slice(1).toLowerCase().startsWith(query2.toLowerCase())).map((cmd) => cmd.desc ? `${cmd.name} ${cmd.desc}` : cmd.name);
11523
13066
  const matches = [];
@@ -11539,25 +13082,26 @@ var init_InputPrompt = __esm({
11539
13082
  }
11540
13083
  }
11541
13084
  setIsSlashCommand(false);
11542
- const textBeforeCursor = text.slice(0, cursor);
11543
- const lastAt = textBeforeCursor.lastIndexOf("@");
11544
- if (lastAt !== -1) {
11545
- const isValidStart = lastAt === 0 || /\s/.test(textBeforeCursor[lastAt - 1]);
11546
- if (isValidStart) {
11547
- const query2 = textBeforeCursor.slice(lastAt + 1);
11548
- if (!/\s/.test(query2)) {
11549
- const matches = allFiles.filter((f) => f.toLowerCase().includes(query2.toLowerCase())).slice(0, 5);
11550
- if (matches.length > 0) {
11551
- setSuggestions(matches);
11552
- setShowSuggestions(true);
11553
- setActiveSuggestion(0);
11554
- setMentionStartIndex(lastAt);
11555
- return;
11556
- }
11557
- }
13085
+ let foundMention = false;
13086
+ for (let i = cursorCol2 - 1; i >= 0; i--) {
13087
+ const char = currentLine[i];
13088
+ if (char === " ") {
13089
+ break;
13090
+ }
13091
+ if (char === "@") {
13092
+ const query2 = currentLine.slice(i + 1, cursorCol2);
13093
+ setMentionQuery(query2);
13094
+ setShowSuggestions(true);
13095
+ setActiveSuggestion(0);
13096
+ setMentionStartIndex({ row: cursorRow, col: i });
13097
+ foundMention = true;
13098
+ break;
11558
13099
  }
11559
13100
  }
11560
- setShowSuggestions(false);
13101
+ if (!foundMention) {
13102
+ setMentionQuery("");
13103
+ setShowSuggestions(false);
13104
+ }
11561
13105
  };
11562
13106
  const completeSuggestion = (submit = false) => {
11563
13107
  if (!showSuggestions || suggestions.length === 0) return;
@@ -11566,27 +13110,35 @@ var init_InputPrompt = __esm({
11566
13110
  if (submit) {
11567
13111
  onSubmit(selectedCmd);
11568
13112
  setValue("");
11569
- setCursorOffset(0);
13113
+ setCursorPos([0, 0]);
11570
13114
  setShowSuggestions(false);
11571
13115
  onInputChange?.("");
11572
13116
  return;
11573
13117
  }
11574
- updateValue(selectedCmd, selectedCmd.length);
13118
+ updateValue(selectedCmd, [0, selectedCmd.length]);
11575
13119
  setShowSuggestions(false);
11576
13120
  return;
11577
13121
  }
11578
13122
  const selectedFile = suggestions[activeSuggestion];
11579
- const textBeforeMention = value.slice(0, mentionStartIndex);
11580
- const textAfterCursor = value.slice(cursorOffset);
11581
- const newValue = textBeforeMention + "@" + selectedFile + " " + textAfterCursor;
11582
- const newCursor = mentionStartIndex + 1 + selectedFile.length + 1;
11583
- updateValue(newValue, newCursor);
13123
+ const mentionStart = mentionStartIndex;
13124
+ const [currentRow, currentCol] = cursorPos;
13125
+ if (!mentionStart) return;
13126
+ const lines2 = value.split("\n");
13127
+ const beforeMention = lines2[mentionStart.row].slice(0, mentionStart.col);
13128
+ const afterCursor = lines2[mentionStart.row].slice(currentCol);
13129
+ const newLine = beforeMention + "@" + selectedFile + " " + afterCursor;
13130
+ const newLines = [...lines2];
13131
+ newLines[mentionStart.row] = newLine;
13132
+ const newCursorCol = mentionStart.col + 1 + selectedFile.length + 1;
13133
+ const newCursorPos = [mentionStart.row, newCursorCol];
13134
+ updateValue(newLines.join("\n"), newCursorPos);
11584
13135
  setShowSuggestions(false);
11585
13136
  };
11586
13137
  useKeypress(
11587
13138
  (key) => {
11588
13139
  if (disabled) return;
11589
13140
  const input = key.sequence;
13141
+ const cursorOffset = getCursorOffset();
11590
13142
  if (input.length > 1 && (input.includes("/") || input.includes("\\"))) {
11591
13143
  let cleanPath = input.trim();
11592
13144
  if (cleanPath.startsWith('"') && cleanPath.endsWith('"') || cleanPath.startsWith("'") && cleanPath.endsWith("'")) {
@@ -11610,7 +13162,7 @@ var init_InputPrompt = __esm({
11610
13162
  return;
11611
13163
  }
11612
13164
  }
11613
- if (key.shift && key.name === "m" && !isAgentRunning) {
13165
+ if ((key.ctrl || key.meta) && key.shift && key.name === "m" && !isAgentRunning) {
11614
13166
  setSelectedModel(getNextModel(selectedModel, isClaudeMax));
11615
13167
  return;
11616
13168
  }
@@ -11659,7 +13211,7 @@ var init_InputPrompt = __esm({
11659
13211
  if (value.trim()) {
11660
13212
  onSubmit(value.trim());
11661
13213
  setValue("");
11662
- setCursorOffset(0);
13214
+ setCursorPos([0, 0]);
11663
13215
  setShowSuggestions(false);
11664
13216
  onInputChange?.("");
11665
13217
  }
@@ -11670,11 +13222,55 @@ var init_InputPrompt = __esm({
11670
13222
  updateValue(newValue, cursorOffset - 1);
11671
13223
  }
11672
13224
  } else if (key.name === "left") {
11673
- setCursorOffset(Math.max(0, cursorOffset - 1));
13225
+ if (key.meta && !key.shift && !key.ctrl) {
13226
+ const textBefore = value.slice(0, cursorOffset);
13227
+ const match = textBefore.match(/\S+\s*$/);
13228
+ const wordStart = match ? textBefore.length - match[0].length : 0;
13229
+ setCursorFromOffset(wordStart);
13230
+ } else {
13231
+ setCursorFromOffset(Math.max(0, cursorOffset - 1));
13232
+ }
11674
13233
  } else if (key.name === "right") {
11675
- setCursorOffset(Math.min(value.length, cursorOffset + 1));
11676
- } else if (key.ctrl && input === "u") {
11677
- updateValue("", 0);
13234
+ if (key.meta && !key.shift && !key.ctrl) {
13235
+ const textAfter = value.slice(cursorOffset);
13236
+ const match = textAfter.match(/^\s*\S+/);
13237
+ const wordEnd = match ? cursorOffset + match[0].length : value.length;
13238
+ setCursorFromOffset(wordEnd);
13239
+ } else {
13240
+ setCursorFromOffset(Math.min(value.length, cursorOffset + 1));
13241
+ }
13242
+ } else if (key.ctrl && key.name === "u") {
13243
+ const newValue = value.slice(cursorOffset);
13244
+ updateValue(newValue, 0);
13245
+ } else if (key.ctrl && key.name === "k") {
13246
+ const newValue = value.slice(0, cursorOffset);
13247
+ updateValue(newValue, cursorOffset);
13248
+ } else if (key.ctrl && key.name === "w") {
13249
+ const textBefore = value.slice(0, cursorOffset);
13250
+ const match = textBefore.match(/\S+\s*$/);
13251
+ const wordStart = match ? textBefore.length - match[0].length : 0;
13252
+ const newValue = value.slice(0, wordStart) + value.slice(cursorOffset);
13253
+ updateValue(newValue, wordStart);
13254
+ } else if (key.meta && key.name === "backspace") {
13255
+ const textBefore = value.slice(0, cursorOffset);
13256
+ const match = textBefore.match(/\S+\s*$/);
13257
+ const wordStart = match ? textBefore.length - match[0].length : 0;
13258
+ const newValue = value.slice(0, wordStart) + value.slice(cursorOffset);
13259
+ updateValue(newValue, wordStart);
13260
+ } else if (key.meta && key.name === "d") {
13261
+ const textAfter = value.slice(cursorOffset);
13262
+ const match = textAfter.match(/^\s*\S+/);
13263
+ const wordEnd = match ? cursorOffset + match[0].length : value.length;
13264
+ const newValue = value.slice(0, cursorOffset) + value.slice(wordEnd);
13265
+ updateValue(newValue, cursorOffset);
13266
+ } else if (key.ctrl && key.name === "a") {
13267
+ setCursorFromOffset(0);
13268
+ } else if (key.ctrl && key.name === "e") {
13269
+ setCursorFromOffset(value.length);
13270
+ } else if (key.name === "home") {
13271
+ setCursorFromOffset(0);
13272
+ } else if (key.name === "end") {
13273
+ setCursorFromOffset(value.length);
11678
13274
  } else if (key.name === "tab" && !showSuggestions) {
11679
13275
  } else if (key.paste) {
11680
13276
  const newValue = value.slice(0, cursorOffset) + input + value.slice(cursorOffset);
@@ -11688,18 +13284,7 @@ var init_InputPrompt = __esm({
11688
13284
  );
11689
13285
  const lines = value ? value.split("\n") : [];
11690
13286
  const hasContent = value.trim().length > 0;
11691
- let cursorLine = 0;
11692
- let cursorCol = cursorOffset;
11693
- let charCount = 0;
11694
- for (let i = 0; i < lines.length; i++) {
11695
- const lineLength = lines[i].length;
11696
- if (charCount + lineLength >= cursorOffset) {
11697
- cursorLine = i;
11698
- cursorCol = cursorOffset - charCount;
11699
- break;
11700
- }
11701
- charCount += lineLength + 1;
11702
- }
13287
+ const [cursorLine, cursorCol] = cursorPos;
11703
13288
  return /* @__PURE__ */ React24.createElement(Box21, { flexDirection: "column", width: "100%" }, showSuggestions && /* @__PURE__ */ React24.createElement(
11704
13289
  Box21,
11705
13290
  {
@@ -11709,13 +13294,13 @@ var init_InputPrompt = __esm({
11709
13294
  marginBottom: 0,
11710
13295
  paddingX: 1
11711
13296
  },
11712
- suggestions.map((item, idx) => {
13297
+ isLoadingFiles && !isSlashCommand ? /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "Searching files...") : suggestions.length > 0 ? suggestions.map((item, idx) => {
11713
13298
  const isSeparator = item.startsWith("\u2500\u2500\u2500\u2500\u2500");
11714
13299
  if (isSeparator) {
11715
13300
  return /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim, key: item }, " ", item);
11716
13301
  }
11717
13302
  return /* @__PURE__ */ React24.createElement(Text19, { color: idx === activeSuggestion ? theme.text.accent : theme.text.dim, key: item }, idx === activeSuggestion ? "\u276F " : " ", item);
11718
- })
13303
+ }) : /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "No matches")
11719
13304
  ), /* @__PURE__ */ React24.createElement(
11720
13305
  Box21,
11721
13306
  {
@@ -11736,7 +13321,7 @@ var init_InputPrompt = __esm({
11736
13321
  }
11737
13322
  return /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.primary, key: idx }, line);
11738
13323
  })), !hasContent && disabled && /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim, italic: true }, "Waiting for agent to complete...")))
11739
- ), /* @__PURE__ */ React24.createElement(Box21, { justifyContent: "space-between", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Box21, { gap: 2 }, /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "\u23F8 plan" : "\u25B6 build"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (shift+tab)")), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "model:"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelDisplayName(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (Cost: "), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelCostLabel(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, ") (shift+m)"))), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: usageStats && usageStats.contextPct >= 90 ? theme.text.error : usageStats && usageStats.contextPct >= 75 ? theme.text.warning : theme.text.dim }, usageStats?.contextPct ?? 0, "% context used"), /* @__PURE__ */ React24.createElement(Text19, { 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", ")"))));
13324
+ ), /* @__PURE__ */ React24.createElement(Box21, { justifyContent: "space-between", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Box21, { gap: 2 }, /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "\u23F8 plan" : "\u25B6 build"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (shift+tab)")), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "model:"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelDisplayName(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (Cost: "), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelCostLabel(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, ") (ctrl+shift+m)"))), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: usageStats && usageStats.contextPct >= 90 ? theme.text.error : usageStats && usageStats.contextPct >= 75 ? theme.text.warning : theme.text.dim }, usageStats?.contextPct ?? 0, "% context used"), /* @__PURE__ */ React24.createElement(Text19, { 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", ")"))));
11740
13325
  });
11741
13326
  InputPromptInner.displayName = "InputPromptInner";
11742
13327
  InputPrompt = memo3(InputPromptInner);
@@ -11746,19 +13331,19 @@ var init_InputPrompt = __esm({
11746
13331
 
11747
13332
  // src/ui/components/McpAddDialog.tsx
11748
13333
  import { Box as Box22, Text as Text20, useInput as useInput6 } from "ink";
11749
- import React25, { useState as useState11 } from "react";
13334
+ import React25, { useState as useState12 } from "react";
11750
13335
  var McpAddDialog;
11751
13336
  var init_McpAddDialog = __esm({
11752
13337
  "src/ui/components/McpAddDialog.tsx"() {
11753
13338
  "use strict";
11754
13339
  init_theme();
11755
13340
  McpAddDialog = ({ onConfirm, onCancel }) => {
11756
- const [mode, setMode] = useState11("name" /* Name */);
11757
- const [serverName, setServerName] = useState11("");
11758
- const [command, setCommand] = useState11("");
11759
- const [args, setArgs] = useState11("");
11760
- const [description, setDescription] = useState11("");
11761
- const [scope, setScope] = useState11("project");
13341
+ const [mode, setMode] = useState12("name" /* Name */);
13342
+ const [serverName, setServerName] = useState12("");
13343
+ const [command, setCommand] = useState12("");
13344
+ const [args, setArgs] = useState12("");
13345
+ const [description, setDescription] = useState12("");
13346
+ const [scope, setScope] = useState12("project");
11762
13347
  useInput6((input, key) => {
11763
13348
  if (key.escape) {
11764
13349
  onCancel();
@@ -11853,7 +13438,7 @@ var init_McpAddDialog = __esm({
11853
13438
 
11854
13439
  // src/ui/components/McpServerSelector.tsx
11855
13440
  import { Box as Box23, Text as Text21, useInput as useInput7 } from "ink";
11856
- import React26, { useState as useState12 } from "react";
13441
+ import React26, { useState as useState13 } from "react";
11857
13442
  var McpServerSelector;
11858
13443
  var init_McpServerSelector = __esm({
11859
13444
  "src/ui/components/McpServerSelector.tsx"() {
@@ -11865,7 +13450,7 @@ var init_McpServerSelector = __esm({
11865
13450
  onSelect,
11866
13451
  onCancel
11867
13452
  }) => {
11868
- const [selectedIndex, setSelectedIndex] = useState12(0);
13453
+ const [selectedIndex, setSelectedIndex] = useState13(0);
11869
13454
  useInput7((input, key) => {
11870
13455
  if (key.escape) {
11871
13456
  onCancel();
@@ -11924,7 +13509,7 @@ var init_McpServerSelector = __esm({
11924
13509
  // src/ui/components/McpServersDisplay.tsx
11925
13510
  import { Box as Box24, Text as Text22, useInput as useInput8 } from "ink";
11926
13511
  import Spinner3 from "ink-spinner";
11927
- import React27, { useEffect as useEffect10, useState as useState13 } from "react";
13512
+ import React27, { useEffect as useEffect11, useState as useState14 } from "react";
11928
13513
  var McpServersDisplay;
11929
13514
  var init_McpServersDisplay = __esm({
11930
13515
  "src/ui/components/McpServersDisplay.tsx"() {
@@ -11938,9 +13523,9 @@ var init_McpServersDisplay = __esm({
11938
13523
  onTest,
11939
13524
  cwd
11940
13525
  }) => {
11941
- const [servers, setServers] = useState13([]);
11942
- const [isTestingAll, setIsTestingAll] = useState13(false);
11943
- useEffect10(() => {
13526
+ const [servers, setServers] = useState14([]);
13527
+ const [isTestingAll, setIsTestingAll] = useState14(false);
13528
+ useEffect11(() => {
11944
13529
  const projectDir = cwd || process.cwd();
11945
13530
  const loadedServers = loadMcpConfig(projectDir);
11946
13531
  const serversArray = Object.values(loadedServers).map((server) => ({
@@ -12010,7 +13595,7 @@ var init_McpServersDisplay = __esm({
12010
13595
 
12011
13596
  // src/ui/components/ProviderSelector.tsx
12012
13597
  import { Box as Box25, Text as Text23, useInput as useInput9 } from "ink";
12013
- import React28, { useState as useState14 } from "react";
13598
+ import React28, { useState as useState15 } from "react";
12014
13599
  var PROVIDERS, ProviderSelector;
12015
13600
  var init_ProviderSelector = __esm({
12016
13601
  "src/ui/components/ProviderSelector.tsx"() {
@@ -12025,7 +13610,7 @@ var init_ProviderSelector = __esm({
12025
13610
  {
12026
13611
  id: "claude-max",
12027
13612
  name: "Claude Max",
12028
- description: "Direct Claude Max subscription (requires Claude Code login)"
13613
+ description: "Direct Claude Max subscription (requires OAuth login)"
12029
13614
  }
12030
13615
  ];
12031
13616
  ProviderSelector = ({
@@ -12040,7 +13625,7 @@ var init_ProviderSelector = __esm({
12040
13625
  const currentIndex = availableProviders.findIndex(
12041
13626
  (p) => p.id === currentProvider
12042
13627
  );
12043
- const [selectedIndex, setSelectedIndex] = useState14(
13628
+ const [selectedIndex, setSelectedIndex] = useState15(
12044
13629
  currentIndex >= 0 ? currentIndex : 0
12045
13630
  );
12046
13631
  useInput9((input, key) => {
@@ -12080,9 +13665,125 @@ var init_ProviderSelector = __esm({
12080
13665
  }
12081
13666
  });
12082
13667
 
12083
- // src/ui/components/SessionSelector.tsx
13668
+ // src/ui/components/QuestionSelector.tsx
12084
13669
  import { Box as Box26, Text as Text24, useInput as useInput10 } from "ink";
12085
- import React29, { useEffect as useEffect11, useState as useState15 } from "react";
13670
+ import React29, { useState as useState16 } from "react";
13671
+ var QuestionSelector;
13672
+ var init_QuestionSelector = __esm({
13673
+ "src/ui/components/QuestionSelector.tsx"() {
13674
+ "use strict";
13675
+ init_theme();
13676
+ QuestionSelector = ({
13677
+ question,
13678
+ options,
13679
+ multiSelect = false,
13680
+ allowCustomAnswer = true,
13681
+ // Default to true - always show "Other" option
13682
+ onSubmit,
13683
+ onCancel
13684
+ }) => {
13685
+ const hasOptions = options && options.length > 0;
13686
+ const allOptions = hasOptions && allowCustomAnswer ? [...options, { label: "Other", description: "Provide a custom response" }] : options;
13687
+ const [selectedIndex, setSelectedIndex] = useState16(0);
13688
+ const [selectedItems, setSelectedItems] = useState16(/* @__PURE__ */ new Set());
13689
+ const [customInputMode, setCustomInputMode] = useState16(!hasOptions);
13690
+ const [customAnswer, setCustomAnswer] = useState16("");
13691
+ useInput10((input, key) => {
13692
+ if (customInputMode) {
13693
+ if (key.return) {
13694
+ if (customAnswer.trim()) {
13695
+ onSubmit([customAnswer.trim()]);
13696
+ }
13697
+ } else if (key.escape) {
13698
+ if (!hasOptions) {
13699
+ onCancel();
13700
+ } else {
13701
+ setCustomInputMode(false);
13702
+ setCustomAnswer("");
13703
+ }
13704
+ } else if (key.backspace || key.delete) {
13705
+ setCustomAnswer((prev) => prev.slice(0, -1));
13706
+ } else if (input && !key.ctrl && !key.meta) {
13707
+ setCustomAnswer((prev) => prev + input);
13708
+ }
13709
+ return;
13710
+ }
13711
+ if (key.upArrow) {
13712
+ setSelectedIndex((prev) => prev > 0 ? prev - 1 : allOptions.length - 1);
13713
+ } else if (key.downArrow) {
13714
+ setSelectedIndex((prev) => prev < allOptions.length - 1 ? prev + 1 : 0);
13715
+ } else if (key.return) {
13716
+ const selectedOption = allOptions[selectedIndex];
13717
+ if (selectedOption.label === "Other" && allowCustomAnswer) {
13718
+ setCustomInputMode(true);
13719
+ return;
13720
+ }
13721
+ if (multiSelect) {
13722
+ setSelectedItems((prev) => {
13723
+ const newSet = new Set(prev);
13724
+ if (newSet.has(selectedIndex)) {
13725
+ newSet.delete(selectedIndex);
13726
+ } else {
13727
+ newSet.add(selectedIndex);
13728
+ }
13729
+ return newSet;
13730
+ });
13731
+ } else {
13732
+ onSubmit([selectedOption.label]);
13733
+ }
13734
+ } else if (input === " " && multiSelect) {
13735
+ setSelectedItems((prev) => {
13736
+ const newSet = new Set(prev);
13737
+ if (newSet.has(selectedIndex)) {
13738
+ newSet.delete(selectedIndex);
13739
+ } else {
13740
+ newSet.add(selectedIndex);
13741
+ }
13742
+ return newSet;
13743
+ });
13744
+ } else if (key.escape || input === "q") {
13745
+ onCancel();
13746
+ } else if (input === "s" && multiSelect && selectedItems.size > 0) {
13747
+ const answers = Array.from(selectedItems).sort((a, b) => a - b).map((idx) => allOptions[idx].label);
13748
+ onSubmit(answers);
13749
+ } else if ((input === "o" || input === "O") && allowCustomAnswer) {
13750
+ setCustomInputMode(true);
13751
+ }
13752
+ });
13753
+ if (customInputMode) {
13754
+ return /* @__PURE__ */ React29.createElement(Box26, { borderColor: theme.border.accent, borderStyle: "round", flexDirection: "column", paddingX: 2, paddingY: 1 }, /* @__PURE__ */ React29.createElement(Box26, { marginBottom: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: theme.text.accent }, question)), /* @__PURE__ */ React29.createElement(Box26, { marginBottom: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, hasOptions ? "Enter your custom answer:" : "Type your answer:")), /* @__PURE__ */ React29.createElement(Box26, { marginBottom: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.primary }, ">", " ", customAnswer, /* @__PURE__ */ React29.createElement(Text24, { backgroundColor: theme.text.accent }, " "))), /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "Enter"), " submit \u2022 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "ESC"), " ", hasOptions ? "back to options" : "cancel")));
13755
+ }
13756
+ return /* @__PURE__ */ React29.createElement(Box26, { borderColor: theme.border.accent, borderStyle: "round", flexDirection: "column", paddingX: 2, paddingY: 1 }, /* @__PURE__ */ React29.createElement(Box26, { marginBottom: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: theme.text.accent }, question)), multiSelect && /* @__PURE__ */ React29.createElement(Box26, { marginBottom: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, "(Select multiple options with ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "Space"), ", submit with ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "S"), ")")), /* @__PURE__ */ React29.createElement(Box26, { flexDirection: "column", marginBottom: 1 }, allOptions.map((option, index) => {
13757
+ const isSelected = index === selectedIndex;
13758
+ const isChosen = selectedItems.has(index);
13759
+ const indicator = isSelected ? "\u25B6 " : " ";
13760
+ const checkbox = multiSelect ? isChosen ? "[\u2713] " : "[ ] " : "";
13761
+ return /* @__PURE__ */ React29.createElement(Box26, { flexDirection: "column", gap: 0, key: `${option.label}-${index}` }, /* @__PURE__ */ React29.createElement(Box26, null, /* @__PURE__ */ React29.createElement(
13762
+ Text24,
13763
+ {
13764
+ backgroundColor: isSelected ? theme.text.accent : void 0,
13765
+ bold: isSelected,
13766
+ color: isSelected ? "black" : theme.text.primary
13767
+ },
13768
+ indicator,
13769
+ checkbox,
13770
+ option.label
13771
+ )), /* @__PURE__ */ React29.createElement(Box26, { marginLeft: multiSelect ? 6 : 4 }, /* @__PURE__ */ React29.createElement(
13772
+ Text24,
13773
+ {
13774
+ color: isSelected ? theme.text.primary : theme.text.dim,
13775
+ dimColor: !isSelected
13776
+ },
13777
+ option.description
13778
+ )));
13779
+ })), /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "\u2191\u2193"), " navigate \u2022 ", multiSelect ? /* @__PURE__ */ React29.createElement(React29.Fragment, null, /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "Space"), " toggle \u2022 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "S"), " submit \u2022", " ") : /* @__PURE__ */ React29.createElement(React29.Fragment, null, /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "Enter"), " select \u2022", " "), /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "ESC"), " cancel")));
13780
+ };
13781
+ }
13782
+ });
13783
+
13784
+ // src/ui/components/SessionSelector.tsx
13785
+ import { Box as Box27, Text as Text25, useInput as useInput11 } from "ink";
13786
+ import React30, { useEffect as useEffect12, useState as useState17 } from "react";
12086
13787
  function getSessionPrefix(authMethod) {
12087
13788
  return authMethod === "api-key" ? "[Team]" : "[Me]";
12088
13789
  }
@@ -12097,13 +13798,13 @@ var init_SessionSelector = __esm({
12097
13798
  onSelect,
12098
13799
  onCancel
12099
13800
  }) => {
12100
- const [allSessions, setAllSessions] = useState15([]);
12101
- const [selectedIndex, setSelectedIndex] = useState15(0);
12102
- const [isLoading, setIsLoading] = useState15(false);
12103
- const [hasMore, setHasMore] = useState15(true);
12104
- const [totalSessions, setTotalSessions] = useState15(0);
12105
- const [error, setError] = useState15(null);
12106
- useEffect11(() => {
13801
+ const [allSessions, setAllSessions] = useState17([]);
13802
+ const [selectedIndex, setSelectedIndex] = useState17(0);
13803
+ const [isLoading, setIsLoading] = useState17(false);
13804
+ const [hasMore, setHasMore] = useState17(true);
13805
+ const [totalSessions, setTotalSessions] = useState17(0);
13806
+ const [error, setError] = useState17(null);
13807
+ useEffect12(() => {
12107
13808
  loadMoreSessions();
12108
13809
  }, []);
12109
13810
  const loadMoreSessions = async () => {
@@ -12129,7 +13830,7 @@ var init_SessionSelector = __esm({
12129
13830
  setIsLoading(false);
12130
13831
  }
12131
13832
  };
12132
- useInput10((input, key) => {
13833
+ useInput11((input, key) => {
12133
13834
  if (allSessions.length === 0) {
12134
13835
  if (key.escape || input === "q") {
12135
13836
  onCancel();
@@ -12153,13 +13854,13 @@ var init_SessionSelector = __esm({
12153
13854
  }
12154
13855
  });
12155
13856
  if (error) {
12156
- return /* @__PURE__ */ React29.createElement(Box26, { borderColor: "red", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "red" }, "Error Loading Sessions"), /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, error), /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, "Press ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "ESC"), " to cancel")));
13857
+ return /* @__PURE__ */ React30.createElement(Box27, { borderColor: "red", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React30.createElement(Text25, { bold: true, color: "red" }, "Error Loading Sessions"), /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.dim }, error), /* @__PURE__ */ React30.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.dim }, "Press ", /* @__PURE__ */ React30.createElement(Text25, { bold: true }, "ESC"), " to cancel")));
12157
13858
  }
12158
13859
  if (allSessions.length === 0 && isLoading) {
12159
- return /* @__PURE__ */ React29.createElement(Box26, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "cyan" }, "Loading Sessions..."), /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, "Fetching your sessions from the server"));
13860
+ return /* @__PURE__ */ React30.createElement(Box27, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React30.createElement(Text25, { bold: true, color: "cyan" }, "Loading Sessions..."), /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.dim }, "Fetching your sessions from the server"));
12160
13861
  }
12161
13862
  if (allSessions.length === 0 && !isLoading) {
12162
- return /* @__PURE__ */ React29.createElement(Box26, { borderColor: "yellow", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "yellow" }, "No Sessions Found"), /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, "No previous sessions available. Start a new conversation!"), /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, "Press ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "ESC"), " to cancel")));
13863
+ return /* @__PURE__ */ React30.createElement(Box27, { borderColor: "yellow", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React30.createElement(Text25, { bold: true, color: "yellow" }, "No Sessions Found"), /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.dim }, "No previous sessions available. Start a new session!"), /* @__PURE__ */ React30.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.dim }, "Press ", /* @__PURE__ */ React30.createElement(Text25, { bold: true }, "ESC"), " to cancel")));
12163
13864
  }
12164
13865
  const VISIBLE_ITEMS3 = 10;
12165
13866
  let startIndex;
@@ -12179,7 +13880,7 @@ var init_SessionSelector = __esm({
12179
13880
  const visibleSessions = allSessions.slice(startIndex, endIndex);
12180
13881
  const MAX_TITLE_WIDTH = 50;
12181
13882
  const PREFIX_WIDTH = 6;
12182
- return /* @__PURE__ */ React29.createElement(Box26, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React29.createElement(Box26, { marginBottom: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "cyan" }, "Select a Session to Resume")), /* @__PURE__ */ React29.createElement(Box26, { flexDirection: "column" }, visibleSessions.map((item, index) => {
13883
+ return /* @__PURE__ */ React30.createElement(Box27, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React30.createElement(Box27, { marginBottom: 1 }, /* @__PURE__ */ React30.createElement(Text25, { bold: true, color: "cyan" }, "Select a Session to Resume")), /* @__PURE__ */ React30.createElement(Box27, { flexDirection: "column" }, visibleSessions.map((item, index) => {
12183
13884
  const actualIndex = startIndex + index;
12184
13885
  const isSelected = actualIndex === selectedIndex;
12185
13886
  const title = item.session.title || "Untitled session";
@@ -12203,18 +13904,18 @@ var init_SessionSelector = __esm({
12203
13904
  const prefixColor = item.prefix === "[Me]" ? "cyan" : "yellow";
12204
13905
  const indicator = isSelected ? "\u25B6 " : " ";
12205
13906
  const bgColor = isSelected ? theme.text.accent : void 0;
12206
- return /* @__PURE__ */ React29.createElement(Box26, { key: item.session.id, width: "100%" }, /* @__PURE__ */ React29.createElement(Text24, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, indicator), /* @__PURE__ */ React29.createElement(Text24, { backgroundColor: bgColor, bold: isSelected, color: prefixColor }, prefix), /* @__PURE__ */ React29.createElement(Text24, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, displayTitle), /* @__PURE__ */ React29.createElement(Text24, { backgroundColor: bgColor, color: theme.text.dim }, "(", dateStr, ")"));
12207
- })), /* @__PURE__ */ React29.createElement(Box26, { flexDirection: "column", marginTop: 1 }, (allSessions.length > VISIBLE_ITEMS3 || totalSessions > allSessions.length) && /* @__PURE__ */ React29.createElement(Box26, { marginBottom: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: "yellow" }, "Showing ", startIndex + 1, "-", endIndex, " of ", totalSessions || allSessions.length, " sessions", hasMore && !isLoading && /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, " \u2022 Scroll for more"))), /* @__PURE__ */ React29.createElement(Box26, null, /* @__PURE__ */ React29.createElement(Text24, { color: theme.text.dim }, "Use ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "\u2191\u2193"), " to navigate \u2022 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "Enter"), " to select \u2022 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "ESC"), " to cancel")), isLoading && /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: "cyan" }, "Loading more sessions..."))));
13907
+ return /* @__PURE__ */ React30.createElement(Box27, { key: item.session.id, width: "100%" }, /* @__PURE__ */ React30.createElement(Text25, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, indicator), /* @__PURE__ */ React30.createElement(Text25, { backgroundColor: bgColor, bold: isSelected, color: prefixColor }, prefix), /* @__PURE__ */ React30.createElement(Text25, { backgroundColor: bgColor, bold: isSelected, color: isSelected ? "black" : theme.text.primary }, displayTitle), /* @__PURE__ */ React30.createElement(Text25, { backgroundColor: bgColor, color: theme.text.dim }, "(", dateStr, ")"));
13908
+ })), /* @__PURE__ */ React30.createElement(Box27, { flexDirection: "column", marginTop: 1 }, (allSessions.length > VISIBLE_ITEMS3 || totalSessions > allSessions.length) && /* @__PURE__ */ React30.createElement(Box27, { marginBottom: 1 }, /* @__PURE__ */ React30.createElement(Text25, { color: "yellow" }, "Showing ", startIndex + 1, "-", endIndex, " of ", totalSessions || allSessions.length, " sessions", hasMore && !isLoading && /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.dim }, " \u2022 Scroll for more"))), /* @__PURE__ */ React30.createElement(Box27, null, /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.dim }, "Use ", /* @__PURE__ */ React30.createElement(Text25, { bold: true }, "\u2191\u2193"), " to navigate \u2022 ", /* @__PURE__ */ React30.createElement(Text25, { bold: true }, "Enter"), " to select \u2022 ", /* @__PURE__ */ React30.createElement(Text25, { bold: true }, "ESC"), " to cancel")), isLoading && /* @__PURE__ */ React30.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React30.createElement(Text25, { color: "cyan" }, "Loading more sessions..."))));
12208
13909
  };
12209
13910
  }
12210
13911
  });
12211
13912
 
12212
13913
  // src/ui/hooks/useModeToggle.ts
12213
- import { useEffect as useEffect12 } from "react";
13914
+ import { useEffect as useEffect13 } from "react";
12214
13915
  function useModeToggle() {
12215
13916
  const { subscribe, unsubscribe } = useKeypressContext();
12216
13917
  const { agentMode, setAgentMode, isAgentRunning } = useSession();
12217
- useEffect12(() => {
13918
+ useEffect13(() => {
12218
13919
  const handleKeypress = (key) => {
12219
13920
  if (key.name === "tab" && key.shift && !isAgentRunning) {
12220
13921
  const newMode = agentMode === "plan" ? "build" : "plan";
@@ -12235,13 +13936,13 @@ var init_useModeToggle = __esm({
12235
13936
  });
12236
13937
 
12237
13938
  // src/ui/hooks/useOverlayEscapeGuard.ts
12238
- import { useCallback as useCallback4, useMemo as useMemo4, useRef as useRef6 } from "react";
13939
+ import { useCallback as useCallback4, useMemo as useMemo4, useRef as useRef7 } from "react";
12239
13940
  var useOverlayEscapeGuard;
12240
13941
  var init_useOverlayEscapeGuard = __esm({
12241
13942
  "src/ui/hooks/useOverlayEscapeGuard.ts"() {
12242
13943
  "use strict";
12243
13944
  useOverlayEscapeGuard = ({ overlays, suppressionMs = 250 }) => {
12244
- const suppressUntilRef = useRef6(0);
13945
+ const suppressUntilRef = useRef7(0);
12245
13946
  const markOverlayClosed = useCallback4(() => {
12246
13947
  suppressUntilRef.current = Date.now() + suppressionMs;
12247
13948
  }, [suppressionMs]);
@@ -12253,11 +13954,11 @@ var init_useOverlayEscapeGuard = __esm({
12253
13954
  });
12254
13955
 
12255
13956
  // src/ui/App.tsx
12256
- import { execSync as execSync6 } from "child_process";
13957
+ import { execSync as execSync5 } from "child_process";
12257
13958
  import { homedir as homedir8 } from "os";
12258
- import { Box as Box27, Text as Text25, useApp as useApp2, useStdout as useStdout2 } from "ink";
13959
+ import { Box as Box28, Text as Text26, useApp as useApp2, useStdout as useStdout2 } from "ink";
12259
13960
  import Spinner4 from "ink-spinner";
12260
- import React30, { useCallback as useCallback5, useEffect as useEffect13, useRef as useRef7, useState as useState16 } from "react";
13961
+ import React31, { useCallback as useCallback5, useEffect as useEffect14, useRef as useRef8, useState as useState18 } from "react";
12261
13962
  var getGitBranch2, getCurrentFolder2, AppContent, App;
12262
13963
  var init_App = __esm({
12263
13964
  "src/ui/App.tsx"() {
@@ -12284,6 +13985,7 @@ var init_App = __esm({
12284
13985
  init_MessageList();
12285
13986
  init_ModelSelector();
12286
13987
  init_ProviderSelector();
13988
+ init_QuestionSelector();
12287
13989
  init_SessionSelector();
12288
13990
  init_SessionContext();
12289
13991
  init_useKeypress();
@@ -12293,7 +13995,7 @@ var init_App = __esm({
12293
13995
  init_theme();
12294
13996
  getGitBranch2 = () => {
12295
13997
  try {
12296
- return execSync6("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
13998
+ return execSync5("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
12297
13999
  } catch {
12298
14000
  return "";
12299
14001
  }
@@ -12306,36 +14008,37 @@ var init_App = __esm({
12306
14008
  }
12307
14009
  return cwd;
12308
14010
  };
12309
- AppContent = ({ config: config2, sessionId, webUrl, queuedTasks = [], onExit, onSubmitTask, apiClient, onResumeSession, onClearSession }) => {
14011
+ AppContent = ({ config: config2, sessionId, webUrl, queuedTasks = [], onExit, onSubmitTask, apiClient, onResumeSession, onClearSession, onQuestionAnswer }) => {
12310
14012
  const { exit } = useApp2();
12311
14013
  const { stdout } = useStdout2();
12312
- const { addMessage, clearMessages, isAgentRunning, setSessionId, setWebUrl, setShouldInterruptAgent, setIsAgentRunning, toggleAllToolOutputs, allToolsExpanded, selectedModel, setSelectedModel, refreshStatic, toggleToolGroups, llmProvider, setLlmProvider } = useSession();
14014
+ const { addMessage, clearMessages, isAgentRunning, setSessionId, setWebUrl, setShouldInterruptAgent, setIsAgentRunning, toggleAllToolOutputs, allToolsExpanded, selectedModel, setSelectedModel, refreshStatic, toggleToolGroups, llmProvider, setLlmProvider, pendingQuestion, setPendingQuestion } = useSession();
12313
14015
  useModeToggle();
12314
- const [terminalWidth, setTerminalWidth] = useState16(process.stdout.columns || 80);
12315
- const [showInput, setShowInput] = useState16(true);
12316
- const [gitBranch] = useState16(() => getGitBranch2());
12317
- const [currentFolder] = useState16(() => getCurrentFolder2(config2.cwd));
12318
- const hasInputContentRef = useRef7(false);
12319
- const [exitWarning, setExitWarning] = useState16(null);
12320
- const inputPromptRef = useRef7(null);
12321
- const [showSessionSelector, setShowSessionSelector] = useState16(false);
12322
- const [showModelSelector, setShowModelSelector] = useState16(false);
12323
- const [showProviderSelector, setShowProviderSelector] = useState16(false);
12324
- const [showFeedbackDialog, setShowFeedbackDialog] = useState16(false);
12325
- const [isLoadingSession, setIsLoadingSession] = useState16(false);
12326
- const [showFixFlow, setShowFixFlow] = useState16(false);
12327
- const [fixRunId, setFixRunId] = useState16(void 0);
12328
- const [showMcpServers, setShowMcpServers] = useState16(false);
12329
- const [showMcpAdd, setShowMcpAdd] = useState16(false);
12330
- const [showMcpSelector, setShowMcpSelector] = useState16(false);
12331
- const [mcpSelectorAction, setMcpSelectorAction] = useState16(
14016
+ const [terminalWidth, setTerminalWidth] = useState18(process.stdout.columns || 80);
14017
+ const [showInput, setShowInput] = useState18(true);
14018
+ const [gitBranch] = useState18(() => getGitBranch2());
14019
+ const [currentFolder] = useState18(() => getCurrentFolder2(config2.cwd));
14020
+ const hasInputContentRef = useRef8(false);
14021
+ const [exitWarning, setExitWarning] = useState18(null);
14022
+ const inputPromptRef = useRef8(null);
14023
+ const [showSessionSelector, setShowSessionSelector] = useState18(false);
14024
+ const [showModelSelector, setShowModelSelector] = useState18(false);
14025
+ const [showProviderSelector, setShowProviderSelector] = useState18(false);
14026
+ const [claudeMaxAvailable, setClaudeMaxAvailable] = useState18(false);
14027
+ const [showFeedbackDialog, setShowFeedbackDialog] = useState18(false);
14028
+ const [isLoadingSession, setIsLoadingSession] = useState18(false);
14029
+ const [showFixFlow, setShowFixFlow] = useState18(false);
14030
+ const [fixRunId, setFixRunId] = useState18(void 0);
14031
+ const [showMcpServers, setShowMcpServers] = useState18(false);
14032
+ const [showMcpAdd, setShowMcpAdd] = useState18(false);
14033
+ const [showMcpSelector, setShowMcpSelector] = useState18(false);
14034
+ const [mcpSelectorAction, setMcpSelectorAction] = useState18(
12332
14035
  "remove"
12333
14036
  );
12334
- const [mcpServers, setMcpServers] = useState16([]);
12335
- const [authState, setAuthState] = useState16(
14037
+ const [mcpServers, setMcpServers] = useState18([]);
14038
+ const [authState, setAuthState] = useState18(
12336
14039
  () => config2.supatestApiKey ? "authenticated" /* Authenticated */ : "unauthenticated" /* Unauthenticated */
12337
14040
  );
12338
- const [showAuthDialog, setShowAuthDialog] = useState16(false);
14041
+ const [showAuthDialog, setShowAuthDialog] = useState18(false);
12339
14042
  const { isOverlayOpen, isCancelSuppressed, markOverlayClosed } = useOverlayEscapeGuard({
12340
14043
  overlays: [
12341
14044
  showSessionSelector,
@@ -12346,15 +14049,16 @@ var init_App = __esm({
12346
14049
  showFixFlow,
12347
14050
  showMcpServers,
12348
14051
  showMcpAdd,
12349
- showMcpSelector
14052
+ showMcpSelector,
14053
+ !!pendingQuestion
12350
14054
  ]
12351
14055
  });
12352
- useEffect13(() => {
14056
+ useEffect14(() => {
12353
14057
  if (!config2.supatestApiKey) {
12354
14058
  setShowAuthDialog(true);
12355
14059
  }
12356
14060
  }, [config2.supatestApiKey]);
12357
- useEffect13(() => {
14061
+ useEffect14(() => {
12358
14062
  if (sessionId) {
12359
14063
  setSessionId(sessionId);
12360
14064
  }
@@ -12369,6 +14073,7 @@ var init_App = __esm({
12369
14073
  type: "assistant",
12370
14074
  content: "Opening browser for authentication..."
12371
14075
  });
14076
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
12372
14077
  try {
12373
14078
  const result = await loginCommand();
12374
14079
  saveToken(result.token, result.expiresAt);
@@ -12463,6 +14168,7 @@ var init_App = __esm({
12463
14168
  return;
12464
14169
  }
12465
14170
  if (command === "/provider") {
14171
+ isClaudeMaxAvailable().then(setClaudeMaxAvailable);
12466
14172
  setShowProviderSelector(true);
12467
14173
  return;
12468
14174
  }
@@ -12515,6 +14221,7 @@ var init_App = __esm({
12515
14221
  type: "assistant",
12516
14222
  content: "Running setup..."
12517
14223
  });
14224
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
12518
14225
  try {
12519
14226
  const result = await setupCommand({ cwd: config2.cwd || process.cwd() });
12520
14227
  addMessage({
@@ -12608,9 +14315,45 @@ var init_App = __esm({
12608
14315
  markOverlayClosed();
12609
14316
  setShowModelSelector(false);
12610
14317
  };
12611
- const handleProviderSelect = (provider) => {
14318
+ const handleProviderSelect = async (provider) => {
12612
14319
  setShowProviderSelector(false);
12613
14320
  markOverlayClosed();
14321
+ if (provider === "claude-max") {
14322
+ const hasAuth = await isClaudeMaxAvailable();
14323
+ if (!hasAuth) {
14324
+ addMessage({
14325
+ type: "assistant",
14326
+ content: "Claude Max requires OAuth authentication. Starting authentication flow..."
14327
+ });
14328
+ try {
14329
+ const { ClaudeOAuthService: ClaudeOAuthService2 } = await Promise.resolve().then(() => (init_claude_oauth(), claude_oauth_exports));
14330
+ const { getSecretStorage: getSecretStorage2 } = await Promise.resolve().then(() => (init_secret_storage(), secret_storage_exports));
14331
+ const secretStorage = getSecretStorage2();
14332
+ const oauthService = new ClaudeOAuthService2(secretStorage);
14333
+ const result = await oauthService.authorize();
14334
+ if (!result.success) {
14335
+ addMessage({
14336
+ type: "error",
14337
+ content: `Authentication failed: ${result.error || "Unknown error"}`,
14338
+ errorType: "error"
14339
+ });
14340
+ return;
14341
+ }
14342
+ addMessage({
14343
+ type: "assistant",
14344
+ content: "\u2705 Successfully authenticated with Claude! Switching to Claude Max provider..."
14345
+ });
14346
+ setClaudeMaxAvailable(true);
14347
+ } catch (error) {
14348
+ addMessage({
14349
+ type: "error",
14350
+ content: `Authentication failed: ${error instanceof Error ? error.message : String(error)}`,
14351
+ errorType: "error"
14352
+ });
14353
+ return;
14354
+ }
14355
+ }
14356
+ }
12614
14357
  setLlmProvider(provider);
12615
14358
  saveSupatestSettings(config2.cwd || process.cwd(), { llmProvider: provider });
12616
14359
  addMessage({
@@ -12661,6 +14404,38 @@ var init_App = __esm({
12661
14404
  markOverlayClosed();
12662
14405
  setShowFeedbackDialog(false);
12663
14406
  };
14407
+ const handleQuestionSubmit = (answers) => {
14408
+ if (!pendingQuestion) return;
14409
+ const formattedAnswer = answers.length === 1 ? answers[0] : answers.join(", ");
14410
+ addMessage({
14411
+ type: "user",
14412
+ content: formattedAnswer
14413
+ });
14414
+ const answerRecord = {
14415
+ "answer": formattedAnswer
14416
+ };
14417
+ if (onQuestionAnswer) {
14418
+ const success = onQuestionAnswer(pendingQuestion.toolUseId, answerRecord);
14419
+ if (!success) {
14420
+ console.warn("[App] Failed to submit answer - no pending question found");
14421
+ }
14422
+ }
14423
+ setPendingQuestion(null);
14424
+ markOverlayClosed();
14425
+ };
14426
+ const handleQuestionCancel = () => {
14427
+ if (!pendingQuestion) return;
14428
+ if (onQuestionAnswer) {
14429
+ onQuestionAnswer(pendingQuestion.toolUseId, null);
14430
+ }
14431
+ const cancelMsg = "I'd prefer not to answer this question right now.";
14432
+ addMessage({
14433
+ type: "user",
14434
+ content: cancelMsg
14435
+ });
14436
+ setPendingQuestion(null);
14437
+ markOverlayClosed();
14438
+ };
12664
14439
  const handleFixFlowCancel = () => {
12665
14440
  markOverlayClosed();
12666
14441
  setShowFixFlow(false);
@@ -12800,8 +14575,8 @@ var init_App = __esm({
12800
14575
  markOverlayClosed();
12801
14576
  setShowMcpServers(true);
12802
14577
  };
12803
- const isInitialMount = useRef7(true);
12804
- useEffect13(() => {
14578
+ const isInitialMount = useRef8(true);
14579
+ useEffect14(() => {
12805
14580
  const handleResize = () => {
12806
14581
  setTerminalWidth(process.stdout.columns || 80);
12807
14582
  };
@@ -12810,7 +14585,7 @@ var init_App = __esm({
12810
14585
  process.stdout.off("resize", handleResize);
12811
14586
  };
12812
14587
  }, []);
12813
- useEffect13(() => {
14588
+ useEffect14(() => {
12814
14589
  if (isInitialMount.current) {
12815
14590
  isInitialMount.current = false;
12816
14591
  return;
@@ -12872,7 +14647,7 @@ var init_App = __esm({
12872
14647
  },
12873
14648
  { isActive: !isOverlayOpen }
12874
14649
  );
12875
- useEffect13(() => {
14650
+ useEffect14(() => {
12876
14651
  if (config2.task) {
12877
14652
  addMessage({
12878
14653
  type: "user",
@@ -12880,14 +14655,14 @@ var init_App = __esm({
12880
14655
  });
12881
14656
  }
12882
14657
  }, []);
12883
- return /* @__PURE__ */ React30.createElement(
12884
- Box27,
14658
+ return /* @__PURE__ */ React31.createElement(
14659
+ Box28,
12885
14660
  {
12886
14661
  flexDirection: "column",
12887
14662
  paddingX: 1
12888
14663
  },
12889
- /* @__PURE__ */ React30.createElement(MessageList, { currentFolder, gitBranch, queuedTasks, terminalWidth }),
12890
- showSessionSelector && apiClient && /* @__PURE__ */ React30.createElement(
14664
+ /* @__PURE__ */ React31.createElement(MessageList, { currentFolder, gitBranch, queuedTasks, terminalWidth }),
14665
+ showSessionSelector && apiClient && /* @__PURE__ */ React31.createElement(
12891
14666
  SessionSelector,
12892
14667
  {
12893
14668
  apiClient,
@@ -12895,8 +14670,8 @@ var init_App = __esm({
12895
14670
  onSelect: handleSessionSelect
12896
14671
  }
12897
14672
  ),
12898
- isLoadingSession && /* @__PURE__ */ React30.createElement(Box27, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React30.createElement(Box27, { flexDirection: "row" }, /* @__PURE__ */ React30.createElement(Box27, { width: 2 }, /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.accent }, /* @__PURE__ */ React30.createElement(Spinner4, { type: "dots" }))), /* @__PURE__ */ React30.createElement(Text25, { bold: true, color: "cyan" }, "Loading session...")), /* @__PURE__ */ React30.createElement(Text25, { color: theme.text.dim }, "Fetching queries and context")),
12899
- showModelSelector && /* @__PURE__ */ React30.createElement(
14673
+ isLoadingSession && /* @__PURE__ */ React31.createElement(Box28, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", padding: 1 }, /* @__PURE__ */ React31.createElement(Box28, { flexDirection: "row" }, /* @__PURE__ */ React31.createElement(Box28, { width: 2 }, /* @__PURE__ */ React31.createElement(Text26, { color: theme.text.accent }, /* @__PURE__ */ React31.createElement(Spinner4, { type: "dots" }))), /* @__PURE__ */ React31.createElement(Text26, { bold: true, color: "cyan" }, "Loading session...")), /* @__PURE__ */ React31.createElement(Text26, { color: theme.text.dim }, "Fetching queries and context")),
14674
+ showModelSelector && /* @__PURE__ */ React31.createElement(
12900
14675
  ModelSelector,
12901
14676
  {
12902
14677
  currentModel: selectedModel,
@@ -12905,29 +14680,29 @@ var init_App = __esm({
12905
14680
  onSelect: handleModelSelect
12906
14681
  }
12907
14682
  ),
12908
- showProviderSelector && /* @__PURE__ */ React30.createElement(
14683
+ showProviderSelector && /* @__PURE__ */ React31.createElement(
12909
14684
  ProviderSelector,
12910
14685
  {
12911
- claudeMaxAvailable: isClaudeMaxAvailable(),
14686
+ claudeMaxAvailable,
12912
14687
  currentProvider: llmProvider,
12913
14688
  onCancel: handleProviderSelectorCancel,
12914
14689
  onSelect: handleProviderSelect
12915
14690
  }
12916
14691
  ),
12917
- showAuthDialog && /* @__PURE__ */ React30.createElement(
14692
+ showAuthDialog && /* @__PURE__ */ React31.createElement(
12918
14693
  AuthDialog,
12919
14694
  {
12920
14695
  onLogin: handleLogin
12921
14696
  }
12922
14697
  ),
12923
- showFeedbackDialog && /* @__PURE__ */ React30.createElement(
14698
+ showFeedbackDialog && /* @__PURE__ */ React31.createElement(
12924
14699
  FeedbackDialog,
12925
14700
  {
12926
14701
  onCancel: handleFeedbackCancel,
12927
14702
  onSubmit: handleFeedbackSubmit
12928
14703
  }
12929
14704
  ),
12930
- showFixFlow && apiClient && /* @__PURE__ */ React30.createElement(
14705
+ showFixFlow && apiClient && /* @__PURE__ */ React31.createElement(
12931
14706
  FixFlow,
12932
14707
  {
12933
14708
  apiClient,
@@ -12937,7 +14712,7 @@ var init_App = __esm({
12937
14712
  onStartFix: handleFixStart
12938
14713
  }
12939
14714
  ),
12940
- showMcpServers && /* @__PURE__ */ React30.createElement(
14715
+ showMcpServers && /* @__PURE__ */ React31.createElement(
12941
14716
  McpServersDisplay,
12942
14717
  {
12943
14718
  cwd: config2.cwd,
@@ -12947,8 +14722,8 @@ var init_App = __esm({
12947
14722
  onTest: handleMcpTest
12948
14723
  }
12949
14724
  ),
12950
- showMcpAdd && /* @__PURE__ */ React30.createElement(McpAddDialog, { onCancel: handleMcpAddCancel, onConfirm: handleMcpAddConfirm }),
12951
- showMcpSelector && /* @__PURE__ */ React30.createElement(
14725
+ showMcpAdd && /* @__PURE__ */ React31.createElement(McpAddDialog, { onCancel: handleMcpAddCancel, onConfirm: handleMcpAddConfirm }),
14726
+ showMcpSelector && /* @__PURE__ */ React31.createElement(
12952
14727
  McpServerSelector,
12953
14728
  {
12954
14729
  action: mcpSelectorAction,
@@ -12957,7 +14732,17 @@ var init_App = __esm({
12957
14732
  servers: mcpServers
12958
14733
  }
12959
14734
  ),
12960
- /* @__PURE__ */ React30.createElement(Box27, { flexDirection: "column" }, !showAuthDialog && /* @__PURE__ */ React30.createElement(AuthBanner, { authState }), showInput && !showSessionSelector && !showAuthDialog && !showModelSelector && !showProviderSelector && !showFeedbackDialog && !showFixFlow && !showMcpServers && !showMcpAdd && !showMcpSelector && !isLoadingSession && /* @__PURE__ */ React30.createElement(Box27, { flexDirection: "column", marginTop: 0, width: "100%" }, exitWarning && /* @__PURE__ */ React30.createElement(Box27, { marginBottom: 0, paddingX: 1 }, /* @__PURE__ */ React30.createElement(Text25, { color: "yellow" }, exitWarning)), /* @__PURE__ */ React30.createElement(
14735
+ pendingQuestion && /* @__PURE__ */ React31.createElement(
14736
+ QuestionSelector,
14737
+ {
14738
+ multiSelect: pendingQuestion.multiSelect,
14739
+ onCancel: handleQuestionCancel,
14740
+ onSubmit: handleQuestionSubmit,
14741
+ options: pendingQuestion.options || [],
14742
+ question: pendingQuestion.question
14743
+ }
14744
+ ),
14745
+ /* @__PURE__ */ React31.createElement(Box28, { flexDirection: "column" }, !showAuthDialog && /* @__PURE__ */ React31.createElement(AuthBanner, { authState }), showInput && !showSessionSelector && !showAuthDialog && !showModelSelector && !showProviderSelector && !showFeedbackDialog && !showFixFlow && !showMcpServers && !showMcpAdd && !showMcpSelector && !pendingQuestion && !isLoadingSession && /* @__PURE__ */ React31.createElement(Box28, { flexDirection: "column", marginTop: 0, width: "100%" }, exitWarning && /* @__PURE__ */ React31.createElement(Box28, { marginBottom: 0, paddingX: 1 }, /* @__PURE__ */ React31.createElement(Text26, { color: "yellow" }, exitWarning)), /* @__PURE__ */ React31.createElement(
12961
14746
  InputPrompt,
12962
14747
  {
12963
14748
  currentFolder,
@@ -12973,13 +14758,13 @@ var init_App = __esm({
12973
14758
  );
12974
14759
  };
12975
14760
  App = (props) => {
12976
- return /* @__PURE__ */ React30.createElement(AppContent, { ...props });
14761
+ return /* @__PURE__ */ React31.createElement(AppContent, { ...props });
12977
14762
  };
12978
14763
  }
12979
14764
  });
12980
14765
 
12981
14766
  // src/ui/hooks/useBracketedPaste.ts
12982
- import { useEffect as useEffect14 } from "react";
14767
+ import { useEffect as useEffect15 } from "react";
12983
14768
  var ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, useBracketedPaste;
12984
14769
  var init_useBracketedPaste = __esm({
12985
14770
  "src/ui/hooks/useBracketedPaste.ts"() {
@@ -12988,7 +14773,7 @@ var init_useBracketedPaste = __esm({
12988
14773
  ENABLE_BRACKETED_PASTE = "\x1B[?2004h";
12989
14774
  DISABLE_BRACKETED_PASTE = "\x1B[?2004l";
12990
14775
  useBracketedPaste = () => {
12991
- useEffect14(() => {
14776
+ useEffect15(() => {
12992
14777
  writeToStdout(ENABLE_BRACKETED_PASTE);
12993
14778
  const cleanup = () => {
12994
14779
  writeToStdout(DISABLE_BRACKETED_PASTE);
@@ -13009,7 +14794,7 @@ __export(interactive_exports, {
13009
14794
  runInteractive: () => runInteractive
13010
14795
  });
13011
14796
  import { render as render2 } from "ink";
13012
- import React31, { useEffect as useEffect15, useRef as useRef8 } from "react";
14797
+ import React32, { useEffect as useEffect16, useRef as useRef9 } from "react";
13013
14798
  function getToolDescription2(toolName, input) {
13014
14799
  switch (toolName) {
13015
14800
  case "Read":
@@ -13142,7 +14927,7 @@ async function runInteractive(config2) {
13142
14927
  webUrl = session.webUrl;
13143
14928
  }
13144
14929
  const { unmount, waitUntilExit } = render2(
13145
- /* @__PURE__ */ React31.createElement(
14930
+ /* @__PURE__ */ React32.createElement(
13146
14931
  InteractiveApp,
13147
14932
  {
13148
14933
  apiClient,
@@ -13158,11 +14943,14 @@ async function runInteractive(config2) {
13158
14943
  stdout: inkStdout,
13159
14944
  stderr: inkStderr,
13160
14945
  stdin: process.stdin,
14946
+ // Keep alternateBuffer: false for native terminal scrollback and text selection
13161
14947
  alternateBuffer: false,
13162
- // Like Gemini CLI - allows terminal to handle scroll & selection
13163
14948
  exitOnCtrlC: false,
13164
- // Reduce flickering by using incremental updates (only changed lines, not entire screen)
13165
- incrementalRendering: true
14949
+ // Disable incrementalRendering - it doesn't work well without alternateBuffer
14950
+ // and can cause more flickering issues
14951
+ incrementalRendering: false,
14952
+ // Prevent Ink from patching console methods
14953
+ patchConsole: false
13166
14954
  }
13167
14955
  );
13168
14956
  unmountFn = unmount;
@@ -13204,7 +14992,7 @@ var init_interactive = __esm({
13204
14992
  init_settings_loader();
13205
14993
  init_stdio();
13206
14994
  init_version();
13207
- AgentRunner = ({ config: config2, sessionId, apiClient, messageBridge, onComplete, onTurnComplete }) => {
14995
+ AgentRunner = ({ config: config2, sessionId, apiClient, messageBridge, onComplete, onTurnComplete, onAgentCreated }) => {
13208
14996
  const {
13209
14997
  addMessage,
13210
14998
  updateLastMessage,
@@ -13212,23 +15000,24 @@ var init_interactive = __esm({
13212
15000
  setIsAgentRunning,
13213
15001
  updateStats,
13214
15002
  setTodos,
13215
- setUsageStats,
13216
15003
  shouldInterruptAgent,
13217
15004
  setShouldInterruptAgent,
13218
15005
  agentMode,
13219
15006
  setAgentMode,
13220
15007
  planFilePath,
13221
15008
  selectedModel,
13222
- llmProvider
15009
+ llmProvider,
15010
+ setPendingQuestion
13223
15011
  } = useSession();
13224
- const agentRef = useRef8(null);
13225
- useEffect15(() => {
15012
+ const { setUsageStats } = useUsageStats();
15013
+ const agentRef = useRef9(null);
15014
+ useEffect16(() => {
13226
15015
  if (shouldInterruptAgent && agentRef.current) {
13227
15016
  agentRef.current.abort();
13228
15017
  setShouldInterruptAgent(false);
13229
15018
  }
13230
15019
  }, [shouldInterruptAgent, setShouldInterruptAgent]);
13231
- useEffect15(() => {
15020
+ useEffect16(() => {
13232
15021
  let isMounted = true;
13233
15022
  const runAgent2 = async () => {
13234
15023
  setIsAgentRunning(true);
@@ -13270,6 +15059,16 @@ var init_interactive = __esm({
13270
15059
  },
13271
15060
  onTurnComplete: () => {
13272
15061
  if (isMounted) onTurnComplete?.();
15062
+ },
15063
+ onAskUserQuestion: (toolId, question, options, multiSelect) => {
15064
+ if (isMounted) {
15065
+ setPendingQuestion({
15066
+ toolUseId: toolId,
15067
+ question,
15068
+ options,
15069
+ multiSelect
15070
+ });
15071
+ }
13273
15072
  }
13274
15073
  },
13275
15074
  apiClient,
@@ -13278,9 +15077,18 @@ var init_interactive = __esm({
13278
15077
  );
13279
15078
  let oauthToken;
13280
15079
  if (llmProvider === "claude-max") {
13281
- if (isClaudeMaxAvailable()) {
13282
- oauthToken = "use-claude-max";
13283
- logger.info("Using Claude Max subscription for LLM calls");
15080
+ if (await isClaudeMaxAvailable()) {
15081
+ const { ClaudeOAuthService: ClaudeOAuthService2 } = await Promise.resolve().then(() => (init_claude_oauth(), claude_oauth_exports));
15082
+ const { getSecretStorage: getSecretStorage2 } = await Promise.resolve().then(() => (init_secret_storage(), secret_storage_exports));
15083
+ const secretStorage = getSecretStorage2();
15084
+ const oauthService = new ClaudeOAuthService2(secretStorage);
15085
+ const token = await oauthService.getAccessToken();
15086
+ if (token) {
15087
+ oauthToken = token;
15088
+ logger.info("Using Claude Max subscription for LLM calls");
15089
+ } else {
15090
+ logger.warn("Claude Max OAuth token not available. Falling back to Supatest Managed.");
15091
+ }
13284
15092
  } else {
13285
15093
  logger.warn("Claude Max selected but not available. Falling back to Supatest Managed.");
13286
15094
  }
@@ -13296,6 +15104,9 @@ var init_interactive = __esm({
13296
15104
  };
13297
15105
  const agent2 = new CoreAgent(presenter, messageBridge);
13298
15106
  agentRef.current = agent2;
15107
+ if (onAgentCreated) {
15108
+ onAgentCreated(agent2);
15109
+ }
13299
15110
  const result = await agent2.run(runConfig);
13300
15111
  if (isMounted) {
13301
15112
  onComplete(result.success, result.providerSessionId);
@@ -13333,20 +15144,21 @@ var init_interactive = __esm({
13333
15144
  addMessage,
13334
15145
  loadMessages,
13335
15146
  setSessionId: setContextSessionId,
13336
- setIsAgentRunning,
13337
- setUsageStats
15147
+ setIsAgentRunning
13338
15148
  } = useSession();
13339
- const [sessionId, setSessionId] = React31.useState(initialSessionId);
13340
- const [currentTask, setCurrentTask] = React31.useState(config2.task);
13341
- const [taskId, setTaskId] = React31.useState(0);
13342
- const [shouldRunAgent, setShouldRunAgent] = React31.useState(!!config2.task);
13343
- const [taskQueue, setTaskQueue] = React31.useState([]);
13344
- const [providerSessionId, setProviderSessionId] = React31.useState();
13345
- const messageBridgeRef = React31.useRef(null);
13346
- const lastSubmitRef = React31.useRef(null);
13347
- const [pendingInjected, setPendingInjected] = React31.useState([]);
13348
- const pendingInjectedRef = React31.useRef([]);
13349
- React31.useEffect(() => {
15149
+ const { setUsageStats } = useUsageStats();
15150
+ const [sessionId, setSessionId] = React32.useState(initialSessionId);
15151
+ const [currentTask, setCurrentTask] = React32.useState(config2.task);
15152
+ const [taskId, setTaskId] = React32.useState(0);
15153
+ const [shouldRunAgent, setShouldRunAgent] = React32.useState(!!config2.task);
15154
+ const [taskQueue, setTaskQueue] = React32.useState([]);
15155
+ const [providerSessionId, setProviderSessionId] = React32.useState();
15156
+ const messageBridgeRef = React32.useRef(null);
15157
+ const agentRef = React32.useRef(null);
15158
+ const lastSubmitRef = React32.useRef(null);
15159
+ const [pendingInjected, setPendingInjected] = React32.useState([]);
15160
+ const pendingInjectedRef = React32.useRef([]);
15161
+ React32.useEffect(() => {
13350
15162
  pendingInjectedRef.current = pendingInjected;
13351
15163
  }, [pendingInjected]);
13352
15164
  const handleSubmitTask = async (task) => {
@@ -13364,6 +15176,7 @@ var init_interactive = __esm({
13364
15176
  });
13365
15177
  setSessionId(session.sessionId);
13366
15178
  setContextSessionId(session.sessionId);
15179
+ setProviderSessionId(void 0);
13367
15180
  } catch (error) {
13368
15181
  const errorMessage = error instanceof ApiError ? error.message : `Failed to create session: ${error instanceof Error ? error.message : String(error)}`;
13369
15182
  addMessage({
@@ -13409,7 +15222,7 @@ var init_interactive = __esm({
13409
15222
  if (shouldRunAgent && !messageBridgeRef.current) {
13410
15223
  messageBridgeRef.current = new MessageBridge(providerSessionId || "");
13411
15224
  }
13412
- React31.useEffect(() => {
15225
+ React32.useEffect(() => {
13413
15226
  if (!shouldRunAgent && taskQueue.length > 0) {
13414
15227
  const [nextTask, ...remaining] = taskQueue;
13415
15228
  setTaskQueue(remaining);
@@ -13423,20 +15236,27 @@ var init_interactive = __esm({
13423
15236
  setShouldRunAgent(true);
13424
15237
  }
13425
15238
  }, [shouldRunAgent, taskQueue, addMessage, providerSessionId]);
13426
- const handleClearSession = React31.useCallback(() => {
15239
+ const handleClearSession = React32.useCallback(() => {
13427
15240
  setSessionId(void 0);
13428
15241
  setContextSessionId(void 0);
13429
15242
  setProviderSessionId(void 0);
13430
15243
  setTaskQueue([]);
13431
15244
  setPendingInjected([]);
13432
15245
  }, [setContextSessionId]);
13433
- return /* @__PURE__ */ React31.createElement(React31.Fragment, null, /* @__PURE__ */ React31.createElement(
15246
+ const handleQuestionAnswer = React32.useCallback((questionId, answers) => {
15247
+ if (agentRef.current) {
15248
+ return agentRef.current.submitAnswer(questionId, answers);
15249
+ }
15250
+ return false;
15251
+ }, []);
15252
+ return /* @__PURE__ */ React32.createElement(React32.Fragment, null, /* @__PURE__ */ React32.createElement(
13434
15253
  App,
13435
15254
  {
13436
15255
  apiClient,
13437
15256
  config: { ...config2, task: currentTask },
13438
15257
  onClearSession: handleClearSession,
13439
15258
  onExit,
15259
+ onQuestionAnswer: handleQuestionAnswer,
13440
15260
  onResumeSession: async (session) => {
13441
15261
  try {
13442
15262
  if (!apiClient) {
@@ -13452,7 +15272,9 @@ var init_interactive = __esm({
13452
15272
  const uiMessages = convertQueriesToUIMessages(queries);
13453
15273
  setSessionId(session.id);
13454
15274
  setContextSessionId(session.id);
13455
- setProviderSessionId(session.providerSessionId || void 0);
15275
+ const resumeProviderSessionId = session.providerSessionId || void 0;
15276
+ logger.debug(`Resume session: ${session.id}, providerSessionId: ${resumeProviderSessionId || "NOT SET"}`);
15277
+ setProviderSessionId(resumeProviderSessionId);
13456
15278
  const contextData = await apiClient.getSessionContext(session.id);
13457
15279
  if (contextData.contextTokens > 0) {
13458
15280
  const cacheRead = contextData.cacheReadTokens || 0;
@@ -13493,13 +15315,16 @@ var init_interactive = __esm({
13493
15315
  sessionId,
13494
15316
  webUrl
13495
15317
  }
13496
- ), shouldRunAgent && currentTask && sessionId && messageBridgeRef.current && /* @__PURE__ */ React31.createElement(
15318
+ ), shouldRunAgent && currentTask && sessionId && messageBridgeRef.current && /* @__PURE__ */ React32.createElement(
13497
15319
  AgentRunner,
13498
15320
  {
13499
15321
  apiClient,
13500
15322
  config: { ...config2, task: currentTask, providerSessionId },
13501
15323
  key: `${taskId}`,
13502
15324
  messageBridge: messageBridgeRef.current,
15325
+ onAgentCreated: (agent2) => {
15326
+ agentRef.current = agent2;
15327
+ },
13503
15328
  onComplete: handleAgentComplete,
13504
15329
  onTurnComplete: () => {
13505
15330
  const pending = pendingInjectedRef.current;
@@ -13518,7 +15343,7 @@ var init_interactive = __esm({
13518
15343
  useBracketedPaste();
13519
15344
  const settings = loadSupatestSettings(props.config.cwd || process.cwd());
13520
15345
  const initialProvider = settings.llmProvider || "supatest-managed";
13521
- return /* @__PURE__ */ React31.createElement(KeypressProvider, null, /* @__PURE__ */ React31.createElement(SessionProvider, { initialLlmProvider: initialProvider, initialModel: props.config.selectedModel }, /* @__PURE__ */ React31.createElement(InteractiveAppContent, { ...props })));
15346
+ return /* @__PURE__ */ React32.createElement(KeypressProvider, null, /* @__PURE__ */ React32.createElement(SessionProvider, { initialLlmProvider: initialProvider, initialModel: props.config.selectedModel }, /* @__PURE__ */ React32.createElement(InteractiveAppContent, { ...props })));
13522
15347
  };
13523
15348
  }
13524
15349
  });
@@ -13526,9 +15351,61 @@ var init_interactive = __esm({
13526
15351
  // src/index.ts
13527
15352
  await init_config();
13528
15353
  init_shared_es();
15354
+ import { Command } from "commander";
15355
+
15356
+ // src/commands/claude-login.ts
15357
+ init_claude_oauth();
15358
+ init_secret_storage();
15359
+ async function claudeLoginCommand() {
15360
+ const secretStorage = getSecretStorage();
15361
+ const oauthService = new ClaudeOAuthService(secretStorage);
15362
+ const isAuth = await oauthService.isAuthenticated();
15363
+ if (isAuth) {
15364
+ console.log("\u2705 You're already authenticated with Claude.");
15365
+ console.log("\nTo re-authenticate, first run: supatest claude-logout\n");
15366
+ return;
15367
+ }
15368
+ const result = await oauthService.authorize();
15369
+ if (!result.success) {
15370
+ console.error(`
15371
+ \u274C Authentication failed: ${result.error}
15372
+ `);
15373
+ process.exit(1);
15374
+ }
15375
+ }
15376
+ async function claudeLogoutCommand() {
15377
+ const secretStorage = getSecretStorage();
15378
+ const oauthService = new ClaudeOAuthService(secretStorage);
15379
+ const isAuth = await oauthService.isAuthenticated();
15380
+ if (!isAuth) {
15381
+ console.log("You're not currently authenticated with Claude.\n");
15382
+ return;
15383
+ }
15384
+ await oauthService.deleteTokens();
15385
+ console.log("\u2705 Successfully logged out from Claude.\n");
15386
+ }
15387
+ async function claudeStatusCommand() {
15388
+ const secretStorage = getSecretStorage();
15389
+ const oauthService = new ClaudeOAuthService(secretStorage);
15390
+ const status = await oauthService.getStatus();
15391
+ if (status.isAuthenticated) {
15392
+ console.log("\u2705 Authenticated with Claude");
15393
+ if (status.expiresAt) {
15394
+ const expiresDate = new Date(status.expiresAt);
15395
+ console.log(` Token expires: ${expiresDate.toLocaleString()}`);
15396
+ }
15397
+ } else {
15398
+ console.log("\u274C Not authenticated with Claude");
15399
+ if (status.error) {
15400
+ console.log(` Error: ${status.error}`);
15401
+ }
15402
+ }
15403
+ console.log();
15404
+ }
15405
+
15406
+ // src/index.ts
13529
15407
  init_setup();
13530
15408
  await init_config();
13531
- import { Command } from "commander";
13532
15409
 
13533
15410
  // src/modes/headless.ts
13534
15411
  init_api_client();
@@ -13542,7 +15419,7 @@ init_react();
13542
15419
  init_MessageList();
13543
15420
  init_SessionContext();
13544
15421
  import { execSync as execSync2 } from "child_process";
13545
- import { homedir as homedir4 } from "os";
15422
+ import { homedir as homedir5 } from "os";
13546
15423
  import { Box as Box13, useApp } from "ink";
13547
15424
  import React14, { useEffect as useEffect2, useRef as useRef4, useState as useState3 } from "react";
13548
15425
  var getGitBranch = () => {
@@ -13554,7 +15431,7 @@ var getGitBranch = () => {
13554
15431
  };
13555
15432
  var getCurrentFolder = () => {
13556
15433
  const cwd = process.cwd();
13557
- const home = homedir4();
15434
+ const home = homedir5();
13558
15435
  if (cwd.startsWith(home)) {
13559
15436
  return `~${cwd.slice(home.length)}`;
13560
15437
  }
@@ -13567,9 +15444,9 @@ var HeadlessAgentRunner = ({ config: config2, sessionId, apiClient, onComplete }
13567
15444
  updateMessageByToolId,
13568
15445
  setIsAgentRunning,
13569
15446
  updateStats,
13570
- setTodos,
13571
- setUsageStats
15447
+ setTodos
13572
15448
  } = useSession();
15449
+ const { setUsageStats } = useUsageStats();
13573
15450
  const agentRef = useRef4(null);
13574
15451
  useEffect2(() => {
13575
15452
  let isMounted = true;
@@ -13801,7 +15678,7 @@ async function runAgent(config2) {
13801
15678
  // src/utils/auto-update.ts
13802
15679
  init_version();
13803
15680
  init_error_logger();
13804
- import { execSync as execSync3, spawn } from "child_process";
15681
+ import { execSync as execSync3, spawn as spawn3 } from "child_process";
13805
15682
  import latestVersion from "latest-version";
13806
15683
  import { gt } from "semver";
13807
15684
  var UPDATE_CHECK_TIMEOUT = 3e3;
@@ -13843,7 +15720,7 @@ Updating Supatest CLI ${CLI_VERSION} \u2192 ${latest}...`);
13843
15720
  });
13844
15721
  }
13845
15722
  console.log("\u2713 Updated successfully\n");
13846
- const child = spawn(process.argv[0], process.argv.slice(1), {
15723
+ const child = spawn3(process.argv[0], process.argv.slice(1), {
13847
15724
  stdio: "inherit",
13848
15725
  detached: false
13849
15726
  });
@@ -14128,6 +16005,33 @@ program.command("setup").description("Check prerequisites and set up required to
14128
16005
  process.exit(1);
14129
16006
  }
14130
16007
  });
16008
+ program.command("claude-login").description("Authenticate with Claude using OAuth (uses your Claude Pro/Max subscription)").action(async () => {
16009
+ try {
16010
+ await claudeLoginCommand();
16011
+ } catch (error) {
16012
+ logError(error, { source: "claude-login" });
16013
+ logger.error(`Claude login failed: ${error instanceof Error ? error.message : String(error)}`);
16014
+ process.exit(1);
16015
+ }
16016
+ });
16017
+ program.command("claude-logout").description("Sign out from Claude OAuth").action(async () => {
16018
+ try {
16019
+ await claudeLogoutCommand();
16020
+ } catch (error) {
16021
+ logError(error, { source: "claude-logout" });
16022
+ logger.error(`Claude logout failed: ${error instanceof Error ? error.message : String(error)}`);
16023
+ process.exit(1);
16024
+ }
16025
+ });
16026
+ program.command("claude-status").description("Show Claude OAuth authentication status").action(async () => {
16027
+ try {
16028
+ await claudeStatusCommand();
16029
+ } catch (error) {
16030
+ logError(error, { source: "claude-status" });
16031
+ logger.error(`Claude status check failed: ${error instanceof Error ? error.message : String(error)}`);
16032
+ process.exit(1);
16033
+ }
16034
+ });
14131
16035
  var filteredArgv = process.argv.filter((arg, index) => {
14132
16036
  return !(arg === "--" && index > 1);
14133
16037
  });