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.
Files changed (67) hide show
  1. package/CONTRIBUTING.md +329 -0
  2. package/LICENSE +21 -0
  3. package/README.md +582 -0
  4. package/cli.ts +17 -0
  5. package/commands/agent.tsx +139 -0
  6. package/commands/agentController.ts +138 -0
  7. package/commands/models.ts +21 -0
  8. package/commands/providers/index.ts +12 -0
  9. package/commands/providers/listProviders.ts +28 -0
  10. package/commands/providers/login.ts +28 -0
  11. package/commands/providers/logout.ts +33 -0
  12. package/commands/providers/setProvider.ts +34 -0
  13. package/config/authProvider.ts +54 -0
  14. package/config/client.ts +163 -0
  15. package/config/config.ts +118 -0
  16. package/config/conversation.json +1 -0
  17. package/config/models.json +50 -0
  18. package/config/paths.ts +96 -0
  19. package/config/providers.json +21 -0
  20. package/config/runtime.ts +130 -0
  21. package/config/systemPrompt.ts +54 -0
  22. package/config/types.ts +88 -0
  23. package/onboarding/FLOW.md +291 -0
  24. package/onboarding/index.ts +56 -0
  25. package/onboarding/providers.ts +47 -0
  26. package/onboarding/setupWizard.tsx +193 -0
  27. package/onboarding/test-reset.ts +55 -0
  28. package/package.json +86 -0
  29. package/tools/createFile.ts +24 -0
  30. package/tools/editFile.ts +85 -0
  31. package/tools/findFiles.ts +94 -0
  32. package/tools/grep.ts +68 -0
  33. package/tools/index.ts +26 -0
  34. package/tools/listFiles.ts +49 -0
  35. package/tools/readFile.ts +53 -0
  36. package/tools/runTests.ts +29 -0
  37. package/tools/terminal.ts +37 -0
  38. package/tools/writeFile.ts +75 -0
  39. package/tui/package.json +0 -0
  40. package/tui/src/app.tsx +60 -0
  41. package/tui/src/components/ApprovalFooter.tsx +29 -0
  42. package/tui/src/components/AsciiLogo.tsx +78 -0
  43. package/tui/src/components/BootScreen.tsx +76 -0
  44. package/tui/src/components/CapabilityRow.tsx +17 -0
  45. package/tui/src/components/CodeBlock.tsx +70 -0
  46. package/tui/src/components/DiffPreview.tsx +46 -0
  47. package/tui/src/components/DiffViewer.tsx +36 -0
  48. package/tui/src/components/HomeFooter.tsx +11 -0
  49. package/tui/src/components/HomeScreen.tsx +71 -0
  50. package/tui/src/components/InlineCode.tsx +13 -0
  51. package/tui/src/components/LogoReveal.tsx +158 -0
  52. package/tui/src/components/Markdown.tsx +280 -0
  53. package/tui/src/components/MessageRenderer.tsx +84 -0
  54. package/tui/src/components/PromptCard.tsx +52 -0
  55. package/tui/src/components/StreamingCursor.tsx +13 -0
  56. package/tui/src/components/ThinkingIndicator.tsx +14 -0
  57. package/tui/src/components/ToolStatus.tsx +26 -0
  58. package/tui/src/header.tsx +25 -0
  59. package/tui/src/hooks/useBootAnimation.ts +67 -0
  60. package/tui/src/hooks/useLogoAnimation.ts +114 -0
  61. package/tui/src/index.ts +5 -0
  62. package/tui/src/prompt.tsx +66 -0
  63. package/tui/src/statusBar.tsx +83 -0
  64. package/tui/src/store/ui-store.ts +234 -0
  65. package/tui/src/store/useUIStore.ts +9 -0
  66. package/tui/src/timeline.tsx +96 -0
  67. 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,9 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { store } from "./ui-store";
3
+
4
+ export function useUIStore() {
5
+ return useSyncExternalStore(
6
+ store.subscribe.bind(store),
7
+ store.getState.bind(store),
8
+ );
9
+ }
@@ -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
+ }
@@ -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;