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.
@@ -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 — to START a daemon use `wave --daemon <socket>` instead)",
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 and wait for the reply",
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("timeout", {
307
- describe: "Seconds to wait for the reply (0 = no limit)",
308
- default: 600,
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
- { timeout: argv.timeout as number },
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: "Answers JSON for AskUserQuestion requests",
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
+ }