wave-code 0.0.8 → 0.0.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ChatInterface.d.ts","sourceRoot":"","sources":["../../src/components/ChatInterface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAgEjC,CAAC"}
1
+ {"version":3,"file":"ChatInterface.d.ts","sourceRoot":"","sources":["../../src/components/ChatInterface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAkEjC,CAAC"}
@@ -9,5 +9,5 @@ export const ChatInterface = () => {
9
9
  if (!sessionId)
10
10
  return null;
11
11
  return (_jsxs(Box, { flexDirection: "column", height: "100%", paddingY: 1, children: [_jsx(MessageList, { messages: messages, isLoading: isLoading, isCommandRunning: isCommandRunning, isCompressing: isCompressing, latestTotalTokens: latestTotalTokens, isExpanded: isExpanded }, String(isExpanded) + sessionId), !isExpanded &&
12
- (isConfirmationVisible ? (_jsx(Confirmation, { toolName: confirmingTool.name, toolInput: confirmingTool.input, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel, onAbort: abortMessage })) : (_jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, userInputHistory: userInputHistory, sendMessage: sendMessage, abortMessage: abortMessage, saveMemory: saveMemory, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand })))] }));
12
+ (isConfirmationVisible ? (_jsx(Confirmation, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel, onAbort: abortMessage })) : (_jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, userInputHistory: userInputHistory, sendMessage: sendMessage, abortMessage: abortMessage, saveMemory: saveMemory, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand })))] }));
13
13
  };
@@ -3,6 +3,8 @@ import type { PermissionDecision } from "wave-agent-sdk";
3
3
  export interface ConfirmationProps {
4
4
  toolName: string;
5
5
  toolInput?: Record<string, unknown>;
6
+ suggestedPrefix?: string;
7
+ hidePersistentOption?: boolean;
6
8
  onDecision: (decision: PermissionDecision) => void;
7
9
  onCancel: () => void;
8
10
  onAbort: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"Confirmation.d.ts","sourceRoot":"","sources":["../../src/components/Confirmation.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAExC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AA2BzD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,UAAU,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACnD,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAQD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAiNpD,CAAC"}
1
+ {"version":3,"file":"Confirmation.d.ts","sourceRoot":"","sources":["../../src/components/Confirmation.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAExC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AA2BzD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACnD,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAQD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAgPpD,CAAC"}
@@ -21,15 +21,18 @@ const getActionDescription = (toolName, toolInput) => {
21
21
  return "Execute operation";
22
22
  }
23
23
  };
24
- export const Confirmation = ({ toolName, toolInput, onDecision, onCancel, onAbort, }) => {
24
+ export const Confirmation = ({ toolName, toolInput, suggestedPrefix, hidePersistentOption, onDecision, onCancel, onAbort, }) => {
25
25
  const [state, setState] = useState({
26
26
  selectedOption: "allow",
27
- alternativeText: "",
28
- hasUserInput: false,
27
+ alternativeText: suggestedPrefix || "",
28
+ hasUserInput: !!suggestedPrefix,
29
29
  });
30
30
  const getAutoOptionText = () => {
31
31
  if (toolName === "Bash") {
32
- return `Yes, and don't ask again for ${toolInput?.command || "this"} commands in this workdir`;
32
+ if (suggestedPrefix) {
33
+ return `Yes, and don't ask again for: ${suggestedPrefix}`;
34
+ }
35
+ return "Yes, and don't ask again for this command in this workdir";
33
36
  }
34
37
  return "Yes, and auto-accept edits";
35
38
  };
@@ -47,9 +50,12 @@ export const Confirmation = ({ toolName, toolInput, onDecision, onCancel, onAbor
47
50
  }
48
51
  else if (state.selectedOption === "auto") {
49
52
  if (toolName === "Bash") {
53
+ const rule = suggestedPrefix
54
+ ? `Bash(${suggestedPrefix}:*)`
55
+ : `Bash(${toolInput?.command})`;
50
56
  onDecision({
51
57
  behavior: "allow",
52
- newPermissionRule: `Bash(${toolInput?.command})`,
58
+ newPermissionRule: rule,
53
59
  });
54
60
  }
55
61
  else {
@@ -77,21 +83,31 @@ export const Confirmation = ({ toolName, toolInput, onDecision, onCancel, onAbor
77
83
  return;
78
84
  }
79
85
  if (input === "2") {
80
- if (toolName === "Bash") {
81
- onDecision({
82
- behavior: "allow",
83
- newPermissionRule: `Bash(${toolInput?.command})`,
84
- });
86
+ if (!hidePersistentOption) {
87
+ if (toolName === "Bash") {
88
+ const rule = suggestedPrefix
89
+ ? `Bash(${suggestedPrefix}:*)`
90
+ : `Bash(${toolInput?.command})`;
91
+ onDecision({
92
+ behavior: "allow",
93
+ newPermissionRule: rule,
94
+ });
95
+ }
96
+ else {
97
+ onDecision({
98
+ behavior: "allow",
99
+ newPermissionMode: "acceptEdits",
100
+ });
101
+ }
102
+ return;
85
103
  }
86
104
  else {
87
- onDecision({
88
- behavior: "allow",
89
- newPermissionMode: "acceptEdits",
90
- });
105
+ // If auto option is hidden, '2' selects alternative
106
+ setState((prev) => ({ ...prev, selectedOption: "alternative" }));
107
+ return;
91
108
  }
92
- return;
93
109
  }
94
- if (input === "3") {
110
+ if (input === "3" && !hidePersistentOption) {
95
111
  setState((prev) => ({ ...prev, selectedOption: "alternative" }));
96
112
  return;
97
113
  }
@@ -99,8 +115,12 @@ export const Confirmation = ({ toolName, toolInput, onDecision, onCancel, onAbor
99
115
  // Handle arrow keys for navigation
100
116
  if (key.upArrow) {
101
117
  setState((prev) => {
102
- if (prev.selectedOption === "alternative")
103
- return { ...prev, selectedOption: "auto" };
118
+ if (prev.selectedOption === "alternative") {
119
+ return {
120
+ ...prev,
121
+ selectedOption: hidePersistentOption ? "allow" : "auto",
122
+ };
123
+ }
104
124
  if (prev.selectedOption === "auto")
105
125
  return { ...prev, selectedOption: "allow" };
106
126
  return prev;
@@ -109,8 +129,12 @@ export const Confirmation = ({ toolName, toolInput, onDecision, onCancel, onAbor
109
129
  }
110
130
  if (key.downArrow) {
111
131
  setState((prev) => {
112
- if (prev.selectedOption === "allow")
113
- return { ...prev, selectedOption: "auto" };
132
+ if (prev.selectedOption === "allow") {
133
+ return {
134
+ ...prev,
135
+ selectedOption: hidePersistentOption ? "alternative" : "auto",
136
+ };
137
+ }
114
138
  if (prev.selectedOption === "auto")
115
139
  return { ...prev, selectedOption: "alternative" };
116
140
  return prev;
@@ -143,6 +167,6 @@ export const Confirmation = ({ toolName, toolInput, onDecision, onCancel, onAbor
143
167
  });
144
168
  const placeholderText = "Type here to tell Wave what to do differently";
145
169
  const showPlaceholder = state.selectedOption === "alternative" && !state.hasUserInput;
146
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "yellow", padding: 1, marginBottom: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Tool: ", toolName] }), _jsx(Text, { color: "yellow", children: getActionDescription(toolName, toolInput) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Do you want to proceed?" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "allow" ? "black" : "white", backgroundColor: state.selectedOption === "allow" ? "yellow" : undefined, bold: state.selectedOption === "allow", children: [state.selectedOption === "allow" ? "> " : " ", "1. Yes"] }) }, "allow-option"), _jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "auto" ? "black" : "white", backgroundColor: state.selectedOption === "auto" ? "yellow" : undefined, bold: state.selectedOption === "auto", children: [state.selectedOption === "auto" ? "> " : " ", "2.", " ", getAutoOptionText()] }) }, "auto-option"), _jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "alternative" ? "black" : "white", backgroundColor: state.selectedOption === "alternative" ? "yellow" : undefined, bold: state.selectedOption === "alternative", children: [state.selectedOption === "alternative" ? "> " : " ", "3.", " ", showPlaceholder ? (_jsx(Text, { color: "gray", dimColor: true, children: placeholderText })) : (_jsx(Text, { children: state.alternativeText ||
147
- "Type here to tell Wave what to do differently" }))] }) }, "alternative-option")] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Use \u2191\u2193 or 1-3 to navigate \u2022 ESC to cancel" }) })] }));
170
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "yellow", padding: 1, marginBottom: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Tool: ", toolName] }), _jsx(Text, { color: "yellow", children: getActionDescription(toolName, toolInput) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Do you want to proceed?" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "allow" ? "black" : "white", backgroundColor: state.selectedOption === "allow" ? "yellow" : undefined, bold: state.selectedOption === "allow", children: [state.selectedOption === "allow" ? "> " : " ", "1. Yes"] }) }, "allow-option"), !hidePersistentOption && (_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "auto" ? "black" : "white", backgroundColor: state.selectedOption === "auto" ? "yellow" : undefined, bold: state.selectedOption === "auto", children: [state.selectedOption === "auto" ? "> " : " ", "2.", " ", getAutoOptionText()] }) }, "auto-option")), _jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "alternative" ? "black" : "white", backgroundColor: state.selectedOption === "alternative" ? "yellow" : undefined, bold: state.selectedOption === "alternative", children: [state.selectedOption === "alternative" ? "> " : " ", hidePersistentOption ? "2. " : "3. ", showPlaceholder ? (_jsx(Text, { color: "gray", dimColor: true, children: placeholderText })) : (_jsx(Text, { children: state.alternativeText ||
171
+ "Type here to tell Wave what to do differently" }))] }) }, "alternative-option")] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Use \u2191\u2193 or 1-", hidePersistentOption ? "2" : "3", " to navigate \u2022 ESC to cancel"] }) })] }));
148
172
  };
