wave-code 0.18.3 → 0.18.5

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 (51) hide show
  1. package/dist/acp/agent.d.ts.map +1 -1
  2. package/dist/acp/agent.js +13 -0
  3. package/dist/components/CommandSelector.d.ts.map +1 -1
  4. package/dist/components/CommandSelector.js +9 -13
  5. package/dist/components/ConfirmationSelector.d.ts.map +1 -1
  6. package/dist/components/ConfirmationSelector.js +0 -1
  7. package/dist/components/DiscoverView.d.ts.map +1 -1
  8. package/dist/components/DiscoverView.js +10 -6
  9. package/dist/components/FileSelector.js +8 -8
  10. package/dist/components/HistorySearch.d.ts.map +1 -1
  11. package/dist/components/HistorySearch.js +6 -13
  12. package/dist/components/InstalledView.d.ts.map +1 -1
  13. package/dist/components/InstalledView.js +10 -6
  14. package/dist/components/LoginCommand.d.ts.map +1 -1
  15. package/dist/components/LoginCommand.js +14 -11
  16. package/dist/components/MarketplaceDetail.d.ts.map +1 -1
  17. package/dist/components/MarketplaceDetail.js +11 -7
  18. package/dist/components/MarketplaceView.d.ts.map +1 -1
  19. package/dist/components/MarketplaceView.js +10 -6
  20. package/dist/components/ModelSelector.d.ts.map +1 -1
  21. package/dist/components/ModelSelector.js +9 -15
  22. package/dist/components/SessionSelector.d.ts +5 -3
  23. package/dist/components/SessionSelector.d.ts.map +1 -1
  24. package/dist/components/SessionSelector.js +11 -7
  25. package/dist/components/WorktreeExitPrompt.d.ts.map +1 -1
  26. package/dist/components/WorktreeExitPrompt.js +3 -3
  27. package/dist/reducers/questionReducer.d.ts +0 -7
  28. package/dist/reducers/questionReducer.d.ts.map +1 -1
  29. package/dist/reducers/questionReducer.js +2 -1
  30. package/dist/reducers/selectorReducer.d.ts +7 -5
  31. package/dist/reducers/selectorReducer.d.ts.map +1 -1
  32. package/dist/reducers/selectorReducer.js +4 -2
  33. package/dist/utils/worktree.d.ts.map +1 -1
  34. package/dist/utils/worktree.js +10 -10
  35. package/package.json +2 -2
  36. package/src/acp/agent.ts +14 -0
  37. package/src/components/CommandSelector.tsx +11 -15
  38. package/src/components/ConfirmationSelector.tsx +0 -1
  39. package/src/components/DiscoverView.tsx +22 -7
  40. package/src/components/FileSelector.tsx +9 -9
  41. package/src/components/HistorySearch.tsx +7 -14
  42. package/src/components/InstalledView.tsx +23 -9
  43. package/src/components/LoginCommand.tsx +33 -36
  44. package/src/components/MarketplaceDetail.tsx +25 -10
  45. package/src/components/MarketplaceView.tsx +16 -7
  46. package/src/components/ModelSelector.tsx +13 -16
  47. package/src/components/SessionSelector.tsx +19 -9
  48. package/src/components/WorktreeExitPrompt.tsx +7 -4
  49. package/src/reducers/questionReducer.ts +2 -6
  50. package/src/reducers/selectorReducer.ts +20 -10
  51. package/src/utils/worktree.ts +29 -19
