@vybestack/llxprt-code 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7

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 (126) hide show
  1. package/package.json +13 -13
  2. package/src/commands/mcp/add.ts +8 -4
  3. package/src/config/cliArgParser.ts +2 -0
  4. package/src/config/config.ts +2 -0
  5. package/src/config/configBuilder.ts +3 -0
  6. package/src/config/configError.ts +23 -0
  7. package/src/config/extensions/extensionEnablement.ts +62 -1
  8. package/src/config/interactiveContext.ts +2 -1
  9. package/src/config/intermediateConfig.ts +4 -0
  10. package/src/config/keyBindings.ts +29 -3
  11. package/src/config/policy.ts +7 -1
  12. package/src/config/postConfigRuntime.ts +20 -14
  13. package/src/config/settings-schema/schema-ui.ts +9 -0
  14. package/src/config/settingsLoader.ts +21 -17
  15. package/src/config/settingsSchema.ts +2 -2
  16. package/src/config/trustedFolders.ts +275 -58
  17. package/src/config/yargsOptions.ts +7 -0
  18. package/src/generated/git-commit.json +1 -1
  19. package/src/nonInteractiveCli.ts +1 -0
  20. package/src/nonInteractiveCliSupport.ts +171 -51
  21. package/src/runtime/interactiveToolScheduler.ts +6 -6
  22. package/src/services/BuiltinCommandLoader.ts +33 -25
  23. package/src/services/FileCommandLoader.ts +34 -7
  24. package/src/test-utils/async.ts +20 -0
  25. package/src/test-utils/render.tsx +10 -1
  26. package/src/ui/App.tsx +17 -10
  27. package/src/ui/AppContainerRuntime.tsx +7 -9
  28. package/src/ui/cliUiRuntime.ts +28 -0
  29. package/src/ui/commands/authCommand.ts +240 -3
  30. package/src/ui/commands/compressCommand.ts +9 -1
  31. package/src/ui/commands/directoryCommand.tsx +1 -1
  32. package/src/ui/commands/logoutCommand.ts +2 -2
  33. package/src/ui/commands/permissionsCommand.ts +77 -20
  34. package/src/ui/commands/policiesCommand.ts +6 -1
  35. package/src/ui/commands/quotaCommand.ts +501 -0
  36. package/src/ui/commands/statsCommand.ts +2 -2
  37. package/src/ui/commands/statsQuota.ts +1 -1
  38. package/src/ui/commands/subagentCommand.ts +59 -27
  39. package/src/ui/components/AuthDialog.tsx +3 -3
  40. package/src/ui/components/Composer.tsx +1 -0
  41. package/src/ui/components/DialogManager.tsx +9 -2
  42. package/src/ui/components/FolderTrustDialog.tsx +71 -60
  43. package/src/ui/components/PermissionsModifyTrustDialog.tsx +211 -146
  44. package/src/ui/components/ProfileCreateWizard/constants.ts +13 -1
  45. package/src/ui/components/QueuedMessagesPanel.tsx +388 -0
  46. package/src/ui/components/inputPromptHooks.ts +2 -0
  47. package/src/ui/components/inputPromptKeyHandlers.ts +14 -0
  48. package/src/ui/components/inputPromptTypes.ts +6 -0
  49. package/src/ui/components/messages/AiMessage.tsx +24 -12
  50. package/src/ui/components/messages/ToolMessage.tsx +26 -40
  51. package/src/ui/containers/AppContainer/builders/buildUIActions.ts +3 -1
  52. package/src/ui/containers/AppContainer/builders/buildUIState.ts +5 -4
  53. package/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +3 -33
  54. package/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +2 -14
  55. package/src/ui/containers/AppContainer/hooks/useAppInput.ts +31 -25
  56. package/src/ui/containers/AppContainer/hooks/useAppLayout.ts +90 -60
  57. package/src/ui/containers/AppContainer/hooks/useDisplayPreferences.ts +10 -0
  58. package/src/ui/containers/AppContainer/hooks/useInputHandling.ts +3 -17
  59. package/src/ui/containers/AppContainer/hooks/useKeybindings.ts +51 -38
  60. package/src/ui/containers/AppContainer/hooks/useSteer.ts +51 -0
  61. package/src/ui/contexts/KeypressContext.tsx +13 -2
  62. package/src/ui/contexts/ShellCommandDisplayContext.tsx +103 -0
  63. package/src/ui/contexts/TodoContext.tsx +0 -13
  64. package/src/ui/contexts/TodoProvider.tsx +1 -34
  65. package/src/ui/contexts/UIActionsContext.tsx +2 -1
  66. package/src/ui/contexts/UIStateContext.tsx +3 -2
  67. package/src/ui/hooks/agentStream/streamUtils.ts +0 -24
  68. package/src/ui/hooks/agentStream/thoughtState.ts +110 -30
  69. package/src/ui/hooks/agentStream/types.ts +1 -0
  70. package/src/ui/hooks/agentStream/useAgentEventStream.ts +8 -17
  71. package/src/ui/hooks/agentStream/useAgentStream.ts +2 -2
  72. package/src/ui/hooks/agentStream/useAgentStreamLifecycle.ts +4 -5
  73. package/src/ui/hooks/agentStream/useAgentStreamOrchestration.ts +12 -5
  74. package/src/ui/hooks/agentStream/useQueuedSubmissions.ts +99 -0
  75. package/src/ui/hooks/agentStream/useStreamEventHandlers.ts +4 -13
  76. package/src/ui/hooks/agentStream/useStreamState.ts +60 -41
  77. package/src/ui/hooks/agentStream/useSubmitQuery.ts +284 -59
  78. package/src/ui/hooks/slashCommandProcessorSupport.ts +4 -0
  79. package/src/ui/hooks/useAgentStream-test-helpers.ts +1 -0
  80. package/src/ui/hooks/useAutoAcceptIndicator.ts +13 -0
  81. package/src/ui/hooks/useFolderTrust.ts +118 -81
  82. package/src/ui/hooks/useGitBranchName.ts +79 -33
  83. package/src/ui/hooks/useIdeTrustListener.ts +5 -19
  84. package/src/ui/hooks/usePermissionsModifyTrust.ts +223 -85
  85. package/src/ui/hooks/useReactToolScheduler.ts +13 -17
  86. package/src/ui/hooks/useTodoPausePreserver.ts +0 -55
  87. package/src/ui/layouts/DefaultAppLayout.tsx +2 -0
  88. package/src/ui/layouts/DefaultAppLayoutHelpers.tsx +37 -162
  89. package/src/ui/layouts/InlineContent.tsx +198 -0
  90. package/src/ui/trustDialogHelpers.ts +190 -0
  91. package/src/utils/cleanup-state.ts +115 -0
  92. package/src/utils/cleanup.ts +32 -34
  93. package/src/utils/sandbox-containers.ts +183 -53
  94. package/src/utils/sandbox-exec.ts +9 -3
  95. package/src/zed-integration/acp-terminal-shell-host.ts +112 -0
  96. package/src/zed-integration/acp-types.ts +35 -0
  97. package/src/zed-integration/fileSystemService.ts +1 -1
  98. package/src/zed-integration/runZedIntegration.ts +142 -0
  99. package/src/zed-integration/zed-agent-event-handler.ts +179 -0
  100. package/src/zed-integration/zed-agent-setup.ts +43 -0
  101. package/src/zed-integration/zed-command-registry.ts +167 -0
  102. package/src/zed-integration/zed-config-options.ts +280 -0
  103. package/src/zed-integration/zed-content-utils.ts +2 -2
  104. package/src/zed-integration/zed-helpers.ts +132 -0
  105. package/src/zed-integration/zed-initialize.ts +64 -0
  106. package/src/zed-integration/zed-plan-update.ts +20 -0
  107. package/src/zed-integration/zed-prompt-command.ts +77 -0
  108. package/src/zed-integration/zed-session-config.ts +119 -0
  109. package/src/zed-integration/zed-session-errors.ts +337 -0
  110. package/src/zed-integration/zed-session-events.ts +216 -0
  111. package/src/zed-integration/zed-session-info.ts +255 -0
  112. package/src/zed-integration/zed-session-lifecycle.ts +186 -0
  113. package/src/zed-integration/zed-session-listing.ts +153 -0
  114. package/src/zed-integration/zed-session-loader.ts +193 -0
  115. package/src/zed-integration/zed-session-pagination.ts +240 -0
  116. package/src/zed-integration/zed-session-replay.ts +564 -0
  117. package/src/zed-integration/zed-stream-batcher.ts +236 -0
  118. package/src/zed-integration/zed-terminal-manager.ts +416 -0
  119. package/src/zed-integration/zed-terminal-setup.ts +72 -0
  120. package/src/zed-integration/zed-test-helpers.ts +349 -4
  121. package/src/zed-integration/zed-tool-handler.ts +47 -5
  122. package/src/zed-integration/zedIntegration.ts +538 -587
  123. package/src/services/todo-continuation/todoContinuationService.ts +0 -610
  124. package/src/ui/containers/AppContainer/hooks/useIdeRestartHotkey.ts +0 -33
  125. package/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +0 -160
  126. package/src/ui/hooks/useTodoContinuation.ts +0 -299
