@sean.holung/minicode 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 (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +241 -0
  3. package/dist/src/agent/agent.js +209 -0
  4. package/dist/src/agent/config.js +151 -0
  5. package/dist/src/agent/types.js +1 -0
  6. package/dist/src/index.js +138 -0
  7. package/dist/src/indexer/cache.js +121 -0
  8. package/dist/src/indexer/code-map.js +92 -0
  9. package/dist/src/indexer/plugin-loader.js +78 -0
  10. package/dist/src/indexer/plugins/typescript.js +327 -0
  11. package/dist/src/indexer/project-index.js +145 -0
  12. package/dist/src/indexer/types.js +1 -0
  13. package/dist/src/model/client.js +374 -0
  14. package/dist/src/prompt/system-prompt.js +91 -0
  15. package/dist/src/safety/guardrails.js +55 -0
  16. package/dist/src/session/session.js +95 -0
  17. package/dist/src/tools/edit-file.js +73 -0
  18. package/dist/src/tools/find-references.js +52 -0
  19. package/dist/src/tools/get-dependencies.js +56 -0
  20. package/dist/src/tools/helpers.js +42 -0
  21. package/dist/src/tools/list-files.js +63 -0
  22. package/dist/src/tools/read-file.js +79 -0
  23. package/dist/src/tools/read-symbol.js +96 -0
  24. package/dist/src/tools/registry.js +68 -0
  25. package/dist/src/tools/run-command.js +92 -0
  26. package/dist/src/tools/search-code-map.js +72 -0
  27. package/dist/src/tools/search.js +153 -0
  28. package/dist/src/tools/write-file.js +44 -0
  29. package/dist/src/ui/app.js +31 -0
  30. package/dist/src/ui/cli-ink.js +168 -0
  31. package/dist/src/ui/components/activity-pane.js +35 -0
  32. package/dist/src/ui/components/header-bar.js +6 -0
  33. package/dist/src/ui/components/input-composer.js +46 -0
  34. package/dist/src/ui/components/tool-timeline-item.js +37 -0
  35. package/dist/src/ui/events.js +1 -0
  36. package/dist/src/ui/state/ui-store.js +89 -0
  37. package/dist/src/ui/theme.js +23 -0
  38. package/dist/tests/agent.test.js +130 -0
  39. package/dist/tests/cache.test.js +37 -0
  40. package/dist/tests/config.test.js +37 -0
  41. package/dist/tests/dependency-graph.test.js +27 -0
  42. package/dist/tests/file-tools.test.js +73 -0
  43. package/dist/tests/find-references.test.js +30 -0
  44. package/dist/tests/get-dependencies.test.js +35 -0
  45. package/dist/tests/guardrails.test.js +18 -0
  46. package/dist/tests/indexer.test.js +201 -0
  47. package/dist/tests/model-client-openai.test.js +84 -0
  48. package/dist/tests/read-symbol.test.js +83 -0
  49. package/dist/tests/search-code-map.test.js +30 -0
  50. package/dist/tests/session.test.js +37 -0
  51. package/dist/tests/system-prompt.test.js +82 -0
  52. package/dist/tests/test-utils.js +18 -0
  53. package/dist/tests/tool-registry.test.js +41 -0
  54. package/package.json +43 -0
@@ -0,0 +1,153 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { resolveWorkspacePath } from "../safety/guardrails.js";
4
+ import { expectNonEmptyString } from "./helpers.js";
5
+ function runCommand(command, args, cwd, timeoutMs) {
6
+ return new Promise((resolve, reject) => {
7
+ const child = spawn(command, args, { cwd });
8
+ let stdout = "";
9
+ let stderr = "";
10
+ let timedOut = false;
11
+ const timeout = setTimeout(() => {
12
+ timedOut = true;
13
+ child.kill("SIGTERM");
14
+ }, timeoutMs);
15
+ child.stdout.on("data", (chunk) => {
16
+ stdout += chunk.toString();
17
+ });
18
+ child.stderr.on("data", (chunk) => {
19
+ stderr += chunk.toString();
20
+ });
21
+ child.on("error", (error) => {
22
+ clearTimeout(timeout);
23
+ reject(error);
24
+ });
25
+ child.on("close", (code) => {
26
+ clearTimeout(timeout);
27
+ if (timedOut) {
28
+ reject(new Error(`Search command timed out after ${timeoutMs} ms.`));
29
+ return;
30
+ }
31
+ resolve({
32
+ stdout,
33
+ stderr,
34
+ code,
35
+ });
36
+ });
37
+ });
38
+ }
39
+ function getOptionalString(input, key) {
40
+ const value = input[key];
41
+ if (value === undefined) {
42
+ return undefined;
43
+ }
44
+ if (typeof value !== "string" || value.trim().length === 0) {
45
+ throw new Error(`Input "${key}" must be a non-empty string.`);
46
+ }
47
+ return value;
48
+ }
49
+ export function createSearchTool(config) {
50
+ return {
51
+ name: "search",
52
+ description: "Search file contents using ripgrep. Use when you don't know the symbol name. When results show a function/class name, use read_symbol next (not read_file).",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ pattern: {
57
+ type: "string",
58
+ description: "Regex pattern to search for.",
59
+ },
60
+ path: {
61
+ type: "string",
62
+ description: "Optional path to search under, relative to workspace root.",
63
+ },
64
+ include: {
65
+ type: "string",
66
+ description: "Optional glob include filter, e.g. *.ts",
67
+ },
68
+ },
69
+ required: ["pattern"],
70
+ additionalProperties: false,
71
+ },
72
+ execute: async (input) => {
73
+ const maxOutputChars = 12_000;
74
+ const pattern = expectNonEmptyString(input, "pattern");
75
+ const requestedPath = getOptionalString(input, "path") ?? ".";
76
+ const include = getOptionalString(input, "include");
77
+ const targetPath = resolveWorkspacePath(requestedPath, config.workspaceRoot);
78
+ const relativeTarget = path.relative(config.workspaceRoot, targetPath) || ".";
79
+ const rgArgs = [
80
+ "--line-number",
81
+ "--color",
82
+ "never",
83
+ "--no-heading",
84
+ "--binary-files",
85
+ "without-match",
86
+ "--glob",
87
+ "!.minicode/**",
88
+ "--glob",
89
+ "!node_modules/**",
90
+ "--glob",
91
+ "!package-lock.json",
92
+ "--glob",
93
+ "!yarn.lock",
94
+ "--glob",
95
+ "!pnpm-lock.yaml",
96
+ "--glob",
97
+ "!*.min.js",
98
+ "-m",
99
+ "50",
100
+ ];
101
+ if (include) {
102
+ rgArgs.push("--glob", include);
103
+ }
104
+ rgArgs.push(pattern, relativeTarget);
105
+ try {
106
+ const result = await runCommand("rg", rgArgs, config.workspaceRoot, config.commandTimeoutMs);
107
+ if (result.code === 1 || result.stdout.trim().length === 0) {
108
+ return "No matches found.";
109
+ }
110
+ if (result.code !== 0) {
111
+ throw new Error(result.stderr || "ripgrep search failed.");
112
+ }
113
+ const output = result.stdout.trimEnd();
114
+ if (output.length > maxOutputChars) {
115
+ return `${output.slice(0, maxOutputChars)}\n\n[... output truncated, ${output.length - maxOutputChars} more chars ...]`;
116
+ }
117
+ return output;
118
+ }
119
+ catch (error) {
120
+ const commandError = error;
121
+ if (commandError.code !== "ENOENT") {
122
+ throw error;
123
+ }
124
+ }
125
+ // Minimal fallback for systems without rg installed.
126
+ const grepArgs = [
127
+ "-RIn",
128
+ "--exclude-dir=.minicode",
129
+ "--exclude-dir=node_modules",
130
+ "--exclude-dir=.git",
131
+ "--exclude=package-lock.json",
132
+ "--exclude=yarn.lock",
133
+ "--exclude=pnpm-lock.yaml",
134
+ "-m",
135
+ "50",
136
+ pattern,
137
+ relativeTarget,
138
+ ];
139
+ const fallbackResult = await runCommand("grep", grepArgs, config.workspaceRoot, config.commandTimeoutMs);
140
+ if (fallbackResult.code === 1 || fallbackResult.stdout.trim().length === 0) {
141
+ return "No matches found.";
142
+ }
143
+ if (fallbackResult.code !== 0) {
144
+ throw new Error(fallbackResult.stderr || "grep search failed.");
145
+ }
146
+ const fallbackOutput = fallbackResult.stdout.trimEnd();
147
+ if (fallbackOutput.length > maxOutputChars) {
148
+ return `${fallbackOutput.slice(0, maxOutputChars)}\n\n[... output truncated, ${fallbackOutput.length - maxOutputChars} more chars ...]`;
149
+ }
150
+ return fallbackOutput;
151
+ },
152
+ };
153
+ }
@@ -0,0 +1,44 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveWorkspacePath } from "../safety/guardrails.js";
4
+ import { expectNonEmptyString } from "./helpers.js";
5
+ function expectString(input, key) {
6
+ const value = input[key];
7
+ if (typeof value !== "string") {
8
+ throw new Error(`Input "${key}" must be a string.`);
9
+ }
10
+ return value;
11
+ }
12
+ export function createWriteFileTool(config, projectIndex) {
13
+ return {
14
+ name: "write_file",
15
+ description: "Create or overwrite a file with the provided content.",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ path: {
20
+ type: "string",
21
+ description: "Path to the file relative to workspace root.",
22
+ },
23
+ content: {
24
+ type: "string",
25
+ description: "The full file content to write.",
26
+ },
27
+ },
28
+ required: ["path", "content"],
29
+ additionalProperties: false,
30
+ },
31
+ execute: async (input) => {
32
+ const requestedPath = expectNonEmptyString(input, "path");
33
+ const content = expectString(input, "content");
34
+ const filePath = resolveWorkspacePath(requestedPath, config.workspaceRoot);
35
+ await mkdir(path.dirname(filePath), { recursive: true });
36
+ await writeFile(filePath, content, "utf8");
37
+ if (projectIndex) {
38
+ const relPath = path.relative(config.workspaceRoot, filePath);
39
+ projectIndex.reindexFile(relPath, content);
40
+ }
41
+ return `Wrote ${content.length} characters to "${requestedPath}".`;
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState, useEffect, useCallback } from "react";
3
+ import { render, Box, Text } from "ink";
4
+ import { HeaderBar } from "./components/header-bar.js";
5
+ import { ActivityPane } from "./components/activity-pane.js";
6
+ import { InputComposer } from "./components/input-composer.js";
7
+ function AppInner({ store, onRunTurn, onCtrlC }) {
8
+ const [state, setState] = useState(store.getState());
9
+ useEffect(() => {
10
+ const unsub = store.subscribe(() => {
11
+ setState(store.getState());
12
+ });
13
+ return unsub;
14
+ }, [store]);
15
+ const handleSubmit = useCallback((input) => {
16
+ onRunTurn(input);
17
+ }, [onRunTurn]);
18
+ const disabled = state.phase !== "idle" && state.phase !== "loading";
19
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ActivityPane, { items: state.items }), _jsx(HeaderBar, { model: state.model, step: state.step, maxSteps: state.maxSteps, inputTokens: state.inputTokens, outputTokens: state.outputTokens, workspaceRoot: state.workspaceRoot, indexStatus: state.indexStatus }), _jsx(InputComposer, { onSubmit: handleSubmit, disabled: disabled, ...(onCtrlC && { onCtrlC }) }), state.errorMessage && (_jsx(Box, { paddingX: 1, children: _jsx(Box, { borderStyle: "single", borderColor: "red", paddingX: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", state.errorMessage] }) }) }))] }));
20
+ }
21
+ export function runInkApp(store, onRunTurn, onCtrlC) {
22
+ process.stdout.write("\x1b[2J\x1b[H");
23
+ const instance = render(React.createElement(AppInner, {
24
+ store,
25
+ onRunTurn,
26
+ ...(onCtrlC && { onCtrlC }),
27
+ }), { exitOnCtrlC: false });
28
+ return {
29
+ waitUntilExit: () => instance.waitUntilExit(),
30
+ };
31
+ }
@@ -0,0 +1,168 @@
1
+ import process from "node:process";
2
+ import { CodingAgent } from "../agent/agent.js";
3
+ import { formatConfigForDisplay, loadAgentConfig } from "../agent/config.js";
4
+ import { computeFileHashes, getWorkspaceCacheDir, loadIndex, saveIndex, } from "../indexer/cache.js";
5
+ import { buildProjectIndex } from "../indexer/project-index.js";
6
+ import { createModelClient } from "../model/client.js";
7
+ import { ToolRegistry } from "../tools/registry.js";
8
+ import { UiStore } from "./state/ui-store.js";
9
+ import { runInkApp } from "./app.js";
10
+ export async function runInkCli(verbose, initialTask) {
11
+ const store = new UiStore();
12
+ store.setPhase("loading");
13
+ const config = await loadAgentConfig();
14
+ const modelClient = createModelClient(config);
15
+ let projectIndex;
16
+ let indexStatus = "building...";
17
+ try {
18
+ const cacheDir = getWorkspaceCacheDir(config.workspaceRoot);
19
+ const fileHashes = await computeFileHashes(config.workspaceRoot);
20
+ const cached = await loadIndex(cacheDir, fileHashes);
21
+ if (cached) {
22
+ projectIndex = cached;
23
+ indexStatus = "ready (cached)";
24
+ }
25
+ else {
26
+ projectIndex = await buildProjectIndex(config.workspaceRoot);
27
+ await saveIndex(projectIndex, cacheDir, fileHashes);
28
+ indexStatus = "ready";
29
+ }
30
+ }
31
+ catch {
32
+ projectIndex = undefined;
33
+ indexStatus = "unavailable (degraded)";
34
+ }
35
+ store.setConfig({
36
+ model: config.model,
37
+ workspaceRoot: config.workspaceRoot,
38
+ maxSteps: config.maxSteps,
39
+ indexStatus,
40
+ });
41
+ store.setPhase("idle");
42
+ const toolRegistry = ToolRegistry.createDefault(config, projectIndex);
43
+ const agent = new CodingAgent({
44
+ config,
45
+ modelClient,
46
+ toolRegistry,
47
+ verbose,
48
+ ...(projectIndex !== undefined ? { projectIndex } : {}),
49
+ ...(verbose
50
+ ? {
51
+ onProgress: (msg) => store.addItem({ type: "system", content: msg }),
52
+ }
53
+ : {}),
54
+ onUiUpdate: (event) => {
55
+ switch (event.type) {
56
+ case "streaming_chunk":
57
+ store.appendToStreamingContent(event.content);
58
+ break;
59
+ case "step":
60
+ store.setStep(event.step);
61
+ break;
62
+ case "thinking":
63
+ store.addItem({ type: "thinking", content: event.content });
64
+ break;
65
+ case "tool_call_start":
66
+ store.addItem({
67
+ type: "tool_call",
68
+ name: event.name,
69
+ input: event.input,
70
+ state: "running",
71
+ });
72
+ store.setPhase("tool_running");
73
+ break;
74
+ case "tool_call_end":
75
+ store.updateLastToolCall({
76
+ state: "success",
77
+ elapsedMs: event.elapsedMs,
78
+ });
79
+ store.addItem({
80
+ type: "tool_result",
81
+ name: event.name,
82
+ content: event.result,
83
+ elapsedMs: event.elapsedMs,
84
+ });
85
+ store.setPhase("model_wait");
86
+ break;
87
+ }
88
+ },
89
+ });
90
+ let turnAbortController = null;
91
+ const handleCtrlC = (inkExit) => {
92
+ if (turnAbortController) {
93
+ turnAbortController.abort();
94
+ }
95
+ else if (inkExit) {
96
+ inkExit();
97
+ }
98
+ else {
99
+ process.exit(0);
100
+ }
101
+ };
102
+ process.on("SIGINT", () => handleCtrlC());
103
+ const onCtrlC = (exit) => handleCtrlC(exit);
104
+ const onRunTurn = async (input) => {
105
+ const trimmed = input.trim();
106
+ if (trimmed.length === 0)
107
+ return;
108
+ store.setError(null);
109
+ if (trimmed === "/exit" || trimmed === "exit" || trimmed === "quit") {
110
+ process.exit(0);
111
+ }
112
+ if (trimmed === "/help") {
113
+ store.addItem({
114
+ type: "system",
115
+ content: 'Commands: "/help", "/config", "/exit". Start with --verbose or -v for detailed logs.',
116
+ });
117
+ return;
118
+ }
119
+ if (trimmed === "/config") {
120
+ store.addItem({
121
+ type: "system",
122
+ content: formatConfigForDisplay(config),
123
+ });
124
+ return;
125
+ }
126
+ store.addItem({ type: "user", content: trimmed });
127
+ store.setPhase("sending");
128
+ store.setStep(0);
129
+ turnAbortController = new AbortController();
130
+ try {
131
+ const { text, usage, streamed } = await agent.runTurn(trimmed, {
132
+ signal: turnAbortController.signal,
133
+ });
134
+ if (!streamed) {
135
+ store.addItem({ type: "assistant", content: text });
136
+ }
137
+ if (usage) {
138
+ store.setTokenUsage(usage.inputTokens, usage.outputTokens);
139
+ store.addItem({
140
+ type: "token_usage",
141
+ inputTokens: usage.inputTokens,
142
+ outputTokens: usage.outputTokens,
143
+ });
144
+ }
145
+ store.setPhase("idle");
146
+ store.setStep(0);
147
+ }
148
+ catch (error) {
149
+ if (error instanceof Error && error.name === "AbortError") {
150
+ store.addItem({ type: "system", content: "Cancelled" });
151
+ }
152
+ else {
153
+ const message = error instanceof Error ? error.message : "Unknown runtime failure";
154
+ store.setError(message);
155
+ }
156
+ store.setPhase("idle");
157
+ store.setStep(0);
158
+ }
159
+ finally {
160
+ turnAbortController = null;
161
+ }
162
+ };
163
+ const { waitUntilExit } = runInkApp(store, onRunTurn, onCtrlC);
164
+ if (initialTask && initialTask.trim().length > 0) {
165
+ await onRunTurn(initialTask);
166
+ }
167
+ await waitUntilExit();
168
+ }
@@ -0,0 +1,35 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text, Static } from "ink";
3
+ import { c } from "../theme.js";
4
+ import { ToolTimelineItem } from "./tool-timeline-item.js";
5
+ const MAX_TOOL_OUTPUT_PREVIEW = 200;
6
+ function ActivityItemRow({ item }) {
7
+ switch (item.type) {
8
+ case "user":
9
+ return (_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [c.blue("You:"), " "] }), _jsx(Text, { children: item.content })] }));
10
+ case "assistant":
11
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [c.cyan("Agent:"), " "] }), _jsx(Text, { children: item.content })] }));
12
+ case "thinking":
13
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [c.magenta("Thinking:"), " "] }), _jsx(Text, { dimColor: true, children: item.content })] }));
14
+ case "tool_call":
15
+ return _jsx(ToolTimelineItem, { item: item });
16
+ case "tool_result": {
17
+ const preview = item.content.length > MAX_TOOL_OUTPUT_PREVIEW
18
+ ? item.content.slice(0, MAX_TOOL_OUTPUT_PREVIEW) +
19
+ "\n[... truncated ...]"
20
+ : item.content;
21
+ return (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: preview }) }));
22
+ }
23
+ case "token_usage":
24
+ return (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["tokens: ", item.inputTokens, " in, ", item.outputTokens, " out"] }) }));
25
+ case "system":
26
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: item.content }) }));
27
+ default:
28
+ return _jsx(Box, {});
29
+ }
30
+ }
31
+ export function ActivityPane({ items }) {
32
+ const completedItems = items.length > 0 ? items.slice(0, -1) : [];
33
+ const lastItem = items.length > 0 ? items[items.length - 1] : undefined;
34
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: completedItems, children: (item, index) => (_jsx(Box, { children: _jsx(ActivityItemRow, { item: item }) }, `item-${index}`)) }), lastItem && (_jsx(Box, { children: _jsx(ActivityItemRow, { item: lastItem }) }, "last-item"))] }));
35
+ }
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { c } from "../theme.js";
4
+ export function HeaderBar({ model, step, maxSteps, inputTokens, outputTokens, workspaceRoot, indexStatus, }) {
5
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: c.cyan("minicode") }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [c.dim("model:"), " "] }), _jsx(Text, { children: model || "—" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [c.dim("steps:"), " "] }), _jsxs(Text, { children: [step, "/", maxSteps] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [c.dim("tokens:"), " "] }), _jsxs(Text, { children: [inputTokens, " in / ", outputTokens, " out"] })] }), _jsxs(Box, { children: [_jsxs(Text, { children: [c.dim("cwd:"), " "] }), _jsx(Text, { children: workspaceRoot || "—" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [c.dim("index:"), " "] }), _jsx(Text, { children: indexStatus || "—" })] })] }));
6
+ }
@@ -0,0 +1,46 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useCallback } from "react";
3
+ import { Box, Text, useInput, useApp } from "ink";
4
+ import { c } from "../theme.js";
5
+ export function InputComposer({ onSubmit, disabled = false, onCtrlC, }) {
6
+ const [value, setValue] = useState("");
7
+ const { exit } = useApp();
8
+ const handleSubmit = useCallback(() => {
9
+ const trimmed = value.trim();
10
+ if (trimmed.length > 0 && !disabled) {
11
+ onSubmit(trimmed);
12
+ setValue("");
13
+ }
14
+ }, [value, disabled, onSubmit]);
15
+ useInput((input, key) => {
16
+ if (key.ctrl && input === "c") {
17
+ if (onCtrlC) {
18
+ onCtrlC(exit);
19
+ }
20
+ else {
21
+ exit();
22
+ }
23
+ return;
24
+ }
25
+ if (key.ctrl && input === "l") {
26
+ setValue("");
27
+ return;
28
+ }
29
+ if (key.escape) {
30
+ setValue("");
31
+ return;
32
+ }
33
+ if (key.return) {
34
+ handleSubmit();
35
+ return;
36
+ }
37
+ if (!disabled && (key.backspace || key.delete || input === "\x7f")) {
38
+ setValue((v) => v.slice(0, -1));
39
+ return;
40
+ }
41
+ if (!disabled && !key.ctrl && !key.meta && input) {
42
+ setValue((v) => v + input);
43
+ }
44
+ });
45
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [c.dim("Input:"), " "] }), _jsx(Text, { children: value }), _jsx(Text, { children: disabled ? "" : "_" })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "[Enter send] [Ctrl+C cancel/quit] [Esc clear]" }) })] }));
46
+ }
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { c } from "../theme.js";
4
+ function formatInput(input, maxLen = 60) {
5
+ const s = JSON.stringify(input);
6
+ return s.length > maxLen ? s.slice(0, maxLen) + "..." : s;
7
+ }
8
+ export function ToolTimelineItem({ item }) {
9
+ const { name, input, state, elapsedMs } = item;
10
+ const argsStr = formatInput(input);
11
+ let icon;
12
+ let colorFn;
13
+ switch (state) {
14
+ case "running":
15
+ case "queued":
16
+ icon = "▶";
17
+ colorFn = c.cyan;
18
+ break;
19
+ case "success":
20
+ icon = "✓";
21
+ colorFn = c.green;
22
+ break;
23
+ case "error":
24
+ icon = "✗";
25
+ colorFn = c.red;
26
+ break;
27
+ case "cancelled":
28
+ icon = "—";
29
+ colorFn = c.dim;
30
+ break;
31
+ default:
32
+ icon = "?";
33
+ colorFn = c.dim;
34
+ }
35
+ const timeStr = elapsedMs !== undefined ? ` (${elapsedMs}ms)` : "";
36
+ return (_jsxs(Box, { children: [_jsx(Text, { children: colorFn(` ${icon} `) }), _jsx(Text, { children: colorFn(`${name}(${argsStr})${timeStr}`) })] }));
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ const DEFAULT_STATE = {
2
+ phase: "idle",
3
+ step: 0,
4
+ maxSteps: 50,
5
+ inputTokens: 0,
6
+ outputTokens: 0,
7
+ model: "",
8
+ workspaceRoot: "",
9
+ indexStatus: "",
10
+ items: [],
11
+ errorMessage: null,
12
+ };
13
+ export class UiStore {
14
+ state = { ...DEFAULT_STATE };
15
+ listeners = new Set();
16
+ getState() {
17
+ return { ...this.state };
18
+ }
19
+ subscribe(listener) {
20
+ this.listeners.add(listener);
21
+ return () => {
22
+ this.listeners.delete(listener);
23
+ };
24
+ }
25
+ notify() {
26
+ for (const listener of this.listeners) {
27
+ listener();
28
+ }
29
+ }
30
+ update(partial) {
31
+ this.state = { ...this.state, ...partial };
32
+ this.notify();
33
+ }
34
+ setConfig(config) {
35
+ this.update(config);
36
+ }
37
+ setPhase(phase) {
38
+ this.update({ phase });
39
+ }
40
+ setStep(step) {
41
+ this.update({ step });
42
+ }
43
+ setTokenUsage(inputTokens, outputTokens) {
44
+ this.update({ inputTokens, outputTokens });
45
+ }
46
+ addItem(item) {
47
+ this.update({
48
+ items: [...this.state.items, item],
49
+ });
50
+ }
51
+ updateLastToolCall(update) {
52
+ const items = [...this.state.items];
53
+ for (let i = items.length - 1; i >= 0; i--) {
54
+ const it = items[i];
55
+ if (it && it.type === "tool_call") {
56
+ items[i] = { ...it, ...update };
57
+ this.update({ items });
58
+ return;
59
+ }
60
+ }
61
+ }
62
+ appendToStreamingContent(chunk) {
63
+ const items = [...this.state.items];
64
+ const last = items[items.length - 1];
65
+ if (last?.type === "assistant") {
66
+ items[items.length - 1] = {
67
+ ...last,
68
+ content: last.content + chunk,
69
+ };
70
+ }
71
+ else {
72
+ items.push({ type: "assistant", content: chunk });
73
+ }
74
+ this.update({ items });
75
+ }
76
+ setError(message) {
77
+ this.update({
78
+ errorMessage: message,
79
+ ...(message !== null ? { phase: "error" } : { phase: "idle" }),
80
+ });
81
+ }
82
+ clearAll() {
83
+ this.update({ items: [] });
84
+ }
85
+ reset() {
86
+ this.state = { ...DEFAULT_STATE };
87
+ this.notify();
88
+ }
89
+ }
@@ -0,0 +1,23 @@
1
+ import pc from "picocolors";
2
+ const noColor = process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "";
3
+ export const c = noColor
4
+ ? {
5
+ dim: (s) => s,
6
+ cyan: (s) => s,
7
+ green: (s) => s,
8
+ yellow: (s) => s,
9
+ red: (s) => s,
10
+ blue: (s) => s,
11
+ magenta: (s) => s,
12
+ bold: (s) => s,
13
+ }
14
+ : {
15
+ dim: pc.dim,
16
+ cyan: pc.cyan,
17
+ green: pc.green,
18
+ yellow: pc.yellow,
19
+ red: pc.red,
20
+ blue: pc.blue,
21
+ magenta: pc.magenta,
22
+ bold: pc.bold,
23
+ };