@supatest/cli 0.0.40 → 0.0.42

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 +2274 -463
  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>
@@ -313,7 +313,7 @@ Use these commands in interactive mode (type them and press Enter):
313
313
  - Choose between "Supatest Managed" (default) or "Claude Max"
314
314
  - Supatest Managed: Uses models through Supatest infrastructure
315
315
  - Claude Max: Uses your Claude subscription directly
316
- - Requires Claude Code login for Claude Max
316
+ - Authentication flow starts automatically when selecting Claude Max
317
317
  - Available on macOS, Linux, and Windows
318
318
 
319
319
  - **/mcp** - Show configured MCP servers
@@ -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,11 +5624,438 @@ 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;
5632
6054
  }
5633
6055
  });
5634
6056
 
5635
6057
  // src/commands/setup.ts
5636
- import { execSync, spawnSync } from "child_process";
6058
+ import { execSync, spawn, spawnSync } from "child_process";
5637
6059
  import fs from "fs";
5638
6060
  import os from "os";
5639
6061
  import path from "path";
@@ -5730,47 +6152,64 @@ function checkNodeVersion() {
5730
6152
  version: nodeVersion.raw
5731
6153
  };
5732
6154
  }
5733
- function installChromium() {
5734
- console.log(" Running: npx playwright install chromium");
5735
- console.log("");
5736
- try {
5737
- const result = spawnSync("npx", ["playwright", "install", "chromium"], {
6155
+ async function installChromium() {
6156
+ return new Promise((resolve2) => {
6157
+ const child = spawn("npx", ["playwright", "install", "chromium"], {
5738
6158
  stdio: "inherit",
5739
6159
  shell: true
5740
6160
  // Required for Windows where npx is npx.cmd
5741
6161
  });
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
- }
6162
+ child.on("close", (code) => {
6163
+ if (code === 0) {
6164
+ resolve2({
6165
+ ok: true,
6166
+ message: "Chromium browser installed successfully."
6167
+ });
6168
+ } else {
6169
+ resolve2({
6170
+ ok: false,
6171
+ message: `Playwright install exited with code ${code}`
6172
+ });
6173
+ }
6174
+ });
6175
+ child.on("error", (error) => {
6176
+ resolve2({
6177
+ ok: false,
6178
+ message: `Failed to install Chromium: ${error.message}`
6179
+ });
6180
+ });
6181
+ });
5758
6182
  }
5759
6183
  function createSupatestConfig(cwd) {
5760
6184
  const supatestDir = path.join(cwd, ".supatest");
5761
6185
  const mcpJsonPath = path.join(supatestDir, "mcp.json");
5762
6186
  try {
6187
+ if (!fs.existsSync(supatestDir)) {
6188
+ fs.mkdirSync(supatestDir, { recursive: true });
6189
+ }
6190
+ let config2;
6191
+ let fileExisted = false;
5763
6192
  if (fs.existsSync(mcpJsonPath)) {
6193
+ fileExisted = true;
6194
+ const existingContent = fs.readFileSync(mcpJsonPath, "utf-8");
6195
+ config2 = JSON.parse(existingContent);
6196
+ } else {
6197
+ config2 = {};
6198
+ }
6199
+ if (!config2.mcpServers || typeof config2.mcpServers !== "object") {
6200
+ config2.mcpServers = {};
6201
+ }
6202
+ if (!config2.mcpServers.playwright) {
6203
+ config2.mcpServers.playwright = DEFAULT_MCP_CONFIG.mcpServers.playwright;
6204
+ }
6205
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
6206
+ if (fileExisted) {
5764
6207
  return {
5765
6208
  ok: true,
5766
- message: "mcp.json already exists",
6209
+ message: "Updated .supatest/mcp.json with Playwright MCP server configuration",
5767
6210
  created: false
5768
6211
  };
5769
6212
  }
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
6213
  return {
5775
6214
  ok: true,
5776
6215
  message: "Created .supatest/mcp.json with Playwright MCP server configuration",
@@ -5799,7 +6238,6 @@ async function setupCommand(options) {
5799
6238
  const output = [];
5800
6239
  const log = (msg) => {
5801
6240
  output.push(msg);
5802
- console.log(msg);
5803
6241
  };
5804
6242
  const result = {
5805
6243
  nodeVersionOk: false,
@@ -5833,7 +6271,7 @@ async function setupCommand(options) {
5833
6271
  result.playwrightInstalled = true;
5834
6272
  } else {
5835
6273
  log(" \u{1F4E6} Chromium browser not found. Installing...\n");
5836
- const chromiumResult = installChromium();
6274
+ const chromiumResult = await installChromium();
5837
6275
  result.playwrightInstalled = chromiumResult.ok;
5838
6276
  log("");
5839
6277
  if (chromiumResult.ok) {
@@ -5891,7 +6329,7 @@ var CLI_VERSION;
5891
6329
  var init_version = __esm({
5892
6330
  "src/version.ts"() {
5893
6331
  "use strict";
5894
- CLI_VERSION = "0.0.40";
6332
+ CLI_VERSION = "0.0.42";
5895
6333
  }
5896
6334
  });
5897
6335
 
@@ -6600,18 +7038,145 @@ var init_api_client = __esm({
6600
7038
  throw new ApiError(response.status, response.statusText, errorText);
6601
7039
  }
6602
7040
  const data = await response.json();
6603
- logger.debug(`Fetched test: ${testId}`);
6604
- return data;
7041
+ logger.debug(`Fetched test: ${testId}`);
7042
+ return data;
7043
+ }
7044
+ /**
7045
+ * Get test result history (for sparkline visualization)
7046
+ * @param testId - The test ID
7047
+ * @param limit - Maximum number of history items
7048
+ * @returns Test history
7049
+ */
7050
+ async getTestHistory(testId, limit = 20) {
7051
+ const url = `${this.apiUrl}/v1/reporting/tests/${testId}/history?limit=${limit}`;
7052
+ logger.debug(`Fetching test history: ${testId}`);
7053
+ const response = await fetch(url, {
7054
+ method: "GET",
7055
+ headers: {
7056
+ Authorization: `Bearer ${this.apiKey}`
7057
+ }
7058
+ });
7059
+ if (!response.ok) {
7060
+ const errorText = await response.text();
7061
+ throw new ApiError(response.status, response.statusText, errorText);
7062
+ }
7063
+ const data = await response.json();
7064
+ logger.debug(`Fetched ${data.history.length} history items for test ${testId}`);
7065
+ return data;
7066
+ }
7067
+ /**
7068
+ * Assign tests to yourself (or someone else)
7069
+ * @param params - Assignment parameters
7070
+ * @returns Assignment result with assigned tests and conflicts
7071
+ */
7072
+ async assignTests(params) {
7073
+ const url = `${this.apiUrl}/v1/fix-assignments/assign`;
7074
+ logger.debug(`Assigning tests`, {
7075
+ runId: params.runId,
7076
+ count: params.testRunIds.length
7077
+ });
7078
+ const response = await fetch(url, {
7079
+ method: "POST",
7080
+ headers: {
7081
+ "Content-Type": "application/json",
7082
+ Authorization: `Bearer ${this.apiKey}`
7083
+ },
7084
+ body: JSON.stringify({
7085
+ testRunIds: params.testRunIds,
7086
+ assignedTo: params.assignedTo
7087
+ })
7088
+ });
7089
+ if (!response.ok) {
7090
+ const errorText = await response.text();
7091
+ throw new ApiError(response.status, response.statusText, errorText);
7092
+ }
7093
+ const data = await response.json();
7094
+ logger.debug(
7095
+ `Assigned ${data.assigned.length} tests, ${data.conflicts.length} conflicts`
7096
+ );
7097
+ return data;
7098
+ }
7099
+ /**
7100
+ * Mark assignment as complete
7101
+ * @param params - Completion parameters
7102
+ * @returns Success status
7103
+ */
7104
+ async completeAssignment(params) {
7105
+ const url = `${this.apiUrl}/v1/fix-assignments/${params.assignmentId}/status`;
7106
+ logger.debug(`Completing assignment`, {
7107
+ assignmentId: params.assignmentId,
7108
+ status: params.status
7109
+ });
7110
+ const response = await fetch(url, {
7111
+ method: "PATCH",
7112
+ headers: {
7113
+ "Content-Type": "application/json",
7114
+ Authorization: `Bearer ${this.apiKey}`
7115
+ },
7116
+ body: JSON.stringify({
7117
+ status: params.status,
7118
+ fixSessionId: params.fixSessionId
7119
+ })
7120
+ });
7121
+ if (!response.ok) {
7122
+ const errorText = await response.text();
7123
+ throw new ApiError(response.status, response.statusText, errorText);
7124
+ }
7125
+ const data = await response.json();
7126
+ logger.debug(`Assignment completed: ${params.assignmentId}`);
7127
+ return data;
7128
+ }
7129
+ /**
7130
+ * Release claim (delete assignment)
7131
+ * @param assignmentId - The assignment ID to release
7132
+ * @returns Success status
7133
+ */
7134
+ async releaseAssignment(assignmentId) {
7135
+ const url = `${this.apiUrl}/v1/fix-assignments/${assignmentId}`;
7136
+ logger.debug(`Releasing assignment: ${assignmentId}`);
7137
+ const response = await fetch(url, {
7138
+ method: "DELETE",
7139
+ headers: {
7140
+ Authorization: `Bearer ${this.apiKey}`
7141
+ }
7142
+ });
7143
+ if (!response.ok) {
7144
+ const errorText = await response.text();
7145
+ throw new ApiError(response.status, response.statusText, errorText);
7146
+ }
7147
+ const data = await response.json();
7148
+ logger.debug(`Assignment released: ${assignmentId}`);
7149
+ return data;
7150
+ }
7151
+ /**
7152
+ * Get my current assignments
7153
+ * @returns List of current assignments
7154
+ */
7155
+ async getMyAssignments() {
7156
+ const url = `${this.apiUrl}/v1/fix-assignments?status=assigned`;
7157
+ logger.debug(`Fetching my assignments`);
7158
+ const response = await fetch(url, {
7159
+ method: "GET",
7160
+ headers: {
7161
+ Authorization: `Bearer ${this.apiKey}`
7162
+ }
7163
+ });
7164
+ if (!response.ok) {
7165
+ const errorText = await response.text();
7166
+ throw new ApiError(response.status, response.statusText, errorText);
7167
+ }
7168
+ const data = await response.json();
7169
+ logger.debug(`Fetched ${data.assignments.length} assignments`);
7170
+ return { assignments: data.assignments };
6605
7171
  }
6606
7172
  /**
6607
- * Get test result history (for sparkline visualization)
6608
- * @param testId - The test ID
6609
- * @param limit - Maximum number of history items
6610
- * @returns Test history
7173
+ * Get assignments for a specific run (to show what others are working on)
7174
+ * @param runId - The run ID
7175
+ * @returns List of assignments for the run
6611
7176
  */
6612
- async getTestHistory(testId, limit = 20) {
6613
- const url = `${this.apiUrl}/v1/reporting/tests/${testId}/history?limit=${limit}`;
6614
- logger.debug(`Fetching test history: ${testId}`);
7177
+ async getRunAssignments(runId) {
7178
+ const url = `${this.apiUrl}/v1/fix-assignments?runId=${runId}&status=assigned`;
7179
+ logger.debug(`Fetching assignments for run: ${runId}`);
6615
7180
  const response = await fetch(url, {
6616
7181
  method: "GET",
6617
7182
  headers: {
@@ -6623,8 +7188,14 @@ var init_api_client = __esm({
6623
7188
  throw new ApiError(response.status, response.statusText, errorText);
6624
7189
  }
6625
7190
  const data = await response.json();
6626
- logger.debug(`Fetched ${data.history.length} history items for test ${testId}`);
6627
- return data;
7191
+ logger.debug(`Fetched ${data.assignments.length} assignments for run ${runId}`);
7192
+ return {
7193
+ assignments: data.assignments.map((a) => ({
7194
+ testRunId: a.testRunId,
7195
+ assignedTo: a.assignedTo,
7196
+ assignedAt: a.assignedAt
7197
+ }))
7198
+ };
6628
7199
  }
6629
7200
  };
6630
7201
  }
@@ -6853,6 +7424,8 @@ var init_agent = __esm({
6853
7424
  presenter;
6854
7425
  abortController = null;
6855
7426
  messageBridge = null;
7427
+ // Map of pending questions waiting for user answers
7428
+ pendingQuestionResolvers = /* @__PURE__ */ new Map();
6856
7429
  constructor(presenter, messageBridge) {
6857
7430
  this.presenter = presenter;
6858
7431
  this.messageBridge = messageBridge ?? null;
@@ -6866,9 +7439,37 @@ var init_agent = __esm({
6866
7439
  this.abortController.abort();
6867
7440
  }
6868
7441
  }
7442
+ /**
7443
+ * Create a promise that waits for user to answer a question.
7444
+ * Used by canUseTool callback to pause agent execution until user responds.
7445
+ */
7446
+ createQuestionPromise(questionId) {
7447
+ return new Promise((resolve2) => {
7448
+ this.pendingQuestionResolvers.set(questionId, resolve2);
7449
+ });
7450
+ }
7451
+ /**
7452
+ * Submit an answer for a pending question.
7453
+ * Called from the UI when user responds to AskUserQuestion.
7454
+ * @param questionId - The unique ID of the question
7455
+ * @param answers - The user's answers, or null if dismissed
7456
+ * @returns true if question was found and resolved, false otherwise
7457
+ */
7458
+ submitAnswer(questionId, answers) {
7459
+ const resolver = this.pendingQuestionResolvers.get(questionId);
7460
+ if (resolver) {
7461
+ resolver(answers);
7462
+ this.pendingQuestionResolvers.delete(questionId);
7463
+ return true;
7464
+ }
7465
+ return false;
7466
+ }
6869
7467
  async run(config2) {
6870
7468
  this.abortController = new AbortController();
6871
7469
  this.presenter.onStart(config2);
7470
+ if (config2.providerSessionId) {
7471
+ this.presenter.onLog(`Resuming with providerSessionId: ${config2.providerSessionId}`);
7472
+ }
6872
7473
  const claudeCodePath = await this.resolveClaudeCodePath();
6873
7474
  let prompt = config2.task;
6874
7475
  if (config2.logs) {
@@ -6951,6 +7552,9 @@ ${projectInstructions}`,
6951
7552
  CLAUDE_CONFIG_DIR: cleanEnv.CLAUDE_CONFIG_DIR || "(using default ~/.claude/)"
6952
7553
  });
6953
7554
  cleanEnv.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "1";
7555
+ cleanEnv.CLAUDE_CODE_ENABLE_TELEMETRY = "0";
7556
+ cleanEnv.DISABLE_TELEMETRY = "1";
7557
+ cleanEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
6954
7558
  const selectedModel = config2.selectedModel || "premium";
6955
7559
  const defaultModel = resolveToAnthropicModel(selectedModel);
6956
7560
  const queryOptions = {
@@ -6982,7 +7586,7 @@ ${projectInstructions}`,
6982
7586
  return servers;
6983
7587
  })(),
6984
7588
  // Resume from previous session if providerSessionId is provided
6985
- // This allows the agent to continue conversations with full context
7589
+ // This allows the agent to continue sessions with full context
6986
7590
  // Note: Sessions expire after ~30 days due to Anthropic's data retention policy
6987
7591
  ...config2.providerSessionId && {
6988
7592
  resume: config2.providerSessionId
@@ -6999,6 +7603,29 @@ ${projectInstructions}`,
6999
7603
  env: cleanEnv,
7000
7604
  stderr: (msg) => {
7001
7605
  this.presenter.onLog(`[Agent Failure] ${msg}`);
7606
+ },
7607
+ // Intercept AskUserQuestion tool to wait for user response
7608
+ canUseTool: async (toolName, input) => {
7609
+ if (toolName !== "AskUserQuestion") {
7610
+ return { behavior: "allow", updatedInput: input };
7611
+ }
7612
+ const questions = input.questions || [];
7613
+ if (questions.length === 0) {
7614
+ logger.warn("[AskUserQuestion] No questions provided");
7615
+ return { behavior: "deny", message: "No questions provided" };
7616
+ }
7617
+ const questionId = `ask-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
7618
+ this.presenter.onToolUse?.(toolName, input, questionId);
7619
+ const answers = await this.createQuestionPromise(questionId);
7620
+ if (answers === null) {
7621
+ this.presenter.onToolResult?.(questionId, JSON.stringify({ dismissed: true }));
7622
+ return { behavior: "deny", message: "User dismissed the question" };
7623
+ }
7624
+ this.presenter.onToolResult?.(questionId, JSON.stringify({ answers }));
7625
+ return {
7626
+ behavior: "allow",
7627
+ updatedInput: { questions, answers }
7628
+ };
7002
7629
  }
7003
7630
  };
7004
7631
  let resultText = "";
@@ -7023,7 +7650,7 @@ ${projectInstructions}`,
7023
7650
  };
7024
7651
  const isSessionExpiredError = (errorMsg) => {
7025
7652
  const expiredPatterns = [
7026
- "no conversation found",
7653
+ "no session found",
7027
7654
  "session not found",
7028
7655
  "session expired",
7029
7656
  "invalid session"
@@ -7031,6 +7658,20 @@ ${projectInstructions}`,
7031
7658
  const lowerError = errorMsg.toLowerCase();
7032
7659
  return expiredPatterns.some((pattern) => lowerError.includes(pattern));
7033
7660
  };
7661
+ const isContextWindowExceededError = (errorMsg) => {
7662
+ const contextPatterns = [
7663
+ "exceed context limit",
7664
+ "exceeds context limit",
7665
+ "context length",
7666
+ "maximum context",
7667
+ "prompt is too long",
7668
+ "input_length and max_tokens",
7669
+ "requested token count exceeds",
7670
+ "context window"
7671
+ ];
7672
+ const lowerError = errorMsg.toLowerCase();
7673
+ return contextPatterns.some((pattern) => lowerError.includes(pattern));
7674
+ };
7034
7675
  const runQuery = async (options) => {
7035
7676
  logger.debug("[agent] Starting query", {
7036
7677
  model: options.model,
@@ -7181,11 +7822,17 @@ ${projectInstructions}`,
7181
7822
  wasInterrupted = true;
7182
7823
  logger.debug("[agent] Query was aborted by user");
7183
7824
  } else if (config2.providerSessionId && isSessionExpiredError(errorMessage)) {
7184
- const expiredMessage = "Can't continue conversation older than 30 days. Please start a new session.";
7825
+ const expiredMessage = "Can't continue session older than 30 days. Please start a new session.";
7185
7826
  this.presenter.onError(expiredMessage, true);
7186
7827
  hasError = true;
7187
7828
  errors.push(expiredMessage);
7188
7829
  logger.debug("[agent] Session expired error");
7830
+ } else if (isContextWindowExceededError(errorMessage)) {
7831
+ 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";
7832
+ this.presenter.onError(contextMessage, true);
7833
+ hasError = true;
7834
+ errors.push(contextMessage);
7835
+ logger.debug("[agent] Context window exceeded error");
7189
7836
  } else {
7190
7837
  logError(error, {
7191
7838
  source: "CoreAgent.run",
@@ -7270,6 +7917,10 @@ function getToolDescription(toolName, input) {
7270
7917
  case "BashOutput":
7271
7918
  case "Command Output":
7272
7919
  return input?.bash_id || "shell output";
7920
+ case "AskUserQuestion": {
7921
+ const question = input?.questions?.[0]?.question || input?.question || "Question";
7922
+ return question.length > 60 ? `${question.substring(0, 60)}...` : question;
7923
+ }
7273
7924
  default:
7274
7925
  return toolName;
7275
7926
  }
@@ -7402,6 +8053,15 @@ var init_react = __esm({
7402
8053
  this.callbacks.onExitPlanMode?.();
7403
8054
  } else if (tool === "EnterPlanMode") {
7404
8055
  this.callbacks.onEnterPlanMode?.();
8056
+ } else if (tool === "AskUserQuestion") {
8057
+ const questions = input?.questions || [];
8058
+ if (questions.length > 0) {
8059
+ const firstQuestion = questions[0];
8060
+ const question = firstQuestion.question || "";
8061
+ const options = firstQuestion.options || [];
8062
+ const multiSelect = firstQuestion.multiSelect || false;
8063
+ this.callbacks.onAskUserQuestion?.(toolId, question, options, multiSelect);
8064
+ }
7405
8065
  }
7406
8066
  streamEventAsync(this.apiClient, this.sessionId, {
7407
8067
  type: "tool_use",
@@ -7559,6 +8219,7 @@ var init_SessionContext = __esm({
7559
8219
  const [allToolsExpanded, setAllToolsExpanded] = useState(true);
7560
8220
  const [toolGroupsExpanded, setToolGroupsExpanded] = useState(false);
7561
8221
  const [staticRemountKey, setStaticRemountKey] = useState(0);
8222
+ const [pendingQuestion, setPendingQuestion] = useState(null);
7562
8223
  const addMessage = useCallback(
7563
8224
  (message) => {
7564
8225
  const expandableTools = ["Bash", "BashOutput", "Command Output"];
@@ -7689,7 +8350,9 @@ var init_SessionContext = __esm({
7689
8350
  llmProvider,
7690
8351
  setLlmProvider,
7691
8352
  staticRemountKey,
7692
- refreshStatic
8353
+ refreshStatic,
8354
+ pendingQuestion,
8355
+ setPendingQuestion
7693
8356
  }), [
7694
8357
  messages,
7695
8358
  addMessage,
@@ -7714,7 +8377,8 @@ var init_SessionContext = __esm({
7714
8377
  selectedModel,
7715
8378
  llmProvider,
7716
8379
  staticRemountKey,
7717
- refreshStatic
8380
+ refreshStatic,
8381
+ pendingQuestion
7718
8382
  ]);
7719
8383
  const usageStatsValue = useMemo(() => ({
7720
8384
  usageStats,
@@ -8445,6 +9109,14 @@ function getCommandDisplay(toolName, input) {
8445
9109
  return input.pattern ? [`"${input.pattern}"${input.path ? ` in ${input.path}` : ""}`] : null;
8446
9110
  case "Glob":
8447
9111
  return input.pattern ? [input.pattern] : null;
9112
+ case "AskUserQuestion": {
9113
+ const questions = input.questions || [];
9114
+ if (questions.length > 0) {
9115
+ const question = questions[0].question || "Question";
9116
+ return [question.length > 80 ? `${question.substring(0, 80)}...` : question];
9117
+ }
9118
+ return null;
9119
+ }
8448
9120
  default:
8449
9121
  return null;
8450
9122
  }
@@ -8460,7 +9132,8 @@ function getToolStyle(toolName) {
8460
9132
  Glob: { icon: "\u{1F50D}", color: theme.tool.search },
8461
9133
  Grep: { icon: "\u{1F50D}", color: theme.tool.search },
8462
9134
  Task: { icon: "\u{1F916}", color: theme.tool.agent },
8463
- TodoWrite: { icon: "\u{1F4DD}", color: theme.text.info }
9135
+ TodoWrite: { icon: "\u{1F4DD}", color: theme.text.info },
9136
+ AskUserQuestion: { icon: "\u2753", color: theme.text.info }
8464
9137
  };
8465
9138
  return styles[toolName] || { icon: "\u{1F527}", color: theme.text.secondary };
8466
9139
  }
@@ -8494,6 +9167,9 @@ function getResultSummary(toolName, result) {
8494
9167
  const lines = result.split("\n").filter((line) => line.trim());
8495
9168
  return lines.length > 0 ? `${lines.length} lines of output` : "No output";
8496
9169
  }
9170
+ case "AskUserQuestion": {
9171
+ return result ? `Answered: ${result.substring(0, 50)}${result.length > 50 ? "..." : ""}` : "Waiting for response...";
9172
+ }
8497
9173
  default:
8498
9174
  return null;
8499
9175
  }
@@ -8578,16 +9254,7 @@ var init_UserMessage = __esm({
8578
9254
  "use strict";
8579
9255
  init_theme();
8580
9256
  UserMessage = ({ text }) => {
8581
- return /* @__PURE__ */ React11.createElement(Box10, { alignItems: "center", flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, { color: theme.text.info }, "\u{1F464} "), /* @__PURE__ */ React11.createElement(
8582
- Box10,
8583
- {
8584
- borderColor: theme.text.dim,
8585
- borderStyle: "round",
8586
- paddingLeft: 1,
8587
- paddingRight: 1
8588
- },
8589
- /* @__PURE__ */ React11.createElement(Text10, { color: theme.text.secondary }, text)
8590
- ));
9257
+ 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));
8591
9258
  };
8592
9259
  }
8593
9260
  });
@@ -8928,7 +9595,7 @@ var init_stdio = __esm({
8928
9595
  });
8929
9596
 
8930
9597
  // src/utils/encryption.ts
8931
- import crypto from "crypto";
9598
+ import crypto2 from "crypto";
8932
9599
  import { hostname, userInfo } from "os";
8933
9600
  function deriveEncryptionKey() {
8934
9601
  const host = hostname();
@@ -8937,7 +9604,7 @@ function deriveEncryptionKey() {
8937
9604
  if (process.env.DEBUG_ENCRYPTION) {
8938
9605
  console.error(`[encryption] hostname=${host}, username=${user}, salt=${salt}`);
8939
9606
  }
8940
- return crypto.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
9607
+ return crypto2.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
8941
9608
  }
8942
9609
  function getEncryptionKey() {
8943
9610
  if (!cachedKey) {
@@ -8947,8 +9614,8 @@ function getEncryptionKey() {
8947
9614
  }
8948
9615
  function encrypt(plaintext) {
8949
9616
  const key = getEncryptionKey();
8950
- const iv = crypto.randomBytes(IV_LENGTH);
8951
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
9617
+ const iv = crypto2.randomBytes(IV_LENGTH);
9618
+ const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
8952
9619
  let encrypted = cipher.update(plaintext, "utf8", "hex");
8953
9620
  encrypted += cipher.final("hex");
8954
9621
  const authTag = cipher.getAuthTag();
@@ -8963,7 +9630,7 @@ function decrypt(encryptedData) {
8963
9630
  const iv = Buffer.from(ivHex, "hex");
8964
9631
  const authTag = Buffer.from(authTagHex, "hex");
8965
9632
  const key = getEncryptionKey();
8966
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
9633
+ const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
8967
9634
  decipher.setAuthTag(authTag);
8968
9635
  let decrypted = decipher.update(encrypted, "hex", "utf8");
8969
9636
  decrypted += decipher.final("utf8");
@@ -9084,7 +9751,7 @@ var init_message_bridge = __esm({
9084
9751
  this.sessionId = sessionId;
9085
9752
  }
9086
9753
  /**
9087
- * Push a user message to be injected into the conversation.
9754
+ * Push a user message to be injected into the session.
9088
9755
  * Call this from the UI when user submits a message during agent execution.
9089
9756
  */
9090
9757
  push(text) {
@@ -9156,15 +9823,15 @@ var init_message_bridge = __esm({
9156
9823
  });
9157
9824
 
9158
9825
  // src/commands/login.ts
9159
- import { spawn as spawn2 } from "child_process";
9160
- import crypto2 from "crypto";
9826
+ import { spawn as spawn3 } from "child_process";
9827
+ import crypto3 from "crypto";
9161
9828
  import http from "http";
9162
9829
  import { platform } from "os";
9163
9830
  function escapeHtml(text) {
9164
9831
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9165
9832
  }
9166
9833
  function generateState() {
9167
- return crypto2.randomBytes(STATE_LENGTH).toString("hex");
9834
+ return crypto3.randomBytes(STATE_LENGTH).toString("hex");
9168
9835
  }
9169
9836
  async function exchangeCodeForToken(code, state) {
9170
9837
  const response = await fetch(`${API_URL}/web/auth/cli-token-exchange`, {
@@ -9197,7 +9864,7 @@ function openBrowser(url) {
9197
9864
  args = [url];
9198
9865
  }
9199
9866
  const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
9200
- spawn2(command, args, options).unref();
9867
+ spawn3(command, args, options).unref();
9201
9868
  }
9202
9869
  function startCallbackServer(port, expectedState) {
9203
9870
  return new Promise((resolve2, reject) => {
@@ -9533,62 +10200,603 @@ var init_login = __esm({
9533
10200
  }
9534
10201
  });
9535
10202
 
9536
- // src/utils/claude-max.ts
9537
- import { execSync as execSync5 } from "child_process";
9538
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
9539
- import { homedir as homedir6 } from "os";
9540
- import { join as join8 } from "path";
9541
- function isClaudeMaxAvailable() {
9542
- const platform2 = process.platform;
9543
- logger.debug("[claude-max] Checking Claude Code credentials", { platform: platform2 });
9544
- if (platform2 === "darwin") {
9545
- return checkMacOSKeychain();
9546
- } else {
9547
- return checkCredentialsFile();
10203
+ // src/utils/claude-oauth.ts
10204
+ var claude_oauth_exports = {};
10205
+ __export(claude_oauth_exports, {
10206
+ ClaudeOAuthService: () => ClaudeOAuthService
10207
+ });
10208
+ import { spawn as spawn4 } from "child_process";
10209
+ import { createHash, randomBytes } from "crypto";
10210
+ import http2 from "http";
10211
+ import { platform as platform2 } from "os";
10212
+ var OAUTH_CONFIG, CALLBACK_PORT, CALLBACK_TIMEOUT_MS2, ClaudeOAuthService;
10213
+ var init_claude_oauth = __esm({
10214
+ "src/utils/claude-oauth.ts"() {
10215
+ "use strict";
10216
+ OAUTH_CONFIG = {
10217
+ clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
10218
+ // Claude Code's client ID
10219
+ authorizationEndpoint: "https://claude.ai/oauth/authorize",
10220
+ tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
10221
+ redirectUri: "http://localhost:8421/callback",
10222
+ // Local callback for CLI
10223
+ scopes: ["user:inference", "user:profile", "org:create_api_key"]
10224
+ };
10225
+ CALLBACK_PORT = 8421;
10226
+ CALLBACK_TIMEOUT_MS2 = 3e5;
10227
+ ClaudeOAuthService = class _ClaudeOAuthService {
10228
+ secretStorage;
10229
+ static TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
10230
+ // 5 minutes
10231
+ pendingCodeVerifier = null;
10232
+ // Store code verifier for PKCE
10233
+ constructor(secretStorage) {
10234
+ this.secretStorage = secretStorage;
10235
+ }
10236
+ /**
10237
+ * Starts the OAuth authorization flow
10238
+ * Opens the default browser for user authentication
10239
+ * Returns after successful authentication
10240
+ */
10241
+ async authorize() {
10242
+ try {
10243
+ const state = this.generateRandomState();
10244
+ const pkce = this.generatePKCEChallenge();
10245
+ this.pendingCodeVerifier = pkce.codeVerifier;
10246
+ const authUrl = this.buildAuthorizationUrl(state, pkce.codeChallenge);
10247
+ console.log("\nAuthenticating with Claude...\n");
10248
+ console.log(`Opening browser to: ${authUrl}
10249
+ `);
10250
+ const tokenPromise = this.startCallbackServer(CALLBACK_PORT, state);
10251
+ try {
10252
+ this.openBrowser(authUrl);
10253
+ } catch (error) {
10254
+ console.warn("Failed to open browser automatically:", error);
10255
+ console.log(`
10256
+ Please manually open this URL in your browser:
10257
+ ${authUrl}
10258
+ `);
10259
+ }
10260
+ await tokenPromise;
10261
+ console.log("\n\u2705 Successfully authenticated with Claude!\n");
10262
+ return { success: true };
10263
+ } catch (error) {
10264
+ this.pendingCodeVerifier = null;
10265
+ return {
10266
+ success: false,
10267
+ error: error instanceof Error ? error.message : "Authentication failed"
10268
+ };
10269
+ }
10270
+ }
10271
+ /**
10272
+ * Start local HTTP server to receive OAuth callback
10273
+ */
10274
+ startCallbackServer(port, expectedState) {
10275
+ return new Promise((resolve2, reject) => {
10276
+ const server = http2.createServer(async (req, res) => {
10277
+ if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
10278
+ res.writeHead(404);
10279
+ res.end("Not Found");
10280
+ return;
10281
+ }
10282
+ const url = new URL(req.url, `http://localhost:${port}`);
10283
+ const code = url.searchParams.get("code");
10284
+ const returnedState = url.searchParams.get("state");
10285
+ const error = url.searchParams.get("error");
10286
+ if (error) {
10287
+ res.writeHead(200, { "Content-Type": "text/html" });
10288
+ res.end(this.buildErrorPage(error));
10289
+ server.close();
10290
+ reject(new Error(`OAuth error: ${error}`));
10291
+ return;
10292
+ }
10293
+ if (returnedState !== expectedState) {
10294
+ const errorMsg = "Security error: state parameter mismatch";
10295
+ res.writeHead(200, { "Content-Type": "text/html" });
10296
+ res.end(this.buildErrorPage(errorMsg));
10297
+ server.close();
10298
+ reject(new Error(errorMsg));
10299
+ return;
10300
+ }
10301
+ if (!code) {
10302
+ const errorMsg = "No authorization code received";
10303
+ res.writeHead(400, { "Content-Type": "text/html" });
10304
+ res.end(this.buildErrorPage(errorMsg));
10305
+ server.close();
10306
+ reject(new Error(errorMsg));
10307
+ return;
10308
+ }
10309
+ try {
10310
+ await this.submitAuthCode(code, returnedState);
10311
+ res.writeHead(200, { "Content-Type": "text/html" });
10312
+ res.end(this.buildSuccessPage());
10313
+ server.close();
10314
+ resolve2();
10315
+ } catch (err) {
10316
+ const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
10317
+ res.writeHead(200, { "Content-Type": "text/html" });
10318
+ res.end(this.buildErrorPage(errorMsg));
10319
+ server.close();
10320
+ reject(err);
10321
+ }
10322
+ });
10323
+ server.on("error", (error) => {
10324
+ if (error.code === "EADDRINUSE") {
10325
+ reject(new Error("Port already in use. Please try again."));
10326
+ } else {
10327
+ reject(error);
10328
+ }
10329
+ });
10330
+ const timeout = setTimeout(() => {
10331
+ server.close();
10332
+ reject(new Error("Authentication timeout - no response received after 5 minutes"));
10333
+ }, CALLBACK_TIMEOUT_MS2);
10334
+ server.on("close", () => {
10335
+ clearTimeout(timeout);
10336
+ });
10337
+ server.listen(port, "127.0.0.1", () => {
10338
+ console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
10339
+ });
10340
+ });
10341
+ }
10342
+ /**
10343
+ * Submits the authorization code and exchanges it for tokens
10344
+ */
10345
+ async submitAuthCode(code, state) {
10346
+ const tokens = await this.exchangeCodeForTokens(code, state);
10347
+ await this.saveTokens(tokens);
10348
+ return tokens;
10349
+ }
10350
+ /**
10351
+ * Exchanges authorization code for access and refresh tokens
10352
+ */
10353
+ async exchangeCodeForTokens(code, state) {
10354
+ if (!this.pendingCodeVerifier) {
10355
+ throw new Error("No PKCE code verifier found. Please start the auth flow first.");
10356
+ }
10357
+ const body = {
10358
+ grant_type: "authorization_code",
10359
+ code,
10360
+ state,
10361
+ // Non-standard: state in body
10362
+ redirect_uri: OAUTH_CONFIG.redirectUri,
10363
+ client_id: OAUTH_CONFIG.clientId,
10364
+ code_verifier: this.pendingCodeVerifier
10365
+ // PKCE verifier
10366
+ };
10367
+ const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
10368
+ method: "POST",
10369
+ headers: {
10370
+ "Content-Type": "application/json"
10371
+ // Non-standard: JSON instead of form-encoded
10372
+ },
10373
+ body: JSON.stringify(body)
10374
+ });
10375
+ this.pendingCodeVerifier = null;
10376
+ if (!response.ok) {
10377
+ const error = await response.text();
10378
+ throw new Error(`Token exchange failed: ${error}`);
10379
+ }
10380
+ const data = await response.json();
10381
+ return {
10382
+ accessToken: data.access_token,
10383
+ refreshToken: data.refresh_token,
10384
+ expiresAt: Date.now() + data.expires_in * 1e3
10385
+ };
10386
+ }
10387
+ /**
10388
+ * Refreshes the access token using the refresh token
10389
+ */
10390
+ async refreshTokens() {
10391
+ const tokens = await this.getTokens();
10392
+ if (!tokens) {
10393
+ throw new Error("No tokens found to refresh");
10394
+ }
10395
+ const body = {
10396
+ grant_type: "refresh_token",
10397
+ refresh_token: tokens.refreshToken,
10398
+ client_id: OAUTH_CONFIG.clientId
10399
+ };
10400
+ const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
10401
+ method: "POST",
10402
+ headers: {
10403
+ "Content-Type": "application/json"
10404
+ },
10405
+ body: JSON.stringify(body)
10406
+ });
10407
+ if (!response.ok) {
10408
+ const error = await response.text();
10409
+ throw new Error(`Token refresh failed: ${error}`);
10410
+ }
10411
+ const data = await response.json();
10412
+ const newTokens = {
10413
+ accessToken: data.access_token,
10414
+ refreshToken: data.refresh_token,
10415
+ expiresAt: Date.now() + data.expires_in * 1e3
10416
+ };
10417
+ await this.saveTokens(newTokens);
10418
+ return newTokens;
10419
+ }
10420
+ /**
10421
+ * Gets the current access token, refreshing if necessary
10422
+ */
10423
+ async getAccessToken() {
10424
+ const tokens = await this.getTokens();
10425
+ if (!tokens) {
10426
+ return null;
10427
+ }
10428
+ if (Date.now() > tokens.expiresAt - _ClaudeOAuthService.TOKEN_REFRESH_BUFFER_MS) {
10429
+ try {
10430
+ const refreshedTokens = await this.refreshTokens();
10431
+ return refreshedTokens.accessToken;
10432
+ } catch (error) {
10433
+ console.warn("Token refresh failed:", error);
10434
+ return null;
10435
+ }
10436
+ }
10437
+ return tokens.accessToken;
10438
+ }
10439
+ /**
10440
+ * Gets the stored tokens
10441
+ */
10442
+ async getTokens() {
10443
+ try {
10444
+ const accessToken = await this.secretStorage.getSecret("claude_oauth_access_token");
10445
+ const refreshToken = await this.secretStorage.getSecret("claude_oauth_refresh_token");
10446
+ const expiresAt = await this.secretStorage.getSecret("claude_oauth_expires_at");
10447
+ if (!accessToken || !refreshToken || !expiresAt) {
10448
+ return null;
10449
+ }
10450
+ return {
10451
+ accessToken,
10452
+ refreshToken,
10453
+ expiresAt: parseInt(expiresAt, 10)
10454
+ };
10455
+ } catch (error) {
10456
+ console.error("Failed to get OAuth tokens:", error);
10457
+ return null;
10458
+ }
10459
+ }
10460
+ /**
10461
+ * Saves OAuth tokens to secure storage
10462
+ */
10463
+ async saveTokens(tokens) {
10464
+ await this.secretStorage.setSecret("claude_oauth_access_token", tokens.accessToken);
10465
+ await this.secretStorage.setSecret("claude_oauth_refresh_token", tokens.refreshToken);
10466
+ await this.secretStorage.setSecret("claude_oauth_expires_at", tokens.expiresAt.toString());
10467
+ }
10468
+ /**
10469
+ * Deletes stored OAuth tokens
10470
+ */
10471
+ async deleteTokens() {
10472
+ await this.secretStorage.deleteSecret("claude_oauth_access_token");
10473
+ await this.secretStorage.deleteSecret("claude_oauth_refresh_token");
10474
+ await this.secretStorage.deleteSecret("claude_oauth_expires_at");
10475
+ }
10476
+ /**
10477
+ * Checks if user is authenticated via OAuth
10478
+ */
10479
+ async isAuthenticated() {
10480
+ const tokens = await this.getTokens();
10481
+ return tokens !== null;
10482
+ }
10483
+ /**
10484
+ * Gets the current OAuth authentication status
10485
+ */
10486
+ async getStatus() {
10487
+ try {
10488
+ const tokens = await this.getTokens();
10489
+ if (!tokens) {
10490
+ return { isAuthenticated: false };
10491
+ }
10492
+ return {
10493
+ isAuthenticated: true,
10494
+ expiresAt: tokens.expiresAt
10495
+ };
10496
+ } catch (error) {
10497
+ return {
10498
+ isAuthenticated: false,
10499
+ error: error instanceof Error ? error.message : "Failed to get OAuth status"
10500
+ };
10501
+ }
10502
+ }
10503
+ /**
10504
+ * Builds the authorization URL with all required parameters
10505
+ */
10506
+ buildAuthorizationUrl(state, codeChallenge) {
10507
+ const params = new URLSearchParams({
10508
+ response_type: "code",
10509
+ client_id: OAUTH_CONFIG.clientId,
10510
+ redirect_uri: OAUTH_CONFIG.redirectUri,
10511
+ scope: OAUTH_CONFIG.scopes.join(" "),
10512
+ state
10513
+ });
10514
+ if (codeChallenge) {
10515
+ params.set("code_challenge", codeChallenge);
10516
+ params.set("code_challenge_method", "S256");
10517
+ }
10518
+ return `${OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
10519
+ }
10520
+ /**
10521
+ * Generates a random state string for CSRF protection
10522
+ */
10523
+ generateRandomState() {
10524
+ return Array.from(crypto.getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
10525
+ }
10526
+ /**
10527
+ * Generates PKCE code verifier and challenge
10528
+ * PKCE (Proof Key for Code Exchange) adds security to OAuth for public clients
10529
+ */
10530
+ generatePKCEChallenge() {
10531
+ const codeVerifier = randomBytes(32).toString("base64url");
10532
+ const hash = createHash("sha256").update(codeVerifier).digest("base64url");
10533
+ return {
10534
+ codeVerifier,
10535
+ codeChallenge: hash
10536
+ };
10537
+ }
10538
+ /**
10539
+ * Open a URL in the default browser cross-platform
10540
+ */
10541
+ openBrowser(url) {
10542
+ const os3 = platform2();
10543
+ let command;
10544
+ let args;
10545
+ switch (os3) {
10546
+ case "darwin":
10547
+ command = "open";
10548
+ args = [url];
10549
+ break;
10550
+ case "win32":
10551
+ command = "start";
10552
+ args = ["", url];
10553
+ break;
10554
+ default:
10555
+ command = "xdg-open";
10556
+ args = [url];
10557
+ break;
10558
+ }
10559
+ const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
10560
+ spawn4(command, args, options).unref();
10561
+ }
10562
+ /**
10563
+ * Build success HTML page
10564
+ */
10565
+ buildSuccessPage() {
10566
+ return `
10567
+ <!DOCTYPE html>
10568
+ <html lang="en">
10569
+ <head>
10570
+ <meta charset="UTF-8" />
10571
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10572
+ <title>Authentication Successful - Supatest CLI</title>
10573
+ <style>
10574
+ body {
10575
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10576
+ display: flex;
10577
+ align-items: center;
10578
+ justify-content: center;
10579
+ height: 100vh;
10580
+ margin: 0;
10581
+ background: #fefefe;
10582
+ }
10583
+ .container {
10584
+ background: white;
10585
+ padding: 3rem 2rem;
10586
+ border-radius: 12px;
10587
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
10588
+ border: 1px solid #e5e7eb;
10589
+ text-align: center;
10590
+ max-width: 400px;
10591
+ }
10592
+ .success-icon {
10593
+ font-size: 48px;
10594
+ margin-bottom: 1rem;
10595
+ }
10596
+ h1 {
10597
+ color: #10b981;
10598
+ margin: 0 0 1rem 0;
10599
+ font-size: 24px;
10600
+ }
10601
+ p {
10602
+ color: #666;
10603
+ margin: 0;
10604
+ line-height: 1.5;
10605
+ }
10606
+ </style>
10607
+ </head>
10608
+ <body>
10609
+ <div class="container">
10610
+ <div class="success-icon">\u2705</div>
10611
+ <h1>Authentication Successful!</h1>
10612
+ <p>You're now authenticated with Claude.</p>
10613
+ <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
10614
+ </div>
10615
+ </body>
10616
+ </html>
10617
+ `;
10618
+ }
10619
+ /**
10620
+ * Build error HTML page
10621
+ */
10622
+ buildErrorPage(errorMessage) {
10623
+ const escapedError = errorMessage.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10624
+ return `
10625
+ <!DOCTYPE html>
10626
+ <html lang="en">
10627
+ <head>
10628
+ <meta charset="UTF-8" />
10629
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10630
+ <title>Authentication Failed - Supatest CLI</title>
10631
+ <style>
10632
+ body {
10633
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10634
+ display: flex;
10635
+ align-items: center;
10636
+ justify-content: center;
10637
+ height: 100vh;
10638
+ margin: 0;
10639
+ background: #fefefe;
10640
+ }
10641
+ .container {
10642
+ background: white;
10643
+ padding: 3rem 2rem;
10644
+ border-radius: 12px;
10645
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
10646
+ border: 1px solid #e5e7eb;
10647
+ text-align: center;
10648
+ max-width: 400px;
10649
+ }
10650
+ .error-icon {
10651
+ font-size: 48px;
10652
+ margin-bottom: 1rem;
10653
+ }
10654
+ h1 {
10655
+ color: #dc2626;
10656
+ margin: 0 0 1rem 0;
10657
+ font-size: 24px;
10658
+ }
10659
+ p {
10660
+ color: #666;
10661
+ margin: 0;
10662
+ line-height: 1.5;
10663
+ }
10664
+ </style>
10665
+ </head>
10666
+ <body>
10667
+ <div class="container">
10668
+ <div class="error-icon">\u274C</div>
10669
+ <h1>Authentication Failed</h1>
10670
+ <p>${escapedError}</p>
10671
+ <p style="margin-top: 1rem;">You can close this window and try again.</p>
10672
+ </div>
10673
+ </body>
10674
+ </html>
10675
+ `;
10676
+ }
10677
+ };
9548
10678
  }
10679
+ });
10680
+
10681
+ // src/utils/secret-storage.ts
10682
+ var secret_storage_exports = {};
10683
+ __export(secret_storage_exports, {
10684
+ deleteSecret: () => deleteSecret,
10685
+ getSecret: () => getSecret,
10686
+ getSecretStorage: () => getSecretStorage,
10687
+ listSecrets: () => listSecrets,
10688
+ setSecret: () => setSecret
10689
+ });
10690
+ import { promises as fs4 } from "fs";
10691
+ import { homedir as homedir6 } from "os";
10692
+ import { dirname as dirname2, join as join8 } from "path";
10693
+ async function getSecret(key) {
10694
+ return storage.getSecret(key);
9549
10695
  }
9550
- function checkMacOSKeychain() {
9551
- try {
9552
- const credentialsJson = execSync5(
9553
- 'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
9554
- { encoding: "utf-8" }
9555
- ).trim();
9556
- if (!credentialsJson) {
9557
- logger.debug("[claude-max] No credentials found in macOS keychain");
9558
- return false;
9559
- }
9560
- const credentials = JSON.parse(credentialsJson);
9561
- const hasOauth = !!credentials.claudeAiOauth?.accessToken;
9562
- logger.debug("[claude-max] macOS keychain credentials check", {
9563
- hasOauth,
9564
- hasRefreshToken: !!credentials.claudeAiOauth?.refreshToken
9565
- });
9566
- return hasOauth;
9567
- } catch (error) {
9568
- logger.debug("[claude-max] Error checking macOS keychain", {
9569
- error: error instanceof Error ? error.message : String(error)
9570
- });
9571
- return false;
9572
- }
10696
+ async function setSecret(key, value) {
10697
+ await storage.setSecret(key, value);
10698
+ }
10699
+ async function deleteSecret(key) {
10700
+ return storage.deleteSecret(key);
10701
+ }
10702
+ async function listSecrets() {
10703
+ return storage.listSecrets();
10704
+ }
10705
+ function getSecretStorage() {
10706
+ return storage;
9573
10707
  }
9574
- function checkCredentialsFile() {
10708
+ var SECRET_FILE_NAME, FileSecretStorage, storage;
10709
+ var init_secret_storage = __esm({
10710
+ "src/utils/secret-storage.ts"() {
10711
+ "use strict";
10712
+ SECRET_FILE_NAME = "secrets.json";
10713
+ FileSecretStorage = class {
10714
+ secretFilePath;
10715
+ constructor() {
10716
+ const rootDirName = process.env.NODE_ENV === "development" ? ".supatest-dev" : ".supatest";
10717
+ const secretsDir = join8(homedir6(), rootDirName, "claude-auth");
10718
+ this.secretFilePath = join8(secretsDir, SECRET_FILE_NAME);
10719
+ }
10720
+ async ensureDirectoryExists() {
10721
+ const dir = dirname2(this.secretFilePath);
10722
+ await fs4.mkdir(dir, { recursive: true, mode: 448 });
10723
+ }
10724
+ async loadSecrets() {
10725
+ try {
10726
+ const data = await fs4.readFile(this.secretFilePath, "utf-8");
10727
+ const secrets = JSON.parse(data);
10728
+ return new Map(Object.entries(secrets));
10729
+ } catch (error) {
10730
+ const err = error;
10731
+ if (err.code === "ENOENT") {
10732
+ return /* @__PURE__ */ new Map();
10733
+ }
10734
+ try {
10735
+ await fs4.unlink(this.secretFilePath);
10736
+ } catch {
10737
+ }
10738
+ return /* @__PURE__ */ new Map();
10739
+ }
10740
+ }
10741
+ async saveSecrets(secrets) {
10742
+ await this.ensureDirectoryExists();
10743
+ const data = Object.fromEntries(secrets);
10744
+ const json = JSON.stringify(data, null, 2);
10745
+ await fs4.writeFile(this.secretFilePath, json, { mode: 384 });
10746
+ }
10747
+ async getSecret(key) {
10748
+ const secrets = await this.loadSecrets();
10749
+ return secrets.get(key) ?? null;
10750
+ }
10751
+ async setSecret(key, value) {
10752
+ const secrets = await this.loadSecrets();
10753
+ secrets.set(key, value);
10754
+ await this.saveSecrets(secrets);
10755
+ }
10756
+ async deleteSecret(key) {
10757
+ const secrets = await this.loadSecrets();
10758
+ if (!secrets.has(key)) {
10759
+ return false;
10760
+ }
10761
+ secrets.delete(key);
10762
+ if (secrets.size === 0) {
10763
+ try {
10764
+ await fs4.unlink(this.secretFilePath);
10765
+ } catch (error) {
10766
+ const err = error;
10767
+ if (err.code !== "ENOENT") {
10768
+ throw error;
10769
+ }
10770
+ }
10771
+ } else {
10772
+ await this.saveSecrets(secrets);
10773
+ }
10774
+ return true;
10775
+ }
10776
+ async listSecrets() {
10777
+ const secrets = await this.loadSecrets();
10778
+ return Array.from(secrets.keys());
10779
+ }
10780
+ };
10781
+ storage = new FileSecretStorage();
10782
+ }
10783
+ });
10784
+
10785
+ // src/utils/claude-max.ts
10786
+ async function isClaudeMaxAvailable() {
10787
+ logger.debug("[claude-max] Checking Supatest OAuth credentials");
9575
10788
  try {
9576
- const credentialsPath = join8(homedir6(), ".claude", ".credentials.json");
9577
- if (!existsSync6(credentialsPath)) {
9578
- logger.debug("[claude-max] Credentials file not found", { path: credentialsPath });
9579
- 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;
9580
10795
  }
9581
- const credentialsJson = readFileSync5(credentialsPath, "utf-8");
9582
- const credentials = JSON.parse(credentialsJson);
9583
- const hasOauth = !!credentials.claudeAiOauth?.accessToken;
9584
- logger.debug("[claude-max] Credentials file check", {
9585
- path: credentialsPath,
9586
- hasOauth,
9587
- hasRefreshToken: !!credentials.claudeAiOauth?.refreshToken
9588
- });
9589
- return hasOauth;
10796
+ logger.debug("[claude-max] No Supatest OAuth credentials found");
10797
+ return false;
9590
10798
  } catch (error) {
9591
- logger.debug("[claude-max] Error checking credentials file", {
10799
+ logger.debug("[claude-max] Error checking Supatest OAuth storage", {
9592
10800
  error: error instanceof Error ? error.message : String(error)
9593
10801
  });
9594
10802
  return false;
@@ -9597,12 +10805,14 @@ function checkCredentialsFile() {
9597
10805
  var init_claude_max = __esm({
9598
10806
  "src/utils/claude-max.ts"() {
9599
10807
  "use strict";
10808
+ init_claude_oauth();
9600
10809
  init_logger();
10810
+ init_secret_storage();
9601
10811
  }
9602
10812
  });
9603
10813
 
9604
10814
  // src/utils/mcp-manager.ts
9605
- 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";
9606
10816
  import { homedir as homedir7 } from "os";
9607
10817
  import { join as join9 } from "path";
9608
10818
  function getGlobalMcpPath() {
@@ -9612,11 +10822,11 @@ function getProjectMcpPath(cwd) {
9612
10822
  return join9(cwd, ".supatest", "mcp.json");
9613
10823
  }
9614
10824
  function loadMcpConfigFromFile(mcpPath, scope) {
9615
- if (!existsSync7(mcpPath)) {
10825
+ if (!existsSync6(mcpPath)) {
9616
10826
  return {};
9617
10827
  }
9618
10828
  try {
9619
- const content = readFileSync6(mcpPath, "utf-8");
10829
+ const content = readFileSync5(mcpPath, "utf-8");
9620
10830
  const config2 = JSON.parse(content);
9621
10831
  if (!config2.mcpServers) {
9622
10832
  return {};
@@ -9650,7 +10860,7 @@ function loadMcpConfig(cwd) {
9650
10860
  }
9651
10861
  function saveMcpConfigToFile(mcpPath, servers) {
9652
10862
  const mcpDir = join9(mcpPath, "..");
9653
- if (!existsSync7(mcpDir)) {
10863
+ if (!existsSync6(mcpDir)) {
9654
10864
  mkdirSync3(mcpDir, { recursive: true });
9655
10865
  }
9656
10866
  const config2 = {
@@ -9834,15 +11044,15 @@ var init_mcp_manager = __esm({
9834
11044
  });
9835
11045
 
9836
11046
  // src/utils/settings-loader.ts
9837
- 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";
9838
11048
  import { join as join10 } from "path";
9839
11049
  function loadSupatestSettings(cwd) {
9840
11050
  const settingsPath = join10(cwd, ".supatest", "settings.json");
9841
- if (!existsSync8(settingsPath)) {
11051
+ if (!existsSync7(settingsPath)) {
9842
11052
  return {};
9843
11053
  }
9844
11054
  try {
9845
- const content = readFileSync7(settingsPath, "utf-8");
11055
+ const content = readFileSync6(settingsPath, "utf-8");
9846
11056
  return JSON.parse(content);
9847
11057
  } catch (error) {
9848
11058
  console.warn(
@@ -9856,7 +11066,7 @@ function saveSupatestSettings(cwd, settings) {
9856
11066
  const settingsDir = join10(cwd, ".supatest");
9857
11067
  const settingsPath = join10(settingsDir, "settings.json");
9858
11068
  try {
9859
- if (!existsSync8(settingsDir)) {
11069
+ if (!existsSync7(settingsDir)) {
9860
11070
  mkdirSync4(settingsDir, { recursive: true });
9861
11071
  }
9862
11072
  const existingSettings = loadSupatestSettings(cwd);
@@ -11035,18 +12245,37 @@ var init_TestSelector = __esm({
11035
12245
  apiClient,
11036
12246
  run,
11037
12247
  onSelect,
11038
- onCancel
12248
+ onCancel,
12249
+ assignments = []
11039
12250
  }) => {
11040
12251
  const [allTests, setAllTests] = useState7([]);
11041
12252
  const [selectedTests, setSelectedTests] = useState7(/* @__PURE__ */ new Set());
11042
12253
  const [cursorIndex, setCursorIndex] = useState7(0);
11043
12254
  const [isLoading, setIsLoading] = useState7(false);
12255
+ const [isLoadingAssignments, setIsLoadingAssignments] = useState7(true);
11044
12256
  const [hasMore, setHasMore] = useState7(true);
11045
12257
  const [totalTests, setTotalTests] = useState7(0);
11046
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));
11047
12263
  useEffect7(() => {
11048
12264
  loadMoreTests();
11049
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
+ };
11050
12279
  const loadMoreTests = async () => {
11051
12280
  if (isLoading || !hasMore) {
11052
12281
  return;
@@ -11072,8 +12301,34 @@ var init_TestSelector = __esm({
11072
12301
  setIsLoading(false);
11073
12302
  }
11074
12303
  };
11075
- const totalItems = allTests.length + 1;
11076
- 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;
11077
12332
  const toggleTest = (testId) => {
11078
12333
  setSelectedTests((prev) => {
11079
12334
  const next = new Set(prev);
@@ -11085,50 +12340,92 @@ var init_TestSelector = __esm({
11085
12340
  return next;
11086
12341
  });
11087
12342
  };
11088
- const selectAll = () => {
11089
- setSelectedTests(new Set(allTests.map((t) => t.id)));
11090
- };
11091
12343
  const selectNone = () => {
11092
12344
  setSelectedTests(/* @__PURE__ */ new Set());
11093
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
+ };
11094
12365
  useInput3((input, key) => {
11095
- if (allTests.length === 0 && !isLoading) {
12366
+ if (allTests.length === 0 && !isLoading && !isLoadingAssignments) {
11096
12367
  if (key.escape || input === "q") {
11097
12368
  onCancel();
11098
12369
  }
11099
12370
  return;
11100
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
+ }
11101
12382
  if (key.upArrow) {
11102
12383
  setCursorIndex((prev) => prev > 0 ? prev - 1 : totalItems - 1);
11103
12384
  } else if (key.downArrow) {
11104
12385
  const newIndex = cursorIndex < totalItems - 1 ? cursorIndex + 1 : 0;
11105
12386
  setCursorIndex(newIndex);
11106
- if (newIndex >= allTests.length - 2 && hasMore && !isLoading) {
12387
+ if (!groupByFile && newIndex >= allTests.length - 2 && hasMore && !isLoading) {
11107
12388
  loadMoreTests();
11108
12389
  }
11109
12390
  } else if (input === " ") {
11110
- if (isOnFixAll) {
11111
- 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
+ }
11112
12401
  } else {
11113
12402
  const testIndex = cursorIndex - 1;
11114
- if (allTests[testIndex]) {
11115
- 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
+ }
11116
12409
  }
11117
12410
  }
11118
- } else if (input === "a") {
11119
- selectAll();
11120
12411
  } else if (input === "n") {
11121
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);
11122
12417
  } else if (key.return) {
11123
- if (isOnFixAll) {
11124
- onSelect(allTests);
12418
+ const availableTests = getFilteredTests();
12419
+ if (isOnFixNext10) {
12420
+ const next10 = availableTests.slice(0, Math.min(10, availableTests.length));
12421
+ onSelect(next10);
11125
12422
  } else if (selectedTests.size > 0) {
11126
- const testsToFix = allTests.filter((t) => selectedTests.has(t.id));
12423
+ const testsToFix = availableTests.filter((t) => selectedTests.has(t.id));
11127
12424
  onSelect(testsToFix);
11128
- } else {
12425
+ } else if (!groupByFile) {
11129
12426
  const testIndex = cursorIndex - 1;
11130
- if (allTests[testIndex]) {
11131
- onSelect([allTests[testIndex]]);
12427
+ if (availableTests[testIndex] && !assignedTestIds.has(availableTests[testIndex].id)) {
12428
+ onSelect([availableTests[testIndex]]);
11132
12429
  }
11133
12430
  }
11134
12431
  } else if (key.escape || input === "q") {
@@ -11145,44 +12442,66 @@ var init_TestSelector = __esm({
11145
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")));
11146
12443
  }
11147
12444
  const testStartIndex = Math.max(0, cursorIndex - 1);
11148
- const adjustedStart = isOnFixAll ? 0 : Math.max(0, testStartIndex - Math.floor(VISIBLE_ITEMS2 / 2));
11149
- const adjustedEnd = Math.min(allTests.length, adjustedStart + VISIBLE_ITEMS2);
11150
- 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);
11151
12448
  const branch = run.git?.branch || "unknown";
11152
12449
  const commit = run.git?.commit?.slice(0, 7) || "";
11153
- 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(
11154
12451
  Text16,
11155
12452
  {
11156
- backgroundColor: isOnFixAll ? theme.text.accent : void 0,
11157
- bold: isOnFixAll,
11158
- color: isOnFixAll ? "black" : theme.text.primary
12453
+ backgroundColor: isOnFixNext10 ? theme.text.accent : void 0,
12454
+ bold: isOnFixNext10,
12455
+ color: isOnFixNext10 ? "black" : theme.text.primary
11159
12456
  },
11160
- isOnFixAll ? "\u25B6 " : " ",
11161
- "[Fix All ",
11162
- allTests.length,
11163
- " Failed Test",
11164
- allTests.length !== 1 ? "s" : "",
12457
+ isOnFixNext10 ? "\u25B6 " : " ",
12458
+ "[Fix Next 10 Available Test",
12459
+ Math.min(10, filteredTests.length) !== 1 ? "s" : "",
11165
12460
  "]"
11166
- )), /* @__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) => {
11167
- const actualIndex = adjustedStart + index;
11168
- const itemIndex = actualIndex + 1;
11169
- const isSelected = itemIndex === cursorIndex;
11170
- const isChecked = selectedTests.has(test.id);
11171
- const checkbox = isChecked ? "[x]" : "[ ]";
11172
- const file = test.file.split("/").pop() || test.file;
11173
- const line = test.location?.line || "";
11174
- const title = test.title;
11175
- const indicator = isSelected ? "\u25B6 " : " ";
11176
- const bgColor = isSelected ? theme.text.accent : void 0;
11177
- 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));
11178
- })), /* @__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 ", allTests.length, " failed tests", hasMore && !isLoading && /* @__PURE__ */ React21.createElement(Text16, { color: theme.text.dim }, " (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..."))));
11179
12498
  };
11180
12499
  }
11181
12500
  });
11182
12501
 
11183
12502
  // src/ui/components/FixFlow.tsx
11184
12503
  import { Box as Box19, Text as Text17, useInput as useInput4 } from "ink";
11185
- import React22, { useState as useState8 } from "react";
12504
+ import React22, { useEffect as useEffect8, useState as useState8 } from "react";
11186
12505
  var FixFlow;
11187
12506
  var init_FixFlow = __esm({
11188
12507
  "src/ui/components/FixFlow.tsx"() {
@@ -11201,6 +12520,8 @@ var init_FixFlow = __esm({
11201
12520
  const [step, setStep] = useState8(initialRunId ? "select-run" : "select-run");
11202
12521
  const [selectedRun, setSelectedRun] = useState8(null);
11203
12522
  const [selectedTests, setSelectedTests] = useState8([]);
12523
+ const [assignments, setAssignments] = useState8([]);
12524
+ const [assignmentIds, setAssignmentIds] = useState8(/* @__PURE__ */ new Map());
11204
12525
  const [loadingProgress, setLoadingProgress] = useState8({ current: 0, total: 0 });
11205
12526
  const [loadError, setLoadError] = useState8(null);
11206
12527
  useInput4((input, key) => {
@@ -11210,7 +12531,7 @@ var init_FixFlow = __esm({
11210
12531
  setStep("select-run");
11211
12532
  } else if (step === "select-run") {
11212
12533
  onCancel();
11213
- } else if (step === "loading-details" && loadError) {
12534
+ } else if ((step === "loading-details" || step === "claiming-tests" || step === "error") && loadError) {
11214
12535
  setLoadError(null);
11215
12536
  setStep("select-tests");
11216
12537
  }
@@ -11219,13 +12540,47 @@ var init_FixFlow = __esm({
11219
12540
  const handleRunSelect = (run) => {
11220
12541
  setSelectedRun(run);
11221
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
+ }
11222
12552
  };
11223
12553
  const handleTestsSelect = async (tests) => {
11224
12554
  setSelectedTests(tests);
11225
- setStep("loading-details");
11226
- setLoadingProgress({ current: 0, total: tests.length });
12555
+ setStep("claiming-tests");
11227
12556
  setLoadError(null);
11228
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 });
11229
12584
  const testDetails = [];
11230
12585
  const batchSize = 5;
11231
12586
  for (let i = 0; i < tests.length; i += batchSize) {
@@ -11241,7 +12596,25 @@ var init_FixFlow = __esm({
11241
12596
  setStep("fixing");
11242
12597
  onStartFix(prompt, tests);
11243
12598
  } catch (err) {
11244
- 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");
11245
12618
  }
11246
12619
  };
11247
12620
  const handleRunCancel = () => {
@@ -11249,8 +12622,47 @@ var init_FixFlow = __esm({
11249
12622
  };
11250
12623
  const handleTestCancel = () => {
11251
12624
  setSelectedRun(null);
12625
+ setAssignments([]);
11252
12626
  setStep("select-run");
11253
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]);
11254
12666
  switch (step) {
11255
12667
  case "select-run":
11256
12668
  return /* @__PURE__ */ React22.createElement(
@@ -11270,20 +12682,25 @@ var init_FixFlow = __esm({
11270
12682
  TestSelector,
11271
12683
  {
11272
12684
  apiClient,
12685
+ assignments,
11273
12686
  onCancel: handleTestCancel,
11274
12687
  onSelect: handleTestsSelect,
11275
12688
  run: selectedRun
11276
12689
  }
11277
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...")));
11278
12693
  case "loading-details":
11279
12694
  if (loadError) {
11280
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")));
11281
12696
  }
11282
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")));
11283
12700
  case "fixing":
11284
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...")));
11285
12702
  case "complete":
11286
- 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...")));
11287
12704
  default:
11288
12705
  return null;
11289
12706
  }
@@ -11291,91 +12708,144 @@ var init_FixFlow = __esm({
11291
12708
  }
11292
12709
  });
11293
12710
 
11294
- // src/ui/utils/file-completion.ts
11295
- import fs4 from "fs";
12711
+ // src/ui/utils/file-search.ts
11296
12712
  import path4 from "path";
11297
- function shouldIgnore(name, isDir) {
11298
- if (name.startsWith(".")) return true;
11299
- if (isDir) return IGNORED_DIRS.has(name);
11300
- const ext = path4.extname(name).toLowerCase();
11301
- return IGNORED_EXTENSIONS.has(ext);
11302
- }
11303
- function getFiles(rootDir = process.cwd(), maxDepth = 3) {
11304
- const files = [];
11305
- function scan(dir, depth) {
11306
- if (depth > maxDepth) return;
11307
- try {
11308
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
11309
- for (const entry of entries) {
11310
- if (shouldIgnore(entry.name, entry.isDirectory())) continue;
11311
- const fullPath = path4.join(dir, entry.name);
11312
- const relativePath = path4.relative(rootDir, fullPath);
11313
- if (entry.isDirectory()) {
11314
- scan(fullPath, depth + 1);
11315
- } else {
11316
- files.push(relativePath);
11317
- }
11318
- }
11319
- } 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;
11320
12728
  }
11321
12729
  }
11322
- scan(rootDir, 0);
11323
- 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);
11324
12770
  }
11325
- var IGNORED_DIRS, IGNORED_EXTENSIONS;
11326
- var init_file_completion = __esm({
11327
- "src/ui/utils/file-completion.ts"() {
12771
+ var init_file_search = __esm({
12772
+ "src/ui/utils/file-search.ts"() {
11328
12773
  "use strict";
11329
- IGNORED_DIRS = /* @__PURE__ */ new Set([
11330
- "node_modules",
11331
- ".git",
11332
- ".turbo",
11333
- "dist",
11334
- "build",
11335
- "coverage",
11336
- ".next",
11337
- ".cache"
11338
- ]);
11339
- IGNORED_EXTENSIONS = /* @__PURE__ */ new Set([
11340
- ".png",
11341
- ".jpg",
11342
- ".jpeg",
11343
- ".gif",
11344
- ".ico",
11345
- ".svg",
11346
- ".woff",
11347
- ".woff2",
11348
- ".ttf",
11349
- ".eot",
11350
- ".mp4",
11351
- ".webm",
11352
- ".mp3",
11353
- ".wav",
11354
- ".zip",
11355
- ".tar",
11356
- ".gz",
11357
- ".7z",
11358
- ".rar",
11359
- ".pdf",
11360
- ".doc",
11361
- ".docx",
11362
- ".xls",
11363
- ".xlsx",
11364
- ".exe",
11365
- ".dll",
11366
- ".so",
11367
- ".dylib",
11368
- ".bin",
11369
- ".map",
11370
- ".lock",
11371
- ".tsbuildinfo"
11372
- ]);
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();
11373
12843
  }
11374
12844
  });
11375
12845
 
11376
12846
  // src/ui/components/ModelSelector.tsx
11377
12847
  import { Box as Box20, Text as Text18, useInput as useInput5 } from "ink";
11378
- import React23, { useState as useState9 } from "react";
12848
+ import React23, { useState as useState10 } from "react";
11379
12849
  function getAvailableModels(isClaudeMax) {
11380
12850
  if (isClaudeMax) {
11381
12851
  return AVAILABLE_MODELS.map((m) => ({
@@ -11410,7 +12880,7 @@ var init_ModelSelector = __esm({
11410
12880
  }) => {
11411
12881
  const models = getAvailableModels(isClaudeMax);
11412
12882
  const currentIndex = models.findIndex((m) => m.id === currentModel);
11413
- const [selectedIndex, setSelectedIndex] = useState9(currentIndex >= 0 ? currentIndex : 0);
12883
+ const [selectedIndex, setSelectedIndex] = useState10(currentIndex >= 0 ? currentIndex : 0);
11414
12884
  useInput5((input, key) => {
11415
12885
  if (key.upArrow) {
11416
12886
  setSelectedIndex((prev) => prev > 0 ? prev - 1 : models.length - 1);
@@ -11452,7 +12922,7 @@ var init_ModelSelector = __esm({
11452
12922
  import path5 from "path";
11453
12923
  import chalk4 from "chalk";
11454
12924
  import { Box as Box21, Text as Text19 } from "ink";
11455
- 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";
11456
12926
  var InputPromptInner, InputPrompt;
11457
12927
  var init_InputPrompt = __esm({
11458
12928
  "src/ui/components/InputPrompt.tsx"() {
@@ -11460,13 +12930,13 @@ var init_InputPrompt = __esm({
11460
12930
  init_shared_es();
11461
12931
  init_command_discovery();
11462
12932
  init_SessionContext();
12933
+ init_useFileCompletion();
11463
12934
  init_useKeypress();
11464
- init_file_completion();
11465
12935
  init_theme();
11466
12936
  init_ModelSelector();
11467
12937
  InputPromptInner = forwardRef(({
11468
12938
  onSubmit,
11469
- 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)",
11470
12940
  disabled = false,
11471
12941
  onHelpToggle,
11472
12942
  cwd,
@@ -11477,13 +12947,38 @@ var init_InputPrompt = __esm({
11477
12947
  }, ref) => {
11478
12948
  const { agentMode, selectedModel, setSelectedModel, isAgentRunning } = useSession();
11479
12949
  const { usageStats } = useUsageStats();
11480
- const [value, setValue] = useState10("");
11481
- const [cursorOffset, setCursorOffset] = useState10(0);
11482
- const [allFiles, setAllFiles] = useState10([]);
11483
- const [suggestions, setSuggestions] = useState10([]);
11484
- const [activeSuggestion, setActiveSuggestion] = useState10(0);
11485
- const [showSuggestions, setShowSuggestions] = useState10(false);
11486
- const [mentionStartIndex, setMentionStartIndex] = useState10(-1);
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("");
11487
12982
  const BUILTIN_SLASH_COMMANDS = [
11488
12983
  { name: "/help", desc: "Show help" },
11489
12984
  { name: "/resume", desc: "Resume session" },
@@ -11499,9 +12994,9 @@ var init_InputPrompt = __esm({
11499
12994
  { name: "/logout", desc: "Log out" },
11500
12995
  { name: "/exit", desc: "Exit CLI" }
11501
12996
  ];
11502
- const [customCommands, setCustomCommands] = useState10([]);
11503
- const [isSlashCommand, setIsSlashCommand] = useState10(false);
11504
- useEffect9(() => {
12997
+ const [customCommands, setCustomCommands] = useState11([]);
12998
+ const [isSlashCommand, setIsSlashCommand] = useState11(false);
12999
+ useEffect10(() => {
11505
13000
  try {
11506
13001
  const projectDir = cwd || process.cwd();
11507
13002
  const discovered = discoverCommands(projectDir);
@@ -11517,29 +13012,55 @@ var init_InputPrompt = __esm({
11517
13012
  useImperativeHandle(ref, () => ({
11518
13013
  clear: () => {
11519
13014
  setValue("");
11520
- setCursorOffset(0);
13015
+ setCursorPos([0, 0]);
11521
13016
  setShowSuggestions(false);
11522
13017
  onInputChange?.("");
11523
13018
  }
11524
13019
  }));
11525
- useEffect9(() => {
11526
- setTimeout(() => {
11527
- try {
11528
- const files = getFiles();
11529
- setAllFiles(files);
11530
- } catch (e) {
11531
- }
11532
- }, 100);
11533
- }, []);
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]);
11534
13032
  const updateValue = (newValue, newCursor) => {
11535
13033
  setValue(newValue);
11536
- 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
+ })();
11537
13049
  checkSuggestions(newValue, newCursor);
11538
13050
  onInputChange?.(newValue);
11539
13051
  };
11540
13052
  const checkSuggestions = (text, cursor) => {
11541
- if (text.startsWith("/") && cursor <= text.length && !text.includes(" ", 1)) {
11542
- 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);
11543
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);
11544
13065
  const customMatches = customCommands.filter((cmd) => cmd.name.slice(1).toLowerCase().startsWith(query2.toLowerCase())).map((cmd) => cmd.desc ? `${cmd.name} ${cmd.desc}` : cmd.name);
11545
13066
  const matches = [];
@@ -11561,25 +13082,26 @@ var init_InputPrompt = __esm({
11561
13082
  }
11562
13083
  }
11563
13084
  setIsSlashCommand(false);
11564
- const textBeforeCursor = text.slice(0, cursor);
11565
- const lastAt = textBeforeCursor.lastIndexOf("@");
11566
- if (lastAt !== -1) {
11567
- const isValidStart = lastAt === 0 || /\s/.test(textBeforeCursor[lastAt - 1]);
11568
- if (isValidStart) {
11569
- const query2 = textBeforeCursor.slice(lastAt + 1);
11570
- if (!/\s/.test(query2)) {
11571
- const matches = allFiles.filter((f) => f.toLowerCase().includes(query2.toLowerCase())).slice(0, 5);
11572
- if (matches.length > 0) {
11573
- setSuggestions(matches);
11574
- setShowSuggestions(true);
11575
- setActiveSuggestion(0);
11576
- setMentionStartIndex(lastAt);
11577
- return;
11578
- }
11579
- }
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;
11580
13099
  }
11581
13100
  }
11582
- setShowSuggestions(false);
13101
+ if (!foundMention) {
13102
+ setMentionQuery("");
13103
+ setShowSuggestions(false);
13104
+ }
11583
13105
  };
11584
13106
  const completeSuggestion = (submit = false) => {
11585
13107
  if (!showSuggestions || suggestions.length === 0) return;
@@ -11588,27 +13110,35 @@ var init_InputPrompt = __esm({
11588
13110
  if (submit) {
11589
13111
  onSubmit(selectedCmd);
11590
13112
  setValue("");
11591
- setCursorOffset(0);
13113
+ setCursorPos([0, 0]);
11592
13114
  setShowSuggestions(false);
11593
13115
  onInputChange?.("");
11594
13116
  return;
11595
13117
  }
11596
- updateValue(selectedCmd, selectedCmd.length);
13118
+ updateValue(selectedCmd, [0, selectedCmd.length]);
11597
13119
  setShowSuggestions(false);
11598
13120
  return;
11599
13121
  }
11600
13122
  const selectedFile = suggestions[activeSuggestion];
11601
- const textBeforeMention = value.slice(0, mentionStartIndex);
11602
- const textAfterCursor = value.slice(cursorOffset);
11603
- const newValue = textBeforeMention + "@" + selectedFile + " " + textAfterCursor;
11604
- const newCursor = mentionStartIndex + 1 + selectedFile.length + 1;
11605
- 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);
11606
13135
  setShowSuggestions(false);
11607
13136
  };
11608
13137
  useKeypress(
11609
13138
  (key) => {
11610
13139
  if (disabled) return;
11611
13140
  const input = key.sequence;
13141
+ const cursorOffset = getCursorOffset();
11612
13142
  if (input.length > 1 && (input.includes("/") || input.includes("\\"))) {
11613
13143
  let cleanPath = input.trim();
11614
13144
  if (cleanPath.startsWith('"') && cleanPath.endsWith('"') || cleanPath.startsWith("'") && cleanPath.endsWith("'")) {
@@ -11632,7 +13162,7 @@ var init_InputPrompt = __esm({
11632
13162
  return;
11633
13163
  }
11634
13164
  }
11635
- if (key.shift && key.name === "m" && !isAgentRunning) {
13165
+ if ((key.ctrl || key.meta) && key.shift && key.name === "m" && !isAgentRunning) {
11636
13166
  setSelectedModel(getNextModel(selectedModel, isClaudeMax));
11637
13167
  return;
11638
13168
  }
@@ -11681,7 +13211,7 @@ var init_InputPrompt = __esm({
11681
13211
  if (value.trim()) {
11682
13212
  onSubmit(value.trim());
11683
13213
  setValue("");
11684
- setCursorOffset(0);
13214
+ setCursorPos([0, 0]);
11685
13215
  setShowSuggestions(false);
11686
13216
  onInputChange?.("");
11687
13217
  }
@@ -11692,11 +13222,55 @@ var init_InputPrompt = __esm({
11692
13222
  updateValue(newValue, cursorOffset - 1);
11693
13223
  }
11694
13224
  } else if (key.name === "left") {
11695
- 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
+ }
11696
13233
  } else if (key.name === "right") {
11697
- setCursorOffset(Math.min(value.length, cursorOffset + 1));
11698
- } else if (key.ctrl && input === "u") {
11699
- 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);
11700
13274
  } else if (key.name === "tab" && !showSuggestions) {
11701
13275
  } else if (key.paste) {
11702
13276
  const newValue = value.slice(0, cursorOffset) + input + value.slice(cursorOffset);
@@ -11710,18 +13284,7 @@ var init_InputPrompt = __esm({
11710
13284
  );
11711
13285
  const lines = value ? value.split("\n") : [];
11712
13286
  const hasContent = value.trim().length > 0;
11713
- let cursorLine = 0;
11714
- let cursorCol = cursorOffset;
11715
- let charCount = 0;
11716
- for (let i = 0; i < lines.length; i++) {
11717
- const lineLength = lines[i].length;
11718
- if (charCount + lineLength >= cursorOffset) {
11719
- cursorLine = i;
11720
- cursorCol = cursorOffset - charCount;
11721
- break;
11722
- }
11723
- charCount += lineLength + 1;
11724
- }
13287
+ const [cursorLine, cursorCol] = cursorPos;
11725
13288
  return /* @__PURE__ */ React24.createElement(Box21, { flexDirection: "column", width: "100%" }, showSuggestions && /* @__PURE__ */ React24.createElement(
11726
13289
  Box21,
11727
13290
  {
@@ -11731,13 +13294,13 @@ var init_InputPrompt = __esm({
11731
13294
  marginBottom: 0,
11732
13295
  paddingX: 1
11733
13296
  },
11734
- suggestions.map((item, idx) => {
13297
+ isLoadingFiles && !isSlashCommand ? /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "Searching files...") : suggestions.length > 0 ? suggestions.map((item, idx) => {
11735
13298
  const isSeparator = item.startsWith("\u2500\u2500\u2500\u2500\u2500");
11736
13299
  if (isSeparator) {
11737
13300
  return /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim, key: item }, " ", item);
11738
13301
  }
11739
13302
  return /* @__PURE__ */ React24.createElement(Text19, { color: idx === activeSuggestion ? theme.text.accent : theme.text.dim, key: item }, idx === activeSuggestion ? "\u276F " : " ", item);
11740
- })
13303
+ }) : /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "No matches")
11741
13304
  ), /* @__PURE__ */ React24.createElement(
11742
13305
  Box21,
11743
13306
  {
@@ -11758,7 +13321,7 @@ var init_InputPrompt = __esm({
11758
13321
  }
11759
13322
  return /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.primary, key: idx }, line);
11760
13323
  })), !hasContent && disabled && /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim, italic: true }, "Waiting for agent to complete...")))
11761
- ), /* @__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)")))));
11762
13325
  });
11763
13326
  InputPromptInner.displayName = "InputPromptInner";
11764
13327
  InputPrompt = memo3(InputPromptInner);
@@ -11768,19 +13331,19 @@ var init_InputPrompt = __esm({
11768
13331
 
11769
13332
  // src/ui/components/McpAddDialog.tsx
11770
13333
  import { Box as Box22, Text as Text20, useInput as useInput6 } from "ink";
11771
- import React25, { useState as useState11 } from "react";
13334
+ import React25, { useState as useState12 } from "react";
11772
13335
  var McpAddDialog;
11773
13336
  var init_McpAddDialog = __esm({
11774
13337
  "src/ui/components/McpAddDialog.tsx"() {
11775
13338
  "use strict";
11776
13339
  init_theme();
11777
13340
  McpAddDialog = ({ onConfirm, onCancel }) => {
11778
- const [mode, setMode] = useState11("name" /* Name */);
11779
- const [serverName, setServerName] = useState11("");
11780
- const [command, setCommand] = useState11("");
11781
- const [args, setArgs] = useState11("");
11782
- const [description, setDescription] = useState11("");
11783
- 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");
11784
13347
  useInput6((input, key) => {
11785
13348
  if (key.escape) {
11786
13349
  onCancel();
@@ -11875,7 +13438,7 @@ var init_McpAddDialog = __esm({
11875
13438
 
11876
13439
  // src/ui/components/McpServerSelector.tsx
11877
13440
  import { Box as Box23, Text as Text21, useInput as useInput7 } from "ink";
11878
- import React26, { useState as useState12 } from "react";
13441
+ import React26, { useState as useState13 } from "react";
11879
13442
  var McpServerSelector;
11880
13443
  var init_McpServerSelector = __esm({
11881
13444
  "src/ui/components/McpServerSelector.tsx"() {
@@ -11887,7 +13450,7 @@ var init_McpServerSelector = __esm({
11887
13450
  onSelect,
11888
13451
  onCancel
11889
13452
  }) => {
11890
- const [selectedIndex, setSelectedIndex] = useState12(0);
13453
+ const [selectedIndex, setSelectedIndex] = useState13(0);
11891
13454
  useInput7((input, key) => {
11892
13455
  if (key.escape) {
11893
13456
  onCancel();
@@ -11946,7 +13509,7 @@ var init_McpServerSelector = __esm({
11946
13509
  // src/ui/components/McpServersDisplay.tsx
11947
13510
  import { Box as Box24, Text as Text22, useInput as useInput8 } from "ink";
11948
13511
  import Spinner3 from "ink-spinner";
11949
- import React27, { useEffect as useEffect10, useState as useState13 } from "react";
13512
+ import React27, { useEffect as useEffect11, useState as useState14 } from "react";
11950
13513
  var McpServersDisplay;
11951
13514
  var init_McpServersDisplay = __esm({
11952
13515
  "src/ui/components/McpServersDisplay.tsx"() {
@@ -11960,9 +13523,9 @@ var init_McpServersDisplay = __esm({
11960
13523
  onTest,
11961
13524
  cwd
11962
13525
  }) => {
11963
- const [servers, setServers] = useState13([]);
11964
- const [isTestingAll, setIsTestingAll] = useState13(false);
11965
- useEffect10(() => {
13526
+ const [servers, setServers] = useState14([]);
13527
+ const [isTestingAll, setIsTestingAll] = useState14(false);
13528
+ useEffect11(() => {
11966
13529
  const projectDir = cwd || process.cwd();
11967
13530
  const loadedServers = loadMcpConfig(projectDir);
11968
13531
  const serversArray = Object.values(loadedServers).map((server) => ({
@@ -12032,7 +13595,7 @@ var init_McpServersDisplay = __esm({
12032
13595
 
12033
13596
  // src/ui/components/ProviderSelector.tsx
12034
13597
  import { Box as Box25, Text as Text23, useInput as useInput9 } from "ink";
12035
- import React28, { useState as useState14 } from "react";
13598
+ import React28, { useState as useState15 } from "react";
12036
13599
  var PROVIDERS, ProviderSelector;
12037
13600
  var init_ProviderSelector = __esm({
12038
13601
  "src/ui/components/ProviderSelector.tsx"() {
@@ -12047,22 +13610,19 @@ var init_ProviderSelector = __esm({
12047
13610
  {
12048
13611
  id: "claude-max",
12049
13612
  name: "Claude Max",
12050
- description: "Direct Claude Max subscription (requires Claude Code login)"
13613
+ description: "Direct Claude Max subscription (requires OAuth login)"
12051
13614
  }
12052
13615
  ];
12053
13616
  ProviderSelector = ({
12054
13617
  currentProvider,
12055
13618
  onSelect,
12056
- onCancel,
12057
- claudeMaxAvailable
13619
+ onCancel
12058
13620
  }) => {
12059
- const availableProviders = PROVIDERS.filter(
12060
- (p) => p.id !== "claude-max" || claudeMaxAvailable
12061
- );
13621
+ const availableProviders = PROVIDERS;
12062
13622
  const currentIndex = availableProviders.findIndex(
12063
13623
  (p) => p.id === currentProvider
12064
13624
  );
12065
- const [selectedIndex, setSelectedIndex] = useState14(
13625
+ const [selectedIndex, setSelectedIndex] = useState15(
12066
13626
  currentIndex >= 0 ? currentIndex : 0
12067
13627
  );
12068
13628
  useInput9((input, key) => {
@@ -12102,9 +13662,125 @@ var init_ProviderSelector = __esm({
12102
13662
  }
12103
13663
  });
12104
13664
 
12105
- // src/ui/components/SessionSelector.tsx
13665
+ // src/ui/components/QuestionSelector.tsx
12106
13666
  import { Box as Box26, Text as Text24, useInput as useInput10 } from "ink";
12107
- import React29, { useEffect as useEffect11, useState as useState15 } from "react";
13667
+ import React29, { useState as useState16 } from "react";
13668
+ var QuestionSelector;
13669
+ var init_QuestionSelector = __esm({
13670
+ "src/ui/components/QuestionSelector.tsx"() {
13671
+ "use strict";
13672
+ init_theme();
13673
+ QuestionSelector = ({
13674
+ question,
13675
+ options,
13676
+ multiSelect = false,
13677
+ allowCustomAnswer = true,
13678
+ // Default to true - always show "Other" option
13679
+ onSubmit,
13680
+ onCancel
13681
+ }) => {
13682
+ const hasOptions = options && options.length > 0;
13683
+ const allOptions = hasOptions && allowCustomAnswer ? [...options, { label: "Other", description: "Provide a custom response" }] : options;
13684
+ const [selectedIndex, setSelectedIndex] = useState16(0);
13685
+ const [selectedItems, setSelectedItems] = useState16(/* @__PURE__ */ new Set());
13686
+ const [customInputMode, setCustomInputMode] = useState16(!hasOptions);
13687
+ const [customAnswer, setCustomAnswer] = useState16("");
13688
+ useInput10((input, key) => {
13689
+ if (customInputMode) {
13690
+ if (key.return) {
13691
+ if (customAnswer.trim()) {
13692
+ onSubmit([customAnswer.trim()]);
13693
+ }
13694
+ } else if (key.escape) {
13695
+ if (!hasOptions) {
13696
+ onCancel();
13697
+ } else {
13698
+ setCustomInputMode(false);
13699
+ setCustomAnswer("");
13700
+ }
13701
+ } else if (key.backspace || key.delete) {
13702
+ setCustomAnswer((prev) => prev.slice(0, -1));
13703
+ } else if (input && !key.ctrl && !key.meta) {
13704
+ setCustomAnswer((prev) => prev + input);
13705
+ }
13706
+ return;
13707
+ }
13708
+ if (key.upArrow) {
13709
+ setSelectedIndex((prev) => prev > 0 ? prev - 1 : allOptions.length - 1);
13710
+ } else if (key.downArrow) {
13711
+ setSelectedIndex((prev) => prev < allOptions.length - 1 ? prev + 1 : 0);
13712
+ } else if (key.return) {
13713
+ const selectedOption = allOptions[selectedIndex];
13714
+ if (selectedOption.label === "Other" && allowCustomAnswer) {
13715
+ setCustomInputMode(true);
13716
+ return;
13717
+ }
13718
+ if (multiSelect) {
13719
+ setSelectedItems((prev) => {
13720
+ const newSet = new Set(prev);
13721
+ if (newSet.has(selectedIndex)) {
13722
+ newSet.delete(selectedIndex);
13723
+ } else {
13724
+ newSet.add(selectedIndex);
13725
+ }
13726
+ return newSet;
13727
+ });
13728
+ } else {
13729
+ onSubmit([selectedOption.label]);
13730
+ }
13731
+ } else if (input === " " && multiSelect) {
13732
+ setSelectedItems((prev) => {
13733
+ const newSet = new Set(prev);
13734
+ if (newSet.has(selectedIndex)) {
13735
+ newSet.delete(selectedIndex);
13736
+ } else {
13737
+ newSet.add(selectedIndex);
13738
+ }
13739
+ return newSet;
13740
+ });
13741
+ } else if (key.escape || input === "q") {
13742
+ onCancel();
13743
+ } else if (input === "s" && multiSelect && selectedItems.size > 0) {
13744
+ const answers = Array.from(selectedItems).sort((a, b) => a - b).map((idx) => allOptions[idx].label);
13745
+ onSubmit(answers);
13746
+ } else if ((input === "o" || input === "O") && allowCustomAnswer) {
13747
+ setCustomInputMode(true);
13748
+ }
13749
+ });
13750
+ if (customInputMode) {
13751
+ 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")));
13752
+ }
13753
+ 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) => {
13754
+ const isSelected = index === selectedIndex;
13755
+ const isChosen = selectedItems.has(index);
13756
+ const indicator = isSelected ? "\u25B6 " : " ";
13757
+ const checkbox = multiSelect ? isChosen ? "[\u2713] " : "[ ] " : "";
13758
+ return /* @__PURE__ */ React29.createElement(Box26, { flexDirection: "column", gap: 0, key: `${option.label}-${index}` }, /* @__PURE__ */ React29.createElement(Box26, null, /* @__PURE__ */ React29.createElement(
13759
+ Text24,
13760
+ {
13761
+ backgroundColor: isSelected ? theme.text.accent : void 0,
13762
+ bold: isSelected,
13763
+ color: isSelected ? "black" : theme.text.primary
13764
+ },
13765
+ indicator,
13766
+ checkbox,
13767
+ option.label
13768
+ )), /* @__PURE__ */ React29.createElement(Box26, { marginLeft: multiSelect ? 6 : 4 }, /* @__PURE__ */ React29.createElement(
13769
+ Text24,
13770
+ {
13771
+ color: isSelected ? theme.text.primary : theme.text.dim,
13772
+ dimColor: !isSelected
13773
+ },
13774
+ option.description
13775
+ )));
13776
+ })), /* @__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")));
13777
+ };
13778
+ }
13779
+ });
13780
+
13781
+ // src/ui/components/SessionSelector.tsx
13782
+ import { Box as Box27, Text as Text25, useInput as useInput11 } from "ink";
13783
+ import React30, { useEffect as useEffect12, useState as useState17 } from "react";
12108
13784
  function getSessionPrefix(authMethod) {
12109
13785
  return authMethod === "api-key" ? "[Team]" : "[Me]";
12110
13786
  }
@@ -12119,13 +13795,13 @@ var init_SessionSelector = __esm({
12119
13795
  onSelect,
12120
13796
  onCancel
12121
13797
  }) => {
12122
- const [allSessions, setAllSessions] = useState15([]);
12123
- const [selectedIndex, setSelectedIndex] = useState15(0);
12124
- const [isLoading, setIsLoading] = useState15(false);
12125
- const [hasMore, setHasMore] = useState15(true);
12126
- const [totalSessions, setTotalSessions] = useState15(0);
12127
- const [error, setError] = useState15(null);
12128
- useEffect11(() => {
13798
+ const [allSessions, setAllSessions] = useState17([]);
13799
+ const [selectedIndex, setSelectedIndex] = useState17(0);
13800
+ const [isLoading, setIsLoading] = useState17(false);
13801
+ const [hasMore, setHasMore] = useState17(true);
13802
+ const [totalSessions, setTotalSessions] = useState17(0);
13803
+ const [error, setError] = useState17(null);
13804
+ useEffect12(() => {
12129
13805
  loadMoreSessions();
12130
13806
  }, []);
12131
13807
  const loadMoreSessions = async () => {
@@ -12151,7 +13827,7 @@ var init_SessionSelector = __esm({
12151
13827
  setIsLoading(false);
12152
13828
  }
12153
13829
  };
12154
- useInput10((input, key) => {
13830
+ useInput11((input, key) => {
12155
13831
  if (allSessions.length === 0) {
12156
13832
  if (key.escape || input === "q") {
12157
13833
  onCancel();
@@ -12175,13 +13851,13 @@ var init_SessionSelector = __esm({
12175
13851
  }
12176
13852
  });
12177
13853
  if (error) {
12178
- 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")));
13854
+ 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")));
12179
13855
  }
12180
13856
  if (allSessions.length === 0 && isLoading) {
12181
- 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"));
13857
+ 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"));
12182
13858
  }
12183
13859
  if (allSessions.length === 0 && !isLoading) {
12184
- 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")));
13860
+ 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")));
12185
13861
  }
12186
13862
  const VISIBLE_ITEMS3 = 10;
12187
13863
  let startIndex;
@@ -12201,7 +13877,7 @@ var init_SessionSelector = __esm({
12201
13877
  const visibleSessions = allSessions.slice(startIndex, endIndex);
12202
13878
  const MAX_TITLE_WIDTH = 50;
12203
13879
  const PREFIX_WIDTH = 6;
12204
- 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) => {
13880
+ 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) => {
12205
13881
  const actualIndex = startIndex + index;
12206
13882
  const isSelected = actualIndex === selectedIndex;
12207
13883
  const title = item.session.title || "Untitled session";
@@ -12225,18 +13901,18 @@ var init_SessionSelector = __esm({
12225
13901
  const prefixColor = item.prefix === "[Me]" ? "cyan" : "yellow";
12226
13902
  const indicator = isSelected ? "\u25B6 " : " ";
12227
13903
  const bgColor = isSelected ? theme.text.accent : void 0;
12228
- 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, ")"));
12229
- })), /* @__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..."))));
13904
+ 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, ")"));
13905
+ })), /* @__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..."))));
12230
13906
  };
12231
13907
  }
12232
13908
  });
12233
13909
 
12234
13910
  // src/ui/hooks/useModeToggle.ts
12235
- import { useEffect as useEffect12 } from "react";
13911
+ import { useEffect as useEffect13 } from "react";
12236
13912
  function useModeToggle() {
12237
13913
  const { subscribe, unsubscribe } = useKeypressContext();
12238
13914
  const { agentMode, setAgentMode, isAgentRunning } = useSession();
12239
- useEffect12(() => {
13915
+ useEffect13(() => {
12240
13916
  const handleKeypress = (key) => {
12241
13917
  if (key.name === "tab" && key.shift && !isAgentRunning) {
12242
13918
  const newMode = agentMode === "plan" ? "build" : "plan";
@@ -12257,13 +13933,13 @@ var init_useModeToggle = __esm({
12257
13933
  });
12258
13934
 
12259
13935
  // src/ui/hooks/useOverlayEscapeGuard.ts
12260
- import { useCallback as useCallback4, useMemo as useMemo4, useRef as useRef6 } from "react";
13936
+ import { useCallback as useCallback4, useMemo as useMemo4, useRef as useRef7 } from "react";
12261
13937
  var useOverlayEscapeGuard;
12262
13938
  var init_useOverlayEscapeGuard = __esm({
12263
13939
  "src/ui/hooks/useOverlayEscapeGuard.ts"() {
12264
13940
  "use strict";
12265
13941
  useOverlayEscapeGuard = ({ overlays, suppressionMs = 250 }) => {
12266
- const suppressUntilRef = useRef6(0);
13942
+ const suppressUntilRef = useRef7(0);
12267
13943
  const markOverlayClosed = useCallback4(() => {
12268
13944
  suppressUntilRef.current = Date.now() + suppressionMs;
12269
13945
  }, [suppressionMs]);
@@ -12275,11 +13951,11 @@ var init_useOverlayEscapeGuard = __esm({
12275
13951
  });
12276
13952
 
12277
13953
  // src/ui/App.tsx
12278
- import { execSync as execSync6 } from "child_process";
13954
+ import { execSync as execSync5 } from "child_process";
12279
13955
  import { homedir as homedir8 } from "os";
12280
- import { Box as Box27, Text as Text25, useApp as useApp2, useStdout as useStdout2 } from "ink";
13956
+ import { Box as Box28, Text as Text26, useApp as useApp2, useStdout as useStdout2 } from "ink";
12281
13957
  import Spinner4 from "ink-spinner";
12282
- import React30, { useCallback as useCallback5, useEffect as useEffect13, useRef as useRef7, useState as useState16 } from "react";
13958
+ import React31, { useCallback as useCallback5, useEffect as useEffect14, useRef as useRef8, useState as useState18 } from "react";
12283
13959
  var getGitBranch2, getCurrentFolder2, AppContent, App;
12284
13960
  var init_App = __esm({
12285
13961
  "src/ui/App.tsx"() {
@@ -12306,6 +13982,7 @@ var init_App = __esm({
12306
13982
  init_MessageList();
12307
13983
  init_ModelSelector();
12308
13984
  init_ProviderSelector();
13985
+ init_QuestionSelector();
12309
13986
  init_SessionSelector();
12310
13987
  init_SessionContext();
12311
13988
  init_useKeypress();
@@ -12315,7 +13992,7 @@ var init_App = __esm({
12315
13992
  init_theme();
12316
13993
  getGitBranch2 = () => {
12317
13994
  try {
12318
- return execSync6("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
13995
+ return execSync5("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
12319
13996
  } catch {
12320
13997
  return "";
12321
13998
  }
@@ -12328,36 +14005,37 @@ var init_App = __esm({
12328
14005
  }
12329
14006
  return cwd;
12330
14007
  };
12331
- AppContent = ({ config: config2, sessionId, webUrl, queuedTasks = [], onExit, onSubmitTask, apiClient, onResumeSession, onClearSession }) => {
14008
+ AppContent = ({ config: config2, sessionId, webUrl, queuedTasks = [], onExit, onSubmitTask, apiClient, onResumeSession, onClearSession, onQuestionAnswer }) => {
12332
14009
  const { exit } = useApp2();
12333
14010
  const { stdout } = useStdout2();
12334
- const { addMessage, clearMessages, isAgentRunning, setSessionId, setWebUrl, setShouldInterruptAgent, setIsAgentRunning, toggleAllToolOutputs, allToolsExpanded, selectedModel, setSelectedModel, refreshStatic, toggleToolGroups, llmProvider, setLlmProvider } = useSession();
14011
+ const { addMessage, clearMessages, isAgentRunning, setSessionId, setWebUrl, setShouldInterruptAgent, setIsAgentRunning, toggleAllToolOutputs, allToolsExpanded, selectedModel, setSelectedModel, refreshStatic, toggleToolGroups, llmProvider, setLlmProvider, pendingQuestion, setPendingQuestion } = useSession();
12335
14012
  useModeToggle();
12336
- const [terminalWidth, setTerminalWidth] = useState16(process.stdout.columns || 80);
12337
- const [showInput, setShowInput] = useState16(true);
12338
- const [gitBranch] = useState16(() => getGitBranch2());
12339
- const [currentFolder] = useState16(() => getCurrentFolder2(config2.cwd));
12340
- const hasInputContentRef = useRef7(false);
12341
- const [exitWarning, setExitWarning] = useState16(null);
12342
- const inputPromptRef = useRef7(null);
12343
- const [showSessionSelector, setShowSessionSelector] = useState16(false);
12344
- const [showModelSelector, setShowModelSelector] = useState16(false);
12345
- const [showProviderSelector, setShowProviderSelector] = useState16(false);
12346
- const [showFeedbackDialog, setShowFeedbackDialog] = useState16(false);
12347
- const [isLoadingSession, setIsLoadingSession] = useState16(false);
12348
- const [showFixFlow, setShowFixFlow] = useState16(false);
12349
- const [fixRunId, setFixRunId] = useState16(void 0);
12350
- const [showMcpServers, setShowMcpServers] = useState16(false);
12351
- const [showMcpAdd, setShowMcpAdd] = useState16(false);
12352
- const [showMcpSelector, setShowMcpSelector] = useState16(false);
12353
- const [mcpSelectorAction, setMcpSelectorAction] = useState16(
14013
+ const [terminalWidth, setTerminalWidth] = useState18(process.stdout.columns || 80);
14014
+ const [showInput, setShowInput] = useState18(true);
14015
+ const [gitBranch] = useState18(() => getGitBranch2());
14016
+ const [currentFolder] = useState18(() => getCurrentFolder2(config2.cwd));
14017
+ const hasInputContentRef = useRef8(false);
14018
+ const [exitWarning, setExitWarning] = useState18(null);
14019
+ const inputPromptRef = useRef8(null);
14020
+ const [showSessionSelector, setShowSessionSelector] = useState18(false);
14021
+ const [showModelSelector, setShowModelSelector] = useState18(false);
14022
+ const [showProviderSelector, setShowProviderSelector] = useState18(false);
14023
+ const [claudeMaxAvailable, setClaudeMaxAvailable] = useState18(false);
14024
+ const [showFeedbackDialog, setShowFeedbackDialog] = useState18(false);
14025
+ const [isLoadingSession, setIsLoadingSession] = useState18(false);
14026
+ const [showFixFlow, setShowFixFlow] = useState18(false);
14027
+ const [fixRunId, setFixRunId] = useState18(void 0);
14028
+ const [showMcpServers, setShowMcpServers] = useState18(false);
14029
+ const [showMcpAdd, setShowMcpAdd] = useState18(false);
14030
+ const [showMcpSelector, setShowMcpSelector] = useState18(false);
14031
+ const [mcpSelectorAction, setMcpSelectorAction] = useState18(
12354
14032
  "remove"
12355
14033
  );
12356
- const [mcpServers, setMcpServers] = useState16([]);
12357
- const [authState, setAuthState] = useState16(
14034
+ const [mcpServers, setMcpServers] = useState18([]);
14035
+ const [authState, setAuthState] = useState18(
12358
14036
  () => config2.supatestApiKey ? "authenticated" /* Authenticated */ : "unauthenticated" /* Unauthenticated */
12359
14037
  );
12360
- const [showAuthDialog, setShowAuthDialog] = useState16(false);
14038
+ const [showAuthDialog, setShowAuthDialog] = useState18(false);
12361
14039
  const { isOverlayOpen, isCancelSuppressed, markOverlayClosed } = useOverlayEscapeGuard({
12362
14040
  overlays: [
12363
14041
  showSessionSelector,
@@ -12368,15 +14046,16 @@ var init_App = __esm({
12368
14046
  showFixFlow,
12369
14047
  showMcpServers,
12370
14048
  showMcpAdd,
12371
- showMcpSelector
14049
+ showMcpSelector,
14050
+ !!pendingQuestion
12372
14051
  ]
12373
14052
  });
12374
- useEffect13(() => {
14053
+ useEffect14(() => {
12375
14054
  if (!config2.supatestApiKey) {
12376
14055
  setShowAuthDialog(true);
12377
14056
  }
12378
14057
  }, [config2.supatestApiKey]);
12379
- useEffect13(() => {
14058
+ useEffect14(() => {
12380
14059
  if (sessionId) {
12381
14060
  setSessionId(sessionId);
12382
14061
  }
@@ -12391,6 +14070,7 @@ var init_App = __esm({
12391
14070
  type: "assistant",
12392
14071
  content: "Opening browser for authentication..."
12393
14072
  });
14073
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
12394
14074
  try {
12395
14075
  const result = await loginCommand();
12396
14076
  saveToken(result.token, result.expiresAt);
@@ -12537,6 +14217,7 @@ var init_App = __esm({
12537
14217
  type: "assistant",
12538
14218
  content: "Running setup..."
12539
14219
  });
14220
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
12540
14221
  try {
12541
14222
  const result = await setupCommand({ cwd: config2.cwd || process.cwd() });
12542
14223
  addMessage({
@@ -12630,9 +14311,45 @@ var init_App = __esm({
12630
14311
  markOverlayClosed();
12631
14312
  setShowModelSelector(false);
12632
14313
  };
12633
- const handleProviderSelect = (provider) => {
14314
+ const handleProviderSelect = async (provider) => {
12634
14315
  setShowProviderSelector(false);
12635
14316
  markOverlayClosed();
14317
+ if (provider === "claude-max") {
14318
+ const hasAuth = await isClaudeMaxAvailable();
14319
+ if (!hasAuth) {
14320
+ addMessage({
14321
+ type: "assistant",
14322
+ content: "Claude Max requires OAuth authentication. Starting authentication flow..."
14323
+ });
14324
+ try {
14325
+ const { ClaudeOAuthService: ClaudeOAuthService2 } = await Promise.resolve().then(() => (init_claude_oauth(), claude_oauth_exports));
14326
+ const { getSecretStorage: getSecretStorage2 } = await Promise.resolve().then(() => (init_secret_storage(), secret_storage_exports));
14327
+ const secretStorage = getSecretStorage2();
14328
+ const oauthService = new ClaudeOAuthService2(secretStorage);
14329
+ const result = await oauthService.authorize();
14330
+ if (!result.success) {
14331
+ addMessage({
14332
+ type: "error",
14333
+ content: `Authentication failed: ${result.error || "Unknown error"}`,
14334
+ errorType: "error"
14335
+ });
14336
+ return;
14337
+ }
14338
+ addMessage({
14339
+ type: "assistant",
14340
+ content: "\u2705 Successfully authenticated with Claude! Switching to Claude Max provider..."
14341
+ });
14342
+ setClaudeMaxAvailable(true);
14343
+ } catch (error) {
14344
+ addMessage({
14345
+ type: "error",
14346
+ content: `Authentication failed: ${error instanceof Error ? error.message : String(error)}`,
14347
+ errorType: "error"
14348
+ });
14349
+ return;
14350
+ }
14351
+ }
14352
+ }
12636
14353
  setLlmProvider(provider);
12637
14354
  saveSupatestSettings(config2.cwd || process.cwd(), { llmProvider: provider });
12638
14355
  addMessage({
@@ -12683,6 +14400,38 @@ var init_App = __esm({
12683
14400
  markOverlayClosed();
12684
14401
  setShowFeedbackDialog(false);
12685
14402
  };
14403
+ const handleQuestionSubmit = (answers) => {
14404
+ if (!pendingQuestion) return;
14405
+ const formattedAnswer = answers.length === 1 ? answers[0] : answers.join(", ");
14406
+ addMessage({
14407
+ type: "user",
14408
+ content: formattedAnswer
14409
+ });
14410
+ const answerRecord = {
14411
+ "answer": formattedAnswer
14412
+ };
14413
+ if (onQuestionAnswer) {
14414
+ const success = onQuestionAnswer(pendingQuestion.toolUseId, answerRecord);
14415
+ if (!success) {
14416
+ console.warn("[App] Failed to submit answer - no pending question found");
14417
+ }
14418
+ }
14419
+ setPendingQuestion(null);
14420
+ markOverlayClosed();
14421
+ };
14422
+ const handleQuestionCancel = () => {
14423
+ if (!pendingQuestion) return;
14424
+ if (onQuestionAnswer) {
14425
+ onQuestionAnswer(pendingQuestion.toolUseId, null);
14426
+ }
14427
+ const cancelMsg = "I'd prefer not to answer this question right now.";
14428
+ addMessage({
14429
+ type: "user",
14430
+ content: cancelMsg
14431
+ });
14432
+ setPendingQuestion(null);
14433
+ markOverlayClosed();
14434
+ };
12686
14435
  const handleFixFlowCancel = () => {
12687
14436
  markOverlayClosed();
12688
14437
  setShowFixFlow(false);
@@ -12822,8 +14571,8 @@ var init_App = __esm({
12822
14571
  markOverlayClosed();
12823
14572
  setShowMcpServers(true);
12824
14573
  };
12825
- const isInitialMount = useRef7(true);
12826
- useEffect13(() => {
14574
+ const isInitialMount = useRef8(true);
14575
+ useEffect14(() => {
12827
14576
  const handleResize = () => {
12828
14577
  setTerminalWidth(process.stdout.columns || 80);
12829
14578
  };
@@ -12832,7 +14581,7 @@ var init_App = __esm({
12832
14581
  process.stdout.off("resize", handleResize);
12833
14582
  };
12834
14583
  }, []);
12835
- useEffect13(() => {
14584
+ useEffect14(() => {
12836
14585
  if (isInitialMount.current) {
12837
14586
  isInitialMount.current = false;
12838
14587
  return;
@@ -12894,7 +14643,7 @@ var init_App = __esm({
12894
14643
  },
12895
14644
  { isActive: !isOverlayOpen }
12896
14645
  );
12897
- useEffect13(() => {
14646
+ useEffect14(() => {
12898
14647
  if (config2.task) {
12899
14648
  addMessage({
12900
14649
  type: "user",
@@ -12902,14 +14651,14 @@ var init_App = __esm({
12902
14651
  });
12903
14652
  }
12904
14653
  }, []);
12905
- return /* @__PURE__ */ React30.createElement(
12906
- Box27,
14654
+ return /* @__PURE__ */ React31.createElement(
14655
+ Box28,
12907
14656
  {
12908
14657
  flexDirection: "column",
12909
14658
  paddingX: 1
12910
14659
  },
12911
- /* @__PURE__ */ React30.createElement(MessageList, { currentFolder, gitBranch, queuedTasks, terminalWidth }),
12912
- showSessionSelector && apiClient && /* @__PURE__ */ React30.createElement(
14660
+ /* @__PURE__ */ React31.createElement(MessageList, { currentFolder, gitBranch, queuedTasks, terminalWidth }),
14661
+ showSessionSelector && apiClient && /* @__PURE__ */ React31.createElement(
12913
14662
  SessionSelector,
12914
14663
  {
12915
14664
  apiClient,
@@ -12917,8 +14666,8 @@ var init_App = __esm({
12917
14666
  onSelect: handleSessionSelect
12918
14667
  }
12919
14668
  ),
12920
- 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")),
12921
- showModelSelector && /* @__PURE__ */ React30.createElement(
14669
+ 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")),
14670
+ showModelSelector && /* @__PURE__ */ React31.createElement(
12922
14671
  ModelSelector,
12923
14672
  {
12924
14673
  currentModel: selectedModel,
@@ -12927,29 +14676,28 @@ var init_App = __esm({
12927
14676
  onSelect: handleModelSelect
12928
14677
  }
12929
14678
  ),
12930
- showProviderSelector && /* @__PURE__ */ React30.createElement(
14679
+ showProviderSelector && /* @__PURE__ */ React31.createElement(
12931
14680
  ProviderSelector,
12932
14681
  {
12933
- claudeMaxAvailable: isClaudeMaxAvailable(),
12934
14682
  currentProvider: llmProvider,
12935
14683
  onCancel: handleProviderSelectorCancel,
12936
14684
  onSelect: handleProviderSelect
12937
14685
  }
12938
14686
  ),
12939
- showAuthDialog && /* @__PURE__ */ React30.createElement(
14687
+ showAuthDialog && /* @__PURE__ */ React31.createElement(
12940
14688
  AuthDialog,
12941
14689
  {
12942
14690
  onLogin: handleLogin
12943
14691
  }
12944
14692
  ),
12945
- showFeedbackDialog && /* @__PURE__ */ React30.createElement(
14693
+ showFeedbackDialog && /* @__PURE__ */ React31.createElement(
12946
14694
  FeedbackDialog,
12947
14695
  {
12948
14696
  onCancel: handleFeedbackCancel,
12949
14697
  onSubmit: handleFeedbackSubmit
12950
14698
  }
12951
14699
  ),
12952
- showFixFlow && apiClient && /* @__PURE__ */ React30.createElement(
14700
+ showFixFlow && apiClient && /* @__PURE__ */ React31.createElement(
12953
14701
  FixFlow,
12954
14702
  {
12955
14703
  apiClient,
@@ -12959,7 +14707,7 @@ var init_App = __esm({
12959
14707
  onStartFix: handleFixStart
12960
14708
  }
12961
14709
  ),
12962
- showMcpServers && /* @__PURE__ */ React30.createElement(
14710
+ showMcpServers && /* @__PURE__ */ React31.createElement(
12963
14711
  McpServersDisplay,
12964
14712
  {
12965
14713
  cwd: config2.cwd,
@@ -12969,8 +14717,8 @@ var init_App = __esm({
12969
14717
  onTest: handleMcpTest
12970
14718
  }
12971
14719
  ),
12972
- showMcpAdd && /* @__PURE__ */ React30.createElement(McpAddDialog, { onCancel: handleMcpAddCancel, onConfirm: handleMcpAddConfirm }),
12973
- showMcpSelector && /* @__PURE__ */ React30.createElement(
14720
+ showMcpAdd && /* @__PURE__ */ React31.createElement(McpAddDialog, { onCancel: handleMcpAddCancel, onConfirm: handleMcpAddConfirm }),
14721
+ showMcpSelector && /* @__PURE__ */ React31.createElement(
12974
14722
  McpServerSelector,
12975
14723
  {
12976
14724
  action: mcpSelectorAction,
@@ -12979,7 +14727,17 @@ var init_App = __esm({
12979
14727
  servers: mcpServers
12980
14728
  }
12981
14729
  ),
12982
- /* @__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(
14730
+ pendingQuestion && /* @__PURE__ */ React31.createElement(
14731
+ QuestionSelector,
14732
+ {
14733
+ multiSelect: pendingQuestion.multiSelect,
14734
+ onCancel: handleQuestionCancel,
14735
+ onSubmit: handleQuestionSubmit,
14736
+ options: pendingQuestion.options || [],
14737
+ question: pendingQuestion.question
14738
+ }
14739
+ ),
14740
+ /* @__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(
12983
14741
  InputPrompt,
12984
14742
  {
12985
14743
  currentFolder,
@@ -12995,13 +14753,13 @@ var init_App = __esm({
12995
14753
  );
12996
14754
  };
12997
14755
  App = (props) => {
12998
- return /* @__PURE__ */ React30.createElement(AppContent, { ...props });
14756
+ return /* @__PURE__ */ React31.createElement(AppContent, { ...props });
12999
14757
  };
13000
14758
  }
13001
14759
  });
13002
14760
 
13003
14761
  // src/ui/hooks/useBracketedPaste.ts
13004
- import { useEffect as useEffect14 } from "react";
14762
+ import { useEffect as useEffect15 } from "react";
13005
14763
  var ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, useBracketedPaste;
13006
14764
  var init_useBracketedPaste = __esm({
13007
14765
  "src/ui/hooks/useBracketedPaste.ts"() {
@@ -13010,7 +14768,7 @@ var init_useBracketedPaste = __esm({
13010
14768
  ENABLE_BRACKETED_PASTE = "\x1B[?2004h";
13011
14769
  DISABLE_BRACKETED_PASTE = "\x1B[?2004l";
13012
14770
  useBracketedPaste = () => {
13013
- useEffect14(() => {
14771
+ useEffect15(() => {
13014
14772
  writeToStdout(ENABLE_BRACKETED_PASTE);
13015
14773
  const cleanup = () => {
13016
14774
  writeToStdout(DISABLE_BRACKETED_PASTE);
@@ -13031,7 +14789,7 @@ __export(interactive_exports, {
13031
14789
  runInteractive: () => runInteractive
13032
14790
  });
13033
14791
  import { render as render2 } from "ink";
13034
- import React31, { useEffect as useEffect15, useRef as useRef8 } from "react";
14792
+ import React32, { useEffect as useEffect16, useRef as useRef9 } from "react";
13035
14793
  function getToolDescription2(toolName, input) {
13036
14794
  switch (toolName) {
13037
14795
  case "Read":
@@ -13164,7 +14922,7 @@ async function runInteractive(config2) {
13164
14922
  webUrl = session.webUrl;
13165
14923
  }
13166
14924
  const { unmount, waitUntilExit } = render2(
13167
- /* @__PURE__ */ React31.createElement(
14925
+ /* @__PURE__ */ React32.createElement(
13168
14926
  InteractiveApp,
13169
14927
  {
13170
14928
  apiClient,
@@ -13229,7 +14987,7 @@ var init_interactive = __esm({
13229
14987
  init_settings_loader();
13230
14988
  init_stdio();
13231
14989
  init_version();
13232
- AgentRunner = ({ config: config2, sessionId, apiClient, messageBridge, onComplete, onTurnComplete }) => {
14990
+ AgentRunner = ({ config: config2, sessionId, apiClient, messageBridge, onComplete, onTurnComplete, onAgentCreated }) => {
13233
14991
  const {
13234
14992
  addMessage,
13235
14993
  updateLastMessage,
@@ -13243,17 +15001,18 @@ var init_interactive = __esm({
13243
15001
  setAgentMode,
13244
15002
  planFilePath,
13245
15003
  selectedModel,
13246
- llmProvider
15004
+ llmProvider,
15005
+ setPendingQuestion
13247
15006
  } = useSession();
13248
15007
  const { setUsageStats } = useUsageStats();
13249
- const agentRef = useRef8(null);
13250
- useEffect15(() => {
15008
+ const agentRef = useRef9(null);
15009
+ useEffect16(() => {
13251
15010
  if (shouldInterruptAgent && agentRef.current) {
13252
15011
  agentRef.current.abort();
13253
15012
  setShouldInterruptAgent(false);
13254
15013
  }
13255
15014
  }, [shouldInterruptAgent, setShouldInterruptAgent]);
13256
- useEffect15(() => {
15015
+ useEffect16(() => {
13257
15016
  let isMounted = true;
13258
15017
  const runAgent2 = async () => {
13259
15018
  setIsAgentRunning(true);
@@ -13295,6 +15054,16 @@ var init_interactive = __esm({
13295
15054
  },
13296
15055
  onTurnComplete: () => {
13297
15056
  if (isMounted) onTurnComplete?.();
15057
+ },
15058
+ onAskUserQuestion: (toolId, question, options, multiSelect) => {
15059
+ if (isMounted) {
15060
+ setPendingQuestion({
15061
+ toolUseId: toolId,
15062
+ question,
15063
+ options,
15064
+ multiSelect
15065
+ });
15066
+ }
13298
15067
  }
13299
15068
  },
13300
15069
  apiClient,
@@ -13303,9 +15072,18 @@ var init_interactive = __esm({
13303
15072
  );
13304
15073
  let oauthToken;
13305
15074
  if (llmProvider === "claude-max") {
13306
- if (isClaudeMaxAvailable()) {
13307
- oauthToken = "use-claude-max";
13308
- logger.info("Using Claude Max subscription for LLM calls");
15075
+ if (await isClaudeMaxAvailable()) {
15076
+ const { ClaudeOAuthService: ClaudeOAuthService2 } = await Promise.resolve().then(() => (init_claude_oauth(), claude_oauth_exports));
15077
+ const { getSecretStorage: getSecretStorage2 } = await Promise.resolve().then(() => (init_secret_storage(), secret_storage_exports));
15078
+ const secretStorage = getSecretStorage2();
15079
+ const oauthService = new ClaudeOAuthService2(secretStorage);
15080
+ const token = await oauthService.getAccessToken();
15081
+ if (token) {
15082
+ oauthToken = token;
15083
+ logger.info("Using Claude Max subscription for LLM calls");
15084
+ } else {
15085
+ logger.warn("Claude Max OAuth token not available. Falling back to Supatest Managed.");
15086
+ }
13309
15087
  } else {
13310
15088
  logger.warn("Claude Max selected but not available. Falling back to Supatest Managed.");
13311
15089
  }
@@ -13321,6 +15099,9 @@ var init_interactive = __esm({
13321
15099
  };
13322
15100
  const agent2 = new CoreAgent(presenter, messageBridge);
13323
15101
  agentRef.current = agent2;
15102
+ if (onAgentCreated) {
15103
+ onAgentCreated(agent2);
15104
+ }
13324
15105
  const result = await agent2.run(runConfig);
13325
15106
  if (isMounted) {
13326
15107
  onComplete(result.success, result.providerSessionId);
@@ -13361,17 +15142,18 @@ var init_interactive = __esm({
13361
15142
  setIsAgentRunning
13362
15143
  } = useSession();
13363
15144
  const { setUsageStats } = useUsageStats();
13364
- const [sessionId, setSessionId] = React31.useState(initialSessionId);
13365
- const [currentTask, setCurrentTask] = React31.useState(config2.task);
13366
- const [taskId, setTaskId] = React31.useState(0);
13367
- const [shouldRunAgent, setShouldRunAgent] = React31.useState(!!config2.task);
13368
- const [taskQueue, setTaskQueue] = React31.useState([]);
13369
- const [providerSessionId, setProviderSessionId] = React31.useState();
13370
- const messageBridgeRef = React31.useRef(null);
13371
- const lastSubmitRef = React31.useRef(null);
13372
- const [pendingInjected, setPendingInjected] = React31.useState([]);
13373
- const pendingInjectedRef = React31.useRef([]);
13374
- React31.useEffect(() => {
15145
+ const [sessionId, setSessionId] = React32.useState(initialSessionId);
15146
+ const [currentTask, setCurrentTask] = React32.useState(config2.task);
15147
+ const [taskId, setTaskId] = React32.useState(0);
15148
+ const [shouldRunAgent, setShouldRunAgent] = React32.useState(!!config2.task);
15149
+ const [taskQueue, setTaskQueue] = React32.useState([]);
15150
+ const [providerSessionId, setProviderSessionId] = React32.useState();
15151
+ const messageBridgeRef = React32.useRef(null);
15152
+ const agentRef = React32.useRef(null);
15153
+ const lastSubmitRef = React32.useRef(null);
15154
+ const [pendingInjected, setPendingInjected] = React32.useState([]);
15155
+ const pendingInjectedRef = React32.useRef([]);
15156
+ React32.useEffect(() => {
13375
15157
  pendingInjectedRef.current = pendingInjected;
13376
15158
  }, [pendingInjected]);
13377
15159
  const handleSubmitTask = async (task) => {
@@ -13389,6 +15171,7 @@ var init_interactive = __esm({
13389
15171
  });
13390
15172
  setSessionId(session.sessionId);
13391
15173
  setContextSessionId(session.sessionId);
15174
+ setProviderSessionId(void 0);
13392
15175
  } catch (error) {
13393
15176
  const errorMessage = error instanceof ApiError ? error.message : `Failed to create session: ${error instanceof Error ? error.message : String(error)}`;
13394
15177
  addMessage({
@@ -13434,7 +15217,7 @@ var init_interactive = __esm({
13434
15217
  if (shouldRunAgent && !messageBridgeRef.current) {
13435
15218
  messageBridgeRef.current = new MessageBridge(providerSessionId || "");
13436
15219
  }
13437
- React31.useEffect(() => {
15220
+ React32.useEffect(() => {
13438
15221
  if (!shouldRunAgent && taskQueue.length > 0) {
13439
15222
  const [nextTask, ...remaining] = taskQueue;
13440
15223
  setTaskQueue(remaining);
@@ -13448,20 +15231,27 @@ var init_interactive = __esm({
13448
15231
  setShouldRunAgent(true);
13449
15232
  }
13450
15233
  }, [shouldRunAgent, taskQueue, addMessage, providerSessionId]);
13451
- const handleClearSession = React31.useCallback(() => {
15234
+ const handleClearSession = React32.useCallback(() => {
13452
15235
  setSessionId(void 0);
13453
15236
  setContextSessionId(void 0);
13454
15237
  setProviderSessionId(void 0);
13455
15238
  setTaskQueue([]);
13456
15239
  setPendingInjected([]);
13457
15240
  }, [setContextSessionId]);
13458
- return /* @__PURE__ */ React31.createElement(React31.Fragment, null, /* @__PURE__ */ React31.createElement(
15241
+ const handleQuestionAnswer = React32.useCallback((questionId, answers) => {
15242
+ if (agentRef.current) {
15243
+ return agentRef.current.submitAnswer(questionId, answers);
15244
+ }
15245
+ return false;
15246
+ }, []);
15247
+ return /* @__PURE__ */ React32.createElement(React32.Fragment, null, /* @__PURE__ */ React32.createElement(
13459
15248
  App,
13460
15249
  {
13461
15250
  apiClient,
13462
15251
  config: { ...config2, task: currentTask },
13463
15252
  onClearSession: handleClearSession,
13464
15253
  onExit,
15254
+ onQuestionAnswer: handleQuestionAnswer,
13465
15255
  onResumeSession: async (session) => {
13466
15256
  try {
13467
15257
  if (!apiClient) {
@@ -13477,7 +15267,9 @@ var init_interactive = __esm({
13477
15267
  const uiMessages = convertQueriesToUIMessages(queries);
13478
15268
  setSessionId(session.id);
13479
15269
  setContextSessionId(session.id);
13480
- setProviderSessionId(session.providerSessionId || void 0);
15270
+ const resumeProviderSessionId = session.providerSessionId || void 0;
15271
+ logger.debug(`Resume session: ${session.id}, providerSessionId: ${resumeProviderSessionId || "NOT SET"}`);
15272
+ setProviderSessionId(resumeProviderSessionId);
13481
15273
  const contextData = await apiClient.getSessionContext(session.id);
13482
15274
  if (contextData.contextTokens > 0) {
13483
15275
  const cacheRead = contextData.cacheReadTokens || 0;
@@ -13518,13 +15310,16 @@ var init_interactive = __esm({
13518
15310
  sessionId,
13519
15311
  webUrl
13520
15312
  }
13521
- ), shouldRunAgent && currentTask && sessionId && messageBridgeRef.current && /* @__PURE__ */ React31.createElement(
15313
+ ), shouldRunAgent && currentTask && sessionId && messageBridgeRef.current && /* @__PURE__ */ React32.createElement(
13522
15314
  AgentRunner,
13523
15315
  {
13524
15316
  apiClient,
13525
15317
  config: { ...config2, task: currentTask, providerSessionId },
13526
15318
  key: `${taskId}`,
13527
15319
  messageBridge: messageBridgeRef.current,
15320
+ onAgentCreated: (agent2) => {
15321
+ agentRef.current = agent2;
15322
+ },
13528
15323
  onComplete: handleAgentComplete,
13529
15324
  onTurnComplete: () => {
13530
15325
  const pending = pendingInjectedRef.current;
@@ -13543,7 +15338,7 @@ var init_interactive = __esm({
13543
15338
  useBracketedPaste();
13544
15339
  const settings = loadSupatestSettings(props.config.cwd || process.cwd());
13545
15340
  const initialProvider = settings.llmProvider || "supatest-managed";
13546
- return /* @__PURE__ */ React31.createElement(KeypressProvider, null, /* @__PURE__ */ React31.createElement(SessionProvider, { initialLlmProvider: initialProvider, initialModel: props.config.selectedModel }, /* @__PURE__ */ React31.createElement(InteractiveAppContent, { ...props })));
15341
+ return /* @__PURE__ */ React32.createElement(KeypressProvider, null, /* @__PURE__ */ React32.createElement(SessionProvider, { initialLlmProvider: initialProvider, initialModel: props.config.selectedModel }, /* @__PURE__ */ React32.createElement(InteractiveAppContent, { ...props })));
13547
15342
  };
13548
15343
  }
13549
15344
  });
@@ -13826,7 +15621,7 @@ async function runAgent(config2) {
13826
15621
  // src/utils/auto-update.ts
13827
15622
  init_version();
13828
15623
  init_error_logger();
13829
- import { execSync as execSync3, spawn } from "child_process";
15624
+ import { execSync as execSync3, spawn as spawn2 } from "child_process";
13830
15625
  import latestVersion from "latest-version";
13831
15626
  import { gt } from "semver";
13832
15627
  var UPDATE_CHECK_TIMEOUT = 3e3;
@@ -13867,8 +15662,24 @@ Updating Supatest CLI ${CLI_VERSION} \u2192 ${latest}...`);
13867
15662
  timeout: INSTALL_TIMEOUT
13868
15663
  });
13869
15664
  }
15665
+ let updateVerified = false;
15666
+ try {
15667
+ const installedVersion = execSync3("npm ls -g @supatest/cli --json 2>/dev/null", {
15668
+ encoding: "utf-8"
15669
+ });
15670
+ const parsed = JSON.parse(installedVersion);
15671
+ const newVersion = parsed.dependencies?.["@supatest/cli"]?.version;
15672
+ if (newVersion && gt(newVersion, CLI_VERSION)) {
15673
+ updateVerified = true;
15674
+ }
15675
+ } catch {
15676
+ }
15677
+ if (!updateVerified) {
15678
+ console.log("Update completed but could not verify new version. Continuing with current version...\n");
15679
+ return;
15680
+ }
13870
15681
  console.log("\u2713 Updated successfully\n");
13871
- const child = spawn(process.argv[0], process.argv.slice(1), {
15682
+ const child = spawn2(process.argv[0], process.argv.slice(1), {
13872
15683
  stdio: "inherit",
13873
15684
  detached: false
13874
15685
  });