wave-code 1.0.0 → 1.0.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 (72) hide show
  1. package/dist/cli.js +20 -1
  2. package/dist/components/App.js +7 -0
  3. package/dist/components/BtwDisplay.js +13 -3
  4. package/dist/components/ChatInterface.js +25 -8
  5. package/dist/components/InputBox.d.ts +1 -3
  6. package/dist/components/InputBox.js +12 -9
  7. package/dist/components/LoadingIndicator.d.ts +1 -2
  8. package/dist/components/LoadingIndicator.js +2 -2
  9. package/dist/components/LoginCommand.js +4 -2
  10. package/dist/components/Markdown.js +13 -16
  11. package/dist/components/Notifications.d.ts +7 -0
  12. package/dist/components/Notifications.js +9 -0
  13. package/dist/components/StatusLine.d.ts +0 -4
  14. package/dist/components/StatusLine.js +6 -10
  15. package/dist/components/TaskList.js +2 -1
  16. package/dist/components/ToolDisplay.d.ts +1 -0
  17. package/dist/components/ToolDisplay.js +17 -9
  18. package/dist/constants/commands.js +0 -6
  19. package/dist/contexts/useChat.d.ts +4 -6
  20. package/dist/contexts/useChat.js +253 -110
  21. package/dist/daemon-cli.d.ts +10 -0
  22. package/dist/daemon-cli.js +15 -0
  23. package/dist/hooks/useInputManager.js +99 -22
  24. package/dist/index.js +10 -0
  25. package/dist/managers/inputHandlers.js +50 -22
  26. package/dist/managers/inputReducer.d.ts +12 -2
  27. package/dist/managers/inputReducer.js +57 -9
  28. package/dist/stdio/agentBridge.d.ts +23 -0
  29. package/dist/stdio/agentBridge.js +134 -16
  30. package/dist/stdio/daemonServer.d.ts +67 -0
  31. package/dist/stdio/daemonServer.js +191 -0
  32. package/dist/stdio/index.d.ts +2 -0
  33. package/dist/stdio/index.js +2 -0
  34. package/dist/stdio/jsonRpcConnection.d.ts +30 -0
  35. package/dist/stdio/jsonRpcConnection.js +127 -0
  36. package/dist/stdio/protocol.d.ts +2 -2
  37. package/dist/stdio/stdioServer.d.ts +2 -7
  38. package/dist/stdio/stdioServer.js +9 -100
  39. package/dist/utils/bracketedPaste.d.ts +39 -0
  40. package/dist/utils/bracketedPaste.js +122 -0
  41. package/dist/utils/markdownTable.d.ts +34 -0
  42. package/dist/utils/markdownTable.js +302 -0
  43. package/dist/utils/throttle.d.ts +3 -3
  44. package/package.json +4 -2
  45. package/src/cli.tsx +20 -1
  46. package/src/components/App.tsx +5 -0
  47. package/src/components/BtwDisplay.tsx +36 -12
  48. package/src/components/ChatInterface.tsx +30 -15
  49. package/src/components/InputBox.tsx +25 -24
  50. package/src/components/LoadingIndicator.tsx +1 -4
  51. package/src/components/LoginCommand.tsx +4 -2
  52. package/src/components/Markdown.tsx +15 -18
  53. package/src/components/Notifications.tsx +31 -0
  54. package/src/components/StatusLine.tsx +17 -44
  55. package/src/components/TaskList.tsx +2 -1
  56. package/src/components/ToolDisplay.tsx +17 -6
  57. package/src/constants/commands.ts +0 -6
  58. package/src/contexts/useChat.tsx +326 -140
  59. package/src/daemon-cli.ts +17 -0
  60. package/src/hooks/useInputManager.ts +108 -22
  61. package/src/index.ts +12 -0
  62. package/src/managers/inputHandlers.ts +49 -22
  63. package/src/managers/inputReducer.ts +66 -11
  64. package/src/stdio/agentBridge.ts +196 -17
  65. package/src/stdio/daemonServer.ts +212 -0
  66. package/src/stdio/index.ts +2 -0
  67. package/src/stdio/jsonRpcConnection.ts +160 -0
  68. package/src/stdio/protocol.ts +5 -2
  69. package/src/stdio/stdioServer.ts +14 -120
  70. package/src/utils/bracketedPaste.ts +170 -0
  71. package/src/utils/markdownTable.ts +359 -0
  72. package/src/utils/throttle.ts +8 -8