@@ -1 +1 @@
1
- {"version":3,"file":"Markdown.d.ts","sourceRoot":"","sources":["../../src/components/Markdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AAKvC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAGD,eAAO,MAAM,QAAQ,2CAA6B,aAAa,6CAe7D,CAAC"}
1
+ {"version":3,"file":"Markdown.d.ts","sourceRoot":"","sources":["../../src/components/Markdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AAIvC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoUD,eAAO,MAAM,QAAQ,2CAA6B,aAAa,6CAQ7D,CAAC"}
@@ -1,22 +1,121 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useMemo } from "react";
3
- import { Text } from "ink";
3
+ import { Box, Text, useStdout } from "ink";
4
4
  import { marked } from "marked";
5
- import TerminalRenderer from "marked-terminal";
6
- // Markdown component using marked-terminal with proper unescape option
7
- export const Markdown = React.memo(({ children }) => {
8
- const result = useMemo(() => {
9
- // Configure marked with TerminalRenderer using default options
10
- marked.setOptions({
11
- renderer: new TerminalRenderer({
12
- // Use official unescape option to handle HTML entities
13
- unescape: true,
14
- }),
5
+ const unescapeHtml = (html) => {
6
+ return html
7
+ .replace(/&amp;/g, "&")
8
+ .replace(/&lt;/g, "<")
9
+ .replace(/&gt;/g, ">")
10
+ .replace(/&quot;/g, '"')
11
+ .replace(/&#39;/g, "'");
12
+ };
13
+ const InlineRenderer = ({ tokens }) => {
14
+ return (_jsx(_Fragment, { children: tokens.map((token, index) => {
15
+ switch (token.type) {
16
+ case "text": {
17
+ const t = token;
18
+ if (t.tokens) {
19
+ return _jsx(InlineRenderer, { tokens: t.tokens }, index);
20
+ }
21
+ return _jsx(Text, { children: unescapeHtml(t.text) }, index);
22
+ }
23
+ case "strong":
24
+ return (_jsx(Text, { bold: true, children: token.tokens ? (_jsx(InlineRenderer, { tokens: token.tokens })) : (unescapeHtml(token.text)) }, index));
25
+ case "em":
26
+ return (_jsx(Text, { italic: true, children: token.tokens ? (_jsx(InlineRenderer, { tokens: token.tokens })) : (unescapeHtml(token.text)) }, index));
27
+ case "codespan":
28
+ return (_jsx(Text, { color: "yellow", children: unescapeHtml(token.text) }, index));
29
+ case "link":
30
+ return (_jsx(Text, { color: "blue", underline: true, children: token.tokens ? (_jsx(InlineRenderer, { tokens: token.tokens })) : (unescapeHtml(token.text)) }, index));
31
+ case "br":
32
+ return _jsx(Text, { children: "\n" }, index);
33
+ case "del":
34
+ return (_jsx(Text, { strikethrough: true, children: token.tokens ? (_jsx(InlineRenderer, { tokens: token.tokens })) : (unescapeHtml(token.text)) }, index));
35
+ default:
36
+ return _jsx(Text, { children: token.raw }, index);
37
+ }
38
+ }) }));
39
+ };
40
+ const TableRenderer = ({ token }) => {
41
+ const { stdout } = useStdout();
42
+ const terminalWidth = (stdout?.columns || 80) - 2;
43
+ const columnWidths = useMemo(() => {
44
+ const numCols = token.header.length;
45
+ const minWidth = 5;
46
+ const maxColWidth = 40;
47
+ const widths = token.header.map((h) => Math.min(maxColWidth, Math.max(minWidth, h.text.length)));
48
+ token.rows.forEach((row) => {
49
+ row.forEach((cell, i) => {
50
+ widths[i] = Math.min(maxColWidth, Math.max(widths[i] || minWidth, cell.text.length));
51
+ });
15
52
  });
16
- const output = marked(children);
17
- return typeof output === "string" ? output.trim() : "";
18
- }, [children]);
19
- return _jsx(Text, { children: result });
53
+ const paddedWidths = widths.map((w) => w + 2);
54
+ const totalWidth = paddedWidths.reduce((a, b) => a + b, 0) + numCols + 1;
55
+ if (totalWidth <= terminalWidth) {
56
+ return paddedWidths;
57
+ }
58
+ // If table is too wide, scale down columns proportionally
59
+ const availableWidth = terminalWidth - numCols - 1;
60
+ const scaleFactor = availableWidth / (totalWidth - numCols - 1);
61
+ return paddedWidths.map((w) => Math.max(minWidth, Math.floor(w * scaleFactor)));
62
+ }, [token, terminalWidth]);
63
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: "gray", width: columnWidths.reduce((a, b) => a + b, 0) + token.header.length + 1, children: [_jsx(Box, { flexDirection: "row", borderStyle: "single", borderBottom: true, borderTop: false, borderLeft: false, borderRight: false, borderColor: "gray", children: token.header.map((cell, i) => (_jsx(Box, { width: columnWidths[i], paddingX: 1, borderStyle: "single", borderLeft: i > 0, borderRight: false, borderTop: false, borderBottom: false, borderColor: "gray", children: _jsx(Text, { bold: true, wrap: "wrap", children: _jsx(InlineRenderer, { tokens: cell.tokens }) }) }, i))) }), token.rows.map((row, rowIndex) => (_jsx(Box, { flexDirection: "row", children: row.map((cell, i) => (_jsx(Box, { width: columnWidths[i], paddingX: 1, borderStyle: "single", borderLeft: i > 0, borderRight: false, borderTop: false, borderBottom: false, borderColor: "gray", children: _jsx(Text, { wrap: "wrap", children: _jsx(InlineRenderer, { tokens: cell.tokens }) }) }, i))) }, rowIndex)))] }));
64
+ };
65
+ const BlockRenderer = ({ tokens }) => {
66
+ return (_jsx(_Fragment, { children: tokens.map((token, index) => {
67
+ switch (token.type) {
68
+ case "heading": {
69
+ const t = token;
70
+ return (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Text, { bold: true, color: "cyan", children: ["#".repeat(t.depth), " ", _jsx(InlineRenderer, { tokens: t.tokens })] }) }, index));
71
+ }
72
+ case "paragraph": {
73
+ const t = token;
74
+ return (_jsx(Box, { marginBottom: 1, flexDirection: "row", flexWrap: "wrap", children: _jsx(InlineRenderer, { tokens: t.tokens }) }, index));
75
+ }
76
+ case "code": {
77
+ const t = token;
78
+ if (t.lang !== undefined) {
79
+ const lines = token.raw.replace(/\n$/, "").split("\n");
80
+ const opening = lines[0];
81
+ const closing = lines[lines.length - 1];
82
+ const content = lines.slice(1, -1).join("\n");
83
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { color: "gray", children: opening }), content && _jsx(Text, { children: content }), _jsx(Text, { color: "gray", children: closing })] }, index));
84
+ }
85
+ return (_jsx(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: _jsx(Text, { children: t.text }) }, index));
86
+ }
87
+ case "list": {
88
+ const t = token;
89
+ return (_jsx(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2, children: t.items.map((item, i) => {
90
+ const start = t.start || 1;
91
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: t.ordered ? `${start + i}. ` : "• " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: item.tokens.map((itemToken, itemIndex) => {
92
+ if (itemToken.type === "text") {
93
+ const it = itemToken;
94
+ return (_jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: _jsx(InlineRenderer, { tokens: it.tokens || [itemToken] }) }, itemIndex));
95
+ }
96
+ return (_jsx(BlockRenderer, { tokens: [itemToken] }, itemIndex));
97
+ }) })] }, i));
98
+ }) }, index));
99
+ }
100
+ case "blockquote": {
101
+ const t = token;
102
+ return (_jsx(Box, { flexDirection: "column", paddingLeft: 2, borderStyle: "single", borderLeft: true, borderRight: false, borderTop: false, borderBottom: false, borderColor: "gray", marginBottom: 1, children: _jsx(BlockRenderer, { tokens: t.tokens }) }, index));
103
+ }
104
+ case "hr":
105
+ return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: "─".repeat(20) }) }, index));
106
+ case "table":
107
+ return _jsx(TableRenderer, { token: token }, index);
108
+ case "space":
109
+ return null;
110
+ default:
111
+ return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: token.raw }) }, index));
112
+ }
113
+ }) }));
114
+ };
115
+ // Markdown component using custom Ink-based renderer
116
+ export const Markdown = React.memo(({ children }) => {
117
+ const tokens = useMemo(() => marked.lexer(children), [children]);
118
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(BlockRenderer, { tokens: tokens }) }));
20
119
  });
