@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
@@ -0,0 +1,388 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type {
8
+ AgentRequestInput,
9
+ ContentBlock,
10
+ IContent,
11
+ } from '@vybestack/llxprt-code-core';
12
+ import type React from 'react';
13
+ import { useMemo } from 'react';
14
+ import { Box, Text } from 'ink';
15
+ import { SemanticColors } from '../colors.js';
16
+ import { Command, getDefaultKeyBindingHint } from '../../config/keyBindings.js';
17
+ import { truncateEnd } from '../utils/responsive.js';
18
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
19
+ import type { QueuedSubmission } from '../hooks/agentStream/types.js';
20
+
21
+ interface QueuedMessagesPanelProps {
22
+ width: number;
23
+ collapsed?: boolean;
24
+ messages: readonly QueuedSubmission[];
25
+ }
26
+
27
+ const PANEL_HEIGHT_RATIO = 0.2;
28
+ const COLLAPSED_PREVIEW_WIDTH_RATIO = 0.55;
29
+ const FALLBACK_KEY_PREVIEW_LENGTH = 20;
30
+ const MIN_EXPANDED_PANEL_HEIGHT = 3;
31
+ const QUEUED_MESSAGES_KEY_HINT = getDefaultKeyBindingHint(
32
+ Command.TOGGLE_QUEUED_MESSAGES,
33
+ );
34
+
35
+ export type QueuedMessagesPanelView =
36
+ | { readonly kind: 'empty' }
37
+ | {
38
+ readonly kind: 'compact';
39
+ readonly width: number;
40
+ readonly summary: string;
41
+ }
42
+ | {
43
+ readonly kind: 'collapsed';
44
+ readonly width: number;
45
+ readonly panelHeight: number;
46
+ readonly summary: string;
47
+ readonly nextPreview?: string;
48
+ }
49
+ | {
50
+ readonly kind: 'expanded';
51
+ readonly width: number;
52
+ readonly panelHeight: number;
53
+ readonly heading: string;
54
+ readonly messages: ReadonlyArray<{
55
+ readonly key: string;
56
+ readonly number: number;
57
+ readonly preview: string;
58
+ }>;
59
+ readonly moreCount: number;
60
+ readonly showMoreIndicator: boolean;
61
+ };
62
+
63
+ /**
64
+ * Extract a human-readable preview string from any AgentRequestInput variant.
65
+ */
66
+ export function extractPreviewText(query: AgentRequestInput): string {
67
+ if (typeof query === 'string') {
68
+ return query;
69
+ }
70
+
71
+ if (Array.isArray(query)) {
72
+ if (query.length === 0) {
73
+ return '(empty message)';
74
+ }
75
+
76
+ if (isIContentArray(query)) {
77
+ return extractTextFromTurns(query);
78
+ }
79
+
80
+ const text = extractTextFromBlocks(query as ContentBlock[]);
81
+ return text.length > 0 ? text : '(non-text message)';
82
+ }
83
+
84
+ if (isIContent(query)) {
85
+ const text = extractTextFromBlocks(query.blocks);
86
+ return text.length > 0 ? text : '(non-text turn)';
87
+ }
88
+
89
+ return '(non-text message)';
90
+ }
91
+
92
+ function extractTextFromTurns(turns: IContent[]): string {
93
+ const text = turns
94
+ .map((turn) => extractTextFromBlocks(turn.blocks))
95
+ .filter((part) => part.length > 0)
96
+ .join(' ');
97
+ return text.length > 0 ? text : '(non-text turn)';
98
+ }
99
+
100
+ function extractTextFromBlocks(blocks: ContentBlock[]): string {
101
+ const parts: string[] = [];
102
+ for (const block of blocks) {
103
+ if (isTextBlock(block) && block.text.length > 0) {
104
+ parts.push(block.text);
105
+ }
106
+ }
107
+ return parts.join(' ');
108
+ }
109
+
110
+ function isTextBlock(block: unknown): block is { type: 'text'; text: string } {
111
+ if (typeof block !== 'object' || block === null) {
112
+ return false;
113
+ }
114
+ const candidate = block as Record<string, unknown>;
115
+ return candidate.type === 'text' && typeof candidate.text === 'string';
116
+ }
117
+
118
+ function isIContent(value: unknown): value is IContent {
119
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
120
+ return false;
121
+ }
122
+ const candidate = value as Record<string, unknown>;
123
+ return (
124
+ (candidate.speaker === 'human' ||
125
+ candidate.speaker === 'ai' ||
126
+ candidate.speaker === 'tool') &&
127
+ Array.isArray(candidate.blocks)
128
+ );
129
+ }
130
+
131
+ function isIContentArray(value: unknown): value is IContent[] {
132
+ return Array.isArray(value) && value.every((item) => isIContent(item));
133
+ }
134
+
135
+ function stableKey(
136
+ message: QueuedSubmission,
137
+ index: number,
138
+ preview: string,
139
+ ): string {
140
+ if (message.queueId !== undefined) {
141
+ return `queued-${message.queueId}`;
142
+ }
143
+ if (message.promptId !== undefined && message.promptId !== '') {
144
+ return `queued-${message.promptId}-${index}`;
145
+ }
146
+ return `queued-${index}-${preview.slice(0, FALLBACK_KEY_PREVIEW_LENGTH)}`;
147
+ }
148
+
149
+ function calculatePanelHeight(rows: number): number {
150
+ return Math.max(1, Math.floor(rows * PANEL_HEIGHT_RATIO));
151
+ }
152
+
153
+ function calculateAvailableRows(panelHeight: number): number {
154
+ const borderRows = panelHeight > 1 ? 1 : 0;
155
+ const headerRows = 1;
156
+ return Math.max(0, panelHeight - borderRows - headerRows);
157
+ }
158
+
159
+ function calculateMaxVisibleItems(
160
+ panelHeight: number,
161
+ messageCount: number,
162
+ ): number {
163
+ const availableRows = calculateAvailableRows(panelHeight);
164
+ if (messageCount <= availableRows) {
165
+ return messageCount;
166
+ }
167
+ if (availableRows <= 1) {
168
+ return 0;
169
+ }
170
+ return availableRows - 1;
171
+ }
172
+
173
+ function queuedMessageSummary(messageCount: number): string {
174
+ return `${messageCount} queued ${messageCount === 1 ? 'message' : 'messages'}`;
175
+ }
176
+
177
+ function calculateExpandedPreviewWidth(
178
+ panelWidth: number,
179
+ messageNumber: number,
180
+ ): number {
181
+ const shellPaddingWidth = panelWidth > 2 ? 2 : 0;
182
+ const numberPrefixWidth = `${messageNumber}. `.length;
183
+ return Math.max(1, panelWidth - shellPaddingWidth - numberPrefixWidth);
184
+ }
185
+
186
+ export function prepareQueuedMessagesPanelView({
187
+ width,
188
+ collapsed = false,
189
+ messages,
190
+ columns,
191
+ rows,
192
+ }: QueuedMessagesPanelProps & {
193
+ columns: number;
194
+ rows: number;
195
+ }): QueuedMessagesPanelView {
196
+ if (messages.length === 0) {
197
+ return { kind: 'empty' };
198
+ }
199
+
200
+ const panelHeight = calculatePanelHeight(rows);
201
+ const boundedWidth = Math.max(1, Math.min(width, columns));
202
+
203
+ if (collapsed) {
204
+ return {
205
+ kind: 'collapsed',
206
+ width: boundedWidth,
207
+ panelHeight,
208
+ summary: queuedMessageSummary(messages.length),
209
+ nextPreview: truncateEnd(
210
+ extractPreviewText(messages[0].query),
211
+ Math.max(1, Math.floor(boundedWidth * COLLAPSED_PREVIEW_WIDTH_RATIO)),
212
+ ),
213
+ };
214
+ }
215
+
216
+ if (panelHeight < MIN_EXPANDED_PANEL_HEIGHT) {
217
+ return {
218
+ kind: 'compact',
219
+ width: boundedWidth,
220
+ summary: queuedMessageSummary(messages.length),
221
+ };
222
+ }
223
+
224
+ const maxVisibleItems = calculateMaxVisibleItems(
225
+ panelHeight,
226
+ messages.length,
227
+ );
228
+ const visibleMessages = messages.slice(0, maxVisibleItems);
229
+ const moreCount = messages.length - visibleMessages.length;
230
+
231
+ return {
232
+ kind: 'expanded',
233
+ width: boundedWidth,
234
+ panelHeight,
235
+ heading: `Queued Messages (${messages.length})`,
236
+ messages: visibleMessages.map((message, index) => {
237
+ const preview = extractPreviewText(message.query);
238
+ const messageNumber = index + 1;
239
+ return {
240
+ key: stableKey(message, index, preview),
241
+ number: messageNumber,
242
+ preview: truncateEnd(
243
+ preview,
244
+ calculateExpandedPreviewWidth(boundedWidth, messageNumber),
245
+ ),
246
+ };
247
+ }),
248
+ moreCount,
249
+ showMoreIndicator: moreCount > 0,
250
+ };
251
+ }
252
+
253
+ function CompactQueuedMessagesPanel({
254
+ view,
255
+ }: {
256
+ view: Extract<QueuedMessagesPanelView, { kind: 'compact' }>;
257
+ }) {
258
+ return (
259
+ <Box width={view.width} height={1} overflow="hidden">
260
+ <Text color={SemanticColors.text.accent} bold wrap="truncate-end">
261
+ {view.summary}
262
+ </Text>
263
+ </Box>
264
+ );
265
+ }
266
+
267
+ function QueuedMessagesPanelShell({
268
+ width,
269
+ panelHeight,
270
+ children,
271
+ }: {
272
+ width: number;
273
+ panelHeight: number;
274
+ children: React.ReactNode;
275
+ }) {
276
+ return (
277
+ <Box
278
+ flexDirection="column"
279
+ width={width}
280
+ height={panelHeight}
281
+ overflow="hidden"
282
+ borderStyle="single"
283
+ borderColor={SemanticColors.text.accent}
284
+ borderTop={panelHeight > 1}
285
+ borderBottom={false}
286
+ borderLeft={false}
287
+ borderRight={false}
288
+ paddingX={width > 2 ? 1 : 0}
289
+ >
290
+ {children}
291
+ </Box>
292
+ );
293
+ }
294
+
295
+ function CollapsedQueuedMessagesPanel({
296
+ view,
297
+ }: {
298
+ view: Extract<QueuedMessagesPanelView, { kind: 'collapsed' }>;
299
+ }) {
300
+ return (
301
+ <QueuedMessagesPanelShell width={view.width} panelHeight={view.panelHeight}>
302
+ <Box flexDirection="row" minHeight={1} overflow="hidden">
303
+ <Text color={SemanticColors.text.primary} bold wrap="truncate-end">
304
+ {view.summary}
305
+ </Text>
306
+ {view.nextPreview !== undefined && (
307
+ <Text color={SemanticColors.text.secondary}>
308
+ {' '}
309
+ • next: {view.nextPreview}
310
+ </Text>
311
+ )}
312
+ <Text color={SemanticColors.text.secondary}>
313
+ {' '}
314
+ • {QUEUED_MESSAGES_KEY_HINT} to expand
315
+ </Text>
316
+ </Box>
317
+ </QueuedMessagesPanelShell>
318
+ );
319
+ }
320
+
321
+ function ExpandedQueuedMessagesPanel({
322
+ view,
323
+ }: {
324
+ view: Extract<QueuedMessagesPanelView, { kind: 'expanded' }>;
325
+ }) {
326
+ return (
327
+ <QueuedMessagesPanelShell width={view.width} panelHeight={view.panelHeight}>
328
+ <Box minHeight={1} marginBottom={0}>
329
+ <Text color={SemanticColors.text.accent} bold>
330
+ {view.heading}
331
+ </Text>
332
+ <Text color={SemanticColors.text.secondary}>
333
+ {' '}
334
+ • {QUEUED_MESSAGES_KEY_HINT} to minimize
335
+ </Text>
336
+ </Box>
337
+ {view.messages.map((message) => (
338
+ <Box key={message.key} flexDirection="row" minHeight={1}>
339
+ <Text color={SemanticColors.text.secondary} bold>
340
+ {message.number}.{' '}
341
+ </Text>
342
+ <Box flexGrow={1} overflow="hidden">
343
+ <Text color={SemanticColors.text.secondary} wrap="truncate-end">
344
+ {message.preview}
345
+ </Text>
346
+ </Box>
347
+ </Box>
348
+ ))}
349
+ {view.showMoreIndicator && (
350
+ <Box flexDirection="row" minHeight={1}>
351
+ <Text color={SemanticColors.text.secondary}>
352
+ ▼ +{view.moreCount} more
353
+ </Text>
354
+ </Box>
355
+ )}
356
+ </QueuedMessagesPanelShell>
357
+ );
358
+ }
359
+
360
+ export function QueuedMessagesPanel({
361
+ width,
362
+ collapsed = false,
363
+ messages,
364
+ }: QueuedMessagesPanelProps) {
365
+ const { columns, rows } = useTerminalSize();
366
+ const view = useMemo(
367
+ () =>
368
+ prepareQueuedMessagesPanelView({
369
+ width,
370
+ collapsed,
371
+ messages,
372
+ columns,
373
+ rows,
374
+ }),
375
+ [width, collapsed, messages, columns, rows],
376
+ );
377
+
378
+ if (view.kind === 'empty') {
379
+ return null;
380
+ }
381
+ if (view.kind === 'compact') {
382
+ return <CompactQueuedMessagesPanel view={view} />;
383
+ }
384
+ if (view.kind === 'collapsed') {
385
+ return <CollapsedQueuedMessagesPanel view={view} />;
386
+ }
387
+ return <ExpandedQueuedMessagesPanel view={view} />;
388
+ }
@@ -190,6 +190,7 @@ type InputPromptState = Pick<
190
190
  InputPromptProps,
