wave-code 0.17.0 → 0.17.2

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 (59) hide show
  1. package/dist/acp/agent.d.ts +1 -0
  2. package/dist/acp/agent.d.ts.map +1 -1
  3. package/dist/acp/agent.js +116 -1
  4. package/dist/components/ChatInterface.d.ts.map +1 -1
  5. package/dist/components/ChatInterface.js +5 -2
  6. package/dist/components/InputBox.d.ts +2 -0
  7. package/dist/components/InputBox.d.ts.map +1 -1
  8. package/dist/components/InputBox.js +29 -8
  9. package/dist/components/LoadingIndicator.d.ts +2 -1
  10. package/dist/components/LoadingIndicator.d.ts.map +1 -1
  11. package/dist/components/LoadingIndicator.js +2 -2
  12. package/dist/components/QueuedMessageList.d.ts.map +1 -1
  13. package/dist/components/QueuedMessageList.js +10 -10
  14. package/dist/components/StatusLine.d.ts +2 -0
  15. package/dist/components/StatusLine.d.ts.map +1 -1
  16. package/dist/components/StatusLine.js +6 -6
  17. package/dist/components/TaskList.d.ts +3 -0
  18. package/dist/components/TaskList.d.ts.map +1 -1
  19. package/dist/components/TaskList.js +171 -28
  20. package/dist/components/TaskNotificationMessage.d.ts.map +1 -1
  21. package/dist/components/TaskNotificationMessage.js +1 -0
  22. package/dist/components/WorkflowManager.d.ts +6 -0
  23. package/dist/components/WorkflowManager.d.ts.map +1 -0
  24. package/dist/components/WorkflowManager.js +102 -0
  25. package/dist/constants/commands.d.ts.map +1 -1
  26. package/dist/constants/commands.js +6 -0
  27. package/dist/contexts/useChat.d.ts +8 -1
  28. package/dist/contexts/useChat.d.ts.map +1 -1
  29. package/dist/contexts/useChat.js +68 -4
  30. package/dist/hooks/useInputManager.d.ts +2 -0
  31. package/dist/hooks/useInputManager.d.ts.map +1 -1
  32. package/dist/hooks/useInputManager.js +22 -13
  33. package/dist/managers/inputHandlers.d.ts.map +1 -1
  34. package/dist/managers/inputHandlers.js +6 -3
  35. package/dist/managers/inputReducer.d.ts +10 -0
  36. package/dist/managers/inputReducer.d.ts.map +1 -1
  37. package/dist/managers/inputReducer.js +41 -4
  38. package/dist/print-cli.d.ts.map +1 -1
  39. package/dist/print-cli.js +3 -31
  40. package/dist/reducers/workflowManagerReducer.d.ts +41 -0
  41. package/dist/reducers/workflowManagerReducer.d.ts.map +1 -0
  42. package/dist/reducers/workflowManagerReducer.js +100 -0
  43. package/package.json +2 -2
  44. package/src/acp/agent.ts +137 -1
  45. package/src/components/ChatInterface.tsx +10 -1
  46. package/src/components/InputBox.tsx +43 -3
  47. package/src/components/LoadingIndicator.tsx +4 -1
  48. package/src/components/QueuedMessageList.tsx +7 -3
  49. package/src/components/StatusLine.tsx +28 -16
  50. package/src/components/TaskList.tsx +213 -19
  51. package/src/components/TaskNotificationMessage.tsx +1 -0
  52. package/src/components/WorkflowManager.tsx +320 -0
  53. package/src/constants/commands.ts +6 -0
  54. package/src/contexts/useChat.tsx +83 -4
  55. package/src/hooks/useInputManager.ts +25 -13
  56. package/src/managers/inputHandlers.ts +5 -4
  57. package/src/managers/inputReducer.ts +54 -6
  58. package/src/print-cli.ts +3 -37
  59. package/src/reducers/workflowManagerReducer.ts +143 -0
package/src/acp/agent.ts CHANGED
@@ -17,6 +17,10 @@ import {
17
17
  ASK_USER_QUESTION_TOOL_NAME,
18
18
  AskUserQuestion,
19
19
  AskUserQuestionOption,
20
+ type Message,
21
+ type TextBlock,
22
+ type ReasoningBlock,
23
+ type ToolBlock,
20
24
  } from "wave-agent-sdk";
