wave-code 0.7.2 → 0.8.1
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/dist/cli.d.ts +2 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -52
- package/dist/components/App.d.ts +3 -4
- package/dist/components/App.d.ts.map +1 -1
- package/dist/components/App.js +49 -6
- package/dist/components/BangDisplay.d.ts +9 -0
- package/dist/components/BangDisplay.d.ts.map +1 -0
- package/dist/components/{CommandOutputDisplay.js → BangDisplay.js} +1 -1
- package/dist/components/ChatInterface.d.ts.map +1 -1
- package/dist/components/ChatInterface.js +3 -2
- package/dist/components/CommandSelector.d.ts.map +1 -1
- package/dist/components/CommandSelector.js +7 -28
- package/dist/components/ConfirmationSelector.d.ts.map +1 -1
- package/dist/components/ConfirmationSelector.js +116 -11
- package/dist/components/HelpView.d.ts +2 -0
- package/dist/components/HelpView.d.ts.map +1 -1
- package/dist/components/HelpView.js +49 -3
- package/dist/components/InputBox.d.ts.map +1 -1
- package/dist/components/InputBox.js +10 -4
- package/dist/components/MarketplaceAddForm.d.ts.map +1 -1
- package/dist/components/MarketplaceAddForm.js +13 -6
- package/dist/components/MarketplaceDetail.d.ts.map +1 -1
- package/dist/components/MarketplaceDetail.js +8 -3
- package/dist/components/MessageBlockItem.js +2 -2
- package/dist/components/MessageList.d.ts +4 -1
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +15 -8
- package/dist/components/PluginDetail.d.ts.map +1 -1
- package/dist/components/PluginDetail.js +14 -3
- package/dist/components/PluginManagerShell.d.ts.map +1 -1
- package/dist/components/PluginManagerShell.js +3 -3
- package/dist/components/PluginManagerTypes.d.ts +2 -0
- package/dist/components/PluginManagerTypes.d.ts.map +1 -1
- package/dist/components/SessionSelector.d.ts.map +1 -1
- package/dist/components/SessionSelector.js +5 -5
- package/dist/components/StatusCommand.d.ts +6 -0
- package/dist/components/StatusCommand.d.ts.map +1 -0
- package/dist/components/StatusCommand.js +28 -0
- package/dist/components/WorktreeExitPrompt.d.ts +13 -0
- package/dist/components/WorktreeExitPrompt.d.ts.map +1 -0
- package/dist/components/WorktreeExitPrompt.js +26 -0
- package/dist/constants/commands.d.ts +3 -0
- package/dist/constants/commands.d.ts.map +1 -0
- package/dist/constants/commands.js +38 -0
- package/dist/contexts/useChat.d.ts +9 -5
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +38 -8
- package/dist/contracts/status.d.ts +8 -0
- package/dist/contracts/status.d.ts.map +1 -0
- package/dist/contracts/status.js +1 -0
- package/dist/hooks/useInputManager.d.ts +2 -0
- package/dist/hooks/useInputManager.d.ts.map +1 -1
- package/dist/hooks/useInputManager.js +12 -0
- package/dist/hooks/usePluginManager.d.ts.map +1 -1
- package/dist/hooks/usePluginManager.js +41 -13
- package/dist/hooks/useTasks.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -4
- package/dist/managers/InputManager.d.ts +6 -0
- package/dist/managers/InputManager.d.ts.map +1 -1
- package/dist/managers/InputManager.js +32 -13
- package/dist/print-cli.d.ts +2 -4
- package/dist/print-cli.d.ts.map +1 -1
- package/dist/print-cli.js +31 -2
- package/dist/session-selector-cli.d.ts +3 -1
- package/dist/session-selector-cli.d.ts.map +1 -1
- package/dist/session-selector-cli.js +2 -2
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/highlightUtils.d.ts.map +1 -1
- package/dist/utils/highlightUtils.js +66 -42
- package/dist/utils/worktree.d.ts +23 -0
- package/dist/utils/worktree.d.ts.map +1 -0
- package/dist/utils/worktree.js +135 -0
- package/package.json +2 -2
- package/src/cli.tsx +36 -59
- package/src/components/App.tsx +99 -11
- package/src/components/{CommandOutputDisplay.tsx → BangDisplay.tsx} +4 -4
- package/src/components/ChatInterface.tsx +8 -0
- package/src/components/CommandSelector.tsx +7 -29
- package/src/components/ConfirmationSelector.tsx +131 -12
- package/src/components/HelpView.tsx +129 -14
- package/src/components/InputBox.tsx +14 -2
- package/src/components/MarketplaceAddForm.tsx +21 -8
- package/src/components/MarketplaceDetail.tsx +19 -4
- package/src/components/MessageBlockItem.tsx +3 -3
- package/src/components/MessageList.tsx +47 -23
- package/src/components/PluginDetail.tsx +30 -6
- package/src/components/PluginManagerShell.tsx +24 -6
- package/src/components/PluginManagerTypes.ts +2 -0
- package/src/components/SessionSelector.tsx +33 -16
- package/src/components/StatusCommand.tsx +94 -0
- package/src/components/WorktreeExitPrompt.tsx +86 -0
- package/src/constants/commands.ts +41 -0
- package/src/contexts/useChat.tsx +57 -13
- package/src/contracts/status.ts +7 -0
- package/src/hooks/useInputManager.ts +12 -0
- package/src/hooks/usePluginManager.ts +47 -13
- package/src/hooks/useTasks.ts +2 -2
- package/src/index.ts +71 -12
- package/src/managers/InputManager.ts +37 -15
- package/src/print-cli.ts +48 -5
- package/src/session-selector-cli.tsx +6 -2
- package/src/types.ts +11 -0
- package/src/utils/highlightUtils.ts +66 -42
- package/src/utils/worktree.ts +164 -0
- package/dist/components/CommandOutputDisplay.d.ts +0 -9
- package/dist/components/CommandOutputDisplay.d.ts.map +0 -1
package/src/cli.tsx
CHANGED
|
@@ -2,13 +2,12 @@ import React from "react";
|
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
import { App } from "./components/App.js";
|
|
4
4
|
import { cleanupLogs } from "./utils/logger.js";
|
|
5
|
+
import { removeWorktree } from "./utils/worktree.js";
|
|
6
|
+
import { BaseAppProps } from "./types.js";
|
|
5
7
|
|
|
6
|
-
export interface CliOptions {
|
|
8
|
+
export interface CliOptions extends BaseAppProps {
|
|
7
9
|
restoreSessionId?: string;
|
|
8
10
|
continueLastSession?: boolean;
|
|
9
|
-
bypassPermissions?: boolean;
|
|
10
|
-
pluginDirs?: string[];
|
|
11
|
-
tools?: string[];
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export async function startCli(options: CliOptions): Promise<void> {
|
|
@@ -18,77 +17,55 @@ export async function startCli(options: CliOptions): Promise<void> {
|
|
|
18
17
|
bypassPermissions,
|
|
19
18
|
pluginDirs,
|
|
20
19
|
tools,
|
|
20
|
+
worktreeSession,
|
|
21
|
+
workdir,
|
|
22
|
+
version,
|
|
23
|
+
model,
|
|
21
24
|
} = options;
|
|
22
25
|
|
|
23
26
|
// Continue with ink-based UI for normal mode
|
|
24
|
-
|
|
25
|
-
let isCleaningUp = false;
|
|
26
|
-
let appUnmounted = false;
|
|
27
|
+
let shouldRemoveWorktree = false;
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.log("\nShutting down gracefully...");
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
// Clean up old log files
|
|
36
|
-
await cleanupLogs().catch((error) => {
|
|
37
|
-
console.warn("Failed to cleanup old logs:", error);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Unmount the React app to trigger cleanup
|
|
41
|
-
if (!appUnmounted) {
|
|
42
|
-
unmount();
|
|
43
|
-
appUnmounted = true;
|
|
44
|
-
// Give React time to cleanup
|
|
45
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
process.exit(0);
|
|
49
|
-
} catch (error: unknown) {
|
|
50
|
-
console.error("Error during cleanup:", error);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
29
|
+
const handleExit = (shouldRemove: boolean) => {
|
|
30
|
+
shouldRemoveWorktree = shouldRemove;
|
|
31
|
+
unmount();
|
|
53
32
|
};
|
|
54
33
|
|
|
55
|
-
// Handle process signals
|
|
56
|
-
process.on("SIGINT", cleanup);
|
|
57
|
-
process.on("SIGTERM", cleanup);
|
|
58
|
-
|
|
59
|
-
// Handle uncaught exceptions
|
|
60
|
-
process.on("uncaughtException", (error) => {
|
|
61
|
-
console.error("Uncaught exception:", error);
|
|
62
|
-
cleanup();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
process.on("unhandledRejection", (reason, promise) => {
|
|
66
|
-
console.error("Unhandled rejection at:", promise, "reason:", reason);
|
|
67
|
-
cleanup();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
34
|
// Render the application
|
|
71
|
-
const { unmount } = render(
|
|
35
|
+
const { unmount, waitUntilExit } = render(
|
|
72
36
|
<App
|
|
73
37
|
restoreSessionId={restoreSessionId}
|
|
74
38
|
continueLastSession={continueLastSession}
|
|
75
39
|
bypassPermissions={bypassPermissions}
|
|
76
40
|
pluginDirs={pluginDirs}
|
|
77
41
|
tools={tools}
|
|
42
|
+
worktreeSession={worktreeSession}
|
|
43
|
+
workdir={workdir}
|
|
44
|
+
version={version}
|
|
45
|
+
model={model}
|
|
46
|
+
onExit={handleExit}
|
|
78
47
|
/>,
|
|
48
|
+
{ exitOnCtrlC: false },
|
|
79
49
|
);
|
|
80
50
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
51
|
+
// Wait for the app to finish unmounting
|
|
52
|
+
await waitUntilExit();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Clean up old log files
|
|
56
|
+
await cleanupLogs().catch((error) => {
|
|
57
|
+
console.warn("Failed to cleanup old logs:", error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Cleanup worktree if requested
|
|
61
|
+
if (shouldRemoveWorktree && worktreeSession) {
|
|
62
|
+
process.chdir(worktreeSession.repoRoot);
|
|
63
|
+
removeWorktree(worktreeSession);
|
|
89
64
|
}
|
|
90
|
-
});
|
|
91
65
|
|
|
92
|
-
|
|
93
|
-
|
|
66
|
+
process.exit(0);
|
|
67
|
+
} catch (error: unknown) {
|
|
68
|
+
console.error("Error during cleanup:", error);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
94
71
|
}
|
package/src/components/App.tsx
CHANGED
|
@@ -1,27 +1,105 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
-
import { useStdout } from "ink";
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import { useStdout, useInput } from "ink";
|
|
3
3
|
import { ChatInterface } from "./ChatInterface.js";
|
|
4
4
|
import { ChatProvider, useChat } from "../contexts/useChat.js";
|
|
5
5
|
import { AppProvider } from "../contexts/useAppConfig.js";
|
|
6
|
+
import { WorktreeExitPrompt } from "./WorktreeExitPrompt.js";
|
|
7
|
+
import {
|
|
8
|
+
hasUncommittedChanges,
|
|
9
|
+
hasNewCommits,
|
|
10
|
+
getDefaultRemoteBranch,
|
|
11
|
+
} from "wave-agent-sdk";
|
|
12
|
+
import { BaseAppProps } from "../types.js";
|
|
6
13
|
|
|
7
|
-
interface AppProps {
|
|
14
|
+
interface AppProps extends BaseAppProps {
|
|
8
15
|
restoreSessionId?: string;
|
|
9
16
|
continueLastSession?: boolean;
|
|
10
|
-
|
|
11
|
-
pluginDirs?: string[];
|
|
12
|
-
tools?: string[];
|
|
17
|
+
onExit: (shouldRemove: boolean) => void;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
interface AppWithProvidersProps extends BaseAppProps {
|
|
21
|
+
onExit: (shouldRemove: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const AppWithProviders: React.FC<AppWithProvidersProps> = ({
|
|
25
|
+
bypassPermissions,
|
|
26
|
+
pluginDirs,
|
|
27
|
+
tools,
|
|
28
|
+
worktreeSession,
|
|
29
|
+
workdir,
|
|
30
|
+
version,
|
|
31
|
+
model,
|
|
32
|
+
onExit,
|
|
33
|
+
}) => {
|
|
34
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
35
|
+
const [worktreeStatus, setWorktreeStatus] = useState<{
|
|
36
|
+
hasUncommittedChanges: boolean;
|
|
37
|
+
hasNewCommits: boolean;
|
|
38
|
+
} | null>(null);
|
|
39
|
+
|
|
40
|
+
const handleSignal = useCallback(async () => {
|
|
41
|
+
if (worktreeSession) {
|
|
42
|
+
const cwd = workdir || worktreeSession.path;
|
|
43
|
+
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
44
|
+
const hasChanges = hasUncommittedChanges(cwd);
|
|
45
|
+
const hasCommits = hasNewCommits(cwd, baseBranch);
|
|
46
|
+
|
|
47
|
+
if (hasChanges || hasCommits) {
|
|
48
|
+
setWorktreeStatus({
|
|
49
|
+
hasUncommittedChanges: hasChanges,
|
|
50
|
+
hasNewCommits: hasCommits,
|
|
51
|
+
});
|
|
52
|
+
setIsExiting(true);
|
|
53
|
+
} else {
|
|
54
|
+
onExit(true);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
onExit(false);
|
|
58
|
+
}
|
|
59
|
+
}, [worktreeSession, workdir, onExit]);
|
|
60
|
+
|
|
61
|
+
useInput((input, key) => {
|
|
62
|
+
if (input === "c" && key.ctrl) {
|
|
63
|
+
handleSignal();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const onSigInt = () => handleSignal();
|
|
69
|
+
const onSigTerm = () => handleSignal();
|
|
70
|
+
|
|
71
|
+
process.on("SIGINT", onSigInt);
|
|
72
|
+
process.on("SIGTERM", onSigTerm);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
process.off("SIGINT", onSigInt);
|
|
76
|
+
process.off("SIGTERM", onSigTerm);
|
|
77
|
+
};
|
|
78
|
+
}, [handleSignal]);
|
|
79
|
+
|
|
80
|
+
if (isExiting && worktreeSession && worktreeStatus) {
|
|
81
|
+
return (
|
|
82
|
+
<WorktreeExitPrompt
|
|
83
|
+
name={worktreeSession.name}
|
|
84
|
+
path={worktreeSession.path}
|
|
85
|
+
hasUncommittedChanges={worktreeStatus.hasUncommittedChanges}
|
|
86
|
+
hasNewCommits={worktreeStatus.hasNewCommits}
|
|
87
|
+
onKeep={() => onExit(false)}
|
|
88
|
+
onRemove={() => onExit(true)}
|
|
89
|
+
onCancel={() => setIsExiting(false)}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
20
94
|
return (
|
|
21
95
|
<ChatProvider
|
|
22
96
|
bypassPermissions={bypassPermissions}
|
|
23
97
|
pluginDirs={pluginDirs}
|
|
24
98
|
tools={tools}
|
|
99
|
+
workdir={workdir}
|
|
100
|
+
worktreeSession={worktreeSession}
|
|
101
|
+
version={version}
|
|
102
|
+
model={model}
|
|
25
103
|
>
|
|
26
104
|
<ChatInterfaceWithRemount />
|
|
27
105
|
</ChatProvider>
|
|
@@ -81,6 +159,11 @@ export const App: React.FC<AppProps> = ({
|
|
|
81
159
|
bypassPermissions,
|
|
82
160
|
pluginDirs,
|
|
83
161
|
tools,
|
|
162
|
+
worktreeSession,
|
|
163
|
+
workdir,
|
|
164
|
+
version,
|
|
165
|
+
model,
|
|
166
|
+
onExit,
|
|
84
167
|
}) => {
|
|
85
168
|
return (
|
|
86
169
|
<AppProvider
|
|
@@ -91,6 +174,11 @@ export const App: React.FC<AppProps> = ({
|
|
|
91
174
|
bypassPermissions={bypassPermissions}
|
|
92
175
|
pluginDirs={pluginDirs}
|
|
93
176
|
tools={tools}
|
|
177
|
+
worktreeSession={worktreeSession}
|
|
178
|
+
workdir={workdir}
|
|
179
|
+
version={version}
|
|
180
|
+
model={model}
|
|
181
|
+
onExit={onExit}
|
|
94
182
|
/>
|
|
95
183
|
</AppProvider>
|
|
96
184
|
);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import type {
|
|
3
|
+
import type { BangBlock } from "wave-agent-sdk";
|
|
4
4
|
|
|
5
|
-
interface
|
|
6
|
-
block:
|
|
5
|
+
interface BangDisplayProps {
|
|
6
|
+
block: BangBlock;
|
|
7
7
|
isExpanded?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const BangDisplay: React.FC<BangDisplayProps> = ({
|
|
11
11
|
block,
|
|
12
12
|
isExpanded = false,
|
|
13
13
|
}) => {
|
|
@@ -36,8 +36,13 @@ export const ChatInterface: React.FC = () => {
|
|
|
36
36
|
handleConfirmationDecision,
|
|
37
37
|
handleConfirmationCancel: originalHandleConfirmationCancel,
|
|
38
38
|
setWasLastDetailsTooTall,
|
|
39
|
+
version,
|
|
40
|
+
workdir,
|
|
41
|
+
getModelConfig,
|
|
39
42
|
} = useChat();
|
|
40
43
|
|
|
44
|
+
const model = getModelConfig().model;
|
|
45
|
+
|
|
41
46
|
const handleDetailsHeightMeasured = useCallback((height: number) => {
|
|
42
47
|
setDetailsHeight(height);
|
|
43
48
|
}, []);
|
|
@@ -91,6 +96,9 @@ export const ChatInterface: React.FC = () => {
|
|
|
91
96
|
messages={messages}
|
|
92
97
|
isExpanded={isExpanded}
|
|
93
98
|
hideDynamicBlocks={isConfirmationVisible}
|
|
99
|
+
version={version}
|
|
100
|
+
workdir={workdir}
|
|
101
|
+
model={model}
|
|
94
102
|
/>
|
|
95
103
|
|
|
96
104
|
{(isLoading || isCommandRunning || isCompressing) &&
|
|
@@ -1,34 +1,7 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import type { SlashCommand } from "wave-agent-sdk";
|
|
4
|
-
|
|
5
|
-
const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
6
|
-
{
|
|
7
|
-
id: "tasks",
|
|
8
|
-
name: "tasks",
|
|
9
|
-
description: "View and manage background tasks (shells and subagents)",
|
|
10
|
-
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
id: "mcp",
|
|
14
|
-
name: "mcp",
|
|
15
|
-
description: "View and manage MCP servers",
|
|
16
|
-
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: "rewind",
|
|
20
|
-
name: "rewind",
|
|
21
|
-
description:
|
|
22
|
-
"Revert conversation and file changes to a previous checkpoint",
|
|
23
|
-
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: "help",
|
|
27
|
-
name: "help",
|
|
28
|
-
description: "Show help and key bindings",
|
|
29
|
-
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
30
|
-
},
|
|
31
|
-
];
|
|
4
|
+
import { AVAILABLE_COMMANDS } from "../constants/commands.js";
|
|
32
5
|
|
|
33
6
|
export interface CommandSelectorProps {
|
|
34
7
|
searchQuery: string;
|
|
@@ -48,8 +21,13 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
|
|
|
48
21
|
const MAX_VISIBLE_ITEMS = 3;
|
|
49
22
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
50
23
|
|
|
24
|
+
// Reset selected index when search query changes
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
setSelectedIndex(0);
|
|
27
|
+
}, [searchQuery]);
|
|
28
|
+
|
|
51
29
|
// Merge agent commands and local commands
|
|
52
|
-
const allCommands = [...
|
|
30
|
+
const allCommands = [...AVAILABLE_COMMANDS, ...commands];
|
|
53
31
|
|
|
54
32
|
// Filter command list
|
|
55
33
|
const filteredCommands = allCommands.filter(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useLayoutEffect, useRef, useState } from "react";
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { Box, Text, useInput, useStdout, measureElement } from "ink";
|
|
3
3
|
import type { PermissionDecision, AskUserQuestionInput } from "wave-agent-sdk";
|
|
4
4
|
import {
|
|
@@ -70,6 +70,25 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
70
70
|
userAnswers: {} as Record<string, string>,
|
|
71
71
|
otherText: "",
|
|
72
72
|
otherCursorPosition: 0,
|
|
73
|
+
savedStates: {} as Record<
|
|
74
|
+
number,
|
|
75
|
+
{
|
|
76
|
+
selectedOptionIndex: number;
|
|
77
|
+
selectedOptionIndices: Set<number>;
|
|
78
|
+
otherText: string;
|
|
79
|
+
otherCursorPosition: number;
|
|
80
|
+
}
|
|
81
|
+
>,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const pendingDecisionRef = useRef<PermissionDecision | null>(null);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (pendingDecisionRef.current) {
|
|
88
|
+
const decision = pendingDecisionRef.current;
|
|
89
|
+
pendingDecisionRef.current = null;
|
|
90
|
+
onDecision(decision);
|
|
91
|
+
}
|
|
73
92
|
});
|
|
74
93
|
|
|
75
94
|
const questions =
|
|
@@ -133,23 +152,75 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
133
152
|
};
|
|
134
153
|
|
|
135
154
|
if (prev.currentQuestionIndex < questions.length - 1) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
const nextIndex = prev.currentQuestionIndex + 1;
|
|
156
|
+
const savedStates = {
|
|
157
|
+
...prev.savedStates,
|
|
158
|
+
[prev.currentQuestionIndex]: {
|
|
159
|
+
selectedOptionIndex: prev.selectedOptionIndex,
|
|
160
|
+
selectedOptionIndices: prev.selectedOptionIndices,
|
|
161
|
+
otherText: prev.otherText,
|
|
162
|
+
otherCursorPosition: prev.otherCursorPosition,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const nextState = savedStates[nextIndex] || {
|
|
139
167
|
selectedOptionIndex: 0,
|
|
140
|
-
selectedOptionIndices: new Set(),
|
|
141
|
-
userAnswers: newAnswers,
|
|
168
|
+
selectedOptionIndices: new Set<number>(),
|
|
142
169
|
otherText: "",
|
|
143
170
|
otherCursorPosition: 0,
|
|
144
171
|
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...prev,
|
|
175
|
+
currentQuestionIndex: nextIndex,
|
|
176
|
+
...nextState,
|
|
177
|
+
userAnswers: newAnswers,
|
|
178
|
+
savedStates,
|
|
179
|
+
};
|
|
145
180
|
} else {
|
|
146
|
-
|
|
181
|
+
const finalAnswers = { ...newAnswers };
|
|
182
|
+
// Also collect from savedStates for any questions that were skipped via Tab
|
|
183
|
+
for (const [idxStr, s] of Object.entries(prev.savedStates)) {
|
|
184
|
+
const idx = parseInt(idxStr);
|
|
185
|
+
const q = questions[idx];
|
|
186
|
+
if (q && !finalAnswers[q.question]) {
|
|
187
|
+
const opts = [...q.options, { label: "Other" }];
|
|
188
|
+
let a = "";
|
|
189
|
+
if (q.multiSelect) {
|
|
190
|
+
const selectedLabels = Array.from(s.selectedOptionIndices)
|
|
191
|
+
.filter((i) => i < q.options.length)
|
|
192
|
+
.map((i) => q.options[i].label);
|
|
193
|
+
const isOtherChecked = s.selectedOptionIndices.has(
|
|
194
|
+
opts.length - 1,
|
|
195
|
+
);
|
|
196
|
+
if (isOtherChecked && s.otherText.trim()) {
|
|
197
|
+
selectedLabels.push(s.otherText.trim());
|
|
198
|
+
}
|
|
199
|
+
a = selectedLabels.join(", ");
|
|
200
|
+
} else {
|
|
201
|
+
if (s.selectedOptionIndex === opts.length - 1) {
|
|
202
|
+
a = s.otherText.trim();
|
|
203
|
+
} else {
|
|
204
|
+
a = opts[s.selectedOptionIndex].label;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (a) finalAnswers[q.question] = a;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Only submit if all questions have been answered
|
|
212
|
+
const allAnswered = questions.every(
|
|
213
|
+
(q) => finalAnswers[q.question],
|
|
214
|
+
);
|
|
215
|
+
if (!allAnswered) return prev;
|
|
216
|
+
|
|
217
|
+
pendingDecisionRef.current = {
|
|
147
218
|
behavior: "allow",
|
|
148
|
-
message: JSON.stringify(
|
|
149
|
-
}
|
|
219
|
+
message: JSON.stringify(finalAnswers),
|
|
220
|
+
};
|
|
150
221
|
return {
|
|
151
222
|
...prev,
|
|
152
|
-
userAnswers:
|
|
223
|
+
userAnswers: finalAnswers,
|
|
153
224
|
};
|
|
154
225
|
}
|
|
155
226
|
});
|
|
@@ -196,6 +267,41 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
196
267
|
}));
|
|
197
268
|
return;
|
|
198
269
|
}
|
|
270
|
+
if (key.tab) {
|
|
271
|
+
setQuestionState((prev) => {
|
|
272
|
+
const direction = key.shift ? -1 : 1;
|
|
273
|
+
let nextIndex = prev.currentQuestionIndex + direction;
|
|
274
|
+
if (nextIndex < 0) nextIndex = questions.length - 1;
|
|
275
|
+
if (nextIndex >= questions.length) nextIndex = 0;
|
|
276
|
+
|
|
277
|
+
if (nextIndex === prev.currentQuestionIndex) return prev;
|
|
278
|
+
|
|
279
|
+
const savedStates = {
|
|
280
|
+
...prev.savedStates,
|
|
281
|
+
[prev.currentQuestionIndex]: {
|
|
282
|
+
selectedOptionIndex: prev.selectedOptionIndex,
|
|
283
|
+
selectedOptionIndices: prev.selectedOptionIndices,
|
|
284
|
+
otherText: prev.otherText,
|
|
285
|
+
otherCursorPosition: prev.otherCursorPosition,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const nextState = savedStates[nextIndex] || {
|
|
290
|
+
selectedOptionIndex: 0,
|
|
291
|
+
selectedOptionIndices: new Set<number>(),
|
|
292
|
+
otherText: "",
|
|
293
|
+
otherCursorPosition: 0,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
...prev,
|
|
298
|
+
currentQuestionIndex: nextIndex,
|
|
299
|
+
...nextState,
|
|
300
|
+
savedStates,
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
199
305
|
|
|
200
306
|
setQuestionState((prev) => {
|
|
201
307
|
const isOtherFocused = prev.selectedOptionIndex === options.length - 1;
|
|
@@ -321,6 +427,19 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
321
427
|
return;
|
|
322
428
|
}
|
|
323
429
|
|
|
430
|
+
if (key.tab) {
|
|
431
|
+
const currentIndex = availableOptions.indexOf(state.selectedOption);
|
|
432
|
+
const direction = key.shift ? -1 : 1;
|
|
433
|
+
let nextIndex = currentIndex + direction;
|
|
434
|
+
if (nextIndex < 0) nextIndex = availableOptions.length - 1;
|
|
435
|
+
if (nextIndex >= availableOptions.length) nextIndex = 0;
|
|
436
|
+
setState((prev) => ({
|
|
437
|
+
...prev,
|
|
438
|
+
selectedOption: availableOptions[nextIndex],
|
|
439
|
+
}));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
324
443
|
if (input && !key.ctrl && !key.meta && !("alt" in key && key.alt)) {
|
|
325
444
|
setState((prev) => {
|
|
326
445
|
const nextText =
|
|
@@ -436,7 +555,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
436
555
|
Question {questionState.currentQuestionIndex + 1} of{" "}
|
|
437
556
|
{questions.length} •
|
|
438
557
|
{currentQuestion.multiSelect ? " Space to toggle •" : ""} Use ↑↓
|
|
439
|
-
to navigate • Enter to confirm
|
|
558
|
+
or Tab to navigate • Enter to confirm
|
|
440
559
|
</Text>
|
|
441
560
|
</Box>
|
|
442
561
|
</Box>
|
|
@@ -531,7 +650,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
531
650
|
</Box>
|
|
532
651
|
</Box>
|
|
533
652
|
<Box marginTop={1}>
|
|
534
|
-
<Text dimColor>Use ↑↓ to navigate • ESC to cancel</Text>
|
|
653
|
+
<Text dimColor>Use ↑↓ or Tab to navigate • ESC to cancel</Text>
|
|
535
654
|
</Box>
|
|
536
655
|
</>
|
|
537
656
|
)}
|