21
120
  // Add display name for debugging
22
121
  Markdown.displayName = "Markdown";
@@ -34,8 +34,10 @@ export interface ChatContextType {
34
34
  confirmingTool?: {
35
35
  name: string;
36
36
  input?: Record<string, unknown>;
37
+ suggestedPrefix?: string;
38
+ hidePersistentOption?: boolean;
37
39
  };
38
- showConfirmation: (toolName: string, toolInput?: Record<string, unknown>) => Promise<PermissionDecision>;
40
+ showConfirmation: (toolName: string, toolInput?: Record<string, unknown>, suggestedPrefix?: string, hidePersistentOption?: boolean) => Promise<PermissionDecision>;
39
41
  hideConfirmation: () => void;
40
42
  handleConfirmationDecision: (decision: PermissionDecision) => void;
41
43
  handleConfirmationCancel: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"useChat.d.ts","sourceRoot":"","sources":["../../src/contexts/useChat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACf,MAAM,gBAAgB,CAAC;AAUxB,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAE3B,UAAU,EAAE,OAAO,CAAC;IAEpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,CACX,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,KAC/C,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAE1B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzE,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,gBAAgB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,mBAAmB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9D,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,wBAAwB,EAAE,CACxB,OAAO,EAAE,MAAM,KACZ;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/D,mBAAmB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAElD,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IAEhD,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAE5C,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAElD,qBAAqB,EAAE,OAAO,CAAC;IAC/B,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IACnE,gBAAgB,EAAE,CAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAChC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACjC,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,0BAA0B,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACnE,wBAAwB,EAAE,MAAM,IAAI,CAAC;CACtC;AAID,eAAO,MAAM,OAAO,uBAMnB,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAqZpD,CAAC"}
1
+ {"version":3,"file":"useChat.d.ts","sourceRoot":"","sources":["../../src/contexts/useChat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACf,MAAM,gBAAgB,CAAC;AAUxB,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAE3B,UAAU,EAAE,OAAO,CAAC;IAEpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,CACX,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,KAC/C,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAE1B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzE,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,gBAAgB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,mBAAmB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9D,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,wBAAwB,EAAE,CACxB,OAAO,EAAE,MAAM,KACZ;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/D,mBAAmB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAElD,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IAEhD,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAE5C,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAElD,qBAAqB,EAAE,OAAO,CAAC;IAC/B,cAAc,CAAC,EAAE;QACf,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;IACF,gBAAgB,EAAE,CAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,eAAe,CAAC,EAAE,MAAM,EACxB,oBAAoB,CAAC,EAAE,OAAO,KAC3B,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACjC,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,0BAA0B,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACnE,wBAAwB,EAAE,MAAM,IAAI,CAAC;CACtC;AAID,eAAO,MAAM,OAAO,uBAMnB,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA0apD,CAAC"}
@@ -42,11 +42,13 @@ export const ChatProvider = ({ children, bypassPermissions, }) => {
42
42
  const [currentConfirmation, setCurrentConfirmation] = useState(null);
43
43
  const agentRef = useRef(null);
44
44
  // Permission confirmation methods with queue support
45
- const showConfirmation = useCallback(async (toolName, toolInput) => {
45
+ const showConfirmation = useCallback(async (toolName, toolInput, suggestedPrefix, hidePersistentOption) => {
46
46
  return new Promise((resolve, reject) => {
47
47
  const queueItem = {
48
48
  toolName,
49
49
  toolInput,
50
+ suggestedPrefix,
51
+ hidePersistentOption,
50
52
  resolver: resolve,
51
53
  reject,
52
54
  };
@@ -98,7 +100,7 @@ export const ChatProvider = ({ children, bypassPermissions, }) => {
98
100
  ? undefined
99
101
  : async (context) => {
100
102
  try {
101
- return await showConfirmation(context.toolName, context.toolInput);
103
+ return await showConfirmation(context.toolName, context.toolInput, context.suggestedPrefix, context.hidePersistentOption);
102
104
  }
103
105
  catch {
104
106
  // If confirmation was cancelled or failed, deny the operation
@@ -261,7 +263,12 @@ export const ChatProvider = ({ children, bypassPermissions, }) => {
261
263
  if (confirmationQueue.length > 0 && !isConfirmationVisible) {
262
264
  const next = confirmationQueue[0];
263
265
  setCurrentConfirmation(next);
264
- setConfirmingTool({ name: next.toolName, input: next.toolInput });
266
+ setConfirmingTool({
267
+ name: next.toolName,
268
+ input: next.toolInput,
269
+ suggestedPrefix: next.suggestedPrefix,
270
+ hidePersistentOption: next.hidePersistentOption,
271
+ });
265
272
  setIsConfirmationVisible(true);
266
273
  setConfirmationQueue((prev) => prev.slice(1));
267
274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "keywords": [
6
6
  "ai",
@@ -29,13 +29,11 @@
29
29
  "glob": "^13.0.0",
30
30
  "ink": "^6.5.1",
31
31
  "marked": "^11.2.0",
32
- "marked-terminal": "^7.3.0",
33
32
  "react": "^19.1.0",
34
33
  "yargs": "^17.7.2",
35
- "wave-agent-sdk": "0.0.10"
34
+ "wave-agent-sdk": "0.0.11"
36
35
  },
37
36
  "devDependencies": {
38
- "@types/marked-terminal": "^6.1.1",
39
37
  "@types/react": "^19.1.8",
40
38
  "@types/yargs": "^17.0.0",
41
39
  "eslint-plugin-react": "^7.37.5",
@@ -60,6 +58,9 @@
60
58
  "watch": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --watch",
61
59
  "test": "vitest run",
62
60
  "lint": "eslint --cache",
63
- "format": "prettier --write ."
61
+ "format": "prettier --write .",
62
+ "version:patch": "node ../../scripts/version.js patch",
63
+ "version:minor": "node ../../scripts/version.js minor",
64
+ "version:major": "node ../../scripts/version.js major"
64
65
  }
65
66
  }
@@ -48,6 +48,8 @@ export const ChatInterface: React.FC = () => {
48
48
  <Confirmation
49
49
  toolName={confirmingTool!.name}
50
50
  toolInput={confirmingTool!.input}
51
+ suggestedPrefix={confirmingTool!.suggestedPrefix}
52
+ hidePersistentOption={confirmingTool!.hidePersistentOption}
51
53
  onDecision={handleConfirmationDecision}
52
54
  onCancel={handleConfirmationCancel}
53
55
  onAbort={abortMessage}
@@ -30,6 +30,8 @@ const getActionDescription = (
30
30
  export interface ConfirmationProps {
31
31
  toolName: string;
32
32
  toolInput?: Record<string, unknown>;
33
+ suggestedPrefix?: string;
34
+ hidePersistentOption?: boolean;
33
35
  onDecision: (decision: PermissionDecision) => void;
34
36
  onCancel: () => void;
35
37
  onAbort: () => void;
@@ -44,19 +46,24 @@ interface ConfirmationState {
44
46
  export const Confirmation: React.FC<ConfirmationProps> = ({
45
47
  toolName,
46
48
  toolInput,
49
+ suggestedPrefix,
50
+ hidePersistentOption,
47
51
  onDecision,
48
52
  onCancel,
49
53
  onAbort,
50
54
  }) => {
51
55
  const [state, setState] = useState<ConfirmationState>({
52
56
  selectedOption: "allow",
53
- alternativeText: "",
54
- hasUserInput: false,
57
+ alternativeText: suggestedPrefix || "",
58
+ hasUserInput: !!suggestedPrefix,
55
59
  });
56
60
 
57
61
  const getAutoOptionText = () => {
58
62
  if (toolName === "Bash") {
59
- return `Yes, and don't ask again for ${toolInput?.command || "this"} commands in this workdir`;
63
+ if (suggestedPrefix) {
64
+ return `Yes, and don't ask again for: ${suggestedPrefix}`;
65
+ }
66
+ return "Yes, and don't ask again for this command in this workdir";
60
67
  }
61
68
  return "Yes, and auto-accept edits";
62
69
  };
@@ -75,9 +82,12 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
75
82
  onDecision({ behavior: "allow" });
76
83
  } else if (state.selectedOption === "auto") {
77
84
  if (toolName === "Bash") {
85
+ const rule = suggestedPrefix
86
+ ? `Bash(${suggestedPrefix}:*)`
87
+ : `Bash(${toolInput?.command})`;
78
88
  onDecision({
79
89
  behavior: "allow",
80
- newPermissionRule: `Bash(${toolInput?.command})`,
90
+ newPermissionRule: rule,
81
91
  });
82
92
  } else {
83
93
  onDecision({
@@ -104,20 +114,29 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
104
114
  return;
105
115
  }
106
116
  if (input === "2") {
107
- if (toolName === "Bash") {
108
- onDecision({
109
- behavior: "allow",
110
- newPermissionRule: `Bash(${toolInput?.command})`,
111
- });
117
+ if (!hidePersistentOption) {
118
+ if (toolName === "Bash") {
119
+ const rule = suggestedPrefix
120
+ ? `Bash(${suggestedPrefix}:*)`
121
+ : `Bash(${toolInput?.command})`;
122
+ onDecision({
123
+ behavior: "allow",
124
+ newPermissionRule: rule,
125
+ });
126
+ } else {
127
+ onDecision({
128
+ behavior: "allow",
129
+ newPermissionMode: "acceptEdits",
130
+ });
131
+ }
132
+ return;
112
133
  } else {
113
- onDecision({
114
- behavior: "allow",
115
- newPermissionMode: "acceptEdits",
116
- });
134
+ // If auto option is hidden, '2' selects alternative
135
+ setState((prev) => ({ ...prev, selectedOption: "alternative" }));
136
+ return;
117
137
  }
118
- return;
119
138
  }
120
- if (input === "3") {
139
+ if (input === "3" && !hidePersistentOption) {
121
140
  setState((prev) => ({ ...prev, selectedOption: "alternative" }));
122
141
  return;
123
142
  }
@@ -126,8 +145,12 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
126
145
  // Handle arrow keys for navigation
127
146
  if (key.upArrow) {
128
147
  setState((prev) => {
129
- if (prev.selectedOption === "alternative")
130
- return { ...prev, selectedOption: "auto" };
148
+ if (prev.selectedOption === "alternative") {
149
+ return {
150
+ ...prev,
151
+ selectedOption: hidePersistentOption ? "allow" : "auto",
152
+ };
153
+ }
131
154
  if (prev.selectedOption === "auto")
132
155
  return { ...prev, selectedOption: "allow" };
133
156
  return prev;
@@ -137,8 +160,12 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
137
160
 
138
161
  if (key.downArrow) {
139
162
  setState((prev) => {
140
- if (prev.selectedOption === "allow")
141
- return { ...prev, selectedOption: "auto" };
163
+ if (prev.selectedOption === "allow") {
164
+ return {
165
+ ...prev,
166
+ selectedOption: hidePersistentOption ? "alternative" : "auto",
167
+ };
168
+ }
142
169
  if (prev.selectedOption === "auto")
143
170
  return { ...prev, selectedOption: "alternative" };
144
171
  return prev;
@@ -208,18 +235,20 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
208
235
  </Box>
209
236
 
210
237
  {/* Option 2: Auto-accept/Persistent */}
211
- <Box key="auto-option">
212
- <Text
213
- color={state.selectedOption === "auto" ? "black" : "white"}
214
- backgroundColor={
215
- state.selectedOption === "auto" ? "yellow" : undefined
216
- }
217
- bold={state.selectedOption === "auto"}
218
- >
219
- {state.selectedOption === "auto" ? "> " : " "}2.{" "}
220
- {getAutoOptionText()}
221
- </Text>
222
- </Box>
238
+ {!hidePersistentOption && (
239
+ <Box key="auto-option">
240
+ <Text
241
+ color={state.selectedOption === "auto" ? "black" : "white"}
242
+ backgroundColor={
243
+ state.selectedOption === "auto" ? "yellow" : undefined
244
+ }
245
+ bold={state.selectedOption === "auto"}
246
+ >
247
+ {state.selectedOption === "auto" ? "> " : " "}2.{" "}
248
+ {getAutoOptionText()}
249
+ </Text>
250
+ </Box>
251
+ )}
223
252
 
224
253
  {/* Option 3: Alternative */}
225
254
  <Box key="alternative-option">
@@ -230,7 +259,8 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
230
259
  }
231
260
  bold={state.selectedOption === "alternative"}
232
261
  >
233
- {state.selectedOption === "alternative" ? "> " : " "}3.{" "}
262
+ {state.selectedOption === "alternative" ? "> " : " "}
263
+ {hidePersistentOption ? "2. " : "3. "}
234
264
  {showPlaceholder ? (
235
265
  <Text color="gray" dimColor>
236
266
  {placeholderText}
@@ -246,7 +276,10 @@ export const Confirmation: React.FC<ConfirmationProps> = ({
246
276
  </Box>
247
277
 
248
278
  <Box marginTop={1}>
249
- <Text dimColor>Use ↑↓ or 1-3 to navigate • ESC to cancel</Text>
279
+ <Text dimColor>
280
+ Use ↑↓ or 1-{hidePersistentOption ? "2" : "3"} to navigate • ESC to
281
+ cancel
282
+ </Text>
250
283
  </Box>
251
284
  </Box>
252
285
  );
@@ -1,28 +1,341 @@
1
1
  import React, { useMemo } from "react";
2
- import { Text } from "ink";
3
- import { marked } from "marked";
4
- import TerminalRenderer from "marked-terminal";
2
+ import { Box, Text, useStdout } from "ink";
3
+ import { marked, type Token, type Tokens } from "marked";
5
4
 
6
5
  export interface MarkdownProps {
7
6
  children: string;
8
7
  }
9
8
 
10
- // Markdown component using marked-terminal with proper unescape option
11
- export const Markdown = React.memo(({ children }: MarkdownProps) => {
12
- const result = useMemo(() => {
13
- // Configure marked with TerminalRenderer using default options
14
- marked.setOptions({
15
- renderer: new TerminalRenderer({
16
- // Use official unescape option to handle HTML entities
17
- unescape: true,
18
- }),
9
+ const unescapeHtml = (html: string) => {
10
+ return html
11
+ .replace(/&amp;/g, "&")
12
+ .replace(/&lt;/g, "<")
13
+ .replace(/&gt;/g, ">")
14
+ .replace(/&quot;/g, '"')
15
+ .replace(/&#39;/g, "'");
16
+ };
17
+
18
+ const InlineRenderer = ({ tokens }: { tokens: Token[] }) => {
19
+ return (
20
+ <>
21
+ {tokens.map((token, index) => {
22
+ switch (token.type) {
23
+ case "text": {
24
+ const t = token as Tokens.Text;
25
+ if (t.tokens) {
26
+ return <InlineRenderer key={index} tokens={t.tokens} />;
27
+ }
28
+ return <Text key={index}>{unescapeHtml(t.text)}</Text>;
29
+ }
30
+ case "strong":
31
+ return (
32
+ <Text key={index} bold>
33
+ {token.tokens ? (
34
+ <InlineRenderer tokens={token.tokens} />
35
+ ) : (
36
+ unescapeHtml((token as Tokens.Strong).text)
37
+ )}
38
+ </Text>
39
+ );
40
+ case "em":
41
+ return (
42
+ <Text key={index} italic>
43
+ {token.tokens ? (
44
+ <InlineRenderer tokens={token.tokens} />
45
+ ) : (
46
+ unescapeHtml((token as Tokens.Em).text)
47
+ )}
48
+ </Text>
49
+ );
50
+ case "codespan":
51
+ return (
52
+ <Text key={index} color="yellow">
53
+ {unescapeHtml((token as Tokens.Codespan).text)}
54
+ </Text>
55
+ );
56
+ case "link":
57
+ return (
58
+ <Text key={index} color="blue" underline>
59
+ {token.tokens ? (
60
+ <InlineRenderer tokens={token.tokens} />
61
+ ) : (
62
+ unescapeHtml((token as Tokens.Link).text)
63
+ )}
64
+ </Text>
65
+ );
66
+ case "br":
67
+ return <Text key={index}>{"\n"}</Text>;
68
+ case "del":
69
+ return (
70
+ <Text key={index} strikethrough>
71
+ {token.tokens ? (
72
+ <InlineRenderer tokens={token.tokens} />
73
+ ) : (
74
+ unescapeHtml((token as Tokens.Del).text)
75
+ )}
76
+ </Text>
77
+ );
78
+ default:
79
+ return <Text key={index}>{token.raw}</Text>;
80
+ }
81
+ })}
82
+ </>
83
+ );
84
+ };
85
+
86
+ const TableRenderer = ({ token }: { token: Tokens.Table }) => {
87
+ const { stdout } = useStdout();
88
+ const terminalWidth = (stdout?.columns || 80) - 2;
89
+
90
+ const columnWidths = useMemo(() => {
91
+ const numCols = token.header.length;
92
+ const minWidth = 5;
93
+ const maxColWidth = 40;
94
+ const widths = token.header.map((h) =>
95
+ Math.min(maxColWidth, Math.max(minWidth, h.text.length)),
96
+ );
97
+
98
+ token.rows.forEach((row) => {
99
+ row.forEach((cell, i) => {
100
+ widths[i] = Math.min(
101
+ maxColWidth,
102
+ Math.max(widths[i] || minWidth, cell.text.length),
103
+ );
104
+ });
19
105
  });
20
106
 
21
- const output = marked(children);
22
- return typeof output === "string" ? output.trim() : "";
23
- }, [children]);
107
+ const paddedWidths = widths.map((w) => w + 2);
108
+ const totalWidth = paddedWidths.reduce((a, b) => a + b, 0) + numCols + 1;
109
+
110
+ if (totalWidth <= terminalWidth) {
111
+ return paddedWidths;
112
+ }
113
+
114
+ // If table is too wide, scale down columns proportionally
115
+ const availableWidth = terminalWidth - numCols - 1;
116
+ const scaleFactor = availableWidth / (totalWidth - numCols - 1);
117
+ return paddedWidths.map((w) =>
118
+ Math.max(minWidth, Math.floor(w * scaleFactor)),
119
+ );
120
+ }, [token, terminalWidth]);
121
+
122
+ return (
123
+ <Box
124
+ flexDirection="column"
125
+ marginBottom={1}
126
+ borderStyle="single"
127
+ borderColor="gray"
128
+ width={columnWidths.reduce((a, b) => a + b, 0) + token.header.length + 1}
129
+ >
130
+ {/* Header */}
131
+ <Box
132
+ flexDirection="row"
133
+ borderStyle="single"
134
+ borderBottom
135
+ borderTop={false}
136
+ borderLeft={false}
137
+ borderRight={false}
138
+ borderColor="gray"
139
+ >
140
+ {token.header.map((cell, i) => (
141
+ <Box
142
+ key={i}
143
+ width={columnWidths[i]}
144
+ paddingX={1}
145
+ borderStyle="single"
146
+ borderLeft={i > 0}
147
+ borderRight={false}
148
+ borderTop={false}
149
+ borderBottom={false}
150
+ borderColor="gray"
151
+ >
152
+ <Text bold wrap="wrap">
153
+ <InlineRenderer tokens={cell.tokens} />
154
+ </Text>
155
+ </Box>
156
+ ))}
157
+ </Box>
158
+ {/* Rows */}
159
+ {token.rows.map((row, rowIndex) => (
160
+ <Box key={rowIndex} flexDirection="row">
161
+ {row.map((cell, i) => (
162
+ <Box
163
+ key={i}
164
+ width={columnWidths[i]}
165
+ paddingX={1}
166
+ borderStyle="single"
167
+ borderLeft={i > 0}
168
+ borderRight={false}
169
+ borderTop={false}
170
+ borderBottom={false}
171
+ borderColor="gray"
172
+ >
173
+ <Text wrap="wrap">
174
+ <InlineRenderer tokens={cell.tokens} />
175
+ </Text>
176
+ </Box>
177
+ ))}
178
+ </Box>
179
+ ))}
180
+ </Box>
181
+ );
182
+ };
183
+
184
+ const BlockRenderer = ({ tokens }: { tokens: Token[] }) => {
185
+ return (
186
+ <>
187
+ {tokens.map((token, index) => {
188
+ switch (token.type) {
189
+ case "heading": {
190
+ const t = token as Tokens.Heading;
191
+ return (
192
+ <Box key={index} marginBottom={1} flexDirection="column">
193
+ <Text bold color="cyan">
194
+ {"#".repeat(t.depth)} <InlineRenderer tokens={t.tokens} />
195
+ </Text>
196
+ </Box>
197
+ );
198
+ }
199
+ case "paragraph": {
200
+ const t = token as Tokens.Paragraph;
201
+ return (
202
+ <Box
203
+ key={index}
204
+ marginBottom={1}
205
+ flexDirection="row"
206
+ flexWrap="wrap"
207
+ >
208
+ <InlineRenderer tokens={t.tokens} />
209
+ </Box>
210
+ );
211
+ }
212
+ case "code": {
213
+ const t = token as Tokens.Code;
214
+ if (t.lang !== undefined) {
215
+ const lines = token.raw.replace(/\n$/, "").split("\n");
216
+ const opening = lines[0];
217
+ const closing = lines[lines.length - 1];
218
+ const content = lines.slice(1, -1).join("\n");
219
+ return (
220
+ <Box
221
+ key={index}
222
+ flexDirection="column"
223
+ paddingX={1}
224
+ marginBottom={1}
225
+ >
226
+ <Text color="gray">{opening}</Text>
227
+ {content && <Text>{content}</Text>}
228
+ <Text color="gray">{closing}</Text>
229
+ </Box>
230
+ );
231
+ }
232
+ return (
233
+ <Box
234
+ key={index}
235
+ flexDirection="column"
236
+ paddingX={1}
237
+ marginBottom={1}
238
+ >
239
+ <Text>{t.text}</Text>
240
+ </Box>
241
+ );
242
+ }
243
+ case "list": {
244
+ const t = token as Tokens.List;
245
+ return (
246
+ <Box
247
+ key={index}
248
+ flexDirection="column"
249
+ marginBottom={1}
250
+ paddingLeft={2}
251
+ >
252
+ {t.items.map((item, i) => {
253
+ const start = t.start || 1;
254
+ return (
255
+ <Box key={i} flexDirection="row">
256
+ <Text color="gray">
257
+ {t.ordered ? `${start + i}. ` : "• "}
258
+ </Text>
259
+ <Box flexDirection="column" flexGrow={1}>
260
+ {item.tokens.map((itemToken, itemIndex) => {
261
+ if (itemToken.type === "text") {
262
+ const it = itemToken as Tokens.Text;
263
+ return (
264
+ <Box
265
+ key={itemIndex}
266
+ flexDirection="row"
267
+ flexWrap="wrap"
268
+ >
269
+ <InlineRenderer
270
+ tokens={it.tokens || [itemToken]}
271
+ />
272
+ </Box>
273
+ );
274
+ }
275
+ return (
276
+ <BlockRenderer
277
+ key={itemIndex}
278
+ tokens={[itemToken]}
279
+ />
280
+ );
281
+ })}
282
+ </Box>
283
+ </Box>
284
+ );
285
+ })}
286
+ </Box>
287
+ );
288
+ }
289
+ case "blockquote": {
290
+ const t = token as Tokens.Blockquote;
291
+ return (
292
+ <Box
293
+ key={index}
294
+ flexDirection="column"
295
+ paddingLeft={2}
296
+ borderStyle="single"
297
+ borderLeft
298
+ borderRight={false}
299
+ borderTop={false}
300
+ borderBottom={false}
301
+ borderColor="gray"
302
+ marginBottom={1}
303
+ >
304
+ <BlockRenderer tokens={t.tokens} />
305
+ </Box>
306
+ );
307
+ }
308
+ case "hr":
309
+ return (
310
+ <Box key={index} marginBottom={1}>
311
+ <Text color="gray">{"─".repeat(20)}</Text>
312
+ </Box>
313
+ );
314
+ case "table":
315
+ return <TableRenderer key={index} token={token as Tokens.Table} />;
316
+ case "space":
317
+ return null;
318
+ default:
319
+ return (
320
+ <Box key={index} marginBottom={1}>
321
+ <Text>{token.raw}</Text>
322
+ </Box>
323
+ );
324
+ }
325
+ })}
326
+ </>
327
+ );
328
+ };
329
+
330
+ // Markdown component using custom Ink-based renderer
331
+ export const Markdown = React.memo(({ children }: MarkdownProps) => {
332
+ const tokens = useMemo(() => marked.lexer(children), [children]);
24
333
 
25
- return <Text>{result}</Text>;
334
+ return (
335
+ <Box flexDirection="column">
336
+ <BlockRenderer tokens={tokens} />
337
+ </Box>
338
+ );
26
339
  });
27
340
 
28
341
  // Add display name for debugging
@@ -63,10 +63,17 @@ export interface ChatContextType {
63
63
  setPermissionMode: (mode: PermissionMode) => void;
64
64
  // Permission confirmation state
65
65
  isConfirmationVisible: boolean;
66
- confirmingTool?: { name: string; input?: Record<string, unknown> };
66
+ confirmingTool?: {
67
+ name: string;
68
+ input?: Record<string, unknown>;
69
+ suggestedPrefix?: string;
70
+ hidePersistentOption?: boolean;
71
+ };
67
72
  showConfirmation: (
68
73
  toolName: string,
69
74
  toolInput?: Record<string, unknown>,
75
+ suggestedPrefix?: string,
76
+ hidePersistentOption?: boolean,
70
77
  ) => Promise<PermissionDecision>;
71
78
  hideConfirmation: () => void;
72
79
  handleConfirmationDecision: (decision: PermissionDecision) => void;
@@ -129,12 +136,20 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
129
136
  // Confirmation state with queue-based architecture
130
137
  const [isConfirmationVisible, setIsConfirmationVisible] = useState(false);
131
138
  const [confirmingTool, setConfirmingTool] = useState<
132
- { name: string; input?: Record<string, unknown> } | undefined
139
+ | {
140
+ name: string;
141
+ input?: Record<string, unknown>;
142
+ suggestedPrefix?: string;
143
+ hidePersistentOption?: boolean;
144
+ }
145
+ | undefined
133
146
  >();
134
147
  const [confirmationQueue, setConfirmationQueue] = useState<
135
148
  Array<{
136
149
  toolName: string;
137
150
  toolInput?: Record<string, unknown>;
151
+ suggestedPrefix?: string;
152
+ hidePersistentOption?: boolean;
138
153
  resolver: (decision: PermissionDecision) => void;
139
154
  reject: () => void;
140
155
  }>
@@ -142,6 +157,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
142
157
  const [currentConfirmation, setCurrentConfirmation] = useState<{
143
158
  toolName: string;
144
159
  toolInput?: Record<string, unknown>;
160
+ suggestedPrefix?: string;
161
+ hidePersistentOption?: boolean;
145
162
  resolver: (decision: PermissionDecision) => void;
146
163
  reject: () => void;
147
164
  } | null>(null);
@@ -153,11 +170,15 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
153
170
  async (
154
171
  toolName: string,
155
172
  toolInput?: Record<string, unknown>,
173
+ suggestedPrefix?: string,
174
+ hidePersistentOption?: boolean,
156
175
  ): Promise<PermissionDecision> => {
157
176
  return new Promise<PermissionDecision>((resolve, reject) => {
158
177
  const queueItem = {
159
178
  toolName,
160
179
  toolInput,
180
+ suggestedPrefix,
181
+ hidePersistentOption,
161
182
  resolver: resolve,
162
183
  reject,
163
184
  };
@@ -219,6 +240,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
219
240
  return await showConfirmation(
220
241
  context.toolName,
221
242
  context.toolInput,
243
+ context.suggestedPrefix,
244
+ context.hidePersistentOption,
222
245
  );
223
246
  } catch {
224
247
  // If confirmation was cancelled or failed, deny the operation
@@ -407,7 +430,12 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
407
430
  if (confirmationQueue.length > 0 && !isConfirmationVisible) {
408
431
  const next = confirmationQueue[0];
409
432
  setCurrentConfirmation(next);
410
- setConfirmingTool({ name: next.toolName, input: next.toolInput });
433
+ setConfirmingTool({
434
+ name: next.toolName,
435
+ input: next.toolInput,
436
+ suggestedPrefix: next.suggestedPrefix,
437
+ hidePersistentOption: next.hidePersistentOption,
438
+ });
411
439
  setIsConfirmationVisible(true);
412
440
  setConfirmationQueue((prev) => prev.slice(1));
413
441
  }