@novaqore/atom 0.1.1 → 0.1.3

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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "./src/setup.js";
3
- import { run } from "./src/cli.js";
2
+ if (!process.env.FORCE_COLOR) process.env.FORCE_COLOR = "3";
3
+
4
+ const { run } = await import("./src/cli.js");
4
5
 
5
6
  run(process.argv).catch((err) => {
6
7
  console.error(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novaqore/atom",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A full-system code agent with root-level access, built for isolated servers.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -60,7 +60,11 @@ export function App({ version, unhinged }) {
60
60
  });
61
61
  }
62
62
  if (screen === "chat") {
63
- return React.createElement(ChatScreen, { version, unhinged });
63
+ return React.createElement(ChatScreen, {
64
+ version,
65
+ unhinged,
66
+ onBack: () => setScreen("main-menu"),
67
+ });
64
68
  }
65
69
  if (screen === "system") {
66
70
  return React.createElement(SystemScreen, {
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { renderMarkdown } from "../../utils/markdown.js";
4
+
5
+ export const AssistantMessage = React.memo(function AssistantMessage({
6
+ content,
7
+ streaming = false,
8
+ }) {
9
+ const rendered = renderMarkdown(content, { streaming });
10
+ const nlIdx = rendered.indexOf("\n");
11
+ const firstLine = nlIdx === -1 ? rendered : rendered.slice(0, nlIdx);
12
+ const rest = nlIdx === -1 ? "" : rendered.slice(nlIdx + 1);
13
+
14
+ return React.createElement(
15
+ Box,
16
+ { marginBottom: streaming ? 0 : 1, flexDirection: "column" },
17
+ React.createElement(
18
+ Text,
19
+ null,
20
+ React.createElement(Text, { bold: true, color: "cyan" }, "Atom:"),
21
+ " ",
22
+ firstLine
23
+ ),
24
+ rest ? React.createElement(Text, null, rest) : null
25
+ );
26
+ });
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ function formatMs(ms) {
5
+ return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
6
+ }
7
+
8
+ export function RunningIndicator({ elapsed }) {
9
+ return React.createElement(
10
+ Box,
11
+ { paddingLeft: 2, marginBottom: 1 },
12
+ React.createElement(
13
+ Text,
14
+ { dimColor: true },
15
+ `⎿ running ${formatMs(elapsed)}`
16
+ )
17
+ );
18
+ }
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { Text } from "ink";
3
+
4
+ function formatName(name) {
5
+ return name.charAt(0).toUpperCase() + name.slice(1);
6
+ }
7
+
8
+ function formatArgs(rawArgs) {
9
+ try {
10
+ const parsed = JSON.parse(rawArgs || "{}");
11
+ const keys = Object.keys(parsed);
12
+ if (keys.length === 1) return String(parsed[keys[0]]);
13
+ return keys.map((k) => `${k}=${JSON.stringify(parsed[k])}`).join(", ");
14
+ } catch {
15
+ return rawArgs || "";
16
+ }
17
+ }
18
+
19
+ export const ToolCall = React.memo(function ToolCall({ tc }) {
20
+ return React.createElement(
21
+ Text,
22
+ null,
23
+ React.createElement(
24
+ Text,
25
+ { bold: true, color: "yellow" },
26
+ formatName(tc.function.name)
27
+ ),
28
+ React.createElement(Text, { dimColor: true }, "("),
29
+ formatArgs(tc.function.arguments),
30
+ React.createElement(Text, { dimColor: true }, ")")
31
+ );
32
+ });
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ const MAX_OUTPUT_LINES = 2;
5
+
6
+ function formatMs(ms) {
7
+ return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
8
+ }
9
+
10
+ export const ToolResult = React.memo(function ToolResult({ msg, duration }) {
11
+ const content = (msg.content || "").trimEnd();
12
+ const dur = duration ? ` · ${formatMs(duration)}` : "";
13
+
14
+ if (!content) {
15
+ return React.createElement(
16
+ Box,
17
+ { paddingLeft: 2, marginBottom: 1 },
18
+ React.createElement(Text, { dimColor: true }, `⎿ (No output)${dur}`)
19
+ );
20
+ }
21
+
22
+ const lines = content.split("\n");
23
+ const visible = lines.slice(0, MAX_OUTPUT_LINES);
24
+ const overflow = lines.length - MAX_OUTPUT_LINES;
25
+
26
+ const children = visible.map((line, idx) => {
27
+ const isLastShown = idx === visible.length - 1 && overflow === 0;
28
+ const suffix = isLastShown ? dur : "";
29
+ return React.createElement(
30
+ Text,
31
+ { key: `line-${idx}`, dimColor: true },
32
+ idx === 0 ? `⎿ ${line}${suffix}` : ` ${line}${suffix}`
33
+ );
34
+ });
35
+ if (overflow > 0) {
36
+ children.push(
37
+ React.createElement(
38
+ Text,
39
+ { key: "overflow", dimColor: true },
40
+ ` … +${overflow} lines${dur}`
41
+ )
42
+ );
43
+ }
44
+
45
+ return React.createElement(
46
+ Box,
47
+ { flexDirection: "column", paddingLeft: 2, marginBottom: 1 },
48
+ children
49
+ );
50
+ });
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ export const UserMessage = React.memo(function UserMessage({ content }) {
5
+ return React.createElement(
6
+ Box,
7
+ { marginBottom: 1 },
8
+ React.createElement(
9
+ Text,
10
+ null,
11
+ React.createElement(Text, { bold: true, color: "white" }, "You:"),
12
+ " ",
13
+ React.createElement(Text, { dimColor: true }, content)
14
+ )
15
+ );
16
+ });
@@ -6,10 +6,14 @@ import { tools, executeTool } from "../tools/index.js";
6
6
  import { buildSystemPrompt } from "../utils/system-prompt.js";
7
7
  import { ChatInput } from "../components/Chat/ChatInput.js";
8
8
  import { ConfirmDangerousCommand } from "../components/Chat/ConfirmDangerousCommand.js";
9
+ import { UserMessage } from "../components/Chat/UserMessage.js";
10
+ import { AssistantMessage } from "../components/Chat/AssistantMessage.js";
11
+ import { ToolCall } from "../components/Chat/ToolCall.js";
12
+ import { ToolResult } from "../components/Chat/ToolResult.js";
13
+ import { RunningIndicator } from "../components/Chat/RunningIndicator.js";
9
14
  import { Header } from "../components/Header.js";
10
- import { renderMarkdown } from "../utils/markdown.js";
11
15
 
12
- export function ChatScreen({ version, unhinged }) {
16
+ export function ChatScreen({ version, unhinged, onBack }) {
13
17
  const [nq] = useState(() => new NovaQoreAI(SERVICE_FILE));
14
18
  const [messages, setMessages] = useState([]);
15
19
  const [input, setInput] = useState("");
@@ -23,6 +27,7 @@ export function ChatScreen({ version, unhinged }) {
23
27
  const [running, setRunning] = useState(null);
24
28
  const [durations, setDurations] = useState({});
25
29
  const stopRef = useRef(null);
30
+ const abortedRef = useRef(false);
26
31
 
27
32
  const toApiMessage = (m) => {
28
33
  if (m.role === "tool") {
@@ -44,11 +49,15 @@ export function ChatScreen({ version, unhinged }) {
44
49
  }, [running?.id]);
45
50
 
46
51
  useInput((_input, key) => {
47
- if (key.escape && sending && stopRef.current) {
52
+ if (!key.escape) return;
53
+ if (sending && stopRef.current) {
54
+ abortedRef.current = true;
48
55
  const stop = stopRef.current;
49
56
  stopRef.current = null;
50
57
  stop();
58
+ return;
51
59
  }
60
+ if (!sending && onBack) onBack();
52
61
  });
53
62
 
54
63
  const askConfirm = (command) =>
@@ -63,9 +72,6 @@ export function ChatScreen({ version, unhinged }) {
63
72
  setSending(true);
64
73
  setError(null);
65
74
 
66
- let acc = "";
67
- let toolCalls = [];
68
-
69
75
  try {
70
76
  while (true) {
71
77
  setStreaming("");
@@ -80,8 +86,8 @@ export function ChatScreen({ version, unhinged }) {
80
86
  );
81
87
  stopRef.current = stop;
82
88
 
83
- acc = "";
84
- toolCalls = [];
89
+ let acc = "";
90
+ const toolCalls = [];
85
91
  for await (const chunk of stream) {
86
92
  if (chunk.timings) {
87
93
  const t = chunk.timings;
@@ -122,12 +128,23 @@ export function ChatScreen({ version, unhinged }) {
122
128
  }
123
129
  }
124
130
 
131
+ setStreaming("");
132
+ setStreamingTools([]);
133
+
134
+ if (abortedRef.current) {
135
+ abortedRef.current = false;
136
+ if (acc.trim()) {
137
+ const partial = { role: "assistant", content: acc };
138
+ convo = [...convo, partial];
139
+ setMessages(convo);
140
+ }
141
+ break;
142
+ }
143
+
125
144
  const assistantMsg = { role: "assistant", content: acc };
126
145
  if (toolCalls.length > 0) assistantMsg.tool_calls = toolCalls;
127
146
  convo = [...convo, assistantMsg];
128
147
  setMessages(convo);
129
- setStreaming("");
130
- setStreamingTools([]);
131
148
 
132
149
  if (toolCalls.length === 0) break;
133
150
 
@@ -155,107 +172,16 @@ export function ChatScreen({ version, unhinged }) {
155
172
  }
156
173
  setSending(false);
157
174
  } catch (err) {
158
- const aborted =
159
- err.name === "AbortError" ||
160
- err.code === "ABORT_ERR" ||
161
- /aborted/i.test(err.message || "");
162
- if (aborted) {
163
- if (acc.trim() || toolCalls.length > 0) {
164
- const partial = { role: "assistant", content: acc };
165
- if (toolCalls.length > 0) partial.tool_calls = toolCalls;
166
- setMessages((prev) => [...prev, partial]);
167
- }
168
- setStreaming("");
169
- setStreamingTools([]);
170
- } else {
171
- setError(
172
- `${err.message} | ${err.cause?.code || ""} ${err.cause?.message || ""}`
173
- );
174
- }
175
+ setError(
176
+ `${err.message} | ${err.cause?.code || ""} ${err.cause?.message || ""}`
177
+ );
175
178
  setSending(false);
176
179
  } finally {
177
180
  stopRef.current = null;
181
+ abortedRef.current = false;
178
182
  }
179
183
  };
180
184
 
181
- const formatToolName = (name) =>
182
- name.charAt(0).toUpperCase() + name.slice(1);
183
-
184
- const formatToolArgs = (rawArgs) => {
185
- try {
186
- const parsed = JSON.parse(rawArgs || "{}");
187
- const keys = Object.keys(parsed);
188
- if (keys.length === 1) return String(parsed[keys[0]]);
189
- return keys.map((k) => `${k}=${JSON.stringify(parsed[k])}`).join(", ");
190
- } catch {
191
- return rawArgs || "";
192
- }
193
- };
194
-
195
- const renderToolCall = (tc, key) =>
196
- React.createElement(
197
- Box,
198
- { key },
199
- React.createElement(
200
- Text,
201
- { bold: true, color: "yellow" },
202
- formatToolName(tc.function.name)
203
- ),
204
- React.createElement(Text, { dimColor: true }, "("),
205
- React.createElement(Text, null, formatToolArgs(tc.function.arguments)),
206
- React.createElement(Text, { dimColor: true }, ")")
207
- );
208
-
209
- const MAX_OUTPUT_LINES = 2;
210
-
211
- const formatMs = (ms) =>
212
- ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
213
-
214
- const renderToolResult = (msg, key) => {
215
- const content = (msg.content || "").trimEnd();
216
- const ms = durations[msg.tool_call_id];
217
- const dur = ms ? ` · ${formatMs(ms)}` : "";
218
- if (!content) {
219
- return React.createElement(
220
- Box,
221
- { key, paddingLeft: 2, marginBottom: 1 },
222
- React.createElement(
223
- Text,
224
- { dimColor: true },
225
- `⎿ (No output)${dur}`
226
- )
227
- );
228
- }
229
- const lines = content.split("\n");
230
- const visible = lines.slice(0, MAX_OUTPUT_LINES);
231
- const overflow = lines.length - MAX_OUTPUT_LINES;
232
-
233
- const children = visible.map((line, idx) => {
234
- const isLastShown = idx === visible.length - 1 && overflow === 0;
235
- const suffix = isLastShown ? dur : "";
236
- return React.createElement(
237
- Text,
238
- { key: `line-${idx}`, dimColor: true },
239
- idx === 0 ? `⎿ ${line}${suffix}` : ` ${line}${suffix}`
240
- );
241
- });
242
- if (overflow > 0) {
243
- children.push(
244
- React.createElement(
245
- Text,
246
- { key: "overflow", dimColor: true },
247
- ` … +${overflow} lines${dur}`
248
- )
249
- );
250
- }
251
-
252
- return React.createElement(
253
- Box,
254
- { key, flexDirection: "column", paddingLeft: 2, marginBottom: 1 },
255
- children
256
- );
257
- };
258
-
259
185
  return React.createElement(
260
186
  Box,
261
187
  { flexDirection: "column" },
@@ -263,88 +189,59 @@ export function ChatScreen({ version, unhinged }) {
263
189
  ...messages.flatMap((msg, i) => {
264
190
  if (msg.role === "user") {
265
191
  return [
266
- React.createElement(
267
- Box,
268
- { key: `m-${i}`, marginBottom: 1 },
269
- React.createElement(Text, { bold: true, color: "white" }, "You: "),
270
- React.createElement(Text, { dimColor: true }, msg.content)
271
- ),
192
+ React.createElement(UserMessage, {
193
+ key: `m-${i}`,
194
+ content: msg.content,
195
+ }),
272
196
  ];
273
197
  }
274
198
  if (msg.role === "assistant") {
275
199
  const nodes = [];
276
200
  if (msg.content) {
277
- const rendered = renderMarkdown(msg.content);
278
- const nlIdx = rendered.indexOf("\n");
279
- const firstLine = nlIdx === -1 ? rendered : rendered.slice(0, nlIdx);
280
- const rest = nlIdx === -1 ? "" : rendered.slice(nlIdx + 1);
281
201
  nodes.push(
282
- React.createElement(
283
- Box,
284
- {
285
- key: `m-${i}`,
286
- marginBottom: 1,
287
- flexDirection: "column",
288
- },
289
- React.createElement(
290
- Box,
291
- null,
292
- React.createElement(
293
- Text,
294
- { bold: true, color: "cyan" },
295
- "Atom:"
296
- ),
297
- React.createElement(Text, null, " ", firstLine)
298
- ),
299
- rest ? React.createElement(Text, null, rest) : null
300
- )
202
+ React.createElement(AssistantMessage, {
203
+ key: `m-${i}`,
204
+ content: msg.content,
205
+ })
301
206
  );
302
207
  }
303
208
  if (msg.tool_calls) {
304
209
  for (let j = 0; j < msg.tool_calls.length; j++) {
305
- nodes.push(renderToolCall(msg.tool_calls[j], `m-${i}-tc-${j}`));
210
+ nodes.push(
211
+ React.createElement(ToolCall, {
212
+ key: `m-${i}-tc-${j}`,
213
+ tc: msg.tool_calls[j],
214
+ })
215
+ );
306
216
  }
307
217
  }
308
218
  return nodes;
309
219
  }
310
220
  if (msg.role === "tool") {
311
- return [renderToolResult(msg, `m-${i}`)];
221
+ return [
222
+ React.createElement(ToolResult, {
223
+ key: `m-${i}`,
224
+ msg,
225
+ duration: durations[msg.tool_call_id],
226
+ }),
227
+ ];
312
228
  }
313
229
  return [];
314
230
  }),
315
231
  streaming &&
316
- (() => {
317
- const rendered = renderMarkdown(streaming, { streaming: true });
318
- const nlIdx = rendered.indexOf("\n");
319
- const firstLine = nlIdx === -1 ? rendered : rendered.slice(0, nlIdx);
320
- const rest = nlIdx === -1 ? "" : rendered.slice(nlIdx + 1);
321
- return React.createElement(
322
- Box,
323
- { key: "streaming", flexDirection: "column" },
324
- React.createElement(
325
- Box,
326
- null,
327
- React.createElement(
328
- Text,
329
- { color: "cyan", bold: true },
330
- "Atom:"
331
- ),
332
- React.createElement(Text, null, " ", firstLine)
333
- ),
334
- rest ? React.createElement(Text, null, rest) : null
335
- );
336
- })(),
337
- ...streamingTools.map((tc, j) => renderToolCall(tc, `streaming-tc-${j}`)),
232
+ React.createElement(AssistantMessage, {
233
+ key: "streaming",
234
+ content: streaming,
235
+ streaming: true,
236
+ }),
237
+ ...streamingTools.map((tc, j) =>
238
+ React.createElement(ToolCall, { key: `streaming-tc-${j}`, tc })
239
+ ),
338
240
  running &&
339
- React.createElement(
340
- Box,
341
- { key: "running", paddingLeft: 2, marginBottom: 1 },
342
- React.createElement(
343
- Text,
344
- { dimColor: true },
345
- `⎿ running ${formatMs(running.elapsed)}`
346
- )
347
- ),
241
+ React.createElement(RunningIndicator, {
242
+ key: "running",
243
+ elapsed: running.elapsed,
244
+ }),
348
245
  error &&
349
246
  React.createElement(Text, { key: "error", color: "red" }, `Error: ${error}`),
350
247
  confirm
package/src/setup.js DELETED
@@ -1,3 +0,0 @@
1
- if (!process.env.FORCE_COLOR) {
2
- process.env.FORCE_COLOR = "3";
3
- }