woopcode 0.1.0
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/CONTRIBUTING.md +329 -0
- package/LICENSE +21 -0
- package/README.md +582 -0
- package/cli.ts +17 -0
- package/commands/agent.tsx +139 -0
- package/commands/agentController.ts +138 -0
- package/commands/models.ts +21 -0
- package/commands/providers/index.ts +12 -0
- package/commands/providers/listProviders.ts +28 -0
- package/commands/providers/login.ts +28 -0
- package/commands/providers/logout.ts +33 -0
- package/commands/providers/setProvider.ts +34 -0
- package/config/authProvider.ts +54 -0
- package/config/client.ts +163 -0
- package/config/config.ts +118 -0
- package/config/conversation.json +1 -0
- package/config/models.json +50 -0
- package/config/paths.ts +96 -0
- package/config/providers.json +21 -0
- package/config/runtime.ts +130 -0
- package/config/systemPrompt.ts +54 -0
- package/config/types.ts +88 -0
- package/onboarding/FLOW.md +291 -0
- package/onboarding/index.ts +56 -0
- package/onboarding/providers.ts +47 -0
- package/onboarding/setupWizard.tsx +193 -0
- package/onboarding/test-reset.ts +55 -0
- package/package.json +86 -0
- package/tools/createFile.ts +24 -0
- package/tools/editFile.ts +85 -0
- package/tools/findFiles.ts +94 -0
- package/tools/grep.ts +68 -0
- package/tools/index.ts +26 -0
- package/tools/listFiles.ts +49 -0
- package/tools/readFile.ts +53 -0
- package/tools/runTests.ts +29 -0
- package/tools/terminal.ts +37 -0
- package/tools/writeFile.ts +75 -0
- package/tui/package.json +0 -0
- package/tui/src/app.tsx +60 -0
- package/tui/src/components/ApprovalFooter.tsx +29 -0
- package/tui/src/components/AsciiLogo.tsx +78 -0
- package/tui/src/components/BootScreen.tsx +76 -0
- package/tui/src/components/CapabilityRow.tsx +17 -0
- package/tui/src/components/CodeBlock.tsx +70 -0
- package/tui/src/components/DiffPreview.tsx +46 -0
- package/tui/src/components/DiffViewer.tsx +36 -0
- package/tui/src/components/HomeFooter.tsx +11 -0
- package/tui/src/components/HomeScreen.tsx +71 -0
- package/tui/src/components/InlineCode.tsx +13 -0
- package/tui/src/components/LogoReveal.tsx +158 -0
- package/tui/src/components/Markdown.tsx +280 -0
- package/tui/src/components/MessageRenderer.tsx +84 -0
- package/tui/src/components/PromptCard.tsx +52 -0
- package/tui/src/components/StreamingCursor.tsx +13 -0
- package/tui/src/components/ThinkingIndicator.tsx +14 -0
- package/tui/src/components/ToolStatus.tsx +26 -0
- package/tui/src/header.tsx +25 -0
- package/tui/src/hooks/useBootAnimation.ts +67 -0
- package/tui/src/hooks/useLogoAnimation.ts +114 -0
- package/tui/src/index.ts +5 -0
- package/tui/src/prompt.tsx +66 -0
- package/tui/src/statusBar.tsx +83 -0
- package/tui/src/store/ui-store.ts +234 -0
- package/tui/src/store/useUIStore.ts +9 -0
- package/tui/src/timeline.tsx +96 -0
- package/tui/src/types.ts +40 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
|
+
import TextInput from "ink-text-input";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import type { AgentController } from "../../commands/agentController";
|
|
5
|
+
|
|
6
|
+
interface PromptProps {
|
|
7
|
+
controller: AgentController;
|
|
8
|
+
onExit: () => Promise<void>;
|
|
9
|
+
value: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
onValueChange: (value: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Prompt({
|
|
15
|
+
controller,
|
|
16
|
+
onExit,
|
|
17
|
+
value,
|
|
18
|
+
placeholder,
|
|
19
|
+
onValueChange,
|
|
20
|
+
}: PromptProps) {
|
|
21
|
+
const isExiting = useRef(false);
|
|
22
|
+
|
|
23
|
+
useInput((input, key) => {
|
|
24
|
+
if (!(key.ctrl && input.toLowerCase() === "c")) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (controller.isBusy()) {
|
|
29
|
+
controller.cancel();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!isExiting.current) {
|
|
34
|
+
isExiting.current = true;
|
|
35
|
+
void onExit();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
async function handleSubmit(input: string) {
|
|
40
|
+
const prompt = input.trim();
|
|
41
|
+
|
|
42
|
+
if (!prompt || controller.isBusy()) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (prompt === "/exit") {
|
|
46
|
+
await onExit();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onValueChange("");
|
|
51
|
+
await controller.run(prompt);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Box>
|
|
56
|
+
<Text color="cyan">❯ </Text>
|
|
57
|
+
<TextInput
|
|
58
|
+
value={value}
|
|
59
|
+
placeholder={placeholder}
|
|
60
|
+
showCursor
|
|
61
|
+
onChange={onValueChange}
|
|
62
|
+
onSubmit={handleSubmit}
|
|
63
|
+
/>
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import Spinner from "ink-spinner";
|
|
3
|
+
import { useUIStore } from "./store/useUIStore";
|
|
4
|
+
|
|
5
|
+
// ─── Pure presentational component ───────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
type StatusState = "ready" | "thinking" | "tool" | "error" | "cancelled";
|
|
8
|
+
|
|
9
|
+
interface StatusBarProps {
|
|
10
|
+
status: StatusState;
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function StatusBar({ status, message }: StatusBarProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Box gap={1}>
|
|
17
|
+
<StatusIcon status={status} />
|
|
18
|
+
<StatusLabel status={status} message={message} />
|
|
19
|
+
</Box>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function StatusIcon({ status }: { status: StatusState }) {
|
|
24
|
+
if (status === "thinking" || status === "tool") {
|
|
25
|
+
return (
|
|
26
|
+
<Text color="cyan">
|
|
27
|
+
<Spinner type="dots" />
|
|
28
|
+
</Text>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (status === "ready") return <Text color="green">✓</Text>;
|
|
32
|
+
if (status === "error") return <Text color="red">✗</Text>;
|
|
33
|
+
// cancelled
|
|
34
|
+
return <Text dimColor>–</Text>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function StatusLabel({
|
|
38
|
+
status,
|
|
39
|
+
message,
|
|
40
|
+
}: {
|
|
41
|
+
status: StatusState;
|
|
42
|
+
message?: string;
|
|
43
|
+
}) {
|
|
44
|
+
if (status === "ready") return <Text dimColor>Ready</Text>;
|
|
45
|
+
if (status === "error")
|
|
46
|
+
return <Text color="red">{message ?? "Error"}</Text>;
|
|
47
|
+
if (status === "cancelled")
|
|
48
|
+
return <Text dimColor>{message ?? "Cancelled"}</Text>;
|
|
49
|
+
// thinking or tool — animated, show message
|
|
50
|
+
return <Text color="cyan">{message}</Text>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Connected wrapper (reads from store) ────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export function ConnectedStatusBar() {
|
|
56
|
+
const { status } = useUIStore();
|
|
57
|
+
const { state, message } = parseStatus(status);
|
|
58
|
+
return <StatusBar status={state} message={message} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseStatus(raw: string): { state: StatusState; message?: string } {
|
|
62
|
+
const lower = raw.toLowerCase();
|
|
63
|
+
|
|
64
|
+
if (lower.includes("thinking")) {
|
|
65
|
+
return { state: "thinking", message: "Thinking…" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (lower.startsWith("running ")) {
|
|
69
|
+
// "Running read_file..." → message = "Running read_file"
|
|
70
|
+
return { state: "tool", message: raw.replace(/\.*$/, "") };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (lower.startsWith("error")) {
|
|
74
|
+
const msg = raw.replace(/^error[:\s]*/i, "").trim();
|
|
75
|
+
return { state: "error", message: msg || "Something went wrong" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (lower.includes("cancelled")) {
|
|
79
|
+
return { state: "cancelled" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { state: "ready" };
|
|
83
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { Listener, PendingEdit, TimeLineItem, UIState } from "../types";
|
|
2
|
+
import type { ToolCall } from "../../../config/types";
|
|
3
|
+
|
|
4
|
+
export class UIStore {
|
|
5
|
+
private state: UIState = {
|
|
6
|
+
timeline: [],
|
|
7
|
+
status: "Ready",
|
|
8
|
+
isThinking: false,
|
|
9
|
+
pendingEdit: null,
|
|
10
|
+
};
|
|
11
|
+
private listeners: Set<Listener> = new Set();
|
|
12
|
+
private activeAssistantId: string | null = null;
|
|
13
|
+
private pendingEmit = false;
|
|
14
|
+
private editResolvers: Map<
|
|
15
|
+
string,
|
|
16
|
+
{ resolve: (approved: boolean) => void; reject: (error: Error) => void }
|
|
17
|
+
> = new Map();
|
|
18
|
+
|
|
19
|
+
getState() {
|
|
20
|
+
return this.state;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
subscribe(listener: Listener) {
|
|
24
|
+
this.listeners.add(listener);
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
this.listeners.delete(listener);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private emit() {
|
|
32
|
+
this.listeners.forEach((listener) => listener());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Batches rapid successive emissions (e.g. streaming tokens) into a single
|
|
36
|
+
// render tick. Non-streaming callers use emit() directly so they stay instant.
|
|
37
|
+
private emitBatched() {
|
|
38
|
+
if (this.pendingEmit) return;
|
|
39
|
+
this.pendingEmit = true;
|
|
40
|
+
queueMicrotask(() => {
|
|
41
|
+
this.pendingEmit = false;
|
|
42
|
+
this.emit();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
addUserMessage(content: string) {
|
|
47
|
+
this.state = {
|
|
48
|
+
...this.state,
|
|
49
|
+
timeline: [
|
|
50
|
+
...this.state.timeline,
|
|
51
|
+
{
|
|
52
|
+
id: crypto.randomUUID(),
|
|
53
|
+
type: "user",
|
|
54
|
+
content,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.emit();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
startTool(tool: ToolCall) {
|
|
63
|
+
this.state = {
|
|
64
|
+
...this.state,
|
|
65
|
+
timeline: [
|
|
66
|
+
...this.state.timeline,
|
|
67
|
+
{
|
|
68
|
+
id: tool.id,
|
|
69
|
+
type: "tool",
|
|
70
|
+
name: tool.name,
|
|
71
|
+
arguments: tool.arguments,
|
|
72
|
+
status: "running",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.emit();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
finishTool(id: string) {
|
|
81
|
+
const tool = this.state.timeline.find(
|
|
82
|
+
(item): item is Extract<TimeLineItem, { type: "tool" }> =>
|
|
83
|
+
item.type === "tool" && item.id === id,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!tool) return;
|
|
87
|
+
|
|
88
|
+
this.state = {
|
|
89
|
+
...this.state,
|
|
90
|
+
timeline: this.state.timeline.map((item) =>
|
|
91
|
+
item.type === "tool" && item.id === id
|
|
92
|
+
? {
|
|
93
|
+
...item,
|
|
94
|
+
status: "completed",
|
|
95
|
+
}
|
|
96
|
+
: item,
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this.emit();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
startAssistantMessage() {
|
|
104
|
+
const id = crypto.randomUUID();
|
|
105
|
+
this.activeAssistantId = id;
|
|
106
|
+
|
|
107
|
+
this.state = {
|
|
108
|
+
...this.state,
|
|
109
|
+
timeline: [
|
|
110
|
+
...this.state.timeline,
|
|
111
|
+
{
|
|
112
|
+
id,
|
|
113
|
+
type: "assistant",
|
|
114
|
+
content: "",
|
|
115
|
+
streaming: true,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.emit();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
appendAssistantText(text: string) {
|
|
124
|
+
if (!this.activeAssistantId) {
|
|
125
|
+
this.startAssistantMessage();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Mutate content in-place to avoid allocating a new array on every token.
|
|
129
|
+
// The state reference is still replaced so useSyncExternalStore detects the change.
|
|
130
|
+
const timeline = this.state.timeline;
|
|
131
|
+
const idx = timeline.findIndex(
|
|
132
|
+
(item) => item.type === "assistant" && item.id === this.activeAssistantId,
|
|
133
|
+
);
|
|
134
|
+
if (idx !== -1) {
|
|
135
|
+
const item = timeline[idx] as Extract<TimeLineItem, { type: "assistant" }>;
|
|
136
|
+
timeline[idx] = { ...item, content: item.content + text };
|
|
137
|
+
}
|
|
138
|
+
// Replace state reference so React sees a new snapshot
|
|
139
|
+
// Also clear isThinking — first token arrived
|
|
140
|
+
this.state = { ...this.state, timeline, isThinking: false };
|
|
141
|
+
|
|
142
|
+
this.emitBatched();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
finishAssistantMessage() {
|
|
146
|
+
this.state = {
|
|
147
|
+
...this.state,
|
|
148
|
+
timeline: this.state.timeline.map((item) =>
|
|
149
|
+
item.type === "assistant" && item.id === this.activeAssistantId
|
|
150
|
+
? {
|
|
151
|
+
...item,
|
|
152
|
+
streaming: false,
|
|
153
|
+
}
|
|
154
|
+
: item,
|
|
155
|
+
),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
this.activeAssistantId = null;
|
|
159
|
+
|
|
160
|
+
this.emit();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setStatus(status: string) {
|
|
164
|
+
const isThinking = status.toLowerCase().includes("thinking");
|
|
165
|
+
this.state = { ...this.state, status, isThinking };
|
|
166
|
+
this.emit();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Pending Edit Management
|
|
170
|
+
setPendingEdit(edit: PendingEdit): Promise<boolean> {
|
|
171
|
+
this.state = {
|
|
172
|
+
...this.state,
|
|
173
|
+
pendingEdit: edit,
|
|
174
|
+
};
|
|
175
|
+
this.emit();
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
this.editResolvers.set(edit.id, { resolve, reject });
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
approvePendingEdit() {
|
|
183
|
+
const edit = this.state.pendingEdit;
|
|
184
|
+
if (!edit) return;
|
|
185
|
+
|
|
186
|
+
const resolver = this.editResolvers.get(edit.id);
|
|
187
|
+
if (resolver) {
|
|
188
|
+
resolver.resolve(true);
|
|
189
|
+
this.editResolvers.delete(edit.id);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.state = {
|
|
193
|
+
...this.state,
|
|
194
|
+
pendingEdit: null,
|
|
195
|
+
};
|
|
196
|
+
this.emit();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
rejectPendingEdit() {
|
|
200
|
+
const edit = this.state.pendingEdit;
|
|
201
|
+
if (!edit) return;
|
|
202
|
+
|
|
203
|
+
const resolver = this.editResolvers.get(edit.id);
|
|
204
|
+
if (resolver) {
|
|
205
|
+
resolver.resolve(false);
|
|
206
|
+
this.editResolvers.delete(edit.id);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.state = {
|
|
210
|
+
...this.state,
|
|
211
|
+
pendingEdit: null,
|
|
212
|
+
};
|
|
213
|
+
this.emit();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
clearPendingEdit() {
|
|
217
|
+
const edit = this.state.pendingEdit;
|
|
218
|
+
if (!edit) return;
|
|
219
|
+
|
|
220
|
+
const resolver = this.editResolvers.get(edit.id);
|
|
221
|
+
if (resolver) {
|
|
222
|
+
resolver.reject(new Error("Edit cancelled"));
|
|
223
|
+
this.editResolvers.delete(edit.id);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.state = {
|
|
227
|
+
...this.state,
|
|
228
|
+
pendingEdit: null,
|
|
229
|
+
};
|
|
230
|
+
this.emit();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const store = new UIStore();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { TimeLineItem } from "./types";
|
|
3
|
+
import { MessageRenderer } from "./components/MessageRenderer";
|
|
4
|
+
import { ToolStatus } from "./components/ToolStatus";
|
|
5
|
+
import { ThinkingIndicator } from "./components/ThinkingIndicator";
|
|
6
|
+
|
|
7
|
+
interface TimelineProps {
|
|
8
|
+
items: TimeLineItem[];
|
|
9
|
+
isThinking: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Timeline({ items, isThinking }: TimelineProps) {
|
|
13
|
+
return (
|
|
14
|
+
<Box flexDirection="column">
|
|
15
|
+
{items.map((item) => (
|
|
16
|
+
<TimelineItem key={item.id} item={item} />
|
|
17
|
+
))}
|
|
18
|
+
{isThinking && <ThinkingIndicator />}
|
|
19
|
+
</Box>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function TimelineItem({ item }: { item: TimeLineItem }) {
|
|
24
|
+
switch (item.type) {
|
|
25
|
+
case "user":
|
|
26
|
+
return (
|
|
27
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
28
|
+
<Text bold dimColor>
|
|
29
|
+
You
|
|
30
|
+
</Text>
|
|
31
|
+
<Text>{item.content}</Text>
|
|
32
|
+
</Box>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
case "assistant":
|
|
36
|
+
return (
|
|
37
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
38
|
+
<Box>
|
|
39
|
+
<Text bold color="cyan">
|
|
40
|
+
Woopcode
|
|
41
|
+
</Text>
|
|
42
|
+
{item.streaming && <Text dimColor> · thinking</Text>}
|
|
43
|
+
</Box>
|
|
44
|
+
<Box paddingLeft={1}>
|
|
45
|
+
<MessageRenderer content={item.content} streaming={item.streaming} />
|
|
46
|
+
</Box>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
case "tool": {
|
|
51
|
+
const label = toolLabel(item.name);
|
|
52
|
+
const target = formatToolArgument(item.arguments);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Box flexDirection="column" marginBottom={1} paddingLeft={1}>
|
|
56
|
+
<Box gap={1}>
|
|
57
|
+
<Text bold color="#888888">{label}</Text>
|
|
58
|
+
{target && <Text>{target}</Text>}
|
|
59
|
+
</Box>
|
|
60
|
+
<ToolStatus status={item.status} />
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatToolArgument(arguments_: Record<string, unknown>) {
|
|
68
|
+
const value =
|
|
69
|
+
arguments_.path ??
|
|
70
|
+
arguments_.query ??
|
|
71
|
+
arguments_.pattern ??
|
|
72
|
+
Object.values(arguments_)[0];
|
|
73
|
+
|
|
74
|
+
if (value === undefined) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toolLabel(name: string): string {
|
|
82
|
+
const map: Record<string, string> = {
|
|
83
|
+
read_file: "READ",
|
|
84
|
+
write_file: "WRITE",
|
|
85
|
+
edit_file: "EDIT",
|
|
86
|
+
create_file: "CREATE",
|
|
87
|
+
delete_file: "DELETE",
|
|
88
|
+
run_command: "RUN",
|
|
89
|
+
execute_command: "RUN",
|
|
90
|
+
search: "SEARCH",
|
|
91
|
+
grep: "SEARCH",
|
|
92
|
+
list_directory: "LIST",
|
|
93
|
+
list_files: "LIST",
|
|
94
|
+
};
|
|
95
|
+
return map[name] ?? name.toUpperCase().replace(/_/g, " ");
|
|
96
|
+
}
|
package/tui/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type TimeLineItem =
|
|
2
|
+
| {
|
|
3
|
+
id: string;
|
|
4
|
+
type: "user";
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
| {
|
|
8
|
+
id: string;
|
|
9
|
+
type: "assistant";
|
|
10
|
+
content: string;
|
|
11
|
+
streaming: boolean;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
id: string;
|
|
15
|
+
type: "tool";
|
|
16
|
+
name: string;
|
|
17
|
+
arguments: Record<string, unknown>;
|
|
18
|
+
status: "running" | "completed";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface PendingEdit {
|
|
22
|
+
id: string;
|
|
23
|
+
filePath: string;
|
|
24
|
+
oldContent: string;
|
|
25
|
+
newContent: string;
|
|
26
|
+
diff: string;
|
|
27
|
+
toolCallId: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UIState {
|
|
31
|
+
timeline: TimeLineItem[];
|
|
32
|
+
status: string;
|
|
33
|
+
isThinking: boolean;
|
|
34
|
+
pendingEdit: PendingEdit | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TimelineProps {
|
|
38
|
+
items: TimeLineItem[];
|
|
39
|
+
}
|
|
40
|
+
export type Listener = () => void;
|