191
191
  | 'buffer'
192
192
  | 'onSubmit'
193
+ | 'onSteer'
193
194
  | 'userMessages'
194
195
  | 'onClearScreen'
195
196
  | 'config'
@@ -390,6 +391,7 @@ const buildInputHandlerDeps = (
390
391
  inputHistory,
391
392
  handleSubmitAndClear: submitState.handleSubmitAndClear,
392
393
  handleSubmit: submitState.handleSubmit,
394
+ handleSteer: state.onSteer,
393
395
  shellHistory: completionState.shellHistory,
394
396
  reverseSearchCompletion: completionState.reverseSearchCompletion,
395
397
  shellPathCompletion: completionState.shellPathCompletion,
@@ -415,6 +415,8 @@ export type InputHandlerDeps = {
415
415
  inputHistory: { navigateUp: () => void; navigateDown: () => void };
416
416
  handleSubmitAndClear: (value: string) => void;
417
417
  handleSubmit: (value: string) => void;
418
+ /** Optional steer callback: injects the text into the active agent loop at the next tool-call boundary. Returns true if consumed (streaming), false to fall through to newline. */
419
+ handleSteer?: (text: string) => boolean;
418
420
  shellHistory: {
419
421
  getPreviousCommand: () => string | null;
420
422
  getNextCommand: () => string | null;
@@ -518,6 +520,18 @@ const handleNavigationInputKeys = (key: Key, deps: InputHandlerDeps): boolean =>
518
520
 
519
521
  const handleSubmitAndEditKeys = (key: Key, deps: InputHandlerDeps): boolean => {
520
522
  const { buffer, handleSubmit, resetCompletionState } = deps;
523
+ // During streaming, Ctrl+Enter (STEER) injects the current buffer as a
524
+ // mid-turn steer. When not streaming, handleSteer returns false and the
525
+ // key falls through to NEWLINE (which also matches Ctrl+Enter) for normal
526
+ // multi-line input.
527
+ if (deps.handleSteer && keyMatchers[Command.STEER](key)) {
528
+ const consumed = deps.handleSteer(buffer.text);
529
+ if (consumed) {
530
+ buffer.setText('');
531
+ resetCompletionState();
532
+ return true;
533
+ }
534
+ }
521
535
  if (keyMatchers[Command.SUBMIT](key)) {
522
536
  submitBufferInput(buffer, handleSubmit);
523
537
  return true;
@@ -14,6 +14,12 @@ import type { CliUiRuntime } from '../cliUiRuntime.js';
14
14
  export interface InputPromptProps {
15
15
  buffer: TextBuffer;
16
16
  onSubmit: (value: string) => void;
17
+ /**
18
+ * Optional steer callback: during streaming, Ctrl+Enter injects the text
19
+ * into the active agent loop at the next tool-call boundary. Returns true
20
+ * if consumed (agent is streaming), false to fall through to newline.
21
+ */
22
+ onSteer?: (text: string) => boolean;
17
23
  userMessages: readonly string[];
18
24
  onClearScreen: () => void;
19
25
  config: CliUiRuntime;
@@ -24,6 +24,16 @@ interface AiMessageProps {
24
24
  thinkingBlocks?: ThinkingBlock[]; // @plan:PLAN-20251202-THINKING-UI.P06
25
25
  }
26
26
 
27
+ export function getVisibleThinkingBlocks(
28
+ showThinking: boolean,
29
+ thinkingBlocks?: ThinkingBlock[],
30
+ ): ThinkingBlock[] | undefined {
31
+ if (!showThinking) {
32
+ return undefined;
33
+ }
34
+ return thinkingBlocks?.filter((block) => block.isHidden !== true);
35
+ }
36
+
27
37
  export const AiMessage: React.FC<AiMessageProps> = ({
28
38
  text,
29
39
  isPending,
@@ -46,10 +56,13 @@ export const AiMessage: React.FC<AiMessageProps> = ({
46
56
  const prefix = ' ';
47
57
  const prefixWidth = prefix.length;
48
58
 
49
- // Don't show thinkingBlocks in pending items - LoadingIndicator shows the
50
- // thought subject/description as spinner text during streaming. Only show
51
- // thinkingBlocks in committed history items to avoid duplication (fixes #922).
52
- const shouldShowThinkingBlocks = showThinking && !isPending;
59
+ // #1723: Show thinking blocks in BOTH pending and committed items so thinking
60
+ // content streams in real-time. The LoadingIndicator still shows the transient
61
+ // thought subject, but the growing thinking block is now visible below it.
62
+ const visibleThinkingBlocks = getVisibleThinkingBlocks(
63
+ showThinking,
64
+ thinkingBlocks,
65
+ );
53
66
 
54
67
  return (
55
68
  <Box flexDirection="column">
@@ -63,14 +76,13 @@ export const AiMessage: React.FC<AiMessageProps> = ({
63
76
  <Text color={Colors.DimComment}>{model}</Text>
64
77
  </Box>
65
78
  )}
66
- {shouldShowThinkingBlocks &&
67
- thinkingBlocks?.map((block, index) => (
68
- <ThinkingBlockDisplay
69
- key={`thinking-${index}`}
70
- block={block}
71
- visible={true}
72
- />
73
- ))}
79
+ {visibleThinkingBlocks?.map((block, index) => (
80
+ <ThinkingBlockDisplay
81
+ key={`thinking-${index}`}
82
+ block={block}
83
+ visible={true}
84
+ />
85
+ ))}
74
86
  <Box flexDirection="row">
75
87
  <Box width={prefixWidth}>
76
88
  <Text
@@ -5,14 +5,14 @@
5
5
  */
6
6
 
7
7
  import type React from 'react';
8
- import { useMemo, useState } from 'react';
8
+ import { useMemo } from 'react';
9
9
  import { Box, Text } from 'ink';
10
10
  import type { IndividualToolCallDisplay } from '../../types.js';
11
11
  import { ToolCallStatus } from '../../types.js';
12
12
  import { Colors } from '../../colors.js';
13
13
  import { theme } from '../../semantic-colors.js';
14
14
  import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
15
- import { useKeypress } from '../../hooks/useKeypress.js';
15
+ import { useShellCommandDisplay } from '../../contexts/ShellCommandDisplayContext.js';
16
16
  import {
17
17
  ShellExecutionService,
18
18
  splitCommands,
@@ -67,12 +67,14 @@ function extractEchoText(cmd: string): string | null {
67
67
  * Compute current subcommand for display when details visible.
68
68
  */
69
69
  function computeCurrentSubcommand(
70
- isDetailsVisible: boolean,
70
+ showFullShellDescription: boolean,
71
71
  status: ToolCallStatus,
72
72
  description: string | undefined,
73
73
  resultDisplay: string | object | undefined,
74
74
  ): string | null {
75
- if (!isDetailsVisible || status !== ToolCallStatus.Executing) return null;
75
+ if (!showFullShellDescription || status !== ToolCallStatus.Executing) {
76
+ return null;
77
+ }
76
78
  if (!description) return null;
77
79
 
78
80
  const outputString =
@@ -164,39 +166,11 @@ function computeShellFocusState(
164
166
  };
165
167
  }
166
168
 
167
- /**
168
- * Hook to manage details visibility toggle.
169
- */
170
- function useDetailsToggle(
171
- status: ToolCallStatus,
172
- isFocused: boolean,
173
- ): [boolean, React.Dispatch<React.SetStateAction<boolean>>] {
174
- const [isDetailsVisible, setIsDetailsVisible] = useState(false);
175
-
176
- useKeypress(
177
- (key) => {
178
- if (
179
- key.ctrl === true &&
180
- key.name === 'r' &&
181
- status === ToolCallStatus.Executing
182
- ) {
183
- setIsDetailsVisible((prev) => !prev);
184
- }
185
- },
186
- { isActive: isFocused },
187
- );
188
-
189
- return [isDetailsVisible, setIsDetailsVisible];
190
- }
191
-
192
169
  /**
193
170
  * Render the executing status hint.
194
171
  */
195
- function renderExecutingHint(
196
- status: ToolCallStatus,
197
- isDetailsVisible: boolean,
198
- ): React.ReactNode {
199
- if (status !== ToolCallStatus.Executing || isDetailsVisible) {
172
+ function renderExecutingHint(showExecutingHint: boolean): React.ReactNode {
173
+ if (!showExecutingHint) {
200
174
  return null;
201
175
  }
202
176
  return (
@@ -260,6 +234,7 @@ function renderToolMessageHeader(
260
234
  name: string,
261
235
  description: string,
262
236
  emphasis: TextEmphasis,
237
+ showFullDescription: boolean,
263
238
  isThisShellFocusable: boolean,
264
239
  isThisShellFocused: boolean,
265
240
  ): React.ReactNode {
@@ -276,6 +251,7 @@ function renderToolMessageHeader(
276
251
  status={status}
277
252
  description={description}
278
253
  emphasis={emphasis}
254
+ showFullDescription={showFullDescription}
279
255
  />
280
256
  {isThisShellFocusable && (
281
257
  <Box marginLeft={1} flexShrink={0}>
@@ -299,7 +275,7 @@ function renderToolMessageContent(
299
275
  borderColor: string,
300
276
  borderDimColor: boolean,
301
277
  status: ToolCallStatus,
302
- isDetailsVisible: boolean,
278
+ showExecutingHint: boolean,
303
279
  currentSubcommand: string | null,
304
280
  resultDisplay: string | object | undefined,
305
281
  availableTerminalHeight: number | undefined,
@@ -323,7 +299,7 @@ function renderToolMessageContent(
323
299
  flexDirection="column"
324
300
  overflowX="hidden"
325
301
  >
326
- {renderExecutingHint(status, isDetailsVisible)}
302
+ {renderExecutingHint(showExecutingHint)}
327
303
  {renderCurrentSubcommand(currentSubcommand)}
328
304
  <ToolResultDisplay
329
305
  resultDisplay={resultDisplay}
@@ -355,6 +331,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
355
331
  }
356
332
 
357
333
  export const ToolMessage: React.FC<ToolMessageProps> = ({
334
+ callId,
358
335
  name,
359
336
  description,
360
337
  resultDisplay,
@@ -372,7 +349,13 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
372
349
  borderColor = Colors.Gray,
373
350
  borderDimColor = false,
374
351
  }) => {
375
- const [isDetailsVisible] = useDetailsToggle(status, isFocused);
352
+ const isShellTool = name === SHELL_NAME || name === SHELL_COMMAND_NAME;
353
+ const showFullShellDescription = useShellCommandDisplay(
354
+ callId,
355
+ name,
356
+ status,
357
+ isFocused,
358
+ );
376
359
 
377
360
  const { isThisShellFocused, isThisShellFocusable } = computeShellFocusState(
378
361
  name,
@@ -386,12 +369,12 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
386
369
  const currentSubcommand = useMemo(
387
370
  () =>
388
371
  computeCurrentSubcommand(
389
- isDetailsVisible,
372
+ showFullShellDescription,
390
373
  status,
391
374
  description,
392
375
  resultDisplay,
393
376
  ),
394
- [isDetailsVisible, status, description, resultDisplay],
377
+ [showFullShellDescription, status, description, resultDisplay],
395
378
  );
396
379
 
397
380
  return (
@@ -405,6 +388,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
405
388
  name,
406
389
  description || '',
407
390
  emphasis,
391
+ showFullShellDescription,
408
392
  isThisShellFocusable,
409
393
  isThisShellFocused,
410
394
  )}
@@ -413,7 +397,9 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
413
397
  borderColor,
414
398
  borderDimColor,
415
399
  status,
416
- isDetailsVisible,
400
+ isShellTool &&
401
+ status === ToolCallStatus.Executing &&
402
+ !showFullShellDescription,
417
403
  currentSubcommand,
418
404
  resultDisplay,
419
405
  availableTerminalHeight,
@@ -25,6 +25,7 @@ export interface UIActionsParams {
25
25
 
26
26
  // Input actions
27
27
  handleUserInputSubmit: (value: string) => void;
28
+ handleSteer: (text: string) => boolean;
28
29
  handleClearScreen: () => void;
29
30
 
30
31
  // Theme dialog
@@ -92,7 +93,7 @@ export interface UIActionsParams {
92
93
  exitToolsDialog: () => void;
93
94
 
94
95
  // Folder trust dialog
95
- handleFolderTrustSelect: (choice: FolderTrustChoice) => void;
96
+ handleFolderTrustSelect: (choice: FolderTrustChoice) => Promise<void>;
96
97
 
97
98
  // Welcome onboarding
98
99
  welcomeActions: UIActions['welcomeActions'];
@@ -198,6 +199,7 @@ export function buildUIActions(params: UIActionsParams): UIActions {
198
199
 
199
200
  // Input actions
200
201
  handleUserInputSubmit: params.handleUserInputSubmit,
202
+ handleSteer: params.handleSteer,
201
203
  handleClearScreen: params.handleClearScreen,
202
204
 
203
205
  // Theme dialog