wave-code 1.1.5 → 1.3.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.
Files changed (46) hide show
  1. package/dist/bundle/wave.mjs +1075 -613
  2. package/package.json +4 -3
  3. package/src/cli.tsx +9 -0
  4. package/src/commands/plugin/install.ts +6 -7
  5. package/src/commands/plugin/uninstall.ts +11 -5
  6. package/src/components/ChatInterface.tsx +3 -0
  7. package/src/components/ConfirmationDetails.tsx +36 -15
  8. package/src/components/ConfirmationSelector.tsx +39 -0
  9. package/src/components/HooksManager.tsx +286 -0
  10. package/src/components/InputBox.tsx +125 -51
  11. package/src/components/Markdown.tsx +21 -8
  12. package/src/components/MarketplaceDetail.tsx +10 -27
  13. package/src/components/MarketplaceView.tsx +17 -3
  14. package/src/components/MessageBlockItem.tsx +0 -5
  15. package/src/components/MessageList.tsx +0 -1
  16. package/src/components/Notifications.tsx +7 -0
  17. package/src/components/PlanView.tsx +146 -0
  18. package/src/components/PluginManagerShell.tsx +3 -3
  19. package/src/components/PluginManagerTypes.ts +2 -1
  20. package/src/components/ResumeCommand.tsx +84 -0
  21. package/src/components/ScrolledContent.tsx +71 -0
  22. package/src/components/SessionSelector.tsx +184 -6
  23. package/src/components/SessionSelectorFlow.tsx +246 -0
  24. package/src/constants/commands.ts +30 -0
  25. package/src/contexts/useChat.tsx +252 -98
  26. package/src/daemon/commands.ts +1046 -67
  27. package/src/daemon-cli.ts +6 -0
  28. package/src/hooks/useInputManager.ts +30 -0
  29. package/src/hooks/useLineScroll.ts +58 -0
  30. package/src/hooks/usePluginManager.ts +104 -78
  31. package/src/index.ts +156 -16
  32. package/src/managers/inputHandlers.ts +16 -9
  33. package/src/managers/inputReducer.ts +39 -9
  34. package/src/reducers/confirmationReducer.ts +20 -1
  35. package/src/reducers/hooksManagerReducer.ts +92 -0
  36. package/src/reducers/selectorReducer.ts +3 -0
  37. package/src/session-selector-cli.tsx +9 -255
  38. package/src/stdio/agentBridge.ts +932 -133
  39. package/src/stdio/daemonServer.ts +26 -82
  40. package/src/stdio/protocol.ts +23 -4
  41. package/src/stdio-cli.ts +60 -0
  42. package/src/utils/rewindCheckpoints.ts +2 -2
  43. package/src/utils/sessionOwnership.ts +62 -0
  44. package/src/utils/worktree.ts +175 -50
  45. package/src/components/BangDisplay.tsx +0 -41
  46. package/src/components/PlanDisplay.tsx +0 -18
@@ -1,263 +1,17 @@
1
- import React, { useState, useCallback, useEffect, useRef } from "react";
2
- import { render, Box, Text } from "ink";
3
- import { existsSync } from "fs";
1
+ import React from "react";
2
+ import { render } from "ink";
4
3
  import {
5
- PathEncoder,
6
- listSessions,
7
- listAllSessions,
8
- truncateContent,
9
- type SessionMetadata,
10
- } from "wave-agent-sdk";
11
- import { SessionSelector } from "./components/SessionSelector.js";
12
- import { setClipboardText } from "./utils/clipboard.js";
4
+ SessionSelectorFlow,
5
+ type SessionSelectorResult,
6
+ } from "./components/SessionSelectorFlow.js";
13
7
 
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
- }
8
+ export type { SessionSelectorResult };
232
9
 
233
10
  /**
234
- * Cross-project message auto-exits after a short delay (mirrors Claude Code's
235
- * resume picker, which prints the cd command and exits).
11
+ * Interactive session picker for `wave -r` (restore without an ID), rendered in
12
+ * its own Ink instance before the main TUI starts. The picker itself is shared
13
+ * with the in-TUI `/resume` command (see `SessionSelectorFlow`).
236
14
  */
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
- }
260
-
261
15
  export async function startSessionSelectorCli({
262
16
  workdir,
263
17
  worktreePaths = [],