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.
- package/dist/components/ChatInterface.d.ts.map +1 -1
- package/dist/components/ChatInterface.js +1 -1
- package/dist/components/Confirmation.d.ts +2 -0
- package/dist/components/Confirmation.d.ts.map +1 -1
- package/dist/components/Confirmation.js +46 -22
- package/dist/components/Markdown.d.ts.map +1 -1
- package/dist/components/Markdown.js +115 -16
- package/dist/contexts/useChat.d.ts +3 -1
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +10 -3
- package/package.json +6 -5
- package/src/components/ChatInterface.tsx +2 -0
- package/src/components/Confirmation.tsx +66 -33
- package/src/components/Markdown.tsx +329 -16
- package/src/contexts/useChat.tsx +31 -3
|
@@ -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,
|
|
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,
|
|
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:
|
|
27
|
+
alternativeText: suggestedPrefix || "",
|
|
28
|
+
hasUserInput: !!suggestedPrefix,
|
|
29
29
|
});
|
|
30
30
|
const getAutoOptionText = () => {
|
|
31
31
|
if (toolName === "Bash") {
|
|
32
|
-
|
|
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:
|
|
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 (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 {
|
|
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 {
|
|
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" ? "> " : " ", "
|
|
147
|
-
"Type here to tell Wave what to do differently" }))] }) }, "alternative-option")] }), _jsx(Box, { marginTop: 1, children:
|
|
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;
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
const unescapeHtml = (html) => {
|
|
6
|
+
return html
|
|
7
|
+
.replace(/&/g, "&")
|
|
8
|
+
.replace(/</g, "<")
|
|
9
|
+
.replace(/>/g, ">")
|
|
10
|
+
.replace(/"/g, '"')
|
|
11
|
+
.replace(/'/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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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;
|
|
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"}
|
package/dist/contexts/useChat.js
CHANGED
|
@@ -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({
|
|
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.
|
|
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.
|
|
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:
|
|
57
|
+
alternativeText: suggestedPrefix || "",
|
|
58
|
+
hasUserInput: !!suggestedPrefix,
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
const getAutoOptionText = () => {
|
|
58
62
|
if (toolName === "Bash") {
|
|
59
|
-
|
|
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:
|
|
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 (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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" ? "> " : " "}
|
|
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>
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
const unescapeHtml = (html: string) => {
|
|
10
|
+
return html
|
|
11
|
+
.replace(/&/g, "&")
|
|
12
|
+
.replace(/</g, "<")
|
|
13
|
+
.replace(/>/g, ">")
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
334
|
+
return (
|
|
335
|
+
<Box flexDirection="column">
|
|
336
|
+
<BlockRenderer tokens={tokens} />
|
|
337
|
+
</Box>
|
|
338
|
+
);
|
|
26
339
|
});
|
|
27
340
|
|
|
28
341
|
// Add display name for debugging
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -63,10 +63,17 @@ export interface ChatContextType {
|
|
|
63
63
|
setPermissionMode: (mode: PermissionMode) => void;
|
|
64
64
|
// Permission confirmation state
|
|
65
65
|
isConfirmationVisible: boolean;
|
|
66
|
-
confirmingTool?: {
|
|
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
|
-
|
|
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({
|
|
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
|
}
|