wave-code 1.0.10 → 1.1.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/bin/wave-code.js +8 -2
- package/dist/bundle/wave.mjs +3231 -0
- package/package.json +24 -25
- package/src/cli.tsx +6 -1
- 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 +17 -2
- 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/SkillsManager.tsx +312 -0
- package/src/components/StatusCommand.tsx +2 -9
- package/src/components/ToolDisplay.tsx +2 -4
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +245 -301
- 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 +7 -1
- package/src/index.ts +57 -10
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +195 -104
- package/src/print-cli.ts +25 -17
- package/src/reducers/selectorReducer.ts +10 -0
- package/src/reducers/skillsManagerReducer.ts +91 -0
- package/src/session-selector-cli.tsx +269 -28
- package/src/stdio/agentBridge.ts +30 -21
- package/src/stdio/daemonServer.ts +3 -1
- package/src/stdio/protocol.ts +2 -0
- 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
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type SlashCommand,
|
|
45
45
|
loadUserConfigEnv,
|
|
46
46
|
type SubagentConfiguration,
|
|
47
|
+
type SkillMetadata,
|
|
47
48
|
} from "wave-agent-sdk";
|
|
48
49
|
import {
|
|
49
50
|
type JsonRpcError,
|
|
@@ -241,6 +242,8 @@ export class AgentBridge {
|
|
|
241
242
|
return this.getSlashCommands(sessionId);
|
|
242
243
|
case "getSubagentConfigurations":
|
|
243
244
|
return this.getSubagentConfigurations(sessionId);
|
|
245
|
+
case "getSkillMetadata":
|
|
246
|
+
return this.getSkillMetadata(sessionId);
|
|
244
247
|
|
|
245
248
|
// ── File / History (global — no session required) ──
|
|
246
249
|
case "searchFiles":
|
|
@@ -375,7 +378,12 @@ export class AgentBridge {
|
|
|
375
378
|
);
|
|
376
379
|
case "removeWorktree":
|
|
377
380
|
return this.removeWorktreeSession(
|
|
378
|
-
p as unknown as {
|
|
381
|
+
p as unknown as {
|
|
382
|
+
path: string;
|
|
383
|
+
branch: string;
|
|
384
|
+
repoRoot: string;
|
|
385
|
+
hookBased?: boolean;
|
|
386
|
+
},
|
|
379
387
|
);
|
|
380
388
|
|
|
381
389
|
default:
|
|
@@ -615,6 +623,7 @@ export class AgentBridge {
|
|
|
615
623
|
repoRoot: string;
|
|
616
624
|
baseBranch: string;
|
|
617
625
|
isNew: boolean;
|
|
626
|
+
hookBased: boolean;
|
|
618
627
|
}> {
|
|
619
628
|
if (!params.workdir) {
|
|
620
629
|
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
|
|
@@ -631,6 +640,7 @@ export class AgentBridge {
|
|
|
631
640
|
repoRoot: session.repoRoot,
|
|
632
641
|
baseBranch: params.baseBranch ?? getDefaultRemoteBranch(params.workdir),
|
|
633
642
|
isNew: session.isNew,
|
|
643
|
+
hookBased: session.hookBased ?? false,
|
|
634
644
|
};
|
|
635
645
|
} catch (e) {
|
|
636
646
|
throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
|
|
@@ -641,31 +651,24 @@ export class AgentBridge {
|
|
|
641
651
|
path: string;
|
|
642
652
|
branch: string;
|
|
643
653
|
repoRoot: string;
|
|
654
|
+
hookBased?: boolean;
|
|
644
655
|
}): 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;
|
|
656
|
+
// Hook-based worktrees are owned by the WorktreeRemove hook; the git-root
|
|
657
|
+
// containment check does not apply to them.
|
|
658
|
+
if (!params.hookBased) {
|
|
659
|
+
// Align with Claude Code v2.1.216+: refuse to remove a worktree whose
|
|
660
|
+
// path is a symlink or resolves outside the repo root. Already-removed
|
|
661
|
+
// (missing) paths pass validation so removal stays idempotent.
|
|
662
|
+
try {
|
|
663
|
+
validateWorktreeRemovalPath(params.path, params.repoRoot);
|
|
664
|
+
} catch (e) {
|
|
665
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
|
|
664
666
|
}
|
|
665
667
|
}
|
|
666
668
|
|
|
667
669
|
// removeWorktree is best-effort/idempotent: already-removed worktrees or
|
|
668
|
-
// branches only log, never throw.
|
|
670
|
+
// branches only log, never throw. Hook-based worktrees go through the
|
|
671
|
+
// WorktreeRemove hook inside removeWorktree.
|
|
669
672
|
await removeWorktree({
|
|
670
673
|
name: "",
|
|
671
674
|
path: params.path,
|
|
@@ -674,6 +677,7 @@ export class AgentBridge {
|
|
|
674
677
|
hasUncommittedChanges: false,
|
|
675
678
|
hasNewCommits: false,
|
|
676
679
|
isNew: false,
|
|
680
|
+
hookBased: params.hookBased,
|
|
677
681
|
});
|
|
678
682
|
return { ok: true };
|
|
679
683
|
}
|
|
@@ -1075,6 +1079,11 @@ export class AgentBridge {
|
|
|
1075
1079
|
return { configurations: entry.agent.getSubagentConfigurations() };
|
|
1076
1080
|
}
|
|
1077
1081
|
|
|
1082
|
+
private getSkillMetadata(sessionId?: string): { skills: SkillMetadata[] } {
|
|
1083
|
+
const entry = this.requireSession(sessionId);
|
|
1084
|
+
return { skills: entry.agent.getSkillMetadata() };
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1078
1087
|
// ── File / History (global) ───────────────────────────────────
|
|
1079
1088
|
|
|
1080
1089
|
private async searchFiles(
|
|
@@ -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/stdio/protocol.ts
CHANGED
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
|
+
};
|