@@ -1,18 +1,28 @@
1
1
  import React, { useReducer, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
+ import type { KnownMarketplace } from "wave-agent-sdk";
3
4
  import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
4
- import { selectorReducer } from "../reducers/selectorReducer.js";
5
+ import {
6
+ selectorReducer,
7
+ type SelectorState,
8
+ } from "../reducers/selectorReducer.js";
5
9
 
6
10
  import { MarketplaceList } from "./MarketplaceList.js";
7
11
 
8
12
  export const MarketplaceView: React.FC = () => {
9
13
  const { marketplaces, actions } = usePluginManagerContext();
10
- const [state, dispatch] = useReducer(selectorReducer, {
14
+ const [state, dispatch] = useReducer(selectorReducer<KnownMarketplace>, {
11
15
  selectedIndex: 0,
12
16
  pendingDecision: null,
13
- });
17
+ items: [],
18
+ } as SelectorState<KnownMarketplace>);
19
+
20
+ const { selectedIndex, pendingDecision, items } = state;
14
21
 
15
- const { selectedIndex, pendingDecision } = state;
22
+ // Sync marketplaces into reducer state
23
+ useEffect(() => {
24
+ dispatch({ type: "SET_ITEMS", items: marketplaces });
25
+ }, [marketplaces]);
16
26
 
17
27
  useInput((input, key) => {
18
28
  if (input === "a") {
@@ -23,21 +33,20 @@ export const MarketplaceView: React.FC = () => {
23
33
  dispatch({
24
34
  type: "HANDLE_KEY",
25
35
  key,
26
- maxIndex: marketplaces.length - 1,
27
36
  hasInsert: false,
28
37
  });
29
38
  });
30
39
 
31
40
  useEffect(() => {
32
41
  if (pendingDecision === "select") {
33
- const mk = marketplaces[selectedIndex];
42
+ const mk = items[selectedIndex];
34
43
  if (mk) {
35
44
  actions.setSelectedId(mk.name);
36
45
  actions.setView("MARKETPLACE_DETAIL");
37
46
  }
38
47
  dispatch({ type: "CLEAR_DECISION" });
39
48
  }
40
- }, [pendingDecision, selectedIndex, marketplaces, actions]);
49
+ }, [pendingDecision, selectedIndex, items, actions]);
41
50
 
42
51
  return (
43
52
  <Box flexDirection="column">
@@ -1,6 +1,9 @@
1
1
  import React, { useReducer, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
- import { selectorReducer } from "../reducers/selectorReducer.js";
3
+ import {
4
+ selectorReducer,
5
+ type SelectorState,
6
+ } from "../reducers/selectorReducer.js";
4
7
 
5
8
  export interface ModelSelectorProps {
6
9
  onCancel: () => void;
@@ -17,29 +20,29 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
17
20
  configuredModels,
18
21
  onSelectModel,
19
22
  }) => {
20
- const [state, dispatch] = useReducer(selectorReducer, {
23
+ const [state, dispatch] = useReducer(selectorReducer<string>, {
21
24
  selectedIndex:
22
25
  configuredModels.indexOf(currentModel) !== -1
23
26
  ? configuredModels.indexOf(currentModel)
24
27
  : 0,
25
28
  pendingDecision: null,
26
- });
29
+ items: configuredModels,
30
+ } as SelectorState<string>);
27
31
 
28
- const { selectedIndex, pendingDecision } = state;
32
+ const { selectedIndex, pendingDecision, items: models } = state;
29
33
 
30
34
  useInput((_input, key) => {
31
35
  dispatch({
32
36
  type: "HANDLE_KEY",
33
37
  key,
34
- maxIndex: configuredModels.length - 1,
35
38
  hasInsert: false,
36
39
  });
37
40
  });
38
41
 
39
42
  useEffect(() => {
40
43
  if (pendingDecision === "select") {
41
- if (configuredModels.length > 0) {
42
- onSelectModel(configuredModels[selectedIndex]);
44
+ if (models.length > 0) {
45
+ onSelectModel(models[selectedIndex]);
43
46
  }
44
47
  onCancel();
45
48
  dispatch({ type: "CLEAR_DECISION" });
@@ -47,23 +50,17 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
47
50
  onCancel();
48
51
  dispatch({ type: "CLEAR_DECISION" });
49
52
  }
50
- }, [
51
- pendingDecision,
52
- selectedIndex,
53
- configuredModels,
54
- onSelectModel,
55
- onCancel,
56
- ]);
53
+ }, [pendingDecision, selectedIndex, models, onSelectModel, onCancel]);
57
54
 
58
55
  // Calculate visible window
59
56
  const startIndex = Math.max(
60
57
  0,
61
58
  Math.min(
62
59
  selectedIndex - Math.floor(MAX_VISIBLE_ITEMS / 2),
63
- Math.max(0, configuredModels.length - MAX_VISIBLE_ITEMS),
60
+ Math.max(0, models.length - MAX_VISIBLE_ITEMS),
64
61
  ),
65
62
  );
66
- const visibleModels = configuredModels.slice(
63
+ const visibleModels = models.slice(
67
64
  startIndex,
68
65
  startIndex + MAX_VISIBLE_ITEMS,
69
66
  );
@@ -1,10 +1,15 @@
1
1
  import React, { useReducer, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import type { SessionMetadata } from "wave-agent-sdk";
4
- import { selectorReducer } from "../reducers/selectorReducer.js";
4
+ import {
5
+ selectorReducer,
6
+ type SelectorState,
7
+ } from "../reducers/selectorReducer.js";
8
+
9
+ type SessionItem = SessionMetadata & { firstMessage?: string };
5
10
 
6
11
  export interface SessionSelectorProps {
7
- sessions: (SessionMetadata & { firstMessage?: string })[];
12
+ sessions: SessionItem[];
8
13
  onSelect: (sessionId: string) => void;
9
14
  onCancel: () => void;
10
15
  }
@@ -14,33 +19,38 @@ export const SessionSelector: React.FC<SessionSelectorProps> = ({
14
19
  onSelect,
15
20
  onCancel,
16
21
  }) => {
17
- const [state, dispatch] = useReducer(selectorReducer, {
22
+ const [state, dispatch] = useReducer(selectorReducer<SessionItem>, {
18
23
  selectedIndex: 0,
19
24
  pendingDecision: null,
20
- });
25
+ items: [],
26
+ } as SelectorState<SessionItem>);
21
27
 
22
- const { selectedIndex, pendingDecision } = state;
28
+ const { selectedIndex, pendingDecision, items } = state;
29
+
30
+ // Sync sessions into reducer state
31
+ useEffect(() => {
32
+ dispatch({ type: "SET_ITEMS", items: sessions });
33
+ }, [sessions]);
23
34
 
24
35
  useInput((_input, key) => {
25
36
  dispatch({
26
37
  type: "HANDLE_KEY",
27
38
  key,
28
- maxIndex: sessions.length - 1,
29
39
  hasInsert: false,
30
40
  });
31
41
  });
32
42
 
33
43
  useEffect(() => {
34
44
  if (pendingDecision === "select") {
35
- if (sessions.length > 0 && selectedIndex < sessions.length) {
36
- onSelect(sessions[selectedIndex].id);
45
+ if (items.length > 0 && selectedIndex < items.length) {
46
+ onSelect(items[selectedIndex].id);
37
47
  }
38
48
  dispatch({ type: "CLEAR_DECISION" });
39
49
  } else if (pendingDecision === "cancel") {
40
50
  onCancel();
41
51
  dispatch({ type: "CLEAR_DECISION" });
42
52
  }
43
- }, [pendingDecision, selectedIndex, sessions, onSelect, onCancel]);
53
+ }, [pendingDecision, selectedIndex, items, onSelect, onCancel]);
44
54
 
45
55
  if (sessions.length === 0) {
46
56
  return (
@@ -1,6 +1,9 @@
1
1
  import React, { useReducer, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
- import { selectorReducer } from "../reducers/selectorReducer.js";
3
+ import {
4
+ selectorReducer,
5
+ type SelectorState,
6
+ } from "../reducers/selectorReducer.js";
4
7
 
5
8
  interface WorktreeExitPromptProps {
6
9
  name: string;
@@ -21,10 +24,11 @@ export const WorktreeExitPrompt: React.FC<WorktreeExitPromptProps> = ({
21
24
  onRemove,
22
25
  onCancel,
23
26
  }) => {
24
- const [state, dispatch] = useReducer(selectorReducer, {
27
+ const [state, dispatch] = useReducer(selectorReducer<string>, {
25
28
  selectedIndex: 0,
26
29
  pendingDecision: null,
27
- });
30
+ items: ["keep", "remove"],
31
+ } as SelectorState<string>);
28
32
 
29
33
  const { selectedIndex, pendingDecision } = state;
30
34
 
@@ -32,7 +36,6 @@ export const WorktreeExitPrompt: React.FC<WorktreeExitPromptProps> = ({
32
36
  dispatch({
33
37
  type: "HANDLE_KEY",
34
38
  key,
35
- maxIndex: 1,
36
39
  hasInsert: false,
37
40
  });
38
41
  });
@@ -52,11 +52,6 @@ export type QuestionAction =
52
52
  type: "HANDLE_KEY";
53
53
  input: string;
54
54
  key: Key;
55
- currentQuestion: {
56
- question: string;
57
- options: Array<{ label: string }>;
58
- multiSelect?: boolean;
59
- };
60
55
  questions: Array<{
61
56
  question: string;
62
57
  options: Array<{ label: string }>;
@@ -265,7 +260,8 @@ export function questionReducer(
265
260
  case "CLEAR_DECISION":
266
261
  return { ...state, decision: null };
267
262
  case "HANDLE_KEY": {
268
- const { input, key, currentQuestion, questions } = action;
263
+ const { input, key, questions } = action;
264
+ const currentQuestion = questions[state.currentQuestionIndex];
269
265
  if (!currentQuestion) return state;
270
266
 
271
267
  const options = [...currentQuestion.options, { label: "Other" }];
@@ -1,31 +1,38 @@
1
1
  import { Key } from "ink";
2
2
 
3
- export interface SelectorState {
3
+ export interface SelectorState<T = unknown> {
4
4
  selectedIndex: number;
5
5
  pendingDecision: "select" | "insert" | "cancel" | null;
6
+ items: T[];
6
7
  }
7
8
 
8
- export type SelectorAction =
9
+ export type SelectorAction<T = unknown> =
9
10
  | { type: "MOVE_UP" }
10
- | { type: "MOVE_DOWN"; maxIndex: number }
11
+ | { type: "MOVE_DOWN" }
11
12
  | { type: "RESET_INDEX" }
12
- | { type: "HANDLE_KEY"; key: Key; maxIndex: number; hasInsert: boolean }
13
+ | { type: "SET_ITEMS"; items: T[] }
14
+ | { type: "HANDLE_KEY"; key: Key; hasInsert: boolean }
13
15
  | { type: "CLEAR_DECISION" };
14
16
 
15
- export function selectorReducer(
16
- state: SelectorState,
17
- action: SelectorAction,
18
- ): SelectorState {
17
+ export function selectorReducer<T = unknown>(
18
+ state: SelectorState<T>,
19
+ action: SelectorAction<T>,
20
+ ): SelectorState<T> {
19
21
  switch (action.type) {
20
22
  case "MOVE_UP":
21
23
  return { ...state, selectedIndex: Math.max(0, state.selectedIndex - 1) };
22
24
  case "MOVE_DOWN":
23
25
  return {
24
26
  ...state,
25
- selectedIndex: Math.min(action.maxIndex, state.selectedIndex + 1),
27
+ selectedIndex: Math.min(
28
+ state.items.length - 1,
29
+ state.selectedIndex + 1,
30
+ ),
26
31
  };
27
32
  case "RESET_INDEX":
28
33
  return { ...state, selectedIndex: 0 };
34
+ case "SET_ITEMS":
35
+ return { ...state, items: action.items, selectedIndex: 0 };
29
36
  case "HANDLE_KEY":
30
37
  if (action.key.upArrow) {
31
38
  return {
@@ -36,7 +43,10 @@ export function selectorReducer(
36
43
  if (action.key.downArrow) {
37
44
  return {
38
45
  ...state,
39
- selectedIndex: Math.min(action.maxIndex, state.selectedIndex + 1),
46
+ selectedIndex: Math.min(
47
+ state.items.length - 1,
48
+ state.selectedIndex + 1,
49
+ ),
40
50
  };
41
51
  }
42
52
  if (action.key.return) {
@@ -1,4 +1,4 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync } from "node:child_process";
2
2
  import * as path from "node:path";
3
3
  import * as fs from "node:fs";
4
4
  import { getDefaultRemoteBranch, getGitMainRepoRoot } from "wave-agent-sdk";
@@ -47,8 +47,9 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
47
47
 
48
48
  try {
49
49
  // Create worktree and branch
50
- execSync(
51
- `git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`,
50
+ execFileSync(
51
+ "git",
52
+ ["worktree", "add", "-b", branchName, worktreePath, baseBranch],
52
53
  {
53
54
  cwd: repoRoot,
54
55
  stdio: ["ignore", "pipe", "pipe"],
@@ -69,7 +70,7 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
69
70
  if (stderr.includes("already exists")) {
70
71
  // If branch already exists, try to add worktree without -b
71
72
  try {
72
- execSync(`git worktree add "${worktreePath}" ${branchName}`, {
73
+ execFileSync("git", ["worktree", "add", worktreePath, branchName], {
73
74
  cwd: repoRoot,
74
75
  stdio: ["ignore", "pipe", "pipe"],
75
76
  });
@@ -95,12 +96,13 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
95
96
  // Base branch not fetched yet — try fetching then retrying
96
97
  const branchNameOnly = baseBranch.split("/").pop()!;
97
98
  try {
98
- execSync(`git fetch origin ${branchNameOnly}`, {
99
+ execFileSync("git", ["fetch", "origin", branchNameOnly], {
99
100
  cwd: repoRoot,
100
101
  stdio: ["ignore", "pipe", "pipe"],
101
102
  });
102
- execSync(
103
- `git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`,
103
+ execFileSync(
104
+ "git",
105
+ ["worktree", "add", "-b", branchName, worktreePath, baseBranch],
104
106
  {
105
107
  cwd: repoRoot,
106
108
  stdio: ["ignore", "pipe", "pipe"],
@@ -118,10 +120,14 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
118
120
  } catch {
119
121
  // Fetch or retry failed — fall back to HEAD
120
122
  try {
121
- execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
122
- cwd: repoRoot,
123
- stdio: ["ignore", "pipe", "pipe"],
124
- });
123
+ execFileSync(
124
+ "git",
125
+ ["worktree", "add", "-b", branchName, worktreePath, "HEAD"],
126
+ {
127
+ cwd: repoRoot,
128
+ stdio: ["ignore", "pipe", "pipe"],
129
+ },
130
+ );
125
131
  return {
126
132
  name,
127
133
  path: worktreePath,
@@ -155,24 +161,28 @@ export function removeWorktree(session: WorktreeSession): void {
155
161
  // Get current branch in worktree before removing it
156
162
  let currentBranch: string | undefined;
157
163
  try {
158
- currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`, {
159
- cwd: session.path,
160
- encoding: "utf8",
161
- stdio: ["ignore", "pipe", "ignore"],
162
- }).trim();
164
+ currentBranch = execFileSync(
165
+ "git",
166
+ ["rev-parse", "--abbrev-ref", "HEAD"],
167
+ {
168
+ cwd: session.path,
169
+ encoding: "utf8",
170
+ stdio: ["ignore", "pipe", "ignore"],
171
+ },
172
+ ).trim();
163
173
  } catch {
164
174
  // Ignore errors getting current branch
165
175
  }
166
176
 
167
177
  // Remove worktree
168
- execSync(`git worktree remove --force "${session.path}"`, {
178
+ execFileSync("git", ["worktree", "remove", "--force", session.path], {
169
179
  cwd: repoRoot,
170
180
  stdio: ["ignore", "pipe", "pipe"],
171
181
  });
172
182
 
173
183
  // Delete original branch
174
184
  try {
175
- execSync(`git branch -D ${session.branch}`, {
185
+ execFileSync("git", ["branch", "-D", session.branch], {
176
186
  cwd: repoRoot,
177
187
  stdio: ["ignore", "pipe", "pipe"],
178
188
  });
@@ -195,7 +205,7 @@ export function removeWorktree(session: WorktreeSession): void {
195
205
  currentBranch !== "master"
196
206
  ) {
197
207
  try {
198
- execSync(`git branch -D ${currentBranch}`, {
208
+ execFileSync("git", ["branch", "-D", currentBranch], {
199
209
  cwd: repoRoot,
200
210
  stdio: ["ignore", "pipe", "pipe"],
201
211
  });