@@ -14,6 +14,7 @@ import { PluginManagerShell } from "./PluginManagerShell.js";
14
14
  import { ModelSelector } from "./ModelSelector.js";
15
15
  import { WorkflowManager } from "./WorkflowManager.js";
16
16
  import { StatusLine } from "./StatusLine.js";
17
+ import { Notifications } from "./Notifications.js";
17
18
  import { BtwDisplay } from "./BtwDisplay.js";
18
19
  import { useInputManager } from "../hooks/useInputManager.js";
19
20
  import { useChat } from "../contexts/useChat.js";
@@ -32,7 +33,6 @@ export interface InputBoxProps {
32
33
  isLoading?: boolean;
33
34
  isCommandRunning?: boolean;
34
35
  isCompacting?: boolean;
35
- isGoalEvaluating?: boolean;
36
36
  workdir?: string;
37
37
  sendMessage?: (
38
38
  message: string,
@@ -50,16 +50,14 @@ export interface InputBoxProps {
50
50
  // Token usage
51
51
  latestTotalTokens?: number;
52
52
  maxInputTokens?: number;
53
- // Goal state
54
- isGoalActive?: boolean;
55
- goalElapsed?: string;
53
+ // Login hint
54
+ showLoginHint?: boolean;
56
55
  }
57
56
 
58
57
  export const InputBox: React.FC<InputBoxProps> = ({
59
58
  isLoading,
60
59
  isCommandRunning,
61
60
  isCompacting,
62
- isGoalEvaluating,
63
61
  sendMessage = () => {},
64
62
  abortMessage = () => {},
65
63
  mcpServers = [],
@@ -69,8 +67,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
69
67
  hasSlashCommand = () => false,
70
68
  latestTotalTokens = 0,
71
69
  maxInputTokens = 200000,
72
- isGoalActive,
73
- goalElapsed,
70
+ showLoginHint = false,
74
71
  }) => {
75
72
  const {
76
73
  permissionMode: chatPermissionMode,
@@ -84,13 +81,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
84
81
  askBtw,
85
82
  clearMessages,
86
83
  compact,
87
- goalCommand,
88
84
  currentModel,
89
85
  configuredModels,
90
86
  setModel,
91
87
  recreateAgent,
92
88
  recallQueuedMessage,
93
89
  queuedMessages,
90
+ setIsBtwActive,
94
91
  } = useChat();
95
92
 
96
93
  // Ref to hold setInputText so queue callbacks can access it before useInputManager returns
@@ -100,12 +97,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
100
97
 
101
98
  // Idle means no AI work in flight. Esc double-press clear only applies when
102
99
  // idle; while busy, Esc keeps its abort semantics.
103
- const isIdle = !(
104
- isLoading ||
105
- isCommandRunning ||
106
- isCompacting ||
107
- isGoalEvaluating
108
- );
100
+ const isIdle = !(isLoading || isCommandRunning || isCompacting);
109
101
 
110
102
  const onRecallQueuedMessage = useCallback(() => {
111
103
  const msg = recallQueuedMessage();
@@ -174,7 +166,6 @@ export const InputBox: React.FC<InputBoxProps> = ({
174
166
  onAskBtw: askBtw,
175
167
  onClearMessages: clearMessages,
176
168
  onCompact: compact,
177
- onGoalCommand: goalCommand,
178
169
  onHasSlashCommand: hasSlashCommand,
179
170
  onAbortMessage: abortMessage,
180
171
  onBackgroundCurrentTask: backgroundCurrentTask,
@@ -192,6 +183,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
192
183
  setInputTextRef.current = setInputText;
193
184
  }, [setInputText]);
194
185
 
186
+ // Sync the btw overlay's visibility to ChatContext so siblings (TaskList)
187
+ // can hide while the side-question is on display (aligned with Claude Code,
188
+ // which suppresses the expanded task list while a local-jsx command shows).
189
+ useEffect(() => {
190
+ setIsBtwActive(btwState.question !== "" || btwState.answer !== undefined);
191
+ }, [btwState.question, btwState.answer, setIsBtwActive]);
192
+
195
193
  // Sync permission mode from useChat to InputManager
196
194
  useEffect(() => {
197
195
  setPermissionMode(chatPermissionMode);
@@ -349,7 +347,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
349
347
  <WorkflowManager onCancel={() => setShowWorkflowManager(false)} />
350
348
  )}
351
349
 
352
- {btwState.question
350
+ {btwState.question || btwState.answer
353
351
  ? null
354
352
  : showBackgroundTaskManager ||
355
353
  showMcpManager ||
@@ -381,14 +379,17 @@ export const InputBox: React.FC<InputBoxProps> = ({
381
379
  )}
382
380
  </Text>
383
381
  </Box>
384
- <StatusLine
385
- permissionMode={permissionMode}
386
- isShellCommand={isShellCommand}
387
- isGoalActive={isGoalActive}
388
- goalElapsed={goalElapsed}
389
- latestTotalTokens={latestTotalTokens}
390
- maxInputTokens={maxInputTokens}
391
- />
382
+ <Box justifyContent="space-between">
383
+ <StatusLine
384
+ permissionMode={permissionMode}
385
+ isShellCommand={isShellCommand}
386
+ />
387
+ <Notifications
388
+ latestTotalTokens={latestTotalTokens}
389
+ maxInputTokens={maxInputTokens}
390
+ showLoginHint={showLoginHint}
391
+ />
392
+ </Box>
392
393
  </Box>
393
394
  )}
394
395
  </Box>
@@ -5,7 +5,6 @@ export interface LoadingIndicatorProps {
5
5
  isLoading?: boolean;
6
6
  isCommandRunning?: boolean;
7
7
  isCompacting?: boolean;
8
- isGoalEvaluating?: boolean;
9
8
  latestTotalTokens?: number;
10
9
  }
11
10
 
@@ -13,12 +12,11 @@ export const LoadingIndicator = ({
13
12
  isLoading = false,
14
13
  isCommandRunning = false,
15
14
  isCompacting = false,
16
- isGoalEvaluating = false,
17
15
  latestTotalTokens = 0,
18
16
  }: LoadingIndicatorProps) => {
19
17
  return (
20
18
  <Box flexDirection="column">
21
- {isLoading && !isCompacting && !isGoalEvaluating && (
19
+ {isLoading && !isCompacting && (
22
20
  <Box>
23
21
  <Text color="yellow">✻ AI is thinking... </Text>
24
22
  {latestTotalTokens > 0 && (
@@ -51,7 +49,6 @@ export const LoadingIndicator = ({
51
49
  {isCompacting && (
52
50
  <Text color="magenta">✻ Compacting message history...</Text>
53
51
  )}
54
- {isGoalEvaluating && <Text color="cyan">✻ Evaluating goal...</Text>}
55
52
  </Box>
56
53
  );
57
54
  };
@@ -80,7 +80,9 @@ export const LoginCommand: React.FC<LoginCommandProps> = ({ onCancel }) => {
80
80
  const handleEnter = async () => {
81
81
  if (isLoadingRef.current) return;
82
82
 
83
- const isAuthenticated = authService.isSSOAuthenticated();
83
+ // A stale-but-present SSO token still counts as logged in; it refreshes
84
+ // lazily on the next API call. Enter toggles logout only when a token exists.
85
+ const isAuthenticated = Boolean(authService.getSSOToken());
84
86
  if (isAuthenticated) {
85
87
  await authService.clearAuth();
86
88
  setMessage("Logged out successfully");
@@ -123,7 +125,7 @@ export const LoginCommand: React.FC<LoginCommandProps> = ({ onCancel }) => {
123
125
  }
124
126
  };
125
127
 
126
- const isAuthenticated = authService.isSSOAuthenticated();
128
+ const isAuthenticated = Boolean(authService.getSSOToken());
127
129
  const token = authService.getSSOToken();
128
130
  const serverUrl = authService.getServerUrl();
129
131
 
@@ -1,8 +1,9 @@
1
1
  import React, { useMemo } from "react";
2
- import { Box, Text } from "ink";
2
+ import { Box, Text, useStdout } from "ink";
3
3
  import { Renderer, marked, type Tokens } from "marked";
4
4
  import chalk from "chalk";
5
5
  import { highlightToAnsi } from "../utils/highlightUtils.js";
6
+ import { renderMarkdownTable } from "../utils/markdownTable.js";
6
7
 
7
8
  export interface MarkdownProps {
8
9
  children: string;
@@ -19,6 +20,10 @@ const unescapeHtml = (html: string) => {
19
20
  };
20
21
 
21
22
  class AnsiRenderer extends Renderer<string> {
23
+ constructor(private readonly columns: number) {
24
+ super();
25
+ }
26
+
22
27
  override code({ text, lang }: Tokens.Code): string {
23
28
  const prefix = lang ? `\`\`\`${lang}` : "```";
24
29
  const suffix = "```";
@@ -82,20 +87,10 @@ class AnsiRenderer extends Renderer<string> {
82
87
  }
83
88
 
84
89
  override table(token: Tokens.Table): string {
85
- const header = token.header.map((cell) => this.tablecell(cell)).join("");
86
- const body = token.rows
87
- .map((row) => row.map((cell) => this.tablecell(cell)).join("") + "\n")
88
- .join("");
89
- return `\n${header}\n${body}\n`;
90
- }
91
-
92
- override tablerow({ text }: Tokens.TableRow): string {
93
- return text + "\n";
94
- }
95
-
96
- override tablecell(token: Tokens.TableCell): string {
97
- const text = token.header ? chalk.bold(token.text) : token.text;
98
- return text + " | ";
90
+ const table = renderMarkdownTable(token, this.columns, (tokens) =>
91
+ this.parser.parseInline(tokens),
92
+ );
93
+ return `\n${table}\n`;
99
94
  }
100
95
 
101
96
  override strong({ tokens }: Tokens.Strong): string {
@@ -140,17 +135,19 @@ class AnsiRenderer extends Renderer<string> {
140
135
  }
141
136
  }
142
137
 
143
- const renderer = new AnsiRenderer();
138
+ const createRenderer = (columns: number) => new AnsiRenderer(columns);
144
139
 
145
140
  // Markdown component using custom ANSI renderer
146
141
  export const Markdown = React.memo(({ children }: MarkdownProps) => {
142
+ const { stdout } = useStdout();
143
+ const columns = stdout?.columns ?? 80;
147
144
  const ansiContent = useMemo(() => {
148
145
  return marked.parse(children, {
149
- renderer,
146
+ renderer: createRenderer(columns),
150
147
  gfm: true,
151
148
  breaks: true,
152
149
  }) as string;
153
- }, [children]);
150
+ }, [children, columns]);
154
151
 
155
152
  return (
156
153
  <Box flexDirection="column">
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ export interface NotificationsProps {
5
+ latestTotalTokens?: number;
6
+ maxInputTokens?: number;
7
+ showLoginHint?: boolean;
8
+ }
9
+
10
+ export const Notifications: React.FC<NotificationsProps> = ({
11
+ latestTotalTokens = 0,
12
+ maxInputTokens = 200000,
13
+ showLoginHint = false,
14
+ }) => {
15
+ const percentage =
16
+ latestTotalTokens > 0
17
+ ? Math.min(Math.round((latestTotalTokens / maxInputTokens) * 100), 100)
18
+ : 0;
19
+
20
+ const contextColor =
21
+ percentage > 95 ? "red" : percentage > 80 ? "yellow" : "gray";
22
+
23
+ return (
24
+ <Box gap={1}>
25
+ {showLoginHint && <Text color="gray">Type /login to authenticate</Text>}
26
+ {percentage > 0 && (
27
+ <Text color={contextColor}>{percentage}% context</Text>
28
+ )}
29
+ </Box>
30
+ );
31
+ };
@@ -4,62 +4,35 @@ import { Box, Text } from "ink";
4
4
  export interface StatusLineProps {
5
5
  permissionMode: string;
6
6
  isShellCommand: boolean;
7
- isGoalActive?: boolean;
8
- goalElapsed?: string;
9
- latestTotalTokens?: number;
10
- maxInputTokens?: number;
11
7
  }
12
8
 
13
9
  export const StatusLine: React.FC<StatusLineProps> = ({
14
10
  permissionMode,
15
11
  isShellCommand,
16
- isGoalActive,
17
- goalElapsed,
18
- latestTotalTokens = 0,
19
- maxInputTokens = 200000,
20
12
  }) => {
21
- const percentage =
22
- latestTotalTokens > 0
23
- ? Math.min(Math.round((latestTotalTokens / maxInputTokens) * 100), 100)
24
- : 0;
25
-
26
- const contextColor =
27
- percentage > 95 ? "red" : percentage > 80 ? "yellow" : "gray";
28
-
29
13
  return (
30
- <Box paddingRight={1} justifyContent="space-between" width="100%">
14
+ <Box>
31
15
  {isShellCommand ? (
32
16
  <Text color="gray">
33
17
  Shell: <Text color="yellow">Run shell command</Text>
34
18
  </Text>
35
19
  ) : (
36
- <Box>
37
- {isGoalActive && (
38
- <Text color="gray">
39
- <Text color="cyan">◎ /goal</Text> active{" "}
40
- {goalElapsed && <Text>({goalElapsed})</Text>} |{" "}
41
- </Text>
42
- )}
43
- <Text color="gray">
44
- Mode:{" "}
45
- <Text
46
- color={
47
- permissionMode === "plan"
48
- ? "yellow"
49
- : permissionMode === "bypassPermissions"
50
- ? "red"
51
- : "cyan"
52
- }
53
- bold={permissionMode === "bypassPermissions"}
54
- >
55
- {permissionMode}
56
- </Text>{" "}
57
- (Shift+Tab to cycle)
58
- </Text>
59
- </Box>
60
- )}
61
- {percentage > 0 && (
62
- <Text color={contextColor}>{percentage}% context</Text>
20
+ <Text color="gray">
21
+ Mode:{" "}
22
+ <Text
23
+ color={
24
+ permissionMode === "plan"
25
+ ? "yellow"
26
+ : permissionMode === "bypassPermissions"
27
+ ? "red"
28
+ : "cyan"
29
+ }
30
+ bold={permissionMode === "bypassPermissions"}
31
+ >
32
+ {permissionMode}
33
+ </Text>{" "}
34
+ (Shift+Tab to cycle)
35
+ </Text>
63
36
  )}
64
37
  </Box>
65
38
  );
@@ -91,7 +91,7 @@ function getDisplayLimit(rows: number | undefined): number {
91
91
 
92
92
  export const TaskList: React.FC = () => {
93
93
  const tasks = useTasks();
94
- const { isTaskListVisible } = useChat();
94
+ const { isTaskListVisible, isBtwActive } = useChat();
95
95
  const { stdout } = useStdout();
96
96
 
97
97
  const completionTimestampsRef = React.useRef<Map<string, number>>(new Map());
@@ -188,6 +188,7 @@ export const TaskList: React.FC = () => {
188
188
  if (
189
189
  tasks.length === 0 ||
190
190
  !isTaskListVisible ||
191
+ isBtwActive ||
191
192
  autoHidden ||
192
193
  (allCompleted && !hadIncompleteRef.current)
193
194
  ) {
@@ -9,6 +9,22 @@ interface ToolDisplayProps {
9
9
  isExpanded?: boolean;
10
10
  }
11
11
 
12
+ // Status dot color for a tool block. In-flight stages (start/streaming/running)
13
+ // never show red — the outcome isn't known until the tool reaches "end".
14
+ // An explicit error overrides everything.
15
+ export const getToolStatusColor = (
16
+ stage: ToolBlock["stage"],
17
+ success?: boolean,
18
+ error?: string | Error,
19
+ ): string => {
20
+ if (error) return "red";
21
+ if (stage === "start" || stage === "streaming") return "gray";
22
+ if (stage === "running") return "yellow";
23
+ if (success) return "green";
24
+ if (success === false) return "red";
25
+ return "gray"; // Unknown state or no state information
26
+ };
27
+
12
28
  export const ToolDisplay: React.FC<ToolDisplayProps> = ({
13
29
  block,
14
30
  isExpanded = false,
@@ -19,12 +35,7 @@ export const ToolDisplay: React.FC<ToolDisplayProps> = ({
19
35
  // Directly use compactParams
20
36
  // (no change needed as we destructured it above)
21
37
 
22
- const getStatusColor = () => {
23
- if (stage === "running") return "yellow";
24
- if (success) return "green";
25
- if (error || success === false) return "red";
26
- return "gray"; // Unknown state or no state information
27
- };
38
+ const getStatusColor = () => getToolStatusColor(stage, success, error);
28
39
 
29
40
  const hasImages = () => {
30
41
  return block.images && block.images.length > 0;
@@ -80,10 +80,4 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
80
80
  description: "Compact conversation history to reduce context usage",
81
81
  handler: () => {},
82
82
  },
83
- {
84
- id: "goal",
85
- name: "goal",
86
- description: "Set, check, or clear an autonomous goal for the session",
87
- handler: () => {},
88
- },
89
83
  ];