@@ -5,13 +5,22 @@
5
5
  */
6
6
 
7
7
  import * as path from 'node:path';
8
+ import * as process from 'node:process';
8
9
  import type {
9
10
  OpenDialogActionReturn,
10
11
  SlashCommand,
11
12
  MessageActionReturn,
12
13
  } from './types.js';
13
14
  import { CommandKind } from './types.js';
14
- import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js';
15
+ import {
16
+ loadTrustedFolders,
17
+ resolveLocalWorkspaceTrust,
18
+ TrustLevel,
19
+ } from '../../config/trustedFolders.js';
20
+ import {
21
+ combineTrustUpdateFailure,
22
+ getTrustCommitErrorMessage,
23
+ } from '../trustDialogHelpers.js';
15
24
 
16
25
  const VALID_TRUST_LEVELS = [
17
26
  TrustLevel.TRUST_FOLDER,
@@ -25,6 +34,7 @@ const VALID_TRUST_LEVELS = [
25
34
  */
26
35
  function parsePermissionsArgs(
27
36
  args: string,
37
+ workingDirectory: string,
28
38
  ): { trustLevel: TrustLevel; targetPath: string } | { error: string } {
29
39
  const trimmed = args.trim();
30
40
 
@@ -68,7 +78,7 @@ function parsePermissionsArgs(
68
78
  if (path.isAbsolute(targetPath)) {
69
79
  normalizedPath = path.normalize(targetPath);
70
80
  } else {
71
- normalizedPath = path.resolve(process.cwd(), targetPath);
81
+ normalizedPath = path.resolve(workingDirectory, targetPath);
72
82
  }
73
83
 
74
84
  return { trustLevel, targetPath: normalizedPath };
@@ -78,8 +88,11 @@ export const permissionsCommand: SlashCommand = {
78
88
  name: 'permissions',
79
89
  description: 'manage folder trust settings',
80
90
  kind: CommandKind.BUILT_IN,
81
- action: (_context, args): OpenDialogActionReturn | MessageActionReturn => {
82
- const parsed = parsePermissionsArgs(args);
91
+ action: (context, args) => {
92
+ const parsed = parsePermissionsArgs(
93
+ args,
94
+ context.services.config?.getWorkingDir() ?? process.cwd(),
95
+ );
83
96
 
84
97
  // If error is empty string, open dialog (no args case)
85
98
  if ('error' in parsed) {
@@ -100,21 +113,65 @@ export const permissionsCommand: SlashCommand = {
100
113
  // We have valid trust level and path, modify trust
101
114
  const { trustLevel, targetPath } = parsed;
102
115
 
103
- try {
104
- const trustedFolders = loadTrustedFolders();
105
- trustedFolders.setValue(targetPath, trustLevel);
106
-
107
- return {
108
- type: 'message',
109
- messageType: 'info',
110
- content: `Trust level set to ${trustLevel} for: ${targetPath}`,
111
- };
112
- } catch (error) {
113
- return {
114
- type: 'message',
115
- messageType: 'error',
116
- content: `Failed to save trust settings: ${error instanceof Error ? error.message : String(error)}`,
117
- };
118
- }
116
+ return (async (): Promise<OpenDialogActionReturn | MessageActionReturn> => {
117
+ let trustedFolders: ReturnType<typeof loadTrustedFolders> | undefined;
118
+ let savedSnapshot:
119
+ | ReturnType<ReturnType<typeof loadTrustedFolders>['snapshotValue']>
120
+ | undefined;
121
+ const config = context.services.config;
122
+ const previousLiveTrust = config?.isTrustedFolder() ?? false;
123
+ let failedPhase: 'persistence' | 'live' = 'persistence';
124
+ try {
125
+ trustedFolders = loadTrustedFolders();
126
+ savedSnapshot = trustedFolders.snapshotValue(targetPath);
127
+ trustedFolders.setValue(targetPath, trustLevel);
128
+ if (config) {
129
+ failedPhase = 'live';
130
+ const cwd = config.getWorkingDir();
131
+ await config.setTrustedFolderLive(
132
+ resolveLocalWorkspaceTrust(
133
+ { folderTrust: config.getFolderTrust() },
134
+ trustedFolders,
135
+ cwd,
136
+ ) ?? false,
137
+ );
138
+ }
139
+ return {
140
+ type: 'message',
141
+ messageType: 'info',
142
+ content: `Trust level set to ${trustLevel} for: ${targetPath}`,
143
+ };
144
+ } catch (error) {
145
+ const rollbackFailures: unknown[] = [];
146
+ if (trustedFolders !== undefined && savedSnapshot !== undefined) {
147
+ try {
148
+ trustedFolders.restoreSnapshot(savedSnapshot);
149
+ } catch (rollbackError) {
150
+ rollbackFailures.push(rollbackError);
151
+ }
152
+ }
153
+ if (config && failedPhase === 'live') {
154
+ try {
155
+ await config.setTrustedFolderLive(previousLiveTrust);
156
+ } catch (rollbackError) {
157
+ rollbackFailures.push(rollbackError);
158
+ }
159
+ }
160
+ const failure = combineTrustUpdateFailure(
161
+ error,
162
+ rollbackFailures,
163
+ 'Trust update and rollback failed',
164
+ );
165
+ return {
166
+ type: 'message',
167
+ messageType: 'error',
168
+ content: getTrustCommitErrorMessage(
169
+ failedPhase,
170
+ failure.error,
171
+ failure.rollbackSucceeded,
172
+ ),
173
+ };
174
+ }
175
+ })();
119
176
  },
120
177
  };
@@ -37,6 +37,7 @@ function getTierBand(priority: number): string {
37
37
  interface PolicyRuleDisplay {
38
38
  priority?: number;
39
39
  toolName?: string;
40
+ toolNamePrefix?: string;
40
41
  decision: PolicyDecision;
41
42
  argsPatternSource?: string;
42
43
  source?: string;
@@ -45,6 +46,7 @@ interface PolicyRuleDisplay {
45
46
  interface PolicyRuleBase {
46
47
  priority?: number;
47
48
  toolName?: string;
49
+ toolNamePrefix?: string;
48
50
  decision: PolicyDecision;
49
51
  source?: string;
50
52
  }
@@ -56,6 +58,7 @@ function toPolicyRuleDisplay<T extends PolicyRuleBase>(
56
58
  return {
57
59
  priority: rule.priority,
58
60
  toolName: rule.toolName,
61
+ toolNamePrefix: rule.toolNamePrefix,
59
62
  decision: rule.decision,
60
63
  argsPatternSource: extractPattern(rule),
61
64
  source: rule.source,
@@ -107,7 +110,9 @@ function formatPolicyOutput(
107
110
  lines.push(`${tier}:`);
108
111
 
109
112
  for (const rule of tierRules) {
110
- const toolName = rule.toolName ?? '*';
113
+ const toolName =
114
+ rule.toolName ??
115
+ (rule.toolNamePrefix !== undefined ? `${rule.toolNamePrefix}*` : '*');
111
116
  const decision = formatDecision(rule.decision);
112
117
  const priority = rule.priority ?? 0;
113
118
  const argsPattern = rule.argsPatternSource
@@ -0,0 +1,501 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {
8
+ CommandKind,
9
+ type SlashCommand,
10
+ type CommandContext,
11
+ type SlashCommandActionReturn,
12
+ type ConfirmActionReturn,
13
+ } from './types.js';
14
+ import { MessageType } from '../types.js';
15
+ import { getRuntimeApi } from '../contexts/RuntimeContext.js';
16
+ import { fetchAllQuotaInfo } from './statsQuota.js';
17
+ import {
18
+ CodexRateLimitResetCreditsResponseSchema,
19
+ consumeCodexRateLimitResetCredit,
20
+ formatCodexResetCredits,
21
+ } from '@vybestack/llxprt-code-providers';
22
+ import type { CodexRateLimitResetCreditsResponse } from '@vybestack/llxprt-code-providers';
23
+ import type { OAuthManager } from '@vybestack/llxprt-code-providers/auth.js';
24
+ import { CodexOAuthTokenSchema } from '@vybestack/llxprt-code-auth';
25
+ import type { CommandArgumentSchema } from './schema/types.js';
26
+ import { randomUUID } from 'node:crypto';
27
+
28
+ // Non-interactive mode (`llxprt-code --command "/quota reset"`) does not load
29
+ // BuiltinCommandLoader (only FileCommandLoader) and createNonInteractiveUI is
30
+ // a no-op sink, so NO built-in slash command works non-interactively today.
31
+ // Making /quota reset work there requires a cross-cutting change to load
32
+ // built-ins + provide a real non-interactive UI sink, which affects every
33
+ // built-in and is out of scope for this issue.
34
+
35
+ const NO_RESET_CREDITS_MSG =
36
+ 'No Codex reset credits available. Reset credits are earned via referrals or purchased.';
37
+ const NOT_AUTHED_CODEX_MSG =
38
+ 'Not authenticated with Codex. Run /auth codex to login.';
39
+ const NO_REDEEMABLE_CREDITS_MSG =
40
+ 'No reset credits available to redeem. Reset credits are earned via referrals or purchased.';
41
+
42
+ interface BucketCredits {
43
+ readonly bucket: string;
44
+ readonly availableCount: number;
45
+ readonly firstCreditId: string | null;
46
+ }
47
+
48
+ /**
49
+ * A bucket + credit guaranteed to be redeemable (non-null credit id).
50
+ * Returned by findRedeemableCredit so callers need no null-narrowing guard.
51
+ */
52
+ interface RedeemableCredit {
53
+ readonly bucket: string;
54
+ readonly firstCreditId: string;
55
+ }
56
+
57
+ interface RedeemableCreditWithToken extends RedeemableCredit {
58
+ readonly accessToken: string;
59
+ readonly accountId: string;
60
+ }
61
+
62
+ interface BucketTokenInfo {
63
+ readonly accessToken: string | null;
64
+ readonly accountId: string | null;
65
+ }
66
+
67
+ /**
68
+ * Parse a raw reset-credits payload via the Zod schema once.
69
+ * Returns the validated response or null when the shape does not validate.
70
+ */
71
+ function safeParseResetCredits(
72
+ raw: Record<string, unknown>,
73
+ ): CodexRateLimitResetCreditsResponse | null {
74
+ const parsed = CodexRateLimitResetCreditsResponseSchema.safeParse(raw);
75
+ return parsed.success ? parsed.data : null;
76
+ }
77
+
78
+ /**
79
+ * Resolve the OAuthManager via the runtime API, returning null when the
80
+ * runtime infrastructure is not registered (mirrors authCommand.getOAuthManager
81
+ * but degrades to null instead of throwing).
82
+ */
83
+ function resolveOAuthManager(): OAuthManager | null {
84
+ try {
85
+ return getRuntimeApi().getCliOAuthManager();
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Resolve the trimmed base-url ephemeral setting, or undefined when blank.
93
+ */
94
+ function resolveBaseUrl(): string | undefined {
95
+ const value = getRuntimeApi().getEphemeralSetting('base-url');
96
+ if (typeof value !== 'string') {
97
+ return undefined;
98
+ }
99
+ const trimmed = value.trim();
100
+ return trimmed === '' ? undefined : trimmed;
101
+ }
102
+
103
+ function addInfo(context: CommandContext, text: string): void {
104
+ context.ui.addItem({ type: MessageType.INFO, text }, Date.now());
105
+ }
106
+
107
+ function addError(context: CommandContext, text: string): void {
108
+ context.ui.addItem({ type: MessageType.ERROR, text }, Date.now());
109
+ }
110
+
111
+ /**
112
+ * Status (and default) action: show quota/rate-limit info for all providers.
113
+ */
114
+ async function statusAction(context: CommandContext): Promise<void> {
115
+ try {
116
+ const runtimeApi = getRuntimeApi();
117
+ const quotaLines = await fetchAllQuotaInfo(runtimeApi);
118
+
119
+ if (quotaLines.length === 0) {
120
+ addInfo(
121
+ context,
122
+ 'No quota information available. Supported providers: Claude Code (OAuth), Codex (OAuth), Z.ai, Synthetic, Chutes, Kimi.',
123
+ );
124
+ return;
125
+ }
126
+
127
+ addInfo(context, quotaLines.join('\n'));
128
+ } catch (error) {
129
+ const msg = error instanceof Error ? error.message : 'Unknown error';
130
+ addError(context, `Failed to retrieve quota information: ${msg}`);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Parse a single bucket's reset-credits payload via the Zod schema.
136
+ * Returns null when the shape does not validate.
137
+ */
138
+ function parseBucketCredits(
139
+ bucket: string,
140
+ raw: Record<string, unknown>,
141
+ ): BucketCredits | null {
142
+ const parsed = safeParseResetCredits(raw);
143
+ if (parsed === null) {
144
+ return null;
145
+ }
146
+ const credits = parsed.rate_limit_reset_credits;
147
+ const availableCount = credits?.available_count ?? 0;
148
+ const creditList = credits?.credits ?? [];
149
+ const firstCreditId =
150
+ creditList.length > 0 && creditList[0]?.id ? creditList[0].id : null;
151
+ return { bucket, availableCount, firstCreditId };
152
+ }
153
+
154
+ /**
155
+ * Format a single bucket's reset credits into display lines (with optional
156
+ * bucket header), or null when there is nothing to show.
157
+ */
158
+ function formatOneBucket(
159
+ bucket: string,
160
+ parsed: CodexRateLimitResetCreditsResponse,
161
+ multiBucket: boolean,
162
+ ): string[] | null {
163
+ const lines = formatCodexResetCredits(parsed);
164
+ if (lines.length === 0) {
165
+ return null;
166
+ }
167
+ const result: string[] = [];
168
+ if (multiBucket) {
169
+ result.push(`### Bucket: ${bucket}\n`);
170
+ }
171
+ result.push(...lines);
172
+ result.push('');
173
+ return result;
174
+ }
175
+
176
+ /**
177
+ * Format all bucket reset-credits into display lines with bucket headers.
178
+ * Each bucket is parsed once via the schema; buckets that fail to validate
179
+ * are skipped.
180
+ */
181
+ function formatAllResetCreditsLines(
182
+ creditsMap: Map<string, Record<string, unknown>>,
183
+ ): string[] {
184
+ const output: string[] = [];
185
+ const multiBucket = creditsMap.size > 1;
186
+
187
+ for (const [bucket, raw] of creditsMap.entries()) {
188
+ const parsed = safeParseResetCredits(raw);
189
+ if (parsed === null) {
190
+ continue;
191
+ }
192
+ const bucketLines = formatOneBucket(bucket, parsed, multiBucket);
193
+ if (bucketLines !== null) {
194
+ output.push(...bucketLines);
195
+ }
196
+ }
197
+
198
+ if (output[output.length - 1] === '') {
199
+ output.pop();
200
+ }
201
+ return output;
202
+ }
203
+
204
+ /**
205
+ * Credits subcommand action: show available Codex reset credits.
206
+ */
207
+ async function creditsAction(context: CommandContext): Promise<void> {
208
+ const oauthManager = resolveOAuthManager();
209
+ if (!oauthManager) {
210
+ addInfo(context, NO_RESET_CREDITS_MSG);
211
+ return;
212
+ }
213
+
214
+ try {
215
+ const creditsMap = await oauthManager.getAllCodexRateLimitResetCredits();
216
+
217
+ if (creditsMap.size === 0) {
218
+ addInfo(context, NO_RESET_CREDITS_MSG);
219
+ return;
220
+ }
221
+
222
+ const lines = formatAllResetCreditsLines(creditsMap);
223
+ if (lines.length === 0) {
224
+ addInfo(context, NO_RESET_CREDITS_MSG);
225
+ return;
226
+ }
227
+
228
+ addInfo(context, lines.join('\n'));
229
+ } catch (error) {
230
+ const msg = error instanceof Error ? error.message : 'Unknown error';
231
+ addError(context, `Failed to retrieve reset credits: ${msg}`);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Determine whether the user is authenticated with Codex.
237
+ */
238
+ async function isCodexAuthed(oauthManager: OAuthManager): Promise<boolean> {
239
+ const token = await oauthManager.getToken('codex');
240
+ if (token !== null) {
241
+ return true;
242
+ }
243
+ const buckets = await oauthManager.listBuckets('codex');
244
+ return buckets.length > 0;
245
+ }
246
+
247
+ /**
248
+ * Find all buckets with a credit that can be redeemed.
249
+ */
250
+ async function findRedeemableCredits(
251
+ oauthManager: OAuthManager,
252
+ ): Promise<readonly RedeemableCredit[]> {
253
+ const creditsMap = await oauthManager.getAllCodexRateLimitResetCredits();
254
+ const redeemableCredits: RedeemableCredit[] = [];
255
+
256
+ for (const [bucket, raw] of creditsMap.entries()) {
257
+ const parsed = parseBucketCredits(bucket, raw);
258
+ if (
259
+ parsed !== null &&
260
+ parsed.availableCount > 0 &&
261
+ parsed.firstCreditId !== null
262
+ ) {
263
+ redeemableCredits.push({
264
+ bucket: parsed.bucket,
265
+ firstCreditId: parsed.firstCreditId,
266
+ });
267
+ }
268
+ }
269
+ return redeemableCredits;
270
+ }
271
+
272
+ /**
273
+ * Resolve the access token + account_id for a Codex bucket.
274
+ */
275
+ async function resolveBucketToken(
276
+ oauthManager: OAuthManager,
277
+ bucket: string,
278
+ ): Promise<BucketTokenInfo> {
279
+ const tokenStore = oauthManager.getTokenStore();
280
+ const token = await tokenStore.getToken('codex', bucket);
281
+ if (token === null) {
282
+ return { accessToken: null, accountId: null };
283
+ }
284
+ const parsed = CodexOAuthTokenSchema.safeParse(token);
285
+ if (!parsed.success) {
286
+ return { accessToken: null, accountId: null };
287
+ }
288
+ // Guard against a token that expired between the credit-listing call and
289
+ // this resolve — sending an expired token to consume would yield a 401 and
290
+ // a misleading generic error.
291
+ if (parsed.data.expiry <= Math.floor(Date.now() / 1000)) {
292
+ return { accessToken: null, accountId: null };
293
+ }
294
+ return {
295
+ accessToken: parsed.data.access_token,
296
+ accountId: parsed.data.account_id,
297
+ };
298
+ }
299
+
300
+ async function findRedeemableCreditWithToken(
301
+ oauthManager: OAuthManager,
302
+ redeemableCredits: readonly RedeemableCredit[],
303
+ ): Promise<RedeemableCreditWithToken | null> {
304
+ for (const redeemable of redeemableCredits) {
305
+ const tokenInfo = await resolveBucketToken(oauthManager, redeemable.bucket);
306
+ if (tokenInfo.accessToken !== null && tokenInfo.accountId !== null) {
307
+ return {
308
+ ...redeemable,
309
+ accessToken: tokenInfo.accessToken,
310
+ accountId: tokenInfo.accountId,
311
+ };
312
+ }
313
+ }
314
+ return null;
315
+ }
316
+
317
+ const RESET_CONFIRMATION_PROMPT =
318
+ 'Redeem a Codex rate-limit-reset credit to reset your rate-limit window? This consumes one earned/purchased reset credit.';
319
+
320
+ /**
321
+ * Build the confirm_action return value for /quota reset, deriving the
322
+ * original raw invocation (falling back to a canonical form when the
323
+ * invocation context is absent or blank).
324
+ */
325
+ function buildResetConfirmation(context: CommandContext): ConfirmActionReturn {
326
+ const rawInvocation =
327
+ context.invocation?.raw && context.invocation.raw.trim() !== ''
328
+ ? context.invocation.raw
329
+ : '/quota reset codex';
330
+ return {
331
+ type: 'confirm_action',
332
+ prompt: RESET_CONFIRMATION_PROMPT,
333
+ originalInvocation: { raw: rawInvocation },
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Consume the redeemable credit and render the result (success / already
339
+ * redeemed / failure) plus a refreshed quota snapshot. Returns void — all
340
+ * user feedback flows through addInfo/addError.
341
+ */
342
+ async function redeemResetCredit(
343
+ context: CommandContext,
344
+ accessToken: string,
345
+ accountId: string,
346
+ creditId: string,
347
+ ): Promise<void> {
348
+ // Both the credit listing (getAllCodexRateLimitResetCredits) and this
349
+ // consume call resolve base-url from the same runtime settings source, so
350
+ // list and consume always target the same backend host.
351
+ const baseUrl = resolveBaseUrl();
352
+ const redeemRequestId = randomUUID();
353
+ const consumeResult = await consumeCodexRateLimitResetCredit(
354
+ accessToken,
355
+ accountId,
356
+ creditId,
357
+ redeemRequestId,
358
+ baseUrl,
359
+ );
360
+
361
+ if (consumeResult === null) {
362
+ addError(
363
+ context,
364
+ 'Failed to reset rate-limit window. Please try again later.',
365
+ );
366
+ return;
367
+ }
368
+
369
+ if (consumeResult.code === 'reset') {
370
+ addInfo(context, 'Rate-limit window reset successfully.');
371
+ } else {
372
+ addInfo(context, 'Credit already redeemed.');
373
+ }
374
+
375
+ // fetchAllQuotaInfo has its own internal error handling and returns [] on
376
+ // failure, so it never throws — no extra guard is needed here.
377
+ const runtimeApi = getRuntimeApi();
378
+ const quotaLines = await fetchAllQuotaInfo(runtimeApi);
379
+ if (quotaLines.length > 0) {
380
+ addInfo(context, quotaLines.join('\n'));
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Reset subcommand action: redeem a Codex rate-limit-reset credit.
386
+ */
387
+ async function resetAction(
388
+ context: CommandContext,
389
+ args: string,
390
+ ): Promise<SlashCommandActionReturn | void> {
391
+ const tokens = args
392
+ .trim()
393
+ .split(/\s+/)
394
+ .filter((t) => t !== '');
395
+ const provider = tokens[0] ?? '';
396
+
397
+ if (provider !== '' && provider !== 'codex') {
398
+ addError(context, "Only 'codex' supports reset.");
399
+ return undefined;
400
+ }
401
+ if (tokens.length > 1) {
402
+ addError(context, 'Too many arguments. Usage: /quota reset [codex]');
403
+ return undefined;
404
+ }
405
+
406
+ const oauthManager = resolveOAuthManager();
407
+ if (!oauthManager) {
408
+ addInfo(context, NOT_AUTHED_CODEX_MSG);
409
+ return undefined;
410
+ }
411
+
412
+ try {
413
+ const authed = await isCodexAuthed(oauthManager);
414
+ if (!authed) {
415
+ addInfo(context, NOT_AUTHED_CODEX_MSG);
416
+ return undefined;
417
+ }
418
+
419
+ const redeemableCredits = await findRedeemableCredits(oauthManager);
420
+ if (redeemableCredits.length === 0) {
421
+ addInfo(context, NO_REDEEMABLE_CREDITS_MSG);
422
+ return undefined;
423
+ }
424
+
425
+ const redeemable = await findRedeemableCreditWithToken(
426
+ oauthManager,
427
+ redeemableCredits,
428
+ );
429
+ if (redeemable === null) {
430
+ addError(
431
+ context,
432
+ 'Codex credentials for all credit-bearing buckets are unavailable or expired. Run /auth codex to re-authenticate.',
433
+ );
434
+ return undefined;
435
+ }
436
+
437
+ // Confirmation gate: redeeming consumes a paid/referral-earned resource,
438
+ // so — like other mutating commands — prompt before performing the
439
+ // mutation. The confirmed re-invocation arrives with
440
+ // overwriteConfirmed === true and skips this gate to actually consume.
441
+ if (context.overwriteConfirmed !== true) {
442
+ return buildResetConfirmation(context);
443
+ }
444
+
445
+ await redeemResetCredit(
446
+ context,
447
+ redeemable.accessToken,
448
+ redeemable.accountId,
449
+ redeemable.firstCreditId,
450
+ );
451
+ return undefined;
452
+ } catch (error) {
453
+ const msg = error instanceof Error ? error.message : 'Unknown error';
454
+ addError(context, `Failed to reset rate-limit window: ${msg}`);
455
+ return undefined;
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Schema for /quota reset autocomplete: offers 'codex' as the provider.
461
+ */
462
+ const resetSchema: CommandArgumentSchema = [
463
+ {
464
+ kind: 'value',
465
+ name: 'provider',
466
+ description: 'Provider to reset',
467
+ options: [{ value: 'codex', description: 'Codex (ChatGPT)' }],
468
+ },
469
+ ];
470
+
471
+ export const quotaCommand: SlashCommand = {
472
+ name: 'quota',
473
+ description:
474
+ 'Manage quota and rate-limit reset. Usage: /quota [status|credits|reset]',
475
+ kind: CommandKind.BUILT_IN,
476
+ action: statusAction,
477
+ subCommands: [
478
+ {
479
+ name: 'status',
480
+ description: 'Show quota/rate-limit status for all providers.',
481
+ kind: CommandKind.BUILT_IN,
482
+ autoExecute: true,
483
+ action: statusAction,
484
+ },
485
+ {
486
+ name: 'credits',
487
+ description: 'Show available Codex rate-limit-reset credits.',
488
+ kind: CommandKind.BUILT_IN,
489
+ autoExecute: true,
490
+ action: creditsAction,
491
+ },
492
+ {
493
+ name: 'reset',
494
+ description:
495
+ 'Redeem a Codex rate-limit-reset credit to reset the rate-limit window.',
496
+ kind: CommandKind.BUILT_IN,
497
+ schema: resetSchema,
498
+ action: resetAction,
499
+ },
500
+ ],
501
+ };
@@ -74,7 +74,7 @@ async function quotaSubcommandAction(context: CommandContext): Promise<void> {
74
74
  context.ui.addItem(
75
75
  {
76
76
  type: MessageType.INFO,
77
- text: 'No quota information available. Supported providers: Anthropic (OAuth), Codex (OAuth), Z.ai, Synthetic, Chutes, Kimi.',
77
+ text: 'No quota information available. Supported providers: Claude Code (OAuth), Codex (OAuth), Z.ai, Synthetic, Chutes, Kimi.',
78
78
  },
79
79
  Date.now(),
80
80
  );
@@ -278,7 +278,7 @@ export const statsCommand: SlashCommand = {
278
278
  {
279
279
  name: 'quota',
280
280
  description:
281
- 'Show quota/usage information for OAuth providers (Anthropic, Codex) and API-key providers (Z.ai, Synthetic, Chutes, Kimi).',
281
+ 'Show quota/usage information for OAuth providers (Claude Code, Codex) and API-key providers (Z.ai, Synthetic, Chutes, Kimi).',
282
282
  kind: CommandKind.BUILT_IN,
283
283
  action: quotaSubcommandAction,
284
284
  },
@@ -305,7 +305,7 @@ async function fetchOAuthQuotaLines(
305
305
 
306
306
  const anthropicLines = formatAnthropicLines(anthropicUsageInfo);
307
307
  if (anthropicLines.length > 0) {
308
- output.push('## Anthropic Quota Information\n');
308
+ output.push('## Claude Code Quota Information\n');
309
309
  output.push(...anthropicLines);
310
310
  }
311
311