@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
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { useCallback, useEffect, useRef } from 'react';
15
15
  import type { Agent } from '@vybestack/llxprt-code-agents';
16
+ import { debugLogger } from '@vybestack/llxprt-code-telemetry';
16
17
  import {
17
18
  type MessageSenderType,
18
19
  type RecordingIntegration,
@@ -41,6 +42,23 @@ import {
41
42
  import type { QueuedSubmission } from './types.js';
42
43
  import type { StreamRuntime } from '../../cliUiRuntime.js';
43
44
 
45
+ export type SubmissionDisposition =
46
+ | 'consumed'
47
+ | 'requeue'
48
+ | 'requeue-await-event';
49
+
50
+ const MAX_QUEUED_SUBMISSION_RETRIES = 3;
51
+ // A one-second pause avoids a tight failure loop while keeping transient
52
+ // submitter-initialization races responsive.
53
+ const QUEUED_SUBMISSION_RETRY_DELAY_MS = 1000;
54
+
55
+ export type SubmissionExecutor = (
56
+ query: AgentRequestInput,
57
+ options?: { isContinuation: boolean },
58
+ prompt_id?: string,
59
+ fromQueue?: boolean,
60
+ ) => Promise<SubmissionDisposition>;
61
+
44
62
  /**
45
63
  * Shared content-prefix identity resolver for the AgentEvent dispatcher. Reads
46
64
  * fresh runtime state at call time so a single stable reference can be reused.
@@ -76,6 +94,12 @@ export interface UseSubmitQueryDeps {
76
94
  thinkingBlocksRef: React.MutableRefObject<ThinkingBlock[]>;
77
95
  turnCancelledRef: React.MutableRefObject<boolean>;
78
96
  queuedSubmissionsRef: React.MutableRefObject<QueuedSubmission[]>;
97
+ enqueueSubmission: (submission: QueuedSubmission) => void;
98
+ requeueSubmission: (submission: QueuedSubmission) => void;
99
+ dequeueSubmission: () => QueuedSubmission | undefined;
100
+ clearSubmissions: () => void;
101
+ tryReserveDrain: () => boolean;
102
+ releaseDrain: () => void;
79
103
  setPendingHistoryItem: React.Dispatch<
80
104
  React.SetStateAction<HistoryItemWithoutId | null>
81
105
  >;
@@ -116,14 +140,7 @@ export interface UseSubmitQueryDeps {
116
140
  ) => Promise<void>)
117
141
  | null
118
142
  >;
119
- submitQueryRef: React.MutableRefObject<
120
- | ((
121
- query: AgentRequestInput,
122
- options?: { isContinuation: boolean },
123
- prompt_id?: string,
124
- ) => Promise<void>)
125
- | null
126
- >;
143
+ submitQueryRef: React.MutableRefObject<SubmissionExecutor | null>;
127
144
  isResponding: boolean;
128
145
  streamingState: StreamingState;
129
146
  }
@@ -155,6 +172,7 @@ export interface UseSubmitQueryReturn {
155
172
 
156
173
  export function useSubmitQuery(deps: UseSubmitQueryDeps): UseSubmitQueryReturn {
157
174
  const { startNewPrompt, getPromptCount } = useSessionStats();
175
+ const activeTurnRef = useRef(false);
158
176
 
159
177
  const handlers = useStreamEventHandlers({
160
178
  runtime: deps.runtime,
@@ -168,7 +186,7 @@ export function useSubmitQuery(deps: UseSubmitQueryDeps): UseSubmitQueryReturn {
168
186
  pendingHistoryItemRef: deps.pendingHistoryItemRef,
169
187
  thinkingBlocksRef: deps.thinkingBlocksRef,
170
188
  turnCancelledRef: deps.turnCancelledRef,
171
- queuedSubmissionsRef: deps.queuedSubmissionsRef,
189
+ clearSubmissions: deps.clearSubmissions,
172
190
  setPendingHistoryItem: deps.setPendingHistoryItem,
173
191
  setIsResponding: deps.setIsResponding,
174
192
  setThought: deps.setThought,
@@ -187,19 +205,28 @@ export function useSubmitQuery(deps: UseSubmitQueryDeps): UseSubmitQueryReturn {
187
205
 
188
206
  const processAgentEvent = useProcessAgentEvent(deps, handlers);
189
207
 
190
- const scheduleNextQueuedSubmission = useScheduleNext(deps);
208
+ const scheduleNextQueuedSubmission = useScheduleNext(deps, activeTurnRef);
191
209
 
192
- const submitQuery = useSubmitQueryCallback({
210
+ const executeSubmission = useSubmitQueryCallback({
193
211
  ...deps,
194
212
  displayUserMessage: handlers.displayUserMessage,
195
213
  prepareQueryForAgent: handlers.prepareQueryForAgent,
196
214
  handleLoopDetectedEvent: handlers.handleLoopDetectedEvent,
197
- scheduleNextQueuedSubmission,
198
215
  startNewPrompt,
199
216
  getPromptCount,
217
+ activeTurnRef,
218
+ scheduleNextQueuedSubmission,
200
219
  });
220
+ const submitQuery = useCallback<UseSubmitQueryReturn['submitQuery']>(
221
+ async (query, options, promptId) => {
222
+ // Dispositions are an internal queue-drain protocol; the public API
223
+ // intentionally remains Promise<void> for existing callers.
224
+ await executeSubmission(query, options, promptId);
225
+ },
226
+ [executeSubmission],
227
+ );
201
228
 
202
- useSubmitQueryEffects(deps, submitQuery, scheduleNextQueuedSubmission);
229
+ useSubmitQueryEffects(deps, executeSubmission, scheduleNextQueuedSubmission);
203
230
 
204
231
  return {
205
232
  submitQuery,
@@ -263,26 +290,36 @@ function useSubmitQueryEffects(
263
290
  submitQuery: ReturnType<typeof useSubmitQueryCallback>,
264
291
  scheduleNextQueuedSubmission: () => void,
265
292
  ) {
293
+ const { submitQueryRef, streamingState, runtime, enqueueSubmission } = deps;
266
294
  useEffect(() => {
267
- deps.submitQueryRef.current = submitQuery;
268
- }, [submitQuery, deps.submitQueryRef]);
295
+ submitQueryRef.current = submitQuery;
296
+ }, [submitQuery, submitQueryRef]);
269
297
 
270
298
  useEffect(() => {
271
- if (deps.streamingState === StreamingState.Idle) {
299
+ if (streamingState === StreamingState.Idle) {
272
300
  scheduleNextQueuedSubmission();
273
301
  }
274
- }, [deps.streamingState, scheduleNextQueuedSubmission]);
302
+ }, [streamingState, scheduleNextQueuedSubmission]);
275
303
 
276
304
  useEffect(() => {
277
- const isAgentBusy = () => deps.streamingState !== StreamingState.Idle;
305
+ const unsubscribe = runtime.events.onMcpClientUpdate(
306
+ scheduleNextQueuedSubmission,
307
+ );
308
+ return () => {
309
+ unsubscribe();
310
+ };
311
+ }, [runtime, scheduleNextQueuedSubmission]);
312
+
313
+ useEffect(() => {
314
+ const isAgentBusy = () => streamingState !== StreamingState.Idle;
278
315
  const triggerAgentTurn = async (message: string) => {
279
- deps.queuedSubmissionsRef.current.push({
316
+ enqueueSubmission({
280
317
  query: [{ type: 'text', text: message }],
281
318
  });
282
319
  scheduleNextQueuedSubmission();
283
320
  };
284
321
 
285
- const unsubscribe = deps.runtime.asyncTasks.setupAsyncTaskAutoTrigger(
322
+ const unsubscribe = runtime.asyncTasks.setupAsyncTaskAutoTrigger(
286
323
  isAgentBusy,
287
324
  triggerAgentTurn,
288
325
  );
@@ -291,32 +328,206 @@ function useSubmitQueryEffects(
291
328
  unsubscribe();
292
329
  };
293
330
  }, [
294
- deps.runtime,
295
- deps.streamingState,
331
+ runtime,
332
+ streamingState,
296
333
  scheduleNextQueuedSubmission,
297
- deps.queuedSubmissionsRef,
334
+ enqueueSubmission,
335
+ ]);
336
+ }
337
+
338
+ function useDrainCleanup(
339
+ drainTimeoutRef: React.MutableRefObject<ReturnType<typeof setTimeout> | null>,
340
+ retryTimeoutRef: React.MutableRefObject<ReturnType<typeof setTimeout> | null>,
341
+ pendingSubmissionRef: React.MutableRefObject<QueuedSubmission | null>,
342
+ mountedRef: React.MutableRefObject<boolean>,
343
+ requeueSubmission: (submission: QueuedSubmission) => void,
344
+ releaseDrain: () => void,
345
+ ) {
346
+ useEffect(() => {
347
+ mountedRef.current = true;
348
+ return () => {
349
+ mountedRef.current = false;
350
+ if (drainTimeoutRef.current !== null) {
351
+ clearTimeout(drainTimeoutRef.current);
352
+ drainTimeoutRef.current = null;
353
+ }
354
+ if (retryTimeoutRef.current !== null) {
355
+ clearTimeout(retryTimeoutRef.current);
356
+ retryTimeoutRef.current = null;
357
+ }
358
+ if (pendingSubmissionRef.current !== null) {
359
+ requeueSubmission(pendingSubmissionRef.current);
360
+ pendingSubmissionRef.current = null;
361
+ releaseDrain();
362
+ }
363
+ };
364
+ }, [
365
+ drainTimeoutRef,
366
+ retryTimeoutRef,
367
+ mountedRef,
368
+ pendingSubmissionRef,
369
+ releaseDrain,
370
+ requeueSubmission,
298
371
  ]);
299
372
  }
300
373
 
301
- function useScheduleNext(deps: UseSubmitQueryDeps) {
302
- return useCallback(() => {
303
- if (deps.queuedSubmissionsRef.current.length === 0) {
374
+ function useDrainSubmission(
375
+ drainTimeoutRef: React.MutableRefObject<ReturnType<typeof setTimeout> | null>,
376
+ retryTimeoutRef: React.MutableRefObject<ReturnType<typeof setTimeout> | null>,
377
+ pendingSubmissionRef: React.MutableRefObject<QueuedSubmission | null>,
378
+ retryCountRef: React.MutableRefObject<number>,
379
+ scheduleRef: React.MutableRefObject<() => void>,
380
+ submitQueryRef: UseSubmitQueryDeps['submitQueryRef'],
381
+ requeueSubmission: UseSubmitQueryDeps['requeueSubmission'],
382
+ releaseDrain: UseSubmitQueryDeps['releaseDrain'],
383
+ ) {
384
+ return useCallback(
385
+ async (next: QueuedSubmission): Promise<void> => {
386
+ drainTimeoutRef.current = null;
387
+ let disposition: SubmissionDisposition = 'requeue-await-event';
388
+ try {
389
+ const submit = submitQueryRef.current;
390
+ if (submit === null) {
391
+ debugLogger.error(
392
+ 'Cannot drain queued submission before the submitter is ready.',
393
+ );
394
+ } else {
395
+ disposition = await submit(
396
+ next.query,
397
+ next.options,
398
+ next.promptId,
399
+ true,
400
+ );
401
+ }
402
+ } catch (error: unknown) {
403
+ debugLogger.error('Queued submission failed unexpectedly:', error);
404
+ } finally {
405
+ const ownsPendingSubmission = pendingSubmissionRef.current === next;
406
+ const shouldRetry =
407
+ ownsPendingSubmission &&
408
+ disposition === 'requeue-await-event' &&
409
+ retryCountRef.current < MAX_QUEUED_SUBMISSION_RETRIES;
410
+ const retryLimitReached =
411
+ ownsPendingSubmission &&
412
+ disposition === 'requeue-await-event' &&
413
+ !shouldRetry;
414
+
415
+ if (ownsPendingSubmission) {
416
+ pendingSubmissionRef.current = null;
417
+ if (disposition !== 'consumed' && !retryLimitReached) {
418
+ requeueSubmission(next);
419
+ }
420
+ releaseDrain();
421
+ }
422
+
423
+ if (shouldRetry) {
424
+ retryCountRef.current += 1;
425
+ retryTimeoutRef.current = setTimeout(() => {
426
+ retryTimeoutRef.current = null;
427
+ scheduleRef.current();
428
+ }, QUEUED_SUBMISSION_RETRY_DELAY_MS);
429
+ }
430
+
431
+ if (ownsPendingSubmission && !shouldRetry) {
432
+ retryCountRef.current = 0;
433
+ if (retryLimitReached) {
434
+ debugLogger.error(
435
+ 'Dropping queued submission after repeated drain failures.',
436
+ );
437
+ }
438
+ scheduleRef.current();
439
+ }
440
+ }
441
+ },
442
+ [
443
+ drainTimeoutRef,
444
+ retryTimeoutRef,
445
+ pendingSubmissionRef,
446
+ retryCountRef,
447
+ scheduleRef,
448
+ submitQueryRef,
449
+ requeueSubmission,
450
+ releaseDrain,
451
+ ],
452
+ );
453
+ }
454
+
455
+ function useScheduleNext(
456
+ deps: UseSubmitQueryDeps,
457
+ activeTurnRef: React.MutableRefObject<boolean>,
458
+ ) {
459
+ const {
460
+ queuedSubmissionsRef,
461
+ dequeueSubmission,
462
+ requeueSubmission,
463
+ submitQueryRef,
464
+ tryReserveDrain,
465
+ releaseDrain,
466
+ } = deps;
467
+ const drainTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
468
+ const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
469
+ const pendingSubmissionRef = useRef<QueuedSubmission | null>(null);
470
+ const retryCountRef = useRef(0);
471
+ const mountedRef = useRef(true);
472
+ // The mutable indirection breaks the schedule/drain callback cycle while
473
+ // allowing delayed retries to invoke the latest stable scheduler.
474
+ const scheduleRef = useRef<() => void>(() => undefined);
475
+ const streamingStateRef = useRef(deps.streamingState);
476
+ streamingStateRef.current = deps.streamingState;
477
+ useDrainCleanup(
478
+ drainTimeoutRef,
479
+ retryTimeoutRef,
480
+ pendingSubmissionRef,
481
+ mountedRef,
482
+ requeueSubmission,
483
+ releaseDrain,
484
+ );
485
+
486
+ const drainSubmission = useDrainSubmission(
487
+ drainTimeoutRef,
488
+ retryTimeoutRef,
489
+ pendingSubmissionRef,
490
+ retryCountRef,
491
+ scheduleRef,
492
+ submitQueryRef,
493
+ requeueSubmission,
494
+ releaseDrain,
495
+ );
496
+
497
+ const schedule = useCallback(() => {
498
+ const cannotDrain =
499
+ !mountedRef.current ||
500
+ activeTurnRef.current ||
501
+ streamingStateRef.current !== StreamingState.Idle ||
502
+ queuedSubmissionsRef.current.length === 0;
503
+ if (cannotDrain || !tryReserveDrain()) {
304
504
  return;
305
505
  }
506
+ if (retryTimeoutRef.current !== null) {
507
+ clearTimeout(retryTimeoutRef.current);
508
+ retryTimeoutRef.current = null;
509
+ }
306
510
 
307
- const next = deps.queuedSubmissionsRef.current.shift();
511
+ const next = dequeueSubmission();
308
512
  if (!next) {
513
+ releaseDrain();
309
514
  return;
310
515
  }
311
-
312
- setTimeout(() => {
313
- void deps.submitQueryRef.current?.(
314
- next.query,
315
- next.options,
316
- next.promptId,
317
- );
516
+ pendingSubmissionRef.current = next;
517
+ drainTimeoutRef.current = setTimeout(() => {
518
+ void drainSubmission(next);
318
519
  }, 0);
319
- }, [deps.queuedSubmissionsRef, deps.submitQueryRef]);
520
+ }, [
521
+ activeTurnRef,
522
+ queuedSubmissionsRef,
523
+ retryTimeoutRef,
524
+ dequeueSubmission,
525
+ drainSubmission,
526
+ tryReserveDrain,
527
+ releaseDrain,
528
+ ]);
529
+ scheduleRef.current = schedule;
530
+ return schedule;
320
531
  }
321
532
 
322
533
  interface SubmitQueryCallbackDeps extends UseSubmitQueryDeps {
@@ -331,39 +542,57 @@ interface SubmitQueryCallbackDeps extends UseSubmitQueryDeps {
331
542
  shouldProceed: boolean;
332
543
  }>;
333
544
  handleLoopDetectedEvent: () => void;
334
- scheduleNextQueuedSubmission: () => void;
335
545
  startNewPrompt: () => void;
336
546
  getPromptCount: () => number;
547
+ activeTurnRef: React.MutableRefObject<boolean>;
548
+ scheduleNextQueuedSubmission: () => void;
337
549
  }
338
550
 
339
551
  function useSubmitQueryCallback(cbd: SubmitQueryCallbackDeps) {
340
- return useCallback(
341
- async (
342
- query: AgentRequestInput,
343
- options?: { isContinuation: boolean },
344
- prompt_id?: string,
345
- ) => {
552
+ // Keep the callback identity stable while every invocation reads current deps.
553
+ const latestCbdRef = useRef(cbd);
554
+ latestCbdRef.current = cbd;
555
+ return useCallback<SubmissionExecutor>(
556
+ async (query, options, promptId, fromQueue = false) => {
557
+ const current = latestCbdRef.current;
346
558
  // submitQuery handles NEW user prompts only; the Agent's event stream
347
559
  // drives multi-turn continuation internally.
348
560
  void options;
349
561
 
350
- if (isQueueable(cbd.streamingState)) {
351
- cbd.queuedSubmissionsRef.current.push({
562
+ if (
563
+ current.activeTurnRef.current ||
564
+ isQueueable(current.streamingState)
565
+ ) {
566
+ if (fromQueue) {
567
+ return 'requeue';
568
+ }
569
+ current.enqueueSubmission({
352
570
  query,
353
- promptId: prompt_id,
571
+ promptId,
354
572
  });
355
- return;
573
+ return 'consumed';
356
574
  }
357
575
 
358
- const turn = initTurn(cbd, query, prompt_id, cbd.getPromptCount);
359
-
360
- if (shouldDisplayUserMessage(turn.trimmedStr)) {
361
- cbd.displayUserMessage(turn.trimmedStr, turn.userMessageTimestamp);
576
+ current.activeTurnRef.current = true;
577
+ try {
578
+ const turn = initTurn(current, query, promptId, current.getPromptCount);
579
+ if (shouldDisplayUserMessage(turn.trimmedStr)) {
580
+ current.displayUserMessage(
581
+ turn.trimmedStr,
582
+ turn.userMessageTimestamp,
583
+ );
584
+ }
585
+
586
+ await runSubmitQueryCore(current, query, turn);
587
+ return 'consumed';
588
+ } finally {
589
+ current.activeTurnRef.current = false;
590
+ // The active-turn owner schedules only after the turn settles. The
591
+ // scheduler's Idle check and reservation then prevent overlapping work.
592
+ current.scheduleNextQueuedSubmission();
362
593
  }
363
-
364
- await runSubmitQueryCore(cbd, query, turn);
365
594
  },
366
- [cbd],
595
+ [],
367
596
  );
368
597
  }
369
598
 
@@ -379,7 +608,6 @@ async function runSubmitQueryCore(
379
608
  turn.promptId,
380
609
  );
381
610
  if (!shouldProceed || queryToSend === null) {
382
- cbd.scheduleNextQueuedSubmission();
383
611
  return;
384
612
  }
385
613
 
@@ -409,11 +637,8 @@ async function runSubmitQueryCore(
409
637
  );
410
638
  }
411
639
  } finally {
412
- // Only clear isResponding when this turn is still the active one. When a
413
- // newer turn supersedes this one it replaces abortControllerRef.current
414
- // with a fresh AbortController; if the signals differ, the newer turn
415
- // already set isResponding(true) and clearing it here would cancel the
416
- // new turn (issue #2259).
640
+ // A superseded turn no longer owns responding state; stale cleanup here
641
+ // would clear the newer turn's active indicator.
417
642
  if (isCurrentTurn(cbd, turn)) {
418
643
  cbd.setIsResponding(false);
419
644
  }
@@ -15,6 +15,8 @@ import {
15
15
  removeMCPStatusChangeListener,
16
16
  } from '@vybestack/llxprt-code-mcp';
17
17
  import {
18
+ CoreEvent,
19
+ coreEvents,
18
20
  GitService,
19
21
  IdeClient,
20
22
  Logger,
@@ -265,11 +267,13 @@ function subscribeToExternalCommandChanges(
265
267
  client.addStatusChangeListener(listener);
266
268
  });
267
269
  addMCPStatusChangeListener(listener);
270
+ coreEvents.on(CoreEvent.FolderTrustChanged, listener);
268
271
  return () => {
269
272
  void IdeClient.getInstance().then((client) => {
270
273
  client.removeStatusChangeListener(listener);
271
274
  });
272
275
  removeMCPStatusChangeListener(listener);
276
+ coreEvents.off(CoreEvent.FolderTrustChanged, listener);
273
277
  };
274
278
  }
275
279
 
@@ -226,6 +226,7 @@ function createBaseAgent(
226
226
  return { text: '', toolCalls: [], finishReason: 'stop' };
227
227
  },
228
228
  stream: streamFn,
229
+ injectSteer: () => {},
229
230
  getProvider: () => 'test',
230
231
  async setProvider() {},
231
232
  getProviderStatus: () => ({
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { useState, useEffect } from 'react';
8
8
  import { ApprovalMode, type Agent } from '@vybestack/llxprt-code-agents';
9
+ import { coreEvents, CoreEvent } from '@vybestack/llxprt-code-core';
9
10
  import { useKeypress } from './useKeypress.js';
10
11
  import { keyMatchers, Command } from '../keyMatchers.js';
11
12
  import type { HistoryItemWithoutId } from '../types.js';
@@ -32,6 +33,18 @@ export function useAutoAcceptIndicator({
32
33
  setShowAutoAcceptIndicator(currentMode);
33
34
  }, [currentMode]);
34
35
 
36
+ useEffect(() => {
37
+ const handleFolderTrustChanged = (trusted: boolean): void => {
38
+ setShowAutoAcceptIndicator(
39
+ trusted ? agent.getApprovalMode() : ApprovalMode.DEFAULT,
40
+ );
41
+ };
42
+ coreEvents.on(CoreEvent.FolderTrustChanged, handleFolderTrustChanged);
43
+ return () => {
44
+ coreEvents.off(CoreEvent.FolderTrustChanged, handleFolderTrustChanged);
45
+ };
46
+ }, [agent]);
47
+
35
48
  useKeypress(
36
49
  (key) => {
37
50
  let nextApprovalMode: ApprovalMode | undefined;