21
25
  import { logger } from "../utils/logger.js";
22
26
  import {
@@ -98,7 +102,7 @@ export class WaveAcpAgent implements AcpAgent {
98
102
 
99
103
  private getSessionConfigOptions(agent: WaveAgent): SessionConfigOption[] {
100
104
  const configuredModels = agent.getConfiguredModels();
101
- const currentModel = agent.getModelConfig().model;
105
+ const currentModel = agent.getModelConfig().model || "";
102
106
 
103
107
  return [
104
108
  {
@@ -262,6 +266,9 @@ export class WaveAcpAgent implements AcpAgent {
262
266
  logger.info(`Loading session: ${sessionId} in ${cwd}`);
263
267
  const agent = await this.createAgent(sessionId, cwd, mcpServers);
264
268
 
269
+ // Replay conversation history via session/update notifications per ACP spec
270
+ await this.replayConversationHistory(agent);
271
+
265
272
  return {
266
273
  modes: this.getSessionModeState(agent),
267
274
  configOptions: this.getSessionConfigOptions(agent),
@@ -799,6 +806,135 @@ export class WaveAcpAgent implements AcpAgent {
799
806
  }
800
807
  }
801
808
 
809
+ private async replayConversationHistory(agent: WaveAgent): Promise<void> {
810
+ const sessionId = agent.sessionId as AcpSessionId;
811
+
812
+ let history: Message[];
813
+ try {
814
+ const thread = await agent.getFullMessageThread();
815
+ history = thread.messages;
816
+ } catch {
817
+ // Fallback to in-memory messages if full thread fails
818
+ history = agent.messages;
819
+ }
820
+
821
+ for (const message of history) {
822
+ if (message.isMeta) continue;
823
+
824
+ const messageId = message.id;
825
+
826
+ for (const block of message.blocks) {
827
+ if (block.type === "text") {
828
+ const textBlock = block as TextBlock;
829
+ const update =
830
+ message.role === "user"
831
+ ? {
832
+ sessionUpdate: "user_message_chunk" as const,
833
+ content: { type: "text" as const, text: textBlock.content },
834
+ messageId,
835
+ }
836
+ : {
837
+ sessionUpdate: "agent_message_chunk" as const,
838
+ content: { type: "text" as const, text: textBlock.content },
839
+ messageId,
840
+ };
841
+ this.connection.sessionUpdate({ sessionId, update });
842
+ } else if (block.type === "reasoning") {
843
+ const reasoningBlock = block as ReasoningBlock;
844
+ this.connection.sessionUpdate({
845
+ sessionId,
846
+ update: {
847
+ sessionUpdate: "agent_thought_chunk",
848
+ content: { type: "text", text: reasoningBlock.content },
849
+ messageId,
850
+ },
851
+ });
852
+ } else if (block.type === "tool") {
853
+ const toolBlock = block as ToolBlock;
854
+ const toolCallId =
855
+ toolBlock.id ||
856
+ "replay-" + Math.random().toString(36).substring(2, 9);
857
+ const effectiveName = toolBlock.name || "Tool";
858
+ const effectiveCompactParams = toolBlock.compactParams;
859
+
860
+ const displayTitle =
861
+ effectiveName && effectiveCompactParams
862
+ ? `${effectiveName}: ${effectiveCompactParams}`
863
+ : effectiveName;
864
+
865
+ let parsedParameters: Record<string, unknown> | undefined;
866
+ if (toolBlock.parameters) {
867
+ try {
868
+ const parsed = JSON.parse(toolBlock.parameters);
869
+ parsedParameters = Array.isArray(parsed)
870
+ ? { args: parsed }
871
+ : parsed;
872
+ } catch {
873
+ // Ignore parse errors
874
+ }
875
+ }
876
+
877
+ const content =
878
+ effectiveName && (parsedParameters || toolBlock.shortResult)
879
+ ? this.getToolContent(
880
+ effectiveName,
881
+ parsedParameters,
882
+ toolBlock.shortResult,
883
+ )
884
+ : undefined;
885
+ const locations =
886
+ effectiveName && parsedParameters
887
+ ? this.getToolLocations(effectiveName, parsedParameters)
888
+ : undefined;
889
+ const kind = effectiveName
890
+ ? this.getToolKind(effectiveName)
891
+ : undefined;
892
+
893
+ // Emit tool_call (creation)
894
+ this.connection.sessionUpdate({
895
+ sessionId,
896
+ update: {
897
+ sessionUpdate: "tool_call",
898
+ toolCallId,
899
+ title: displayTitle,
900
+ status: "pending",
901
+ content,
902
+ locations,
903
+ kind,
904
+ rawInput: parsedParameters,
905
+ },
906
+ });
907
+
908
+ // Emit tool_call_update with final status
909
+ const status: ToolCallStatus =
910
+ toolBlock.stage === "end"
911
+ ? toolBlock.success
912
+ ? "completed"
913
+ : "failed"
914
+ : toolBlock.stage === "running"
915
+ ? "in_progress"
916
+ : "pending";
917
+
918
+ this.connection.sessionUpdate({
919
+ sessionId,
920
+ update: {
921
+ sessionUpdate: "tool_call_update",
922
+ toolCallId,
923
+ status,
924
+ title: displayTitle,
925
+ rawOutput: toolBlock.result || toolBlock.error,
926
+ content,
927
+ locations,
928
+ kind,
929
+ rawInput: parsedParameters,
930
+ },
931
+ });
932
+ }
933
+ // Skip: image, bang, compact, error, file_history, task_notification blocks
934
+ }
935
+ }
936
+ }
937
+
802
938
  private createCallbacks(sessionId: string): AgentOptions["callbacks"] {
803
939
  const getAgent = () => this.agents.get(sessionId);
804
940
  const toolStates = new Map<
@@ -37,6 +37,9 @@ export const ChatInterface: React.FC = () => {
37
37
  workdir,
38
38
  remountKey,
39
39
  requestRemount,
40
+ isGoalActive,
41
+ goalElapsed,
42
+ isGoalEvaluating,
40
43
  } = useChat();
41
44
 
42
45
  const displayMessages = messages;
@@ -80,11 +83,15 @@ export const ChatInterface: React.FC = () => {
80
83
 
81
84
  {!isConfirmationVisible && !isExpanded && (
82
85
  <>
83
- {(isLoading || isCommandRunning || isCompacting) && (
86
+ {(isLoading ||
87
+ isCommandRunning ||
88
+ isCompacting ||
89
+ isGoalEvaluating) && (
84
90
  <LoadingIndicator
85
91
  isLoading={isLoading}
86
92
  isCommandRunning={isCommandRunning}
87
93
  isCompacting={isCompacting}
94
+ isGoalEvaluating={isGoalEvaluating}
88
95
  latestTotalTokens={latestTotalTokens}
89
96
  />
90
97
  )}
@@ -102,6 +109,8 @@ export const ChatInterface: React.FC = () => {
102
109
  hasSlashCommand={hasSlashCommand}
103
110
  latestTotalTokens={latestTotalTokens}
104
111
  maxInputTokens={maxInputTokens}
112
+ isGoalActive={isGoalActive}
113
+ goalElapsed={goalElapsed}
105
114
  />
106
115
  </>
107
116
  )}
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from "react";
1
+ import React, { useEffect, useRef, useCallback } from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import { useInput } from "ink";
4
4
  import { FileSelector } from "./FileSelector.js";
@@ -12,6 +12,7 @@ import { StatusCommand } from "./StatusCommand.js";
12
12
  import { LoginCommand } from "./LoginCommand.js";
13
13
  import { PluginManagerShell } from "./PluginManagerShell.js";
14
14
  import { ModelSelector } from "./ModelSelector.js";
15
+ import { WorkflowManager } from "./WorkflowManager.js";
15
16
  import { StatusLine } from "./StatusLine.js";
16
17
  import { BtwDisplay } from "./BtwDisplay.js";
17
18
  import { useInputManager } from "../hooks/useInputManager.js";
@@ -47,6 +48,9 @@ export interface InputBoxProps {
47
48
  // Token usage
48
49
  latestTotalTokens?: number;
49
50
  maxInputTokens?: number;
51
+ // Goal state
52
+ isGoalActive?: boolean;
53
+ goalElapsed?: string;
50
54
  }
51
55
 
52
56
  export const InputBox: React.FC<InputBoxProps> = ({
@@ -59,6 +63,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
59
63
  hasSlashCommand = () => false,
60
64
  latestTotalTokens = 0,
61
65
  maxInputTokens = 128000,
66
+ isGoalActive,
67
+ goalElapsed,
62
68
  }) => {
63
69
  const {
64
70
  permissionMode: chatPermissionMode,
@@ -74,8 +80,23 @@ export const InputBox: React.FC<InputBoxProps> = ({
74
80
  configuredModels,
75
81
  setModel,
76
82
  recreateAgent,
83
+ recallQueuedMessage,
84
+ queuedMessages,
77
85
  } = useChat();
78
86
 
87
+ // Ref to hold setInputText so queue callbacks can access it before useInputManager returns
88
+ const setInputTextRef = useRef<(text: string) => void>(() => {});
89
+
90
+ const hasQueuedMessages = (queuedMessages?.length ?? 0) > 0;
91
+
92
+ const onRecallQueuedMessage = useCallback(() => {
93
+ const msg = recallQueuedMessage();
94
+ if (msg) {
95
+ const prefix = msg.type === "bang" ? "!" : "";
96
+ setInputTextRef.current(prefix + msg.content);
97
+ }
98
+ }, [recallQueuedMessage]);
99
+
79
100
  // Input manager with all input state and functionality (including images)
80
101
  const {
81
102
  inputText,
@@ -110,6 +131,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
110
131
  showLoginCommand,
111
132
  showPluginManager,
112
133
  showModelSelector,
134
+ showWorkflowManager,
113
135
  setShowBackgroundTaskManager,
114
136
  setShowMcpManager,
115
137
  setShowRewindManager,
@@ -118,6 +140,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
118
140
  setShowLoginCommand,
119
141
  setShowPluginManager,
120
142
  setShowModelSelector,
143
+ setShowWorkflowManager,
121
144
  // Permission mode
122
145
  permissionMode,
123
146
  setPermissionMode,
@@ -127,6 +150,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
127
150
  handleInput,
128
151
  // Manager ready state
129
152
  isManagerReady,
153
+ // Direct state setters
154
+ setInputText,
130
155
  } = useInputManager({
131
156
  onSendMessage: sendMessage,
132
157
  onAskBtw: askBtw,
@@ -137,8 +162,15 @@ export const InputBox: React.FC<InputBoxProps> = ({
137
162
  sessionId,
138
163
  workdir: workingDirectory,
139
164
  getFullMessageThread,
165
+ hasQueuedMessages,
166
+ onRecallQueuedMessage,
140
167
  });
141
168
 
169
+ // Keep setInputText ref updated for queue callbacks
170
+ useEffect(() => {
171
+ setInputTextRef.current = setInputText;
172
+ }, [setInputText]);
173
+
142
174
  // Sync permission mode from useChat to InputManager
143
175
  useEffect(() => {
144
176
  setPermissionMode(chatPermissionMode);
@@ -157,7 +189,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
157
189
  showPluginManager ||
158
190
  showModelSelector ||
159
191
  showBackgroundTaskManager ||
160
- showMcpManager
192
+ showMcpManager ||
193
+ showWorkflowManager
161
194
  ) {
162
195
  return;
163
196
  }
@@ -291,13 +324,18 @@ export const InputBox: React.FC<InputBoxProps> = ({
291
324
  />
292
325
  )}
293
326
 
327
+ {showWorkflowManager && (
328
+ <WorkflowManager onCancel={() => setShowWorkflowManager(false)} />
329
+ )}
330
+
294
331
  {showBackgroundTaskManager ||
295
332
  showMcpManager ||
296
333
  showRewindManager ||
297
334
  showHelp ||
298
335
  showStatusCommand ||
299
336
  showLoginCommand ||
300
- showPluginManager || (
337
+ showPluginManager ||
338
+ showWorkflowManager || (
301
339
  <Box flexDirection="column">
302
340
  <Box
303
341
  borderStyle="single"
@@ -330,6 +368,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
330
368
  permissionMode={permissionMode}
331
369
  isShellCommand={isShellCommand}
332
370
  isBtwActive={btwState.isActive}
371
+ isGoalActive={isGoalActive}
372
+ goalElapsed={goalElapsed}
333
373
  latestTotalTokens={latestTotalTokens}
334
374
  maxInputTokens={maxInputTokens}
335
375
  />
@@ -5,6 +5,7 @@ export interface LoadingIndicatorProps {
5
5
  isLoading?: boolean;
6
6
  isCommandRunning?: boolean;
7
7
  isCompacting?: boolean;
8
+ isGoalEvaluating?: boolean;
8
9
  latestTotalTokens?: number;
9
10
  }
10
11
 
@@ -12,11 +13,12 @@ export const LoadingIndicator = ({
12
13
  isLoading = false,
13
14
  isCommandRunning = false,
14
15
  isCompacting = false,
16
+ isGoalEvaluating = false,
15
17
  latestTotalTokens = 0,
16
18
  }: LoadingIndicatorProps) => {
17
19
  return (
18
20
  <Box flexDirection="column">
19
- {isLoading && !isCompacting && (
21
+ {isLoading && !isCompacting && !isGoalEvaluating && (
20
22
  <Box>
21
23
  <Text color="yellow">✻ AI is thinking... </Text>
22
24
  {latestTotalTokens > 0 && (
@@ -49,6 +51,7 @@ export const LoadingIndicator = ({
49
51
  {isCompacting && (
50
52
  <Text color="magenta">✻ Compacting message history...</Text>
51
53
  )}
54
+ {isGoalEvaluating && <Text color="cyan">✻ Evaluating goal...</Text>}
52
55
  </Box>
53
56
  );
54
57
  };
@@ -18,15 +18,19 @@ export const QueuedMessageList: React.FC = () => {
18
18
  const displayText = prefix + (content || (hasImages ? "[Images]" : ""));
19
19
 
20
20
  return (
21
- <Box key={index}>
21
+ <Box key={msg.id ?? index}>
22
+ <Text color="gray">[{index + 1}] </Text>
22
23
  <Text color="gray" italic>
23
- {displayText.length > 60
24
- ? `${displayText.substring(0, 57)}...`
24
+ {displayText.length > 55
25
+ ? `${displayText.substring(0, 52)}...`
25
26
  : displayText}
26
27
  </Text>
27
28
  </Box>
28
29
  );
29
30
  })}
31
+ <Text color="gray" dimColor>
32
+ {" "}↑ recall
33
+ </Text>
30
34
  </Box>
31
35
  );
32
36
  };
@@ -5,6 +5,8 @@ export interface StatusLineProps {
5
5
  permissionMode: string;
6
6
  isShellCommand: boolean;
7
7
  isBtwActive: boolean;
8
+ isGoalActive?: boolean;
9
+ goalElapsed?: string;
8
10
  latestTotalTokens?: number;
9
11
  maxInputTokens?: number;
10
12
  }
@@ -13,6 +15,8 @@ export const StatusLine: React.FC<StatusLineProps> = ({
13
15
  permissionMode,
14
16
  isShellCommand,
15
17
  isBtwActive,
18
+ isGoalActive,
19
+ goalElapsed,
16
20
  latestTotalTokens = 0,
17
21
  maxInputTokens = 128000,
18
22
  }) => {
@@ -35,22 +39,30 @@ export const StatusLine: React.FC<StatusLineProps> = ({
35
39
  Shell: <Text color="yellow">Run shell command</Text>
36
40
  </Text>
37
41
  ) : (
38
- <Text color="gray">
39
- Mode:{" "}
40
- <Text
41
- color={
42
- permissionMode === "plan"
43
- ? "yellow"
44
- : permissionMode === "bypassPermissions"
45
- ? "red"
46
- : "cyan"
47
- }
48
- bold={permissionMode === "bypassPermissions"}
49
- >
50
- {permissionMode}
51
- </Text>{" "}
52
- (Shift+Tab to cycle)
53
- </Text>
42
+ <Box>
43
+ {isGoalActive && (
44
+ <Text color="gray">
45
+ <Text color="cyan">◎ /goal</Text> active{" "}
46
+ {goalElapsed && <Text>({goalElapsed})</Text>} |{" "}
47
+ </Text>
48
+ )}
49
+ <Text color="gray">
50
+ Mode:{" "}
51
+ <Text
52
+ color={
53
+ permissionMode === "plan"
54
+ ? "yellow"
55
+ : permissionMode === "bypassPermissions"
56
+ ? "red"
57
+ : "cyan"
58
+ }
59
+ bold={permissionMode === "bypassPermissions"}
60
+ >
61
+ {permissionMode}
62
+ </Text>{" "}
63
+ (Shift+Tab to cycle)
64
+ </Text>
65
+ </Box>
54
66
  )}
55
67
  {percentage > 0 && (
56
68
  <Text color={contextColor}>{percentage}% context</Text>