wave-code 1.0.10 → 1.1.0
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/bin/wave-code.js +8 -2
- package/dist/bundle/wave.mjs +3207 -0
- package/package.json +24 -25
- package/src/commands/update.ts +2 -7
- package/src/components/App.tsx +3 -7
- package/src/components/BtwDisplay.tsx +10 -1
- package/src/components/ChatInterface.tsx +21 -52
- package/src/components/ConfirmationDetails.tsx +144 -37
- package/src/components/DiffDisplay.tsx +335 -301
- package/src/components/InputBox.tsx +3 -1
- package/src/components/LoadingIndicator.tsx +16 -2
- package/src/components/MessageBlockItem.tsx +2 -4
- package/src/components/MessageList.tsx +75 -14
- package/src/components/ReasoningDisplay.tsx +2 -4
- package/src/components/SessionSelector.tsx +82 -11
- package/src/components/StatusCommand.tsx +2 -9
- package/src/components/ToolDisplay.tsx +2 -4
- package/src/contexts/useChat.tsx +156 -335
- package/src/daemon/commands.ts +57 -27
- package/src/daemon/jsonRpcClient.ts +1 -1
- package/src/daemon/socketClient.ts +2 -2
- package/src/hooks/useInputManager.ts +1 -1
- package/src/index.ts +57 -10
- package/src/managers/inputReducer.ts +186 -104
- package/src/print-cli.ts +21 -17
- package/src/reducers/selectorReducer.ts +10 -0
- package/src/session-selector-cli.tsx +269 -28
- package/src/stdio/agentBridge.ts +22 -21
- package/src/stdio/daemonServer.ts +3 -1
- package/src/utils/clipboard.ts +21 -0
- package/src/utils/highlightUtils.ts +46 -1
- package/src/utils/readPackageJson.ts +29 -0
- package/src/utils/streamingText.ts +11 -0
- package/src/utils/worktree.ts +191 -43
- package/dist/cli.d.ts +0 -6
- package/dist/cli.js +0 -53
- package/dist/commands/plugin/disable.d.ts +0 -5
- package/dist/commands/plugin/disable.js +0 -15
- package/dist/commands/plugin/enable.d.ts +0 -5
- package/dist/commands/plugin/enable.js +0 -15
- package/dist/commands/plugin/install.d.ts +0 -5
- package/dist/commands/plugin/install.js +0 -21
- package/dist/commands/plugin/list.d.ts +0 -1
- package/dist/commands/plugin/list.js +0 -31
- package/dist/commands/plugin/marketplace.d.ts +0 -10
- package/dist/commands/plugin/marketplace.js +0 -90
- package/dist/commands/plugin/uninstall.d.ts +0 -3
- package/dist/commands/plugin/uninstall.js +0 -16
- package/dist/commands/plugin/update.d.ts +0 -3
- package/dist/commands/plugin/update.js +0 -15
- package/dist/commands/update.d.ts +0 -1
- package/dist/commands/update.js +0 -97
- package/dist/components/AgentsManager.d.ts +0 -7
- package/dist/components/AgentsManager.js +0 -109
- package/dist/components/App.d.ts +0 -9
- package/dist/components/App.js +0 -76
- package/dist/components/BackgroundTaskManager.d.ts +0 -5
- package/dist/components/BackgroundTaskManager.js +0 -99
- package/dist/components/BangDisplay.d.ts +0 -8
- package/dist/components/BangDisplay.js +0 -17
- package/dist/components/BtwDisplay.d.ts +0 -7
- package/dist/components/BtwDisplay.js +0 -19
- package/dist/components/ChatInterface.d.ts +0 -2
- package/dist/components/ChatInterface.js +0 -61
- package/dist/components/CommandSelector.d.ts +0 -10
- package/dist/components/CommandSelector.js +0 -66
- package/dist/components/CompactDisplay.d.ts +0 -8
- package/dist/components/CompactDisplay.js +0 -13
- package/dist/components/ConfirmationDetails.d.ts +0 -9
- package/dist/components/ConfirmationDetails.js +0 -45
- package/dist/components/ConfirmationSelector.d.ts +0 -13
- package/dist/components/ConfirmationSelector.js +0 -127
- package/dist/components/DiffDisplay.d.ts +0 -8
- package/dist/components/DiffDisplay.js +0 -201
- package/dist/components/DiscoverView.d.ts +0 -2
- package/dist/components/DiscoverView.js +0 -37
- package/dist/components/FileSelector.d.ts +0 -11
- package/dist/components/FileSelector.js +0 -50
- package/dist/components/HelpView.d.ts +0 -7
- package/dist/components/HelpView.js +0 -74
- package/dist/components/HistorySearch.d.ts +0 -8
- package/dist/components/HistorySearch.js +0 -66
- package/dist/components/InputBox.d.ts +0 -24
- package/dist/components/InputBox.js +0 -149
- package/dist/components/InstalledView.d.ts +0 -2
- package/dist/components/InstalledView.js +0 -45
- package/dist/components/LoadingIndicator.d.ts +0 -10
- package/dist/components/LoadingIndicator.js +0 -6
- package/dist/components/LoginCommand.d.ts +0 -5
- package/dist/components/LoginCommand.js +0 -153
- package/dist/components/Markdown.d.ts +0 -5
- package/dist/components/Markdown.js +0 -127
- package/dist/components/MarketplaceAddForm.d.ts +0 -2
- package/dist/components/MarketplaceAddForm.js +0 -55
- package/dist/components/MarketplaceDetail.d.ts +0 -2
- package/dist/components/MarketplaceDetail.js +0 -74
- package/dist/components/MarketplaceList.d.ts +0 -8
- package/dist/components/MarketplaceList.js +0 -17
- package/dist/components/MarketplaceView.d.ts +0 -2
- package/dist/components/MarketplaceView.js +0 -41
- package/dist/components/McpManager.d.ts +0 -9
- package/dist/components/McpManager.js +0 -88
- package/dist/components/MessageBlockItem.d.ts +0 -8
- package/dist/components/MessageBlockItem.js +0 -15
- package/dist/components/MessageList.d.ts +0 -10
- package/dist/components/MessageList.js +0 -80
- package/dist/components/ModelSelector.d.ts +0 -8
- package/dist/components/ModelSelector.js +0 -45
- package/dist/components/Notifications.d.ts +0 -7
- package/dist/components/Notifications.js +0 -9
- package/dist/components/PlanDisplay.d.ts +0 -7
- package/dist/components/PlanDisplay.js +0 -6
- package/dist/components/PluginDetail.d.ts +0 -2
- package/dist/components/PluginDetail.js +0 -87
- package/dist/components/PluginList.d.ts +0 -13
- package/dist/components/PluginList.js +0 -15
- package/dist/components/PluginManagerShell.d.ts +0 -6
- package/dist/components/PluginManagerShell.js +0 -92
- package/dist/components/PluginManagerTypes.d.ts +0 -35
- package/dist/components/PluginManagerTypes.js +0 -1
- package/dist/components/QueuedMessageList.d.ts +0 -2
- package/dist/components/QueuedMessageList.js +0 -18
- package/dist/components/ReasoningDisplay.d.ts +0 -8
- package/dist/components/ReasoningDisplay.js +0 -13
- package/dist/components/RewindCommand.d.ts +0 -12
- package/dist/components/RewindCommand.js +0 -78
- package/dist/components/SessionSelector.d.ts +0 -12
- package/dist/components/SessionSelector.js +0 -47
- package/dist/components/StatusCommand.d.ts +0 -5
- package/dist/components/StatusCommand.js +0 -28
- package/dist/components/StatusLine.d.ts +0 -6
- package/dist/components/StatusLine.js +0 -9
- package/dist/components/TaskList.d.ts +0 -5
- package/dist/components/TaskList.js +0 -197
- package/dist/components/TaskNotificationMessage.d.ts +0 -5
- package/dist/components/TaskNotificationMessage.js +0 -12
- package/dist/components/ToolDisplay.d.ts +0 -9
- package/dist/components/ToolDisplay.js +0 -55
- package/dist/components/WorkflowManager.d.ts +0 -5
- package/dist/components/WorkflowManager.js +0 -102
- package/dist/components/WorktreeExitPrompt.d.ts +0 -12
- package/dist/components/WorktreeExitPrompt.js +0 -35
- package/dist/constants/commands.d.ts +0 -2
- package/dist/constants/commands.js +0 -92
- package/dist/contexts/PluginManagerContext.d.ts +0 -3
- package/dist/contexts/PluginManagerContext.js +0 -9
- package/dist/contexts/useAppConfig.d.ts +0 -10
- package/dist/contexts/useAppConfig.js +0 -13
- package/dist/contexts/useChat.d.ts +0 -101
- package/dist/contexts/useChat.js +0 -993
- package/dist/contracts/status.d.ts +0 -7
- package/dist/contracts/status.js +0 -1
- package/dist/daemon/commands.d.ts +0 -49
- package/dist/daemon/commands.js +0 -341
- package/dist/daemon/jsonRpcClient.d.ts +0 -38
- package/dist/daemon/jsonRpcClient.js +0 -129
- package/dist/daemon/socketClient.d.ts +0 -13
- package/dist/daemon/socketClient.js +0 -26
- package/dist/daemon-cli.d.ts +0 -10
- package/dist/daemon-cli.js +0 -15
- package/dist/hooks/useInputManager.d.ts +0 -75
- package/dist/hooks/useInputManager.js +0 -583
- package/dist/hooks/usePluginManager.d.ts +0 -4
- package/dist/hooks/usePluginManager.js +0 -265
- package/dist/hooks/useTasks.d.ts +0 -1
- package/dist/hooks/useTasks.js +0 -5
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -447
- package/dist/managers/inputHandlers.d.ts +0 -30
- package/dist/managers/inputHandlers.js +0 -625
- package/dist/managers/inputReducer.d.ts +0 -271
- package/dist/managers/inputReducer.js +0 -1121
- package/dist/plugin-manager-cli.d.ts +0 -5
- package/dist/plugin-manager-cli.js +0 -12
- package/dist/print-cli.d.ts +0 -10
- package/dist/print-cli.js +0 -209
- package/dist/reducers/agentsManagerReducer.d.ts +0 -26
- package/dist/reducers/agentsManagerReducer.js +0 -54
- package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
- package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
- package/dist/reducers/confirmationReducer.d.ts +0 -37
- package/dist/reducers/confirmationReducer.js +0 -193
- package/dist/reducers/helpSelectorReducer.d.ts +0 -25
- package/dist/reducers/helpSelectorReducer.js +0 -52
- package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
- package/dist/reducers/marketplaceAddFormReducer.js +0 -58
- package/dist/reducers/mcpManagerReducer.d.ts +0 -37
- package/dist/reducers/mcpManagerReducer.js +0 -74
- package/dist/reducers/pluginDetailReducer.d.ts +0 -32
- package/dist/reducers/pluginDetailReducer.js +0 -70
- package/dist/reducers/questionReducer.d.ts +0 -82
- package/dist/reducers/questionReducer.js +0 -375
- package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
- package/dist/reducers/rewindSelectorReducer.js +0 -37
- package/dist/reducers/selectorReducer.d.ts +0 -23
- package/dist/reducers/selectorReducer.js +0 -42
- package/dist/reducers/workflowManagerReducer.d.ts +0 -40
- package/dist/reducers/workflowManagerReducer.js +0 -100
- package/dist/session-selector-cli.d.ts +0 -3
- package/dist/session-selector-cli.js +0 -25
- package/dist/stdio/agentBridge.d.ts +0 -139
- package/dist/stdio/agentBridge.js +0 -999
- package/dist/stdio/daemonServer.d.ts +0 -67
- package/dist/stdio/daemonServer.js +0 -191
- package/dist/stdio/index.d.ts +0 -5
- package/dist/stdio/index.js +0 -5
- package/dist/stdio/jsonRpcConnection.d.ts +0 -39
- package/dist/stdio/jsonRpcConnection.js +0 -135
- package/dist/stdio/protocol.d.ts +0 -41
- package/dist/stdio/protocol.js +0 -26
- package/dist/stdio/stdioServer.d.ts +0 -36
- package/dist/stdio/stdioServer.js +0 -51
- package/dist/stdio-cli.d.ts +0 -8
- package/dist/stdio-cli.js +0 -15
- package/dist/types.d.ts +0 -20
- package/dist/types.js +0 -1
- package/dist/utils/bracketedPaste.d.ts +0 -39
- package/dist/utils/bracketedPaste.js +0 -122
- package/dist/utils/clipboard.d.ts +0 -21
- package/dist/utils/clipboard.js +0 -347
- package/dist/utils/constants.d.ts +0 -16
- package/dist/utils/constants.js +0 -18
- package/dist/utils/highlightUtils.d.ts +0 -1
- package/dist/utils/highlightUtils.js +0 -93
- package/dist/utils/inputUtils.d.ts +0 -17
- package/dist/utils/inputUtils.js +0 -67
- package/dist/utils/logger.d.ts +0 -71
- package/dist/utils/logger.js +0 -259
- package/dist/utils/markdownTable.d.ts +0 -34
- package/dist/utils/markdownTable.js +0 -302
- package/dist/utils/rewindCheckpoints.d.ts +0 -8
- package/dist/utils/rewindCheckpoints.js +0 -17
- package/dist/utils/throttle.d.ts +0 -14
- package/dist/utils/throttle.js +0 -55
- package/dist/utils/toolParameterTransforms.d.ts +0 -18
- package/dist/utils/toolParameterTransforms.js +0 -73
- package/dist/utils/usageSummary.d.ts +0 -28
- package/dist/utils/usageSummary.js +0 -121
- package/dist/utils/version.d.ts +0 -1
- package/dist/utils/version.js +0 -4
- package/dist/utils/worktree.d.ts +0 -35
- package/dist/utils/worktree.js +0 -219
|
@@ -1,41 +1,282 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { render, Box } from "ink";
|
|
3
|
-
import {
|
|
1
|
+
import React, { useState, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { render, Box, Text } from "ink";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import {
|
|
5
|
+
PathEncoder,
|
|
6
|
+
listSessions,
|
|
7
|
+
listAllSessions,
|
|
8
|
+
truncateContent,
|
|
9
|
+
type SessionMetadata,
|
|
10
|
+
} from "wave-agent-sdk";
|
|
4
11
|
import { SessionSelector } from "./components/SessionSelector.js";
|
|
12
|
+
import { setClipboardText } from "./utils/clipboard.js";
|
|
13
|
+
|
|
14
|
+
export interface SessionSelectorResult {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
/**
|
|
17
|
+
* When the selected session belongs to a sibling git worktree of the same
|
|
18
|
+
* repo, the caller should chdir into this path before restoring.
|
|
19
|
+
*/
|
|
20
|
+
resumeWorkdir?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type SessionItem = SessionMetadata & { firstMessage?: string };
|
|
24
|
+
|
|
25
|
+
/** POSIX single-quote wrapping for use inside a `cd <path> && ...` command. */
|
|
26
|
+
function quotePath(p: string): string {
|
|
27
|
+
return `'${p.replace(/'/g, `'\\''`)}'`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SessionOwnership =
|
|
31
|
+
| { kind: "current" }
|
|
32
|
+
| { kind: "worktree"; resumeWorkdir: string }
|
|
33
|
+
| { kind: "cross-project"; command: string };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Decide how a selected session relates to the current working directory.
|
|
37
|
+
*
|
|
38
|
+
* Ownership is decided by comparing encoded directory names — the decoded
|
|
39
|
+
* workdir is lossy for paths containing "-", but re-encoding it reproduces the
|
|
40
|
+
* original encoded directory name exactly.
|
|
41
|
+
*
|
|
42
|
+
* - current: session belongs to the current directory → resume in place
|
|
43
|
+
* - worktree: session belongs to a sibling same-repo worktree → resume
|
|
44
|
+
* after chdir into it (skipped when the worktree dir is gone)
|
|
45
|
+
* - cross-project: session belongs to another project → print a cd command
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveSessionOwnership(
|
|
48
|
+
session: { id: string; workdir: string },
|
|
49
|
+
opts: { worktreePaths: string[]; currentWorkdir: string },
|
|
50
|
+
): Promise<SessionOwnership> {
|
|
51
|
+
const encoder = new PathEncoder();
|
|
52
|
+
// encode() resolves the path via realpath and throws ENOENT when the
|
|
53
|
+
// directory no longer exists (e.g. a deleted worktree); fall back to the
|
|
54
|
+
// pure string encodeSync so the picker still works for ghost sessions.
|
|
55
|
+
let sessionEncoded: string;
|
|
56
|
+
try {
|
|
57
|
+
sessionEncoded = await encoder.encode(session.workdir);
|
|
58
|
+
} catch {
|
|
59
|
+
sessionEncoded = encoder.encodeSync(session.workdir);
|
|
60
|
+
}
|
|
61
|
+
const cwdEncoded = await encoder.encode(opts.currentWorkdir);
|
|
62
|
+
|
|
63
|
+
if (sessionEncoded === cwdEncoded) {
|
|
64
|
+
return { kind: "current" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const wt of opts.worktreePaths) {
|
|
68
|
+
let wtEncoded: string;
|
|
69
|
+
try {
|
|
70
|
+
wtEncoded = await encoder.encode(wt);
|
|
71
|
+
} catch {
|
|
72
|
+
wtEncoded = encoder.encodeSync(wt);
|
|
73
|
+
}
|
|
74
|
+
if (wtEncoded === sessionEncoded && existsSync(wt)) {
|
|
75
|
+
return { kind: "worktree", resumeWorkdir: wt };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
kind: "cross-project",
|
|
81
|
+
command: `cd ${quotePath(session.workdir)} && wave --restore ${session.id}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Interactive session picker for `wave -r` (restore without an ID).
|
|
87
|
+
*
|
|
88
|
+
* Three scopes, toggled with Ctrl+A / Ctrl+W (mirroring Claude Code's resume
|
|
89
|
+
* picker):
|
|
90
|
+
* - current: sessions in the current working directory (default)
|
|
91
|
+
* - worktrees: sessions in any worktree of the same git repository
|
|
92
|
+
* - all: sessions across every project directory
|
|
93
|
+
*
|
|
94
|
+
* Resume decision on selection:
|
|
95
|
+
* - current dir session → resume in place
|
|
96
|
+
* - same-repo worktree → resume after chdir into that worktree
|
|
97
|
+
* - other project → print `cd <path> && wave --restore <id>`,
|
|
98
|
+
* copy it to the clipboard, and exit
|
|
99
|
+
*/
|
|
100
|
+
function SessionSelectorFlow({
|
|
101
|
+
currentWorkdir,
|
|
102
|
+
worktreePaths,
|
|
103
|
+
onDone,
|
|
104
|
+
}: {
|
|
105
|
+
currentWorkdir: string;
|
|
106
|
+
worktreePaths: string[];
|
|
107
|
+
onDone: (result: SessionSelectorResult | null) => void;
|
|
108
|
+
}): React.ReactNode {
|
|
109
|
+
const [showAllProjects, setShowAllProjects] = useState(false);
|
|
110
|
+
const [showAllWorktrees, setShowAllWorktrees] = useState(false);
|
|
111
|
+
const [sessions, setSessions] = useState<SessionItem[]>([]);
|
|
112
|
+
const [loading, setLoading] = useState(true);
|
|
113
|
+
const [crossProjectCommand, setCrossProjectCommand] = useState<string | null>(
|
|
114
|
+
null,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Mirror of the scope flags for use inside async reload/decision callbacks.
|
|
118
|
+
const scopeRef = useRef({ showAllProjects: false, showAllWorktrees: false });
|
|
119
|
+
|
|
120
|
+
const loadSessions = useCallback(
|
|
121
|
+
async (allProjects: boolean, allWorktrees: boolean) => {
|
|
122
|
+
if (allProjects) {
|
|
123
|
+
return listAllSessions();
|
|
124
|
+
}
|
|
125
|
+
if (allWorktrees) {
|
|
126
|
+
return listAllSessions({ worktreePaths, workdir: currentWorkdir });
|
|
127
|
+
}
|
|
128
|
+
return listSessions(currentWorkdir);
|
|
129
|
+
},
|
|
130
|
+
[worktreePaths, currentWorkdir],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const reload = useCallback(
|
|
134
|
+
async (allProjects: boolean, allWorktrees: boolean) => {
|
|
135
|
+
setLoading(true);
|
|
136
|
+
try {
|
|
137
|
+
const result = await loadSessions(allProjects, allWorktrees);
|
|
138
|
+
setSessions(
|
|
139
|
+
result.map((s) => ({
|
|
140
|
+
...s,
|
|
141
|
+
firstMessage: truncateContent(s.firstMessage || "No content", 80),
|
|
142
|
+
})),
|
|
143
|
+
);
|
|
144
|
+
} finally {
|
|
145
|
+
setLoading(false);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
[loadSessions],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Initial load: current directory scope
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
void reload(false, false);
|
|
154
|
+
}, [reload]);
|
|
155
|
+
|
|
156
|
+
const handleToggleAllProjects = useCallback(
|
|
157
|
+
(value: boolean) => {
|
|
158
|
+
scopeRef.current.showAllProjects = value;
|
|
159
|
+
setShowAllProjects(value);
|
|
160
|
+
void reload(value, scopeRef.current.showAllWorktrees);
|
|
161
|
+
},
|
|
162
|
+
[reload],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const handleToggleAllWorktrees = useCallback(
|
|
166
|
+
(value: boolean) => {
|
|
167
|
+
scopeRef.current.showAllWorktrees = value;
|
|
168
|
+
setShowAllWorktrees(value);
|
|
169
|
+
void reload(scopeRef.current.showAllProjects, value);
|
|
170
|
+
},
|
|
171
|
+
[reload],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const handleSelect = useCallback(
|
|
175
|
+
async (sessionId: string) => {
|
|
176
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
177
|
+
if (!session) return;
|
|
178
|
+
|
|
179
|
+
const ownership = await resolveSessionOwnership(session, {
|
|
180
|
+
worktreePaths,
|
|
181
|
+
currentWorkdir,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
switch (ownership.kind) {
|
|
185
|
+
case "current":
|
|
186
|
+
onDone({ sessionId });
|
|
187
|
+
return;
|
|
188
|
+
case "worktree":
|
|
189
|
+
onDone({ sessionId, resumeWorkdir: ownership.resumeWorkdir });
|
|
190
|
+
return;
|
|
191
|
+
case "cross-project":
|
|
192
|
+
// Different project — show the cd command instead of resuming in place
|
|
193
|
+
await setClipboardText(ownership.command);
|
|
194
|
+
setCrossProjectCommand(ownership.command);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
[sessions, worktreePaths, currentWorkdir, onDone],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (crossProjectCommand) {
|
|
202
|
+
return (
|
|
203
|
+
<CrossProjectMessage
|
|
204
|
+
command={crossProjectCommand}
|
|
205
|
+
onExit={() => onDone(null)}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (loading) {
|
|
211
|
+
return (
|
|
212
|
+
<Box>
|
|
213
|
+
<Text dimColor>Loading conversations…</Text>
|
|
214
|
+
</Box>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<Box padding={1}>
|
|
220
|
+
<SessionSelector
|
|
221
|
+
sessions={sessions}
|
|
222
|
+
worktreePaths={worktreePaths}
|
|
223
|
+
showProjectPath={showAllProjects || showAllWorktrees}
|
|
224
|
+
onSelect={(id) => void handleSelect(id)}
|
|
225
|
+
onCancel={() => onDone(null)}
|
|
226
|
+
onToggleAllProjects={handleToggleAllProjects}
|
|
227
|
+
onToggleAllWorktrees={handleToggleAllWorktrees}
|
|
228
|
+
/>
|
|
229
|
+
</Box>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Cross-project message auto-exits after a short delay (mirrors Claude Code's
|
|
235
|
+
* resume picker, which prints the cd command and exits).
|
|
236
|
+
*/
|
|
237
|
+
function CrossProjectMessage({
|
|
238
|
+
command,
|
|
239
|
+
onExit,
|
|
240
|
+
}: {
|
|
241
|
+
command: string;
|
|
242
|
+
onExit: () => void;
|
|
243
|
+
}): React.ReactNode {
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
const timer = setTimeout(onExit, 100);
|
|
246
|
+
return () => clearTimeout(timer);
|
|
247
|
+
}, [onExit]);
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<Box flexDirection="column" gap={1}>
|
|
251
|
+
<Text>This conversation is from a different directory.</Text>
|
|
252
|
+
<Box flexDirection="column">
|
|
253
|
+
<Text>To resume, run:</Text>
|
|
254
|
+
<Text> {command}</Text>
|
|
255
|
+
</Box>
|
|
256
|
+
<Text dimColor>(Command copied to clipboard)</Text>
|
|
257
|
+
</Box>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
5
260
|
|
|
6
261
|
export async function startSessionSelectorCli({
|
|
7
262
|
workdir,
|
|
263
|
+
worktreePaths = [],
|
|
8
264
|
}: {
|
|
9
265
|
workdir?: string;
|
|
10
|
-
|
|
266
|
+
worktreePaths?: string[];
|
|
267
|
+
} = {}): Promise<SessionSelectorResult | null> {
|
|
11
268
|
const currentWorkdir = workdir || process.cwd();
|
|
12
|
-
const sessions = await listSessions(currentWorkdir);
|
|
13
|
-
|
|
14
|
-
if (sessions.length === 0) {
|
|
15
|
-
console.log(`No sessions found for workdir: ${currentWorkdir}`);
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const sessionsWithContent = sessions.map((s) => ({
|
|
20
|
-
...s,
|
|
21
|
-
firstMessage: truncateContent(s.firstMessage || "No content", 80),
|
|
22
|
-
}));
|
|
23
269
|
|
|
24
270
|
return new Promise((resolve) => {
|
|
25
271
|
const { unmount } = render(
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
unmount();
|
|
35
|
-
resolve(null);
|
|
36
|
-
}}
|
|
37
|
-
/>
|
|
38
|
-
</Box>,
|
|
272
|
+
<SessionSelectorFlow
|
|
273
|
+
currentWorkdir={currentWorkdir}
|
|
274
|
+
worktreePaths={worktreePaths}
|
|
275
|
+
onDone={(result) => {
|
|
276
|
+
unmount();
|
|
277
|
+
resolve(result);
|
|
278
|
+
}}
|
|
279
|
+
/>,
|
|
39
280
|
);
|
|
40
281
|
});
|
|
41
282
|
}
|
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -375,7 +375,12 @@ export class AgentBridge {
|
|
|
375
375
|
);
|
|
376
376
|
case "removeWorktree":
|
|
377
377
|
return this.removeWorktreeSession(
|
|
378
|
-
p as unknown as {
|
|
378
|
+
p as unknown as {
|
|
379
|
+
path: string;
|
|
380
|
+
branch: string;
|
|
381
|
+
repoRoot: string;
|
|
382
|
+
hookBased?: boolean;
|
|
383
|
+
},
|
|
379
384
|
);
|
|
380
385
|
|
|
381
386
|
default:
|
|
@@ -615,6 +620,7 @@ export class AgentBridge {
|
|
|
615
620
|
repoRoot: string;
|
|
616
621
|
baseBranch: string;
|
|
617
622
|
isNew: boolean;
|
|
623
|
+
hookBased: boolean;
|
|
618
624
|
}> {
|
|
619
625
|
if (!params.workdir) {
|
|
620
626
|
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
|
|
@@ -631,6 +637,7 @@ export class AgentBridge {
|
|
|
631
637
|
repoRoot: session.repoRoot,
|
|
632
638
|
baseBranch: params.baseBranch ?? getDefaultRemoteBranch(params.workdir),
|
|
633
639
|
isNew: session.isNew,
|
|
640
|
+
hookBased: session.hookBased ?? false,
|
|
634
641
|
};
|
|
635
642
|
} catch (e) {
|
|
636
643
|
throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
|
|
@@ -641,31 +648,24 @@ export class AgentBridge {
|
|
|
641
648
|
path: string;
|
|
642
649
|
branch: string;
|
|
643
650
|
repoRoot: string;
|
|
651
|
+
hookBased?: boolean;
|
|
644
652
|
}): Promise<{ ok: true }> {
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// the session that runs in this worktree, if it is still registered.
|
|
656
|
-
for (const entry of this.sessions.values()) {
|
|
657
|
-
if (entry.agent.workingDirectory === params.path) {
|
|
658
|
-
try {
|
|
659
|
-
await entry.agent.triggerWorktreeRemoveHook(params.path);
|
|
660
|
-
} catch (e) {
|
|
661
|
-
logger.warn("WorktreeRemove hooks execution failed:", e);
|
|
662
|
-
}
|
|
663
|
-
break;
|
|
653
|
+
// Hook-based worktrees are owned by the WorktreeRemove hook; the git-root
|
|
654
|
+
// containment check does not apply to them.
|
|
655
|
+
if (!params.hookBased) {
|
|
656
|
+
// Align with Claude Code v2.1.216+: refuse to remove a worktree whose
|
|
657
|
+
// path is a symlink or resolves outside the repo root. Already-removed
|
|
658
|
+
// (missing) paths pass validation so removal stays idempotent.
|
|
659
|
+
try {
|
|
660
|
+
validateWorktreeRemovalPath(params.path, params.repoRoot);
|
|
661
|
+
} catch (e) {
|
|
662
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
|
|
664
663
|
}
|
|
665
664
|
}
|
|
666
665
|
|
|
667
666
|
// removeWorktree is best-effort/idempotent: already-removed worktrees or
|
|
668
|
-
// branches only log, never throw.
|
|
667
|
+
// branches only log, never throw. Hook-based worktrees go through the
|
|
668
|
+
// WorktreeRemove hook inside removeWorktree.
|
|
669
669
|
await removeWorktree({
|
|
670
670
|
name: "",
|
|
671
671
|
path: params.path,
|
|
@@ -674,6 +674,7 @@ export class AgentBridge {
|
|
|
674
674
|
hasUncommittedChanges: false,
|
|
675
675
|
hasNewCommits: false,
|
|
676
676
|
isNew: false,
|
|
677
|
+
hookBased: params.hookBased,
|
|
677
678
|
});
|
|
678
679
|
return { ok: true };
|
|
679
680
|
}
|
|
@@ -104,7 +104,9 @@ export class DaemonServer {
|
|
|
104
104
|
probe.once("connect", () => {
|
|
105
105
|
probe.destroy();
|
|
106
106
|
reject(
|
|
107
|
-
new Error(
|
|
107
|
+
new Error(
|
|
108
|
+
`Another wave daemon is already listening on ${this.socketPath}`,
|
|
109
|
+
),
|
|
108
110
|
);
|
|
109
111
|
});
|
|
110
112
|
probe.once("error", (err: NodeJS.ErrnoException) => {
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -9,6 +9,27 @@ export interface ClipboardImageResult {
|
|
|
9
9
|
mimeType?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Write text to the system clipboard via the terminal OSC 52 escape sequence.
|
|
14
|
+
* Works in most modern terminals without external tools (mirrors Claude
|
|
15
|
+
* Code's setClipboard). Callers must treat a `false` return as "clipboard
|
|
16
|
+
* unavailable" and still surface the text itself.
|
|
17
|
+
*
|
|
18
|
+
* @param text - Text to copy
|
|
19
|
+
* @returns Promise resolving to true when the sequence was written
|
|
20
|
+
*/
|
|
21
|
+
export async function setClipboardText(text: string): Promise<boolean> {
|
|
22
|
+
try {
|
|
23
|
+
process.stdout.write(
|
|
24
|
+
`\x1b]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`,
|
|
25
|
+
);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
// stdout unavailable — clipboard could not be set
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
/**
|
|
13
34
|
* Read image from clipboard
|
|
14
35
|
* @returns Promise<ClipboardImageResult> Result containing image path or error information
|
|
@@ -1,7 +1,52 @@
|
|
|
1
|
-
import hljs from "highlight.js";
|
|
1
|
+
import hljs from "highlight.js/lib/core";
|
|
2
|
+
import javascript from "highlight.js/lib/languages/javascript";
|
|
3
|
+
import typescript from "highlight.js/lib/languages/typescript";
|
|
4
|
+
import bash from "highlight.js/lib/languages/bash";
|
|
5
|
+
import json from "highlight.js/lib/languages/json";
|
|
6
|
+
import markdown from "highlight.js/lib/languages/markdown";
|
|
7
|
+
import python from "highlight.js/lib/languages/python";
|
|
8
|
+
import yaml from "highlight.js/lib/languages/yaml";
|
|
9
|
+
import xml from "highlight.js/lib/languages/xml";
|
|
10
|
+
import css from "highlight.js/lib/languages/css";
|
|
11
|
+
import sql from "highlight.js/lib/languages/sql";
|
|
12
|
+
import rust from "highlight.js/lib/languages/rust";
|
|
13
|
+
import go from "highlight.js/lib/languages/go";
|
|
14
|
+
import java from "highlight.js/lib/languages/java";
|
|
15
|
+
import cpp from "highlight.js/lib/languages/cpp";
|
|
16
|
+
import c from "highlight.js/lib/languages/c";
|
|
17
|
+
import csharp from "highlight.js/lib/languages/csharp";
|
|
18
|
+
import php from "highlight.js/lib/languages/php";
|
|
19
|
+
import ruby from "highlight.js/lib/languages/ruby";
|
|
20
|
+
import swift from "highlight.js/lib/languages/swift";
|
|
21
|
+
import kotlin from "highlight.js/lib/languages/kotlin";
|
|
22
|
+
import ini from "highlight.js/lib/languages/ini";
|
|
2
23
|
import { parse, Node, HTMLElement, TextNode } from "node-html-parser";
|
|
3
24
|
import chalk from "chalk";
|
|
4
25
|
|
|
26
|
+
// Register only the languages we auto-detect (the full default import pulls
|
|
27
|
+
// in all 384 languages, ~1.5MB of the bundle).
|
|
28
|
+
hljs.registerLanguage("javascript", javascript);
|
|
29
|
+
hljs.registerLanguage("typescript", typescript);
|
|
30
|
+
hljs.registerLanguage("bash", bash);
|
|
31
|
+
hljs.registerLanguage("json", json);
|
|
32
|
+
hljs.registerLanguage("markdown", markdown);
|
|
33
|
+
hljs.registerLanguage("python", python);
|
|
34
|
+
hljs.registerLanguage("yaml", yaml);
|
|
35
|
+
hljs.registerLanguage("xml", xml);
|
|
36
|
+
hljs.registerLanguage("css", css);
|
|
37
|
+
hljs.registerLanguage("sql", sql);
|
|
38
|
+
hljs.registerLanguage("rust", rust);
|
|
39
|
+
hljs.registerLanguage("go", go);
|
|
40
|
+
hljs.registerLanguage("java", java);
|
|
41
|
+
hljs.registerLanguage("cpp", cpp);
|
|
42
|
+
hljs.registerLanguage("c", c);
|
|
43
|
+
hljs.registerLanguage("csharp", csharp);
|
|
44
|
+
hljs.registerLanguage("php", php);
|
|
45
|
+
hljs.registerLanguage("ruby", ruby);
|
|
46
|
+
hljs.registerLanguage("swift", swift);
|
|
47
|
+
hljs.registerLanguage("kotlin", kotlin);
|
|
48
|
+
hljs.registerLanguage("ini", ini);
|
|
49
|
+
|
|
5
50
|
const theme: Record<string, (text: string) => string> = {
|
|
6
51
|
"hljs-keyword": chalk.blue,
|
|
7
52
|
"hljs-built_in": chalk.cyan,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads the nearest package.json by walking up from this module's location.
|
|
7
|
+
* Works for both the tsc output (dist/<subdir>/...) and the single-file esbuild
|
|
8
|
+
* bundle (dist/bundle/wave.mjs), whose relative depths differ.
|
|
9
|
+
*/
|
|
10
|
+
export function readNearestPackageJson(): {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
} {
|
|
15
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
for (;;) {
|
|
17
|
+
const candidate = path.join(dir, "package.json");
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(candidate, "utf-8"));
|
|
20
|
+
} catch {
|
|
21
|
+
// Not here — keep walking up.
|
|
22
|
+
}
|
|
23
|
+
const parent = path.dirname(dir);
|
|
24
|
+
if (parent === dir) {
|
|
25
|
+
throw new Error("package.json not found above this module");
|
|
26
|
+
}
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-line tail representation of in-flight streaming text: newlines are
|
|
3
|
+
* flattened to `\n` and, when longer than `maxLen` characters, only the last
|
|
4
|
+
* `maxLen` characters are kept behind an ellipsis. Shared by the streaming
|
|
5
|
+
* displays (assistant text, reasoning, tool parameters) and the loading
|
|
6
|
+
* indicators (compaction / /btw) so the tail style stays consistent.
|
|
7
|
+
*/
|
|
8
|
+
export const streamingTail = (content: string, maxLen = 30): string => {
|
|
9
|
+
const flat = content.replace(/\n/g, "\\n");
|
|
10
|
+
return flat.length > maxLen ? `…${flat.slice(-maxLen)}` : flat;
|
|
11
|
+
};
|