@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,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type * as acp from '@agentclientprotocol/sdk';
8
+
9
+ /**
10
+ * The pinned ACP SDK (0.14.1) does not yet export `CloseSessionRequest`,
11
+ * `DeleteSessionRequest`, or their responses, nor does its
12
+ * `ClientCapabilities` type include the `session` field used for
13
+ * config-option gating. These local type aliases keep the integration
14
+ * strongly-typed without resorting to `any`.
15
+ */
16
+
17
+ export interface CloseSessionRequest {
18
+ readonly sessionId: acp.SessionId;
19
+ }
20
+
21
+ export type CloseSessionResponse = Record<string, never>;
22
+
23
+ export interface DeleteSessionRequest {
24
+ readonly sessionId: acp.SessionId;
25
+ }
26
+
27
+ export type DeleteSessionResponse = Record<string, never>;
28
+
29
+ export type ClientCapabilitiesWithSession = NonNullable<
30
+ acp.InitializeRequest['clientCapabilities']
31
+ > & {
32
+ readonly session?: {
33
+ readonly configOptions?: boolean;
34
+ };
35
+ };
@@ -14,7 +14,7 @@ export class AcpFileSystemService implements FileSystemService {
14
14
  constructor(
15
15
  private readonly connection: acp.AgentSideConnection,
16
16
  private readonly sessionId: string,
17
- private readonly capabilities: acp.FileSystemCapability,
17
+ private readonly capabilities: acp.FileSystemCapabilities,
18
18
  private readonly fallback: FileSystemService,
19
19
  ) {}
20
20
 
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { type Config, createInkStdio } from '@vybestack/llxprt-code-core';
8
+ import { DebugLogger } from '@vybestack/llxprt-code-telemetry';
9
+ import * as acp from '@agentclientprotocol/sdk';
10
+ import { Readable, Writable } from 'node:stream';
11
+ import * as process from 'node:process';
12
+ import { setCliRuntimeContext } from '@vybestack/llxprt-code-providers/runtime.js';
13
+ import type { LoadedSettings } from '../config/settings.js';
14
+ import { runExitCleanup } from '../utils/cleanup.js';
15
+ import { ZedAgent } from './zedIntegration.js';
16
+
17
+ const DISPOSAL_SIGNALS: readonly NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
18
+
19
+ /**
20
+ * Listener target for process-signal-driven disposal. Injectable so tests can
21
+ * drive the disposal path without sending real OS signals.
22
+ */
23
+ export type SignalListenerTarget = {
24
+ on(signal: NodeJS.Signals, listener: () => void): unknown;
25
+ off(signal: NodeJS.Signals, listener: () => void): unknown;
26
+ };
27
+
28
+ /**
29
+ * Builds the disposal action performed when a disposal signal fires.
30
+ *
31
+ * The transport-ownership rule: {@link ndJsonStream} calls
32
+ * `input.getReader()` on the web stream, which **locks** it. Calling
33
+ * `.cancel()` on a locked `ReadableStream` rejects with `TypeError: Cannot
34
+ * cancel a readable stream that is locked`, so disposal silently no-ops and
35
+ * `connection.closed` never resolves — the process hangs.
36
+ *
37
+ * Instead, we destroy the **owned** Node.js `Readable` source that
38
+ * `Readable.toWeb` wraps. Destroying the source propagates an abort/EOF into
39
+ * the web stream's active reader, which the SDK's `ndJsonStream` read loop
40
+ * detects (either a stream-end `{ done: true }` chunk or an underlying error).
41
+ * That causes the SDK to call `controller.close()` / `controller.error()`,
42
+ * resolving `connection.closed` naturally so the `finally` cleanup runs.
43
+ */
44
+ export function buildSignalDisposalHandler(
45
+ source: Readable,
46
+ logger: Pick<DebugLogger, 'debug'>,
47
+ ): () => void {
48
+ return () => {
49
+ try {
50
+ // destroy() is idempotent and safe to call on an already-ended stream.
51
+ // It propagates to the web stream via Readable.toWeb's adapter, causing
52
+ // the SDK's locked reader to observe the end/error.
53
+ source.destroy();
54
+ } catch (error) {
55
+ logger.debug(
56
+ () => `Signal-driven transport destroy failed: ${String(error)}`,
57
+ );
58
+ }
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Installs process-signal listeners that trigger graceful disposal. Returns a
64
+ * disposer that removes the listeners.
65
+ *
66
+ * Registers listeners so Node's default SIGINT/SIGTERM behavior (immediate
67
+ * process termination) is suppressed long enough for the `finally` cleanup in
68
+ * {@link runZedIntegration} to run.
69
+ */
70
+ export function installDisposalSignalHandlers(
71
+ onSignal: () => void,
72
+ signals: readonly NodeJS.Signals[] = DISPOSAL_SIGNALS,
73
+ target: SignalListenerTarget = process,
74
+ ): () => void {
75
+ for (const signal of signals) {
76
+ target.on(signal, onSignal);
77
+ }
78
+ return () => {
79
+ for (const signal of signals) {
80
+ target.off(signal, onSignal);
81
+ }
82
+ };
83
+ }
84
+
85
+ async function cleanupAgents(
86
+ agents: readonly ZedAgent[],
87
+ logger: DebugLogger,
88
+ ): Promise<void> {
89
+ const disposalResults = await Promise.allSettled(
90
+ agents.map((agent) => agent.disposeAll()),
91
+ );
92
+ const rejected = disposalResults.filter(
93
+ (result): result is PromiseRejectedResult => result.status === 'rejected',
94
+ );
95
+ for (const result of rejected) {
96
+ logger.warn(() => `Zed agent cleanup failed: ${String(result.reason)}`);
97
+ }
98
+ try {
99
+ await runExitCleanup();
100
+ } catch (cleanupError) {
101
+ logger.debug(() => `Exit cleanup failed: ${String(cleanupError)}`);
102
+ }
103
+ }
104
+
105
+ export async function runZedIntegration(
106
+ config: Config,
107
+ settings: LoadedSettings,
108
+ ): Promise<void> {
109
+ const logger = new DebugLogger('llxprt:zed-integration');
110
+ logger.debug(() => 'Starting Zed integration');
111
+ const { stdout: workingStdout } = createInkStdio();
112
+ const stdout = Writable.toWeb(workingStdout) as WritableStream;
113
+ // Keep the owned Node.js Readable reference. Destroying this source (not the
114
+ // locked web stream) is the only way to make the ACP ndJsonStream reader
115
+ // observe EOF/abort so connection.closed settles.
116
+ const stdinSource = process.stdin;
117
+ const stdin = Readable.toWeb(stdinSource) as ReadableStream<Uint8Array>;
118
+ setCliRuntimeContext(config.getSettingsService(), config, {
119
+ runtimeId: 'cli.runtime.zed',
120
+ metadata: { source: 'zed-integration', stage: 'bootstrap' },
121
+ allowDefaultHandoff: true,
122
+ });
123
+ const agents: ZedAgent[] = [];
124
+ const removeSignalHandlers = installDisposalSignalHandlers(
125
+ buildSignalDisposalHandler(stdinSource, logger),
126
+ );
127
+ try {
128
+ const stream = acp.ndJsonStream(stdout, stdin);
129
+ const connection = new acp.AgentSideConnection((conn) => {
130
+ const agent = new ZedAgent(config, settings, conn);
131
+ agents.push(agent);
132
+ return agent;
133
+ }, stream);
134
+ await connection.closed;
135
+ } catch (error) {
136
+ logger.warn(() => `Zed agent connection error: ${error}`);
137
+ throw error;
138
+ } finally {
139
+ removeSignalHandlers();
140
+ await cleanupAgents(agents, logger);
141
+ }
142
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type * as acp from '@agentclientprotocol/sdk';
8
+ import type { AgentEvent } from '@vybestack/llxprt-code-agents';
9
+ import { DebugLogger } from '@vybestack/llxprt-code-core';
10
+ import {
11
+ extractThoughtText,
12
+ mapDoneReasonToStopReason,
13
+ translateErrorEvent,
14
+ translateIdleTimeout,
15
+ } from './zed-helpers.js';
16
+ import type { StreamBatcher } from './zed-stream-batcher.js';
17
+ import {
18
+ emitToolCallStart,
19
+ emitToolResult,
20
+ emitToolStatus,
21
+ } from './zed-tool-handler.js';
22
+
23
+ const logger = new DebugLogger('llxprt:zed-integration:agent-events');
24
+
25
+ /**
26
+ * Flushes pending batched chunks without letting a flush failure replace the
27
+ * original event context, so an 'error' event is not swallowed and a 'done'
28
+ * stop reason is not dropped by a flush exception.
29
+ */
30
+ async function safeFlush(batcher: StreamBatcher): Promise<void> {
31
+ try {
32
+ await batcher.flush();
33
+ } catch (error) {
34
+ logger.debug(() => `Stream flush failed: ${String(error)}`);
35
+ }
36
+ }
37
+
38
+ interface AgentEventHandlers {
39
+ sendUpdate(update: acp.SessionUpdate): Promise<void>;
40
+ sendUsage(
41
+ usage: Extract<AgentEvent, { type: 'usage' }>['usage'],
42
+ ): Promise<void>;
43
+ handleConfirmation(
44
+ event: Extract<AgentEvent, { type: 'tool-confirmation' }>,
45
+ ): Promise<void>;
46
+ resolveToolKind(toolName: string): string | undefined;
47
+ }
48
+
49
+ export async function handleZedAgentEvent(
50
+ event: AgentEvent,
51
+ batcher: StreamBatcher,
52
+ handlers: AgentEventHandlers,
53
+ ): Promise<acp.StopReason | null> {
54
+ switch (event.type) {
55
+ case 'text':
56
+ batcher.append(event.text, false);
57
+ return null;
58
+ case 'thinking':
59
+ return handleThinking(event, batcher);
60
+ case 'tool-call':
61
+ return handleToolEvent(event, batcher, handlers);
62
+ case 'tool-status':
63
+ return handleToolUpdate(event, batcher, handlers);
64
+ case 'tool-result':
65
+ return handleToolResultEvent(event, batcher, handlers);
66
+ case 'tool-confirmation':
67
+ await safeFlush(batcher);
68
+ await handlers.handleConfirmation(event);
69
+ return null;
70
+ case 'done':
71
+ await safeFlush(batcher);
72
+ return mapDoneReasonToStopReason(event.reason);
73
+ case 'error':
74
+ await safeFlush(batcher);
75
+ throw translateErrorEvent(event);
76
+ case 'idle-timeout':
77
+ await safeFlush(batcher);
78
+ throw translateIdleTimeout(event);
79
+ case 'invalid-stream':
80
+ await safeFlush(batcher);
81
+ throw new Error(
82
+ 'Agent produced an invalid stream that could not be recovered.',
83
+ );
84
+ case 'hook-blocked':
85
+ await safeFlush(batcher);
86
+ if (event.info.systemMessage !== undefined) {
87
+ await handlers.sendUpdate({
88
+ sessionUpdate: 'agent_message_chunk',
89
+ content: { type: 'text', text: event.info.systemMessage },
90
+ });
91
+ }
92
+ return null;
93
+ case 'loop-detected':
94
+ await safeFlush(batcher);
95
+ return 'end_turn';
96
+ case 'notice':
97
+ return handleNotice(event, batcher, handlers);
98
+ case 'usage':
99
+ await safeFlush(batcher);
100
+ await handlers.sendUsage(event.usage);
101
+ return null;
102
+ case 'context-warning':
103
+ case 'compression':
104
+ case 'model-info':
105
+ case 'retry':
106
+ case 'citation':
107
+ return null;
108
+ default:
109
+ return assertNever(event);
110
+ }
111
+ }
112
+
113
+ function handleThinking(
114
+ event: Extract<AgentEvent, { type: 'thinking' }>,
115
+ batcher: StreamBatcher,
116
+ ): null {
117
+ const thoughtText = extractThoughtText(event.thought);
118
+ if (thoughtText.length > 0) batcher.append(thoughtText, true);
119
+ return null;
120
+ }
121
+
122
+ async function handleToolEvent(
123
+ event: Extract<AgentEvent, { type: 'tool-call' }>,
124
+ batcher: StreamBatcher,
125
+ handlers: AgentEventHandlers,
126
+ ): Promise<null> {
127
+ await safeFlush(batcher);
128
+ await emitToolCallStart(
129
+ event.call,
130
+ handlers.sendUpdate,
131
+ handlers.resolveToolKind(event.call.name),
132
+ );
133
+ return null;
134
+ }
135
+
136
+ async function handleToolUpdate(
137
+ event: Extract<AgentEvent, { type: 'tool-status' }>,
138
+ batcher: StreamBatcher,
139
+ handlers: AgentEventHandlers,
140
+ ): Promise<null> {
141
+ await safeFlush(batcher);
142
+ await emitToolStatus(
143
+ event.update,
144
+ handlers.sendUpdate,
145
+ handlers.resolveToolKind(event.update.name),
146
+ );
147
+ return null;
148
+ }
149
+
150
+ async function handleToolResultEvent(
151
+ event: Extract<AgentEvent, { type: 'tool-result' }>,
152
+ batcher: StreamBatcher,
153
+ handlers: AgentEventHandlers,
154
+ ): Promise<null> {
155
+ await safeFlush(batcher);
156
+ await emitToolResult(
157
+ event.result,
158
+ handlers.sendUpdate,
159
+ handlers.resolveToolKind(event.result.name),
160
+ );
161
+ return null;
162
+ }
163
+
164
+ async function handleNotice(
165
+ event: Extract<AgentEvent, { type: 'notice' }>,
166
+ batcher: StreamBatcher,
167
+ handlers: AgentEventHandlers,
168
+ ): Promise<null> {
169
+ await safeFlush(batcher);
170
+ await handlers.sendUpdate({
171
+ sessionUpdate: 'agent_message_chunk',
172
+ content: { type: 'text', text: event.message },
173
+ });
174
+ return null;
175
+ }
176
+
177
+ function assertNever(event: never): never {
178
+ throw new Error(`Unhandled agent event: ${JSON.stringify(event)}`);
179
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type { Agent } from '@vybestack/llxprt-code-agents';
8
+
9
+ export async function enableZedSessionRecording(
10
+ agent: Agent,
11
+ onFailure: (error: unknown) => void,
12
+ ): Promise<void> {
13
+ try {
14
+ await agent.session.setRecording({ enabled: true });
15
+ } catch (error) {
16
+ try {
17
+ onFailure(error);
18
+ } catch {
19
+ // Recording remains best-effort even when failure notification fails.
20
+ }
21
+ }
22
+ }
23
+
24
+ export async function buildZedSession<T>(
25
+ agent: Agent,
26
+ build: () => T | Promise<T>,
27
+ onDisposeFailure: (error: unknown) => void,
28
+ ): Promise<T> {
29
+ try {
30
+ return await build();
31
+ } catch (error) {
32
+ try {
33
+ await agent.dispose();
34
+ } catch (disposeError) {
35
+ try {
36
+ onDisposeFailure(disposeError);
37
+ } catch {
38
+ // Preserve the original session build failure.
39
+ }
40
+ }
41
+ throw error;
42
+ }
43
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type * as acp from '@agentclientprotocol/sdk';
8
+ import type { Agent } from '@vybestack/llxprt-code-agents';
9
+ import { DebugLogger } from '@vybestack/llxprt-code-core';
10
+
11
+ const logger = new DebugLogger('llxprt:zed-integration:commands');
12
+
13
+ export interface ZedCommandContext {
14
+ readonly agent: Agent;
15
+ }
16
+
17
+ export interface ZedCommandResult {
18
+ readonly text: string;
19
+ }
20
+
21
+ interface ZedCommandDefinition {
22
+ readonly name: string;
23
+ readonly description: string;
24
+ readonly inputHint?: string;
25
+ readonly handler: (
26
+ context: ZedCommandContext,
27
+ args: string,
28
+ ) => Promise<ZedCommandResult>;
29
+ }
30
+
31
+ export function parseZedCommandPrompt(
32
+ prompt: string,
33
+ ): { readonly name: string; readonly args: string } | null {
34
+ if (!prompt.startsWith('/')) return null;
35
+ const value = prompt.slice(1).trimStart();
36
+ if (value.length === 0) return null;
37
+ const boundary = value.search(/\s/);
38
+ return boundary === -1
39
+ ? { name: value.toLowerCase(), args: '' }
40
+ : {
41
+ name: value.slice(0, boundary).toLowerCase(),
42
+ args: value.slice(boundary + 1).trim(),
43
+ };
44
+ }
45
+
46
+ function formatCompression(
47
+ result: Awaited<ReturnType<Agent['compress']>>,
48
+ ): string {
49
+ const counts =
50
+ result.originalTokenCount === undefined ||
51
+ result.newTokenCount === undefined
52
+ ? ''
53
+ : ` (${result.originalTokenCount} → ${result.newTokenCount} tokens)`;
54
+ return `Context compression ${result.status}${counts}.`;
55
+ }
56
+
57
+ const COMMANDS: readonly ZedCommandDefinition[] = [
58
+ {
59
+ name: 'compact',
60
+ description: 'Compress conversation history to free context space',
61
+ handler: async ({ agent }) => ({
62
+ text: formatCompression(await agent.compress()),
63
+ }),
64
+ },
65
+ {
66
+ name: 'tools',
67
+ description: 'List available tools and their enabled status',
68
+ handler: async ({ agent }) => {
69
+ const tools = agent.tools.list();
70
+ return {
71
+ text:
72
+ tools.length === 0
73
+ ? 'No tools available.'
74
+ : `Available tools:\n${tools
75
+ .map((tool) => ` ${tool.enabled ? '[x]' : '[ ]'} ${tool.name}`)
76
+ .join('\n')}`,
77
+ };
78
+ },
79
+ },
80
+ {
81
+ name: 'memory',
82
+ description: 'Show memory files currently in use',
83
+ handler: async ({ agent }) => {
84
+ const paths = agent.memory.getFilePaths();
85
+ return {
86
+ text:
87
+ paths.length === 0
88
+ ? 'No memory files in use.'
89
+ : `Memory files:\n${paths.map((path) => ` ${path}`).join('\n')}`,
90
+ };
91
+ },
92
+ },
93
+ {
94
+ name: 'profile',
95
+ description: 'List saved profiles and identify the startup default',
96
+ handler: async ({ agent }) => {
97
+ const profiles = agent.profiles.list();
98
+ return {
99
+ text:
100
+ profiles.length === 0
101
+ ? 'No profiles available.'
102
+ : `Profiles:\n${profiles
103
+ .map(
104
+ (profile) =>
105
+ ` ${profile.name}${profile.isDefault ? ' (default)' : ''}`,
106
+ )
107
+ .join('\n')}`,
108
+ };
109
+ },
110
+ },
111
+ {
112
+ name: 'model',
113
+ description: 'Show the current model',
114
+ handler: async ({ agent }) => ({
115
+ text: `Current model: ${agent.getModel()}`,
116
+ }),
117
+ },
118
+ {
119
+ name: 'task',
120
+ description: 'Show active subagent tasks',
121
+ handler: async ({ agent }) => {
122
+ const tasks = agent.tasks.list();
123
+ return {
124
+ text:
125
+ tasks.length === 0
126
+ ? 'No active subagent tasks.'
127
+ : `Subagent tasks:\n${tasks
128
+ .map((task) => ` [${task.status}] ${task.goalPrompt}`)
129
+ .join('\n')}`,
130
+ };
131
+ },
132
+ },
133
+ ];
134
+
135
+ const COMMAND_MAP = new Map(COMMANDS.map((command) => [command.name, command]));
136
+
137
+ export function getZedAvailableCommands(): acp.AvailableCommand[] {
138
+ return COMMANDS.map(({ name, description, inputHint }) => ({
139
+ name,
140
+ description,
141
+ ...(inputHint === undefined ? {} : { input: { hint: inputHint } }),
142
+ }));
143
+ }
144
+
145
+ export function buildAvailableCommandsUpdate(): acp.SessionUpdate {
146
+ return {
147
+ sessionUpdate: 'available_commands_update',
148
+ availableCommands: getZedAvailableCommands(),
149
+ };
150
+ }
151
+
152
+ export async function executeZedCommand(
153
+ prompt: string,
154
+ context: ZedCommandContext,
155
+ ): Promise<ZedCommandResult | null> {
156
+ const parsed = parseZedCommandPrompt(prompt);
157
+ if (parsed === null) return null;
158
+ const command = COMMAND_MAP.get(parsed.name);
159
+ if (command === undefined) return null;
160
+ try {
161
+ return await command.handler(context, parsed.args);
162
+ } catch (error) {
163
+ const detail = error instanceof Error ? error.message : String(error);
164
+ logger.error(() => `Command /${command.name} failed: ${detail}`);
165
+ return { text: `Command /${command.name} failed. See logs for details.` };
166
+ }
167
+ }