wave-code 1.1.4 → 1.2.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/dist/bundle/wave.mjs +546 -525
- package/package.json +2 -2
- package/src/commands/plugin/install.ts +5 -5
- package/src/components/ChatInterface.tsx +2 -0
- package/src/components/HooksManager.tsx +286 -0
- package/src/components/InputBox.tsx +86 -49
- package/src/components/MessageBlockItem.tsx +0 -5
- package/src/components/MessageList.tsx +0 -1
- package/src/components/PlanView.tsx +141 -0
- package/src/constants/commands.ts +12 -0
- package/src/contexts/useChat.tsx +94 -70
- package/src/daemon/commands.ts +486 -30
- package/src/hooks/useInputManager.ts +14 -0
- package/src/hooks/useLineScroll.ts +58 -0
- package/src/index.ts +94 -8
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +10 -0
- package/src/reducers/hooksManagerReducer.ts +92 -0
- package/src/stdio/agentBridge.ts +552 -55
- package/src/stdio/daemonServer.ts +26 -82
- package/src/stdio/protocol.ts +14 -4
- package/src/stdio-cli.ts +54 -0
- package/src/utils/rewindCheckpoints.ts +2 -2
- package/src/utils/worktree.ts +175 -50
- package/src/components/BangDisplay.tsx +0 -41
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useInput } from "ink";
|
|
3
|
+
|
|
4
|
+
export interface UseLineScrollOptions {
|
|
5
|
+
/** Total number of content rows in the scrollable area. */
|
|
6
|
+
totalLines: number;
|
|
7
|
+
/** Number of rows visible at once (computed from the available height). */
|
|
8
|
+
visibleCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fixed-height line scrolling shared by overlays whose content may exceed the
|
|
13
|
+
* terminal: keeps a scroll offset clamped to the visible window and handles
|
|
14
|
+
* PgUp/PgDn (full page) plus Ctrl+u/Ctrl+d (half page, aligned with Claude
|
|
15
|
+
* Code's modal pager keys). Returns the clamped offset and the ↑/↓ indicator
|
|
16
|
+
* flags so callers can render "N more" lines.
|
|
17
|
+
*
|
|
18
|
+
* Scroll position starts at the top on mount; callers that display a new
|
|
19
|
+
* content set remount this hook (e.g. an overlay component keyed by content),
|
|
20
|
+
* which resets the offset naturally.
|
|
21
|
+
*/
|
|
22
|
+
export function useLineScroll({
|
|
23
|
+
totalLines,
|
|
24
|
+
visibleCount,
|
|
25
|
+
}: UseLineScrollOptions): {
|
|
26
|
+
scrollOffset: number;
|
|
27
|
+
hasMoreAbove: boolean;
|
|
28
|
+
hasMoreBelow: boolean;
|
|
29
|
+
} {
|
|
30
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
31
|
+
|
|
32
|
+
const maxScroll = Math.max(0, totalLines - visibleCount);
|
|
33
|
+
const halfPage = Math.max(1, Math.ceil(visibleCount / 2));
|
|
34
|
+
|
|
35
|
+
useInput((input, key) => {
|
|
36
|
+
if (key.pageUp) {
|
|
37
|
+
setScrollOffset((offset) => Math.max(0, offset - visibleCount));
|
|
38
|
+
}
|
|
39
|
+
if (key.pageDown) {
|
|
40
|
+
setScrollOffset((offset) => Math.min(maxScroll, offset + visibleCount));
|
|
41
|
+
}
|
|
42
|
+
// Ctrl+u/Ctrl+d scroll half a page — fallback for keyboards without
|
|
43
|
+
// PgUp/PgDn (aligned with Claude Code's modal pager keys).
|
|
44
|
+
if (key.ctrl && input === "u") {
|
|
45
|
+
setScrollOffset((offset) => Math.max(0, offset - halfPage));
|
|
46
|
+
}
|
|
47
|
+
if (key.ctrl && input === "d") {
|
|
48
|
+
setScrollOffset((offset) => Math.min(maxScroll, offset + halfPage));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const clampedOffset = Math.min(scrollOffset, maxScroll);
|
|
53
|
+
return {
|
|
54
|
+
scrollOffset: clampedOffset,
|
|
55
|
+
hasMoreAbove: clampedOffset > 0,
|
|
56
|
+
hasMoreBelow: clampedOffset + visibleCount < totalLines,
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -188,7 +188,7 @@ export async function main() {
|
|
|
188
188
|
})
|
|
189
189
|
.option("scope", {
|
|
190
190
|
alias: "s",
|
|
191
|
-
describe: "Scope to enable the plugin in",
|
|
191
|
+
describe: "Scope to enable the plugin in (default: user)",
|
|
192
192
|
choices: ["user", "project", "local"],
|
|
193
193
|
type: "string",
|
|
194
194
|
});
|
|
@@ -251,10 +251,47 @@ export async function main() {
|
|
|
251
251
|
})
|
|
252
252
|
.command(
|
|
253
253
|
"daemon",
|
|
254
|
-
"Manage the wave daemon (client subcommands —
|
|
254
|
+
"Manage the wave daemon (client subcommands — daemons start on demand, or directly via `wave --daemon <socket>`)",
|
|
255
255
|
(yargs) => {
|
|
256
256
|
return yargs
|
|
257
257
|
.help()
|
|
258
|
+
.command(
|
|
259
|
+
"create",
|
|
260
|
+
"Create a new session hosted by the daemon and print its sessionId",
|
|
261
|
+
(yargs) => {
|
|
262
|
+
return yargs
|
|
263
|
+
.option("workdir", {
|
|
264
|
+
describe:
|
|
265
|
+
"Working directory for the new session (default: current directory)",
|
|
266
|
+
type: "string",
|
|
267
|
+
})
|
|
268
|
+
.option("permission-mode", {
|
|
269
|
+
describe:
|
|
270
|
+
"Permission mode for the new session (default: bypassPermissions; options: default, bypassPermissions, acceptEdits, plan, dontAsk)",
|
|
271
|
+
type: "string",
|
|
272
|
+
})
|
|
273
|
+
.option("model", {
|
|
274
|
+
describe:
|
|
275
|
+
"Model override for the new session (default: configured model)",
|
|
276
|
+
type: "string",
|
|
277
|
+
})
|
|
278
|
+
.option("worktree", {
|
|
279
|
+
describe:
|
|
280
|
+
"Create the session in a new git worktree (name optional — auto-generated when omitted)",
|
|
281
|
+
type: "string",
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
async (argv) => {
|
|
285
|
+
const { daemonCreateCommand, DEFAULT_DAEMON_SOCKET } =
|
|
286
|
+
await import("./daemon/commands.js");
|
|
287
|
+
await daemonCreateCommand(DEFAULT_DAEMON_SOCKET, {
|
|
288
|
+
workdir: argv.workdir as string | undefined,
|
|
289
|
+
permissionMode: argv.permissionMode as string | undefined,
|
|
290
|
+
model: argv.model as string | undefined,
|
|
291
|
+
worktree: argv.worktree as string | undefined,
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
)
|
|
258
295
|
.command(
|
|
259
296
|
"list",
|
|
260
297
|
"List sessions hosted by the daemon (in-memory registry)",
|
|
@@ -292,7 +329,7 @@ export async function main() {
|
|
|
292
329
|
)
|
|
293
330
|
.command(
|
|
294
331
|
"send <sessionId> <message>",
|
|
295
|
-
"Inject a message into a session
|
|
332
|
+
"Inject a message into a session (async by default; --wait N to get the reply)",
|
|
296
333
|
(yargs) => {
|
|
297
334
|
return yargs
|
|
298
335
|
.positional("sessionId", {
|
|
@@ -303,9 +340,10 @@ export async function main() {
|
|
|
303
340
|
describe: "Message to send",
|
|
304
341
|
type: "string",
|
|
305
342
|
})
|
|
306
|
-
.option("
|
|
307
|
-
describe:
|
|
308
|
-
|
|
343
|
+
.option("wait", {
|
|
344
|
+
describe:
|
|
345
|
+
"Seconds to wait for the reply and print it (0 = fire-and-forget, the default)",
|
|
346
|
+
default: 0,
|
|
309
347
|
type: "number",
|
|
310
348
|
});
|
|
311
349
|
},
|
|
@@ -316,7 +354,7 @@ export async function main() {
|
|
|
316
354
|
DEFAULT_DAEMON_SOCKET,
|
|
317
355
|
argv.sessionId as string,
|
|
318
356
|
argv.message as string,
|
|
319
|
-
{
|
|
357
|
+
{ wait: argv.wait as number },
|
|
320
358
|
);
|
|
321
359
|
},
|
|
322
360
|
)
|
|
@@ -346,7 +384,8 @@ export async function main() {
|
|
|
346
384
|
type: "string",
|
|
347
385
|
})
|
|
348
386
|
.option("answer", {
|
|
349
|
-
describe:
|
|
387
|
+
describe:
|
|
388
|
+
'AskUserQuestion answers: JSON object of {question: answer} or option numbers per question (e.g. "0" or "1,0")',
|
|
350
389
|
type: "string",
|
|
351
390
|
})
|
|
352
391
|
.option("rule", {
|
|
@@ -394,6 +433,53 @@ export async function main() {
|
|
|
394
433
|
);
|
|
395
434
|
},
|
|
396
435
|
)
|
|
436
|
+
.command(
|
|
437
|
+
"destroy <sessionId>",
|
|
438
|
+
"Destroy a daemon session (idempotent)",
|
|
439
|
+
(yargs) => {
|
|
440
|
+
return yargs
|
|
441
|
+
.positional("sessionId", {
|
|
442
|
+
describe: "Session ID of the session to destroy",
|
|
443
|
+
type: "string",
|
|
444
|
+
})
|
|
445
|
+
.option("remove-worktree", {
|
|
446
|
+
describe:
|
|
447
|
+
"Also remove the session's git worktree (resolved via git, removed via protocol removeWorktree) before destroying",
|
|
448
|
+
type: "boolean",
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
async (argv) => {
|
|
452
|
+
const { daemonDestroyCommand, DEFAULT_DAEMON_SOCKET } =
|
|
453
|
+
await import("./daemon/commands.js");
|
|
454
|
+
await daemonDestroyCommand(
|
|
455
|
+
DEFAULT_DAEMON_SOCKET,
|
|
456
|
+
argv.sessionId as string,
|
|
457
|
+
{
|
|
458
|
+
removeWorktree: argv.removeWorktree as boolean | undefined,
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
},
|
|
462
|
+
)
|
|
463
|
+
.command(
|
|
464
|
+
"stop",
|
|
465
|
+
"Gracefully stop the daemon (idempotent — no-op when it is not running)",
|
|
466
|
+
{},
|
|
467
|
+
async () => {
|
|
468
|
+
const { daemonStopCommand, DEFAULT_DAEMON_SOCKET } =
|
|
469
|
+
await import("./daemon/commands.js");
|
|
470
|
+
await daemonStopCommand(DEFAULT_DAEMON_SOCKET);
|
|
471
|
+
},
|
|
472
|
+
)
|
|
473
|
+
.command(
|
|
474
|
+
"restart",
|
|
475
|
+
"Restart the daemon (graceful stop + relaunch; starts one when not running)",
|
|
476
|
+
{},
|
|
477
|
+
async () => {
|
|
478
|
+
const { daemonRestartCommand, DEFAULT_DAEMON_SOCKET } =
|
|
479
|
+
await import("./daemon/commands.js");
|
|
480
|
+
await daemonRestartCommand(DEFAULT_DAEMON_SOCKET);
|
|
481
|
+
},
|
|
482
|
+
)
|
|
397
483
|
.demandCommand(1, "Please specify a daemon subcommand");
|
|
398
484
|
},
|
|
399
485
|
)
|
|
@@ -372,6 +372,8 @@ export const handleCommandSelect = (
|
|
|
372
372
|
dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
|
|
373
373
|
} else if (command === "skills") {
|
|
374
374
|
dispatch({ type: "SET_SHOW_SKILLS_MANAGER", payload: true });
|
|
375
|
+
} else if (command === "hooks") {
|
|
376
|
+
dispatch({ type: "SET_SHOW_HOOKS_MANAGER", payload: true });
|
|
375
377
|
} else if (command === "rewind") {
|
|
376
378
|
dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
|
|
377
379
|
} else if (command === "help") {
|
|
@@ -109,6 +109,7 @@ export interface InputManagerCallbacks {
|
|
|
109
109
|
onClearMessages?: () => Promise<void>;
|
|
110
110
|
onCompact?: (instructions?: string) => Promise<void>;
|
|
111
111
|
onAddDir?: (args?: string) => Promise<void>;
|
|
112
|
+
onPlanCommand?: (args?: string) => Promise<void> | void;
|
|
112
113
|
sessionId?: string;
|
|
113
114
|
workdir?: string;
|
|
114
115
|
getFullMessageThread?: () => Promise<{
|
|
@@ -148,6 +149,7 @@ export interface InputState {
|
|
|
148
149
|
showModelSelector: boolean;
|
|
149
150
|
showWorkflowManager: boolean;
|
|
150
151
|
showSkillsManager: boolean;
|
|
152
|
+
showHooksManager: boolean;
|
|
151
153
|
permissionMode: PermissionMode;
|
|
152
154
|
selectorJustUsed: boolean;
|
|
153
155
|
history: PromptEntry[];
|
|
@@ -187,6 +189,7 @@ export const initialState: InputState = {
|
|
|
187
189
|
showModelSelector: false,
|
|
188
190
|
showWorkflowManager: false,
|
|
189
191
|
showSkillsManager: false,
|
|
192
|
+
showHooksManager: false,
|
|
190
193
|
permissionMode: "default",
|
|
191
194
|
selectorJustUsed: false,
|
|
192
195
|
history: [],
|
|
@@ -469,6 +472,7 @@ export type InputAction =
|
|
|
469
472
|
| { type: "SET_SHOW_MODEL_SELECTOR"; payload: boolean }
|
|
470
473
|
| { type: "SET_SHOW_WORKFLOW_MANAGER"; payload: boolean }
|
|
471
474
|
| { type: "SET_SHOW_SKILLS_MANAGER"; payload: boolean }
|
|
475
|
+
| { type: "SET_SHOW_HOOKS_MANAGER"; payload: boolean }
|
|
472
476
|
| { type: "SET_PERMISSION_MODE"; payload: PermissionMode }
|
|
473
477
|
| { type: "SET_SELECTOR_JUST_USED"; payload: boolean }
|
|
474
478
|
| { type: "INSERT_TEXT_WITH_PLACEHOLDER"; payload: string }
|
|
@@ -705,6 +709,12 @@ export function inputReducer(
|
|
|
705
709
|
showSkillsManager: action.payload,
|
|
706
710
|
selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
|
|
707
711
|
};
|
|
712
|
+
case "SET_SHOW_HOOKS_MANAGER":
|
|
713
|
+
return {
|
|
714
|
+
...state,
|
|
715
|
+
showHooksManager: action.payload,
|
|
716
|
+
selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
|
|
717
|
+
};
|
|
708
718
|
case "SET_PERMISSION_MODE":
|
|
709
719
|
return { ...state, permissionMode: action.payload };
|
|
710
720
|
case "SET_SELECTOR_JUST_USED":
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Key } from "ink";
|
|
2
|
+
|
|
3
|
+
export type PendingEffect = { type: "CANCEL" };
|
|
4
|
+
|
|
5
|
+
export interface HooksManagerState {
|
|
6
|
+
selectedIndex: number;
|
|
7
|
+
viewMode: "list" | "detail";
|
|
8
|
+
pendingEffect: PendingEffect | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type HooksManagerAction =
|
|
12
|
+
| { type: "MOVE_UP" }
|
|
13
|
+
| { type: "MOVE_DOWN"; itemCount: number }
|
|
14
|
+
| { type: "SET_VIEW_MODE"; viewMode: "list" | "detail" }
|
|
15
|
+
| {
|
|
16
|
+
type: "HANDLE_KEY";
|
|
17
|
+
input: string;
|
|
18
|
+
key: Key;
|
|
19
|
+
itemCount: number;
|
|
20
|
+
}
|
|
21
|
+
| { type: "CLEAR_PENDING_EFFECT" };
|
|
22
|
+
|
|
23
|
+
export function hooksManagerReducer(
|
|
24
|
+
state: HooksManagerState,
|
|
25
|
+
action: HooksManagerAction,
|
|
26
|
+
): HooksManagerState {
|
|
27
|
+
switch (action.type) {
|
|
28
|
+
case "MOVE_UP":
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
selectedIndex: Math.max(0, state.selectedIndex - 1),
|
|
32
|
+
};
|
|
33
|
+
case "MOVE_DOWN":
|
|
34
|
+
return {
|
|
35
|
+
...state,
|
|
36
|
+
selectedIndex: Math.min(
|
|
37
|
+
Math.max(0, action.itemCount - 1),
|
|
38
|
+
state.selectedIndex + 1,
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
case "SET_VIEW_MODE":
|
|
42
|
+
return { ...state, viewMode: action.viewMode };
|
|
43
|
+
case "HANDLE_KEY": {
|
|
44
|
+
const { key, itemCount } = action;
|
|
45
|
+
|
|
46
|
+
if (key.return) {
|
|
47
|
+
if (state.viewMode === "list") {
|
|
48
|
+
return { ...state, viewMode: "detail" };
|
|
49
|
+
}
|
|
50
|
+
// Aligned with AgentsManager/SkillsManager detail view: Enter
|
|
51
|
+
// returns to the list.
|
|
52
|
+
return { ...state, viewMode: "list" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (key.escape) {
|
|
56
|
+
if (state.viewMode === "detail") {
|
|
57
|
+
return { ...state, viewMode: "list" };
|
|
58
|
+
}
|
|
59
|
+
return { ...state, pendingEffect: { type: "CANCEL" } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Detail view does not respond to arrow keys (aligned with
|
|
63
|
+
// AgentsManager/SkillsManager).
|
|
64
|
+
if (state.viewMode === "detail") {
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (key.upArrow) {
|
|
69
|
+
return {
|
|
70
|
+
...state,
|
|
71
|
+
selectedIndex: Math.max(0, state.selectedIndex - 1),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (key.downArrow) {
|
|
76
|
+
return {
|
|
77
|
+
...state,
|
|
78
|
+
selectedIndex: Math.min(
|
|
79
|
+
Math.max(0, itemCount - 1),
|
|
80
|
+
state.selectedIndex + 1,
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
case "CLEAR_PENDING_EFFECT":
|
|
88
|
+
return { ...state, pendingEffect: null };
|
|
89
|
+
default:
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
92
|
+
}
|