@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.
- package/package.json +13 -13
- package/src/commands/mcp/add.ts +8 -4
- package/src/config/cliArgParser.ts +2 -0
- package/src/config/config.ts +2 -0
- package/src/config/configBuilder.ts +3 -0
- package/src/config/configError.ts +23 -0
- package/src/config/extensions/extensionEnablement.ts +62 -1
- package/src/config/interactiveContext.ts +2 -1
- package/src/config/intermediateConfig.ts +4 -0
- package/src/config/keyBindings.ts +29 -3
- package/src/config/policy.ts +7 -1
- package/src/config/postConfigRuntime.ts +20 -14
- package/src/config/settings-schema/schema-ui.ts +9 -0
- package/src/config/settingsLoader.ts +21 -17
- package/src/config/settingsSchema.ts +2 -2
- package/src/config/trustedFolders.ts +275 -58
- package/src/config/yargsOptions.ts +7 -0
- package/src/generated/git-commit.json +1 -1
- package/src/nonInteractiveCli.ts +1 -0
- package/src/nonInteractiveCliSupport.ts +171 -51
- package/src/runtime/interactiveToolScheduler.ts +6 -6
- package/src/services/BuiltinCommandLoader.ts +33 -25
- package/src/services/FileCommandLoader.ts +34 -7
- package/src/test-utils/async.ts +20 -0
- package/src/test-utils/render.tsx +10 -1
- package/src/ui/App.tsx +17 -10
- package/src/ui/AppContainerRuntime.tsx +7 -9
- package/src/ui/cliUiRuntime.ts +28 -0
- package/src/ui/commands/authCommand.ts +240 -3
- package/src/ui/commands/compressCommand.ts +9 -1
- package/src/ui/commands/directoryCommand.tsx +1 -1
- package/src/ui/commands/logoutCommand.ts +2 -2
- package/src/ui/commands/permissionsCommand.ts +77 -20
- package/src/ui/commands/policiesCommand.ts +6 -1
- package/src/ui/commands/quotaCommand.ts +501 -0
- package/src/ui/commands/statsCommand.ts +2 -2
- package/src/ui/commands/statsQuota.ts +1 -1
- package/src/ui/commands/subagentCommand.ts +59 -27
- package/src/ui/components/AuthDialog.tsx +3 -3
- package/src/ui/components/Composer.tsx +1 -0
- package/src/ui/components/DialogManager.tsx +9 -2
- package/src/ui/components/FolderTrustDialog.tsx +71 -60
- package/src/ui/components/PermissionsModifyTrustDialog.tsx +211 -146
- package/src/ui/components/ProfileCreateWizard/constants.ts +13 -1
- package/src/ui/components/QueuedMessagesPanel.tsx +388 -0
- package/src/ui/components/inputPromptHooks.ts +2 -0
- package/src/ui/components/inputPromptKeyHandlers.ts +14 -0
- package/src/ui/components/inputPromptTypes.ts +6 -0
- package/src/ui/components/messages/AiMessage.tsx +24 -12
- package/src/ui/components/messages/ToolMessage.tsx +26 -40
- package/src/ui/containers/AppContainer/builders/buildUIActions.ts +3 -1
- package/src/ui/containers/AppContainer/builders/buildUIState.ts +5 -4
- package/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +3 -33
- package/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +2 -14
- package/src/ui/containers/AppContainer/hooks/useAppInput.ts +31 -25
- package/src/ui/containers/AppContainer/hooks/useAppLayout.ts +90 -60
- package/src/ui/containers/AppContainer/hooks/useDisplayPreferences.ts +10 -0
- package/src/ui/containers/AppContainer/hooks/useInputHandling.ts +3 -17
- package/src/ui/containers/AppContainer/hooks/useKeybindings.ts +51 -38
- package/src/ui/containers/AppContainer/hooks/useSteer.ts +51 -0
- package/src/ui/contexts/KeypressContext.tsx +13 -2
- package/src/ui/contexts/ShellCommandDisplayContext.tsx +103 -0
- package/src/ui/contexts/TodoContext.tsx +0 -13
- package/src/ui/contexts/TodoProvider.tsx +1 -34
- package/src/ui/contexts/UIActionsContext.tsx +2 -1
- package/src/ui/contexts/UIStateContext.tsx +3 -2
- package/src/ui/hooks/agentStream/streamUtils.ts +0 -24
- package/src/ui/hooks/agentStream/thoughtState.ts +110 -30
- package/src/ui/hooks/agentStream/types.ts +1 -0
- package/src/ui/hooks/agentStream/useAgentEventStream.ts +8 -17
- package/src/ui/hooks/agentStream/useAgentStream.ts +2 -2
- package/src/ui/hooks/agentStream/useAgentStreamLifecycle.ts +4 -5
- package/src/ui/hooks/agentStream/useAgentStreamOrchestration.ts +12 -5
- package/src/ui/hooks/agentStream/useQueuedSubmissions.ts +99 -0
- package/src/ui/hooks/agentStream/useStreamEventHandlers.ts +4 -13
- package/src/ui/hooks/agentStream/useStreamState.ts +60 -41
- package/src/ui/hooks/agentStream/useSubmitQuery.ts +284 -59
- package/src/ui/hooks/slashCommandProcessorSupport.ts +4 -0
- package/src/ui/hooks/useAgentStream-test-helpers.ts +1 -0
- package/src/ui/hooks/useAutoAcceptIndicator.ts +13 -0
- package/src/ui/hooks/useFolderTrust.ts +118 -81
- package/src/ui/hooks/useGitBranchName.ts +79 -33
- package/src/ui/hooks/useIdeTrustListener.ts +5 -19
- package/src/ui/hooks/usePermissionsModifyTrust.ts +223 -85
- package/src/ui/hooks/useReactToolScheduler.ts +13 -17
- package/src/ui/hooks/useTodoPausePreserver.ts +0 -55
- package/src/ui/layouts/DefaultAppLayout.tsx +2 -0
- package/src/ui/layouts/DefaultAppLayoutHelpers.tsx +37 -162
- package/src/ui/layouts/InlineContent.tsx +198 -0
- package/src/ui/trustDialogHelpers.ts +190 -0
- package/src/utils/cleanup-state.ts +115 -0
- package/src/utils/cleanup.ts +32 -34
- package/src/utils/sandbox-containers.ts +183 -53
- package/src/utils/sandbox-exec.ts +9 -3
- package/src/zed-integration/acp-terminal-shell-host.ts +112 -0
- package/src/zed-integration/acp-types.ts +35 -0
- package/src/zed-integration/fileSystemService.ts +1 -1
- package/src/zed-integration/runZedIntegration.ts +142 -0
- package/src/zed-integration/zed-agent-event-handler.ts +179 -0
- package/src/zed-integration/zed-agent-setup.ts +43 -0
- package/src/zed-integration/zed-command-registry.ts +167 -0
- package/src/zed-integration/zed-config-options.ts +280 -0
- package/src/zed-integration/zed-content-utils.ts +2 -2
- package/src/zed-integration/zed-helpers.ts +132 -0
- package/src/zed-integration/zed-initialize.ts +64 -0
- package/src/zed-integration/zed-plan-update.ts +20 -0
- package/src/zed-integration/zed-prompt-command.ts +77 -0
- package/src/zed-integration/zed-session-config.ts +119 -0
- package/src/zed-integration/zed-session-errors.ts +337 -0
- package/src/zed-integration/zed-session-events.ts +216 -0
- package/src/zed-integration/zed-session-info.ts +255 -0
- package/src/zed-integration/zed-session-lifecycle.ts +186 -0
- package/src/zed-integration/zed-session-listing.ts +153 -0
- package/src/zed-integration/zed-session-loader.ts +193 -0
- package/src/zed-integration/zed-session-pagination.ts +240 -0
- package/src/zed-integration/zed-session-replay.ts +564 -0
- package/src/zed-integration/zed-stream-batcher.ts +236 -0
- package/src/zed-integration/zed-terminal-manager.ts +416 -0
- package/src/zed-integration/zed-terminal-setup.ts +72 -0
- package/src/zed-integration/zed-test-helpers.ts +349 -4
- package/src/zed-integration/zed-tool-handler.ts +47 -5
- package/src/zed-integration/zedIntegration.ts +538 -587
- package/src/services/todo-continuation/todoContinuationService.ts +0 -610
- package/src/ui/containers/AppContainer/hooks/useIdeRestartHotkey.ts +0 -33
- package/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +0 -160
- package/src/ui/hooks/useTodoContinuation.ts +0 -299
|
@@ -7,19 +7,32 @@
|
|
|
7
7
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
8
8
|
import { ExitCodes } from '@vybestack/llxprt-code-core';
|
|
9
9
|
import { DebugLogger } from '@vybestack/llxprt-code-telemetry';
|
|
10
|
-
import type { LoadedSettings
|
|
10
|
+
import type { LoadedSettings } from '../../config/settings.js';
|
|
11
11
|
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
|
|
12
12
|
import {
|
|
13
13
|
loadTrustedFolders,
|
|
14
|
+
resolveLocalWorkspaceTrust,
|
|
14
15
|
TrustLevel,
|
|
15
16
|
isWorkspaceTrusted,
|
|
17
|
+
type TrustedFolderSnapshot,
|
|
16
18
|
} from '../../config/trustedFolders.js';
|
|
17
19
|
import { type HistoryItemWithoutId, MessageType } from '../types.js';
|
|
20
|
+
import type { CliUiRuntime } from '../cliUiRuntime.js';
|
|
18
21
|
import process from 'node:process';
|
|
22
|
+
import {
|
|
23
|
+
combineTrustUpdateFailure,
|
|
24
|
+
getTrustCommitErrorMessage,
|
|
25
|
+
} from '../trustDialogHelpers.js';
|
|
26
|
+
|
|
27
|
+
export type FolderTrustRuntime = Pick<
|
|
28
|
+
CliUiRuntime,
|
|
29
|
+
'getWorkingDir' | 'setTrustedFolderLive' | 'isTrustedFolder'
|
|
30
|
+
>;
|
|
19
31
|
|
|
20
32
|
const debug = new DebugLogger('llxprt:ui:useFolderTrust');
|
|
21
33
|
|
|
22
34
|
type AddItemFn = (item: HistoryItemWithoutId, timestamp: number) => number;
|
|
35
|
+
type SetDialogOpenFn = (open: boolean) => void;
|
|
23
36
|
|
|
24
37
|
function getTrustLevelFromChoice(choice: FolderTrustChoice): TrustLevel | null {
|
|
25
38
|
switch (choice) {
|
|
@@ -34,39 +47,6 @@ function getTrustLevelFromChoice(choice: FolderTrustChoice): TrustLevel | null {
|
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
function saveTrustLevel(
|
|
38
|
-
cwd: string,
|
|
39
|
-
trustLevel: TrustLevel,
|
|
40
|
-
addItem?: AddItemFn,
|
|
41
|
-
): boolean {
|
|
42
|
-
try {
|
|
43
|
-
const trustedFolders = loadTrustedFolders();
|
|
44
|
-
trustedFolders.setValue(cwd, trustLevel);
|
|
45
|
-
return true;
|
|
46
|
-
} catch {
|
|
47
|
-
if (addItem) {
|
|
48
|
-
addItem(
|
|
49
|
-
{
|
|
50
|
-
type: MessageType.ERROR,
|
|
51
|
-
text: 'Failed to save trust settings. Exiting LLxprt Code.',
|
|
52
|
-
},
|
|
53
|
-
Date.now(),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
setTimeout(() => {
|
|
57
|
-
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
|
|
58
|
-
}, 100);
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function computeNewTrustedState(trustLevel: TrustLevel): boolean {
|
|
64
|
-
return (
|
|
65
|
-
trustLevel === TrustLevel.TRUST_FOLDER ||
|
|
66
|
-
trustLevel === TrustLevel.TRUST_PARENT
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
50
|
function showStartupMessage(
|
|
71
51
|
trusted: boolean | undefined = undefined,
|
|
72
52
|
addItem: AddItemFn | undefined,
|
|
@@ -76,77 +56,134 @@ function showStartupMessage(
|
|
|
76
56
|
debug.log(
|
|
77
57
|
'Folder is untrusted - displaying permissions command hint on startup',
|
|
78
58
|
);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
}
|
|
59
|
+
addItem?.(
|
|
60
|
+
{
|
|
61
|
+
type: MessageType.INFO,
|
|
62
|
+
text: 'This folder is not trusted. Some features may be disabled. Use the `/permissions` command to change the trust level.',
|
|
63
|
+
},
|
|
64
|
+
Date.now(),
|
|
65
|
+
);
|
|
88
66
|
startupMessageSent.current = true;
|
|
89
67
|
}
|
|
90
68
|
}
|
|
91
69
|
|
|
70
|
+
async function applyFolderTrustChoice(
|
|
71
|
+
choice: FolderTrustChoice,
|
|
72
|
+
settings: LoadedSettings,
|
|
73
|
+
config: FolderTrustRuntime | undefined,
|
|
74
|
+
addItem: AddItemFn | undefined,
|
|
75
|
+
setDialogOpen: SetDialogOpenFn,
|
|
76
|
+
mountedRef: React.MutableRefObject<boolean>,
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const trustLevel = getTrustLevelFromChoice(choice);
|
|
79
|
+
if (trustLevel === null) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const workingDirectory = config?.getWorkingDir() ?? process.cwd();
|
|
83
|
+
let trustedFolders: ReturnType<typeof loadTrustedFolders> | undefined;
|
|
84
|
+
let savedSnapshot: TrustedFolderSnapshot | undefined;
|
|
85
|
+
let previousLiveTrust = false;
|
|
86
|
+
let failedPhase: 'persistence' | 'live' = 'persistence';
|
|
87
|
+
try {
|
|
88
|
+
trustedFolders = loadTrustedFolders();
|
|
89
|
+
savedSnapshot = trustedFolders.snapshotValue(workingDirectory);
|
|
90
|
+
previousLiveTrust = config?.isTrustedFolder() ?? false;
|
|
91
|
+
trustedFolders.setValue(workingDirectory, trustLevel);
|
|
92
|
+
const newIsTrusted =
|
|
93
|
+
resolveLocalWorkspaceTrust(
|
|
94
|
+
settings.merged,
|
|
95
|
+
trustedFolders,
|
|
96
|
+
workingDirectory,
|
|
97
|
+
) ?? false;
|
|
98
|
+
failedPhase = 'live';
|
|
99
|
+
await config?.setTrustedFolderLive(newIsTrusted);
|
|
100
|
+
if (mountedRef.current) {
|
|
101
|
+
setDialogOpen(false);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const rollbackFailures: unknown[] = [];
|
|
105
|
+
if (trustedFolders !== undefined && savedSnapshot !== undefined) {
|
|
106
|
+
try {
|
|
107
|
+
trustedFolders.restoreSnapshot(savedSnapshot);
|
|
108
|
+
} catch (rollbackError) {
|
|
109
|
+
rollbackFailures.push(rollbackError);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (failedPhase === 'live') {
|
|
113
|
+
try {
|
|
114
|
+
await config?.setTrustedFolderLive(previousLiveTrust);
|
|
115
|
+
} catch (rollbackError) {
|
|
116
|
+
rollbackFailures.push(rollbackError);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!mountedRef.current) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const failure = combineTrustUpdateFailure(
|
|
123
|
+
error,
|
|
124
|
+
rollbackFailures,
|
|
125
|
+
'Trust update and rollback failed',
|
|
126
|
+
);
|
|
127
|
+
const message = `${getTrustCommitErrorMessage(
|
|
128
|
+
failedPhase,
|
|
129
|
+
failure.error,
|
|
130
|
+
failure.rollbackSucceeded,
|
|
131
|
+
)} Exiting LLxprt Code.`;
|
|
132
|
+
debug.error(message);
|
|
133
|
+
addItem?.({ type: MessageType.ERROR, text: message }, Date.now());
|
|
134
|
+
setTimeout(() => process.exit(ExitCodes.FATAL_CONFIG_ERROR), 100);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
92
138
|
export const useFolderTrust = (
|
|
93
139
|
settings: LoadedSettings,
|
|
94
140
|
addItem?: AddItemFn,
|
|
141
|
+
config?: FolderTrustRuntime,
|
|
95
142
|
) => {
|
|
96
|
-
// Folder trust feature flag removed - now using settings directly
|
|
97
143
|
const { folderTrust } = settings.merged;
|
|
98
|
-
const initialTrust = isWorkspaceTrusted(
|
|
99
|
-
|
|
144
|
+
const initialTrust = isWorkspaceTrusted(
|
|
145
|
+
settings.merged,
|
|
146
|
+
config?.getWorkingDir() ?? process.cwd(),
|
|
147
|
+
);
|
|
100
148
|
const [isFolderTrustDialogOpen, setIsFolderTrustDialogOpen] = useState(
|
|
101
149
|
initialTrust === undefined,
|
|
102
150
|
);
|
|
103
|
-
const [isRestarting, setIsRestarting] = useState(false);
|
|
104
151
|
const startupMessageSent = useRef(false);
|
|
105
152
|
const previousFolderTrust = useRef(folderTrust);
|
|
153
|
+
const mountedRef = useRef(true);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
mountedRef.current = true;
|
|
157
|
+
return () => {
|
|
158
|
+
mountedRef.current = false;
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
106
161
|
|
|
107
162
|
useEffect(() => {
|
|
108
163
|
const folderTrustChanged = previousFolderTrust.current !== folderTrust;
|
|
109
164
|
previousFolderTrust.current = folderTrust;
|
|
110
|
-
const trusted = isWorkspaceTrusted(
|
|
165
|
+
const trusted = isWorkspaceTrusted(
|
|
166
|
+
settings.merged,
|
|
167
|
+
config?.getWorkingDir() ?? process.cwd(),
|
|
168
|
+
);
|
|
111
169
|
if (folderTrustChanged) {
|
|
112
|
-
setIsTrusted(trusted);
|
|
113
170
|
setIsFolderTrustDialogOpen(trusted === undefined);
|
|
114
171
|
}
|
|
115
|
-
|
|
116
172
|
showStartupMessage(trusted, addItem, startupMessageSent);
|
|
117
|
-
}, [folderTrust, addItem]);
|
|
173
|
+
}, [folderTrust, addItem, config, settings.merged]);
|
|
118
174
|
|
|
119
175
|
const handleFolderTrustSelect = useCallback(
|
|
120
|
-
(choice: FolderTrustChoice) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const newIsTrusted = computeNewTrustedState(trustLevel);
|
|
134
|
-
setIsTrusted(newIsTrusted);
|
|
135
|
-
|
|
136
|
-
const needsRestart = wasTrusted !== newIsTrusted;
|
|
137
|
-
if (needsRestart) {
|
|
138
|
-
setIsRestarting(true);
|
|
139
|
-
setIsFolderTrustDialogOpen(true);
|
|
140
|
-
} else {
|
|
141
|
-
setIsFolderTrustDialogOpen(false);
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
[isTrusted, addItem],
|
|
176
|
+
(choice: FolderTrustChoice): Promise<void> =>
|
|
177
|
+
applyFolderTrustChoice(
|
|
178
|
+
choice,
|
|
179
|
+
settings,
|
|
180
|
+
config,
|
|
181
|
+
addItem,
|
|
182
|
+
setIsFolderTrustDialogOpen,
|
|
183
|
+
mountedRef,
|
|
184
|
+
),
|
|
185
|
+
[addItem, config, settings],
|
|
145
186
|
);
|
|
146
187
|
|
|
147
|
-
return {
|
|
148
|
-
isFolderTrustDialogOpen,
|
|
149
|
-
handleFolderTrustSelect,
|
|
150
|
-
isRestarting,
|
|
151
|
-
};
|
|
188
|
+
return { isFolderTrustDialogOpen, handleFolderTrustSelect };
|
|
152
189
|
};
|
|
@@ -4,56 +4,97 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
7
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
8
8
|
import { exec } from 'node:child_process';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import fsPromises from 'node:fs/promises';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Debounce window for coalescing rapid reflog writes caused by compound
|
|
15
|
+
* git commands (e.g. `git checkout main && git fetch && git pull`). Without
|
|
16
|
+
* this, each write to `.git/logs/HEAD` spawns an overlapping `exec()`,
|
|
17
|
+
* increasing the chance that a stale result clobbers the latest branch name.
|
|
18
|
+
*
|
|
19
|
+
* 200ms was chosen empirically: compound git commands (checkout + fetch +
|
|
20
|
+
* pull) typically write the reflog 2-3 times within ~50ms. A 200ms window
|
|
21
|
+
* safely covers the burst while keeping the visible latency below a typical
|
|
22
|
+
* user's perception threshold. It is also well under the 3000ms fs.watchFile
|
|
23
|
+
* polling interval, so it never delays a legitimate single-command update
|
|
24
|
+
* beyond what the polling itself already imposes.
|
|
25
|
+
*/
|
|
26
|
+
const FETCH_DEBOUNCE_MS = 200;
|
|
27
|
+
|
|
28
|
+
/** Exposed for tests so timing assertions track the production constant. */
|
|
29
|
+
export { FETCH_DEBOUNCE_MS };
|
|
30
|
+
|
|
13
31
|
export function useGitBranchName(cwd: string): string | undefined {
|
|
14
32
|
const [branchName, setBranchName] = useState<string | undefined>(undefined);
|
|
15
33
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
// Monotonic token that guards against stale exec callbacks. Each fetch
|
|
35
|
+
// increments the token; when an exec callback fires, it only applies the
|
|
36
|
+
// result if its token still matches the latest one. This prevents an
|
|
37
|
+
// earlier-started exec (whose callback resolves after a newer one) from
|
|
38
|
+
// overwriting the branch name with a stale value.
|
|
39
|
+
const fetchTokenRef = useRef(0);
|
|
40
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
|
+
|
|
42
|
+
const cancelledRef = useRef(false);
|
|
43
|
+
|
|
44
|
+
const fetchBranchName = useCallback(() => {
|
|
45
|
+
const token = ++fetchTokenRef.current;
|
|
46
|
+
exec(
|
|
47
|
+
'git rev-parse --abbrev-ref HEAD',
|
|
48
|
+
{ cwd },
|
|
49
|
+
(error, stdout, _stderr) => {
|
|
50
|
+
if (cancelledRef.current || token !== fetchTokenRef.current) return;
|
|
51
|
+
if (error) {
|
|
52
|
+
setBranchName(undefined);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const branch = stdout.toString().trim();
|
|
56
|
+
if (branch && branch !== 'HEAD') {
|
|
57
|
+
setBranchName(branch);
|
|
58
|
+
} else {
|
|
59
|
+
exec(
|
|
60
|
+
'git rev-parse --short HEAD',
|
|
61
|
+
{ cwd },
|
|
62
|
+
(error, stdout, _stderr) => {
|
|
63
|
+
if (cancelledRef.current || token !== fetchTokenRef.current)
|
|
64
|
+
return;
|
|
65
|
+
if (error) {
|
|
66
|
+
setBranchName(undefined);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
setBranchName(stdout.toString().trim());
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
}, [cwd]);
|
|
76
|
+
|
|
77
|
+
const scheduleFetch = useCallback(() => {
|
|
78
|
+
if (debounceRef.current) {
|
|
79
|
+
clearTimeout(debounceRef.current);
|
|
80
|
+
}
|
|
81
|
+
debounceRef.current = setTimeout(() => {
|
|
82
|
+
debounceRef.current = null;
|
|
83
|
+
fetchBranchName();
|
|
84
|
+
}, FETCH_DEBOUNCE_MS);
|
|
85
|
+
}, [fetchBranchName]);
|
|
46
86
|
|
|
47
87
|
useEffect(() => {
|
|
48
88
|
fetchBranchName(); // Initial fetch
|
|
49
89
|
|
|
50
90
|
const gitLogsHeadPath = path.join(cwd, '.git', 'logs', 'HEAD');
|
|
51
91
|
let cancelled = false;
|
|
92
|
+
cancelledRef.current = false;
|
|
52
93
|
|
|
53
94
|
const onGitLogsHeadChange = (curr: fs.Stats, prev: fs.Stats) => {
|
|
54
95
|
if (cancelled) return;
|
|
55
96
|
if (curr.mtimeMs !== prev.mtimeMs || curr.size !== prev.size) {
|
|
56
|
-
|
|
97
|
+
scheduleFetch();
|
|
57
98
|
}
|
|
58
99
|
};
|
|
59
100
|
|
|
@@ -77,9 +118,14 @@ export function useGitBranchName(cwd: string): string | undefined {
|
|
|
77
118
|
|
|
78
119
|
return () => {
|
|
79
120
|
cancelled = true;
|
|
121
|
+
cancelledRef.current = true;
|
|
122
|
+
if (debounceRef.current) {
|
|
123
|
+
clearTimeout(debounceRef.current);
|
|
124
|
+
debounceRef.current = null;
|
|
125
|
+
}
|
|
80
126
|
fs.unwatchFile(gitLogsHeadPath, onGitLogsHeadChange);
|
|
81
127
|
};
|
|
82
|
-
}, [cwd, fetchBranchName]);
|
|
128
|
+
}, [cwd, fetchBranchName, scheduleFetch]);
|
|
83
129
|
|
|
84
130
|
return branchName;
|
|
85
131
|
}
|
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useCallback,
|
|
7
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
8
8
|
|
|
9
9
|
import { ideContext } from '@vybestack/llxprt-code-core';
|
|
10
10
|
import type { IdeState } from '../cliUiRuntime.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* This hook listens for trust status updates from the IDE companion extension
|
|
14
|
-
*
|
|
15
|
-
* if a restart is needed because the trust state has changed.
|
|
13
|
+
* This hook listens for trust status updates from the IDE companion extension
|
|
14
|
+
* and provides the current IDE trust status.
|
|
16
15
|
*/
|
|
17
|
-
export function useIdeTrustListener(ide: IdeState) {
|
|
16
|
+
export function useIdeTrustListener(ide: Pick<IdeState, 'getIdeClient'>) {
|
|
18
17
|
const subscribe = useCallback(
|
|
19
18
|
(onStoreChange: () => void) => {
|
|
20
19
|
const ideClient = ide.getIdeClient();
|
|
@@ -34,18 +33,5 @@ export function useIdeTrustListener(ide: IdeState) {
|
|
|
34
33
|
|
|
35
34
|
const isIdeTrusted = useSyncExternalStore(subscribe, getSnapshot);
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
const [initialTrustValue] = useState(isIdeTrusted);
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
if (
|
|
42
|
-
!needsRestart &&
|
|
43
|
-
initialTrustValue !== undefined &&
|
|
44
|
-
initialTrustValue !== isIdeTrusted
|
|
45
|
-
) {
|
|
46
|
-
setNeedsRestart(true);
|
|
47
|
-
}
|
|
48
|
-
}, [isIdeTrusted, initialTrustValue, needsRestart]);
|
|
49
|
-
|
|
50
|
-
return { isIdeTrusted, needsRestart };
|
|
36
|
+
return { isIdeTrusted };
|
|
51
37
|
}
|