novacode 0.5.3 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novacode",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Open-source multi-provider coding agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",
@@ -65,10 +65,8 @@ export class EventStream<T, R> {
65
65
  const item = await new Promise<T | undefined>((resolve) => {
66
66
  this.#resolve = resolve as (value: T) => void
67
67
  })
68
- if (item !== undefined && this.#events.length === 0) {
68
+ if (item !== undefined) {
69
69
  yield item
70
- } else if (this.#events.length > 0) {
71
- yield this.#events.shift() as T
72
70
  }
73
71
  }
74
72
  }
@@ -1,3 +1,4 @@
1
+ import { unlinkSync } from "node:fs"
1
2
  import { join } from "node:path"
2
3
  import BetterSqlite3 from "better-sqlite3"
3
4
  import type { Msg, Session } from "../types.ts"
@@ -43,10 +44,32 @@ export class SessionStore {
43
44
  #db: BetterSqlite3.Database
44
45
 
45
46
  constructor(dbPath: string) {
46
- this.#db = new BetterSqlite3(dbPath)
47
- this.#db.pragma("journal_mode = WAL")
48
- this.#db.pragma("foreign_keys = ON")
49
- this.#db.exec(SCHEMA)
47
+ this.#db = SessionStore.#open(dbPath)
48
+ }
49
+
50
+ // Opens and fully initialises the DB. If anything throws (e.g. corrupt file),
51
+ // the bad file is deleted and a fresh DB is created and returned.
52
+ static #open(dbPath: string): BetterSqlite3.Database {
53
+ const init = (db: BetterSqlite3.Database) => {
54
+ db.pragma("journal_mode = WAL")
55
+ db.pragma("foreign_keys = ON")
56
+ db.exec(SCHEMA)
57
+ return db
58
+ }
59
+ try {
60
+ return init(new BetterSqlite3(dbPath))
61
+ } catch {
62
+ // Delete the main DB and WAL sidecar files — all three must go or
63
+ // SQLite will fail again trying to replay a corrupt WAL on reopen.
64
+ for (const f of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
65
+ try {
66
+ unlinkSync(f)
67
+ } catch {
68
+ // file may already be absent — ignore
69
+ }
70
+ }
71
+ return init(new BetterSqlite3(dbPath))
72
+ }
50
73
  }
51
74
 
52
75
  create(cwd: string, model: string, provider: string): Session {
package/src/tui/app.tsx CHANGED
@@ -6,8 +6,9 @@ import { COMMANDS, dispatch } from "../commands/index.ts"
6
6
  import type { SessionStore } from "../session/store.ts"
7
7
  import type { Msg, Prompts } from "../types.ts"
8
8
  import { checkForUpdate, getCurrentVersion } from "../update.ts"
9
- import { formatToolArgs } from "../util.ts"
10
- import { formatMarkdown } from "./markdown.ts"
9
+ import { Cursor, LiveArea } from "./components/liveArea.tsx"
10
+ import { hasMeaningfulContent, Message } from "./components/message.tsx"
11
+ import { StatusBar } from "./components/statusBar.tsx"
11
12
  import { ConfirmPrompt, PasswordPrompt, SelectPrompt } from "./prompts.tsx"
12
13
 
13
14
  type PromptMode =
@@ -42,32 +43,8 @@ export async function interactive(
42
43
  }
43
44
  }
44
45
 
45
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
46
-
47
- function Spinner() {
48
- const [frame, setFrame] = useState(0)
49
-
50
- useEffect(() => {
51
- const timer = setInterval(() => {
52
- setFrame((f) => (f + 1) % SPINNER_FRAMES.length)
53
- }, 80)
54
- return () => clearInterval(timer)
55
- }, [])
56
-
57
- return <Text color="yellow">{SPINNER_FRAMES[frame]}</Text>
58
- }
59
-
60
- function Cursor() {
61
- const [visible, setVisible] = useState(true)
62
- useEffect(() => {
63
- const timer = setInterval(() => setVisible((v) => !v), 530)
64
- return () => clearInterval(timer)
65
- }, [])
66
- return <Text color="green">{visible ? "│" : " "}</Text>
67
- }
68
-
69
46
  function App({
70
- agent: initialAgent,
47
+ agent,
71
48
  store,
72
49
  sessionId,
73
50
  }: {
@@ -75,17 +52,13 @@ function App({
75
52
  store: SessionStore
76
53
  sessionId: string
77
54
  }) {
78
- const [agent, _setAgent] = useState(initialAgent)
79
- const [msgs, setMsgs] = useState<Msg[]>(initialAgent.messages)
55
+ const [msgs, setMsgs] = useState<Msg[]>(agent.messages)
80
56
  const [stream, setStream] = useState("")
81
57
  const [thinkStream, setThinkStream] = useState("")
82
58
  const [busy, setBusy] = useState(false)
83
59
  const [input, setInput] = useState("")
84
60
  const [status, setStatus] = useState("")
85
- const [usage, setUsage] = useState<{ in: number; out: number }>({
86
- in: 0,
87
- out: 0,
88
- })
61
+ const [usage, setUsage] = useState<{ in: number; out: number }>({ in: 0, out: 0 })
89
62
  const [selCmdIdx, setSelCmdIdx] = useState(0)
90
63
  const [mode, setMode] = useState<PromptMode>({ type: "chat" })
91
64
  const resolveRef = useRef<((v: unknown) => void) | null>(null)
@@ -241,7 +214,6 @@ function App({
241
214
  return
242
215
  }
243
216
 
244
- // Record user message before starting the stream
245
217
  const userMsg: Msg = { role: "user", content: line, ts: Date.now() }
246
218
  commitMsg(userMsg)
247
219
 
@@ -269,10 +241,9 @@ function App({
269
241
  break
270
242
  case "assistant_msg":
271
243
  commitMsg(ev.msg)
272
- setTimeout(() => {
273
- setStream("")
274
- setThinkStream("")
275
- }, 0)
244
+ setStream("")
245
+ setThinkStream("")
246
+
276
247
  break
277
248
  case "tool_call":
278
249
  setStatus(chalk.dim(`⏳ ${ev.call.name}…`))
@@ -336,41 +307,18 @@ function App({
336
307
 
337
308
  return (
338
309
  <Box flexDirection="column" paddingX={1}>
339
- {/* Messages - pushed to scrollback as they finish */}
340
310
  <Static items={visibleMsgs}>
341
311
  {(m, i) => <Message key={`${m.ts}-${i}`} msg={m} isFirst={i === 0} />}
342
312
  </Static>
343
313
 
344
- {/* Live Area (Streaming, active tool calls, working indicator) */}
345
- {isLiveActive && (
346
- <Box flexDirection="column" marginTop={visibleMsgs.length > 0 ? 1 : 0}>
347
- {thinkStream && (
348
- <Text dimColor italic>
349
- {thinkStream}
350
- </Text>
351
- )}
352
- {stream && (
353
- <Box flexDirection="row">
354
- <Box flexGrow={1} flexShrink={1}>
355
- <Text>
356
- {formatMarkdown(stream)}
357
- {!input && <Cursor />}
358
- </Text>
359
- </Box>
360
- </Box>
361
- )}
362
- {busy && !stream && (
363
- <Box flexDirection="row">
364
- <Box marginRight={1}>
365
- <Spinner />
366
- </Box>
367
- <Text dimColor>{status ? status.replace("⏳ ", "") : chalk.yellow("working…")}</Text>
368
- </Box>
369
- )}
370
- </Box>
371
- )}
314
+ <LiveArea
315
+ stream={stream}
316
+ thinkStream={thinkStream}
317
+ busy={busy}
318
+ status={status}
319
+ hasMessages={visibleMsgs.length > 0}
320
+ />
372
321
 
373
- {/* Input & Footer (Live) */}
374
322
  <Box flexDirection="column" marginTop={visibleMsgs.length > 0 || isLiveActive ? 1 : 0}>
375
323
  {updateInfo && (
376
324
  <Box
@@ -403,155 +351,14 @@ function App({
403
351
  </Box>
404
352
  </Box>
405
353
 
406
- {/* Dynamic Status / Info Line */}
407
- <Box justifyContent="space-between">
408
- <Box>
409
- {suggestions.length > 0 ? (
410
- <Box flexDirection="column" marginLeft={2}>
411
- {suggestions.map((s, i) => (
412
- <Box key={s.name}>
413
- <Text
414
- color={i === selCmdIdx ? "black" : "yellow"}
415
- backgroundColor={i === selCmdIdx ? "yellow" : undefined}
416
- >
417
- /{s.name.padEnd(10)}
418
- </Text>
419
- <Text dimColor> {s.desc}</Text>
420
- </Box>
421
- ))}
422
- </Box>
423
- ) : (
424
- <Text dimColor>Enter to send · /help for commands</Text>
425
- )}
426
- </Box>
427
-
428
- <Box>
429
- <Text dimColor>{formatTokenUsage(usage.in, agent.model.contextWindow)}</Text>
430
- <Text dimColor> │ </Text>
431
- <Text dimColor>{agent.model.id}</Text>
432
- {busy && <Text dimColor> │ Esc to stop</Text>}
433
- </Box>
434
- </Box>
354
+ <StatusBar
355
+ model={agent.model}
356
+ usage={usage}
357
+ busy={busy}
358
+ suggestions={suggestions}
359
+ selCmdIdx={selCmdIdx}
360
+ />
435
361
  </Box>
436
362
  </Box>
437
363
  )
438
364
  }
439
-
440
- function fmtK(n: number): string {
441
- const k = n / 1000
442
- return k >= 100 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`
443
- }
444
-
445
- function formatTokenUsage(used: number, contextWindow: number): string {
446
- if (used === 0) return `0/${fmtK(contextWindow)}`
447
- const pct = Math.round((used / contextWindow) * 100)
448
- return `${fmtK(used)}/${fmtK(contextWindow)} (${pct}%)`
449
- }
450
-
451
- const TOOL_STYLE: Record<string, string> = {
452
- read: "blue",
453
- write: "magenta",
454
- edit: "yellow",
455
- bash: "cyan",
456
- glob: "green",
457
- find: "green",
458
- grep: "green",
459
- tree: "green",
460
- }
461
-
462
- function hasMeaningfulContent(msg: Msg): boolean {
463
- if (msg.role === "user") return true
464
- if (msg.role === "tool_result") return true
465
- if (msg.role === "assistant") {
466
- if (msg.model === "system") return true
467
- return msg.content.some((c) => {
468
- if (c.type === "thinking") return c.text.trim().length > 0
469
- if (c.type === "text") return c.text.trim().length > 0
470
- return false
471
- })
472
- }
473
- return false
474
- }
475
-
476
- function Message({ msg, isFirst }: { msg: Msg; isFirst: boolean }) {
477
- if (msg.role === "user") {
478
- return (
479
- <Box marginTop={isFirst ? 0 : 1} flexDirection="row">
480
- <Box flexShrink={0} marginRight={1}>
481
- <Text bold color="green">
482
- {">"}
483
- </Text>
484
- </Box>
485
- <Box flexGrow={1} flexShrink={1}>
486
- <Text>
487
- {typeof msg.content === "string"
488
- ? msg.content
489
- : msg.content.map((c) => (c.type === "text" ? c.text : "")).join("")}
490
- </Text>
491
- </Box>
492
- </Box>
493
- )
494
- }
495
-
496
- if (msg.role === "assistant") {
497
- if (msg.model === "system") {
498
- return (
499
- <Box flexDirection="column" marginTop={0}>
500
- {msg.content.map((c, i) =>
501
- // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
502
- c.type === "text" ? <Text key={i}>{formatMarkdown(c.text)}</Text> : null,
503
- )}
504
- </Box>
505
- )
506
- }
507
-
508
- const hasVisibleContent = msg.content.some((c) => c.type === "text" || c.type === "thinking")
509
- if (!hasVisibleContent) return null
510
-
511
- return (
512
- <Box flexDirection="column" marginTop={0}>
513
- {msg.content.map((c, i) => {
514
- if (c.type === "thinking") {
515
- return (
516
- // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
517
- <Text key={i} dimColor italic>
518
- {c.text}
519
- </Text>
520
- )
521
- }
522
- if (c.type === "text") {
523
- // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
524
- return <Text key={i}>{formatMarkdown(c.text)}</Text>
525
- }
526
- return null
527
- })}
528
- </Box>
529
- )
530
- }
531
-
532
- if (msg.role === "tool_result") {
533
- const args = msg.args ? formatToolArgs(msg.args, true) : ""
534
-
535
- const resText = msg.content
536
- .map((c) => (c.type === "text" ? c.text : ""))
537
- .join("")
538
- .trim()
539
-
540
- const isRead = msg.tool === "read"
541
- const lineCount = isRead ? resText.split("\n").length : 0
542
- const color = TOOL_STYLE[msg.tool] || "white"
543
-
544
- return (
545
- <Box flexDirection="row">
546
- <Text color={msg.isError ? "red" : "green"}>{msg.isError ? "✗" : "✓"} </Text>
547
- <Text color={color} bold>
548
- {msg.tool}
549
- </Text>
550
- {args && <Text> {args}</Text>}
551
- {isRead && !msg.isError && <Text dimColor> ({lineCount} lines)</Text>}
552
- {msg.isError && resText && <Text color="red"> {resText.slice(0, 80)}</Text>}
553
- </Box>
554
- )
555
- }
556
- return null
557
- }
@@ -0,0 +1,73 @@
1
+ import chalk from "chalk"
2
+ import { Box, Text } from "ink"
3
+ import { useEffect, useState } from "react"
4
+ import { formatMarkdown } from "../markdown.ts"
5
+
6
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
7
+
8
+ export function Spinner() {
9
+ const [frame, setFrame] = useState(0)
10
+
11
+ useEffect(() => {
12
+ const timer = setInterval(() => {
13
+ setFrame((f) => (f + 1) % SPINNER_FRAMES.length)
14
+ }, 80)
15
+ return () => clearInterval(timer)
16
+ }, [])
17
+
18
+ return <Text color="yellow">{SPINNER_FRAMES[frame]}</Text>
19
+ }
20
+
21
+ export function Cursor() {
22
+ const [visible, setVisible] = useState(true)
23
+ useEffect(() => {
24
+ const timer = setInterval(() => setVisible((v) => !v), 530)
25
+ return () => clearInterval(timer)
26
+ }, [])
27
+ return <Text color="green">{visible ? "│" : " "}</Text>
28
+ }
29
+
30
+ export function LiveArea({
31
+ stream,
32
+ thinkStream,
33
+ busy,
34
+ status,
35
+ hasMessages,
36
+ }: {
37
+ stream: string
38
+ thinkStream: string
39
+ busy: boolean
40
+ status: string
41
+ hasMessages: boolean
42
+ }) {
43
+ const isActive = !!(stream || thinkStream || busy)
44
+ if (!isActive) return null
45
+
46
+ return (
47
+ <Box flexDirection="column" marginTop={hasMessages ? 1 : 0}>
48
+ {thinkStream && (
49
+ <Text dimColor italic>
50
+ {thinkStream}
51
+ </Text>
52
+ )}
53
+ {stream && (
54
+ <Box flexDirection="row">
55
+ <Box flexGrow={1} flexShrink={1}>
56
+ <Text>
57
+ {formatMarkdown(stream)}
58
+ <Cursor />
59
+ </Text>
60
+ </Box>
61
+ </Box>
62
+ )}
63
+ {busy && !stream && (
64
+ <Box flexDirection="row">
65
+ <Box marginRight={1}>
66
+ <Spinner />
67
+ </Box>
68
+ <Text dimColor>{status ? status.replace("⏳ ", "") : chalk.yellow("working…")}</Text>
69
+ </Box>
70
+ )}
71
+ </Box>
72
+ )
73
+ }
@@ -0,0 +1,113 @@
1
+ import { Box, Text } from "ink"
2
+ import type { Msg } from "../../types.ts"
3
+ import { formatToolArgs } from "../../util.ts"
4
+ import { formatMarkdown } from "../markdown.ts"
5
+
6
+ const TOOL_STYLE: Record<string, string> = {
7
+ read: "blue",
8
+ write: "magenta",
9
+ edit: "yellow",
10
+ bash: "cyan",
11
+ glob: "green",
12
+ find: "green",
13
+ grep: "green",
14
+ tree: "green",
15
+ }
16
+
17
+ export function hasMeaningfulContent(msg: Msg): boolean {
18
+ if (msg.role === "user") return true
19
+ if (msg.role === "tool_result") return true
20
+ if (msg.role === "assistant") {
21
+ if (msg.model === "system") return true
22
+ return msg.content.some((c) => {
23
+ if (c.type === "thinking") return c.text.trim().length > 0
24
+ if (c.type === "text") return c.text.trim().length > 0
25
+ return false
26
+ })
27
+ }
28
+ return false
29
+ }
30
+
31
+ export function Message({ msg, isFirst }: { msg: Msg; isFirst: boolean }) {
32
+ if (msg.role === "user") {
33
+ return (
34
+ <Box marginTop={isFirst ? 0 : 1} flexDirection="row">
35
+ <Box flexShrink={0} marginRight={1}>
36
+ <Text bold color="green">
37
+ {">"}
38
+ </Text>
39
+ </Box>
40
+ <Box flexGrow={1} flexShrink={1}>
41
+ <Text>
42
+ {typeof msg.content === "string"
43
+ ? msg.content
44
+ : msg.content.map((c) => (c.type === "text" ? c.text : "")).join("")}
45
+ </Text>
46
+ </Box>
47
+ </Box>
48
+ )
49
+ }
50
+
51
+ if (msg.role === "assistant") {
52
+ if (msg.model === "system") {
53
+ return (
54
+ <Box flexDirection="column" marginTop={0}>
55
+ {msg.content.map((c, i) =>
56
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
57
+ c.type === "text" ? <Text key={i}>{formatMarkdown(c.text)}</Text> : null,
58
+ )}
59
+ </Box>
60
+ )
61
+ }
62
+
63
+ const hasVisibleContent = msg.content.some((c) => c.type === "text" || c.type === "thinking")
64
+ if (!hasVisibleContent) return null
65
+
66
+ return (
67
+ <Box flexDirection="column" marginTop={0}>
68
+ {msg.content.map((c, i) => {
69
+ if (c.type === "thinking") {
70
+ return (
71
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
72
+ <Text key={i} dimColor italic>
73
+ {c.text}
74
+ </Text>
75
+ )
76
+ }
77
+ if (c.type === "text") {
78
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
79
+ return <Text key={i}>{formatMarkdown(c.text)}</Text>
80
+ }
81
+ return null
82
+ })}
83
+ </Box>
84
+ )
85
+ }
86
+
87
+ if (msg.role === "tool_result") {
88
+ const args = msg.args ? formatToolArgs(msg.args, true) : ""
89
+
90
+ const resText = msg.content
91
+ .map((c) => (c.type === "text" ? c.text : ""))
92
+ .join("")
93
+ .trim()
94
+
95
+ const isRead = msg.tool === "read"
96
+ const lineCount = isRead ? resText.split("\n").length : 0
97
+ const color = TOOL_STYLE[msg.tool] || "white"
98
+
99
+ return (
100
+ <Box flexDirection="row">
101
+ <Text color={msg.isError ? "red" : "green"}>{msg.isError ? "✗" : "✓"} </Text>
102
+ <Text color={color} bold>
103
+ {msg.tool}
104
+ </Text>
105
+ {args && <Text> {args}</Text>}
106
+ {isRead && !msg.isError && <Text dimColor> ({lineCount} lines)</Text>}
107
+ {msg.isError && resText && <Text color="red"> {resText.slice(0, 80)}</Text>}
108
+ </Box>
109
+ )
110
+ }
111
+
112
+ return null
113
+ }
@@ -0,0 +1,58 @@
1
+ import { Box, Text } from "ink"
2
+ import type { Model } from "../../types.ts"
3
+
4
+ function fmtK(n: number): string {
5
+ const k = n / 1000
6
+ return k >= 100 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`
7
+ }
8
+
9
+ function formatTokenUsage(used: number, contextWindow: number): string {
10
+ if (used === 0) return `0/${fmtK(contextWindow)}`
11
+ const pct = Math.round((used / contextWindow) * 100)
12
+ return `${fmtK(used)}/${fmtK(contextWindow)} (${pct}%)`
13
+ }
14
+
15
+ export function StatusBar({
16
+ model,
17
+ usage,
18
+ busy,
19
+ suggestions,
20
+ selCmdIdx,
21
+ }: {
22
+ model: Model
23
+ usage: { in: number; out: number }
24
+ busy: boolean
25
+ suggestions: Array<{ name: string; desc: string }>
26
+ selCmdIdx: number
27
+ }) {
28
+ return (
29
+ <Box justifyContent="space-between">
30
+ <Box>
31
+ {suggestions.length > 0 ? (
32
+ <Box flexDirection="column" marginLeft={2}>
33
+ {suggestions.map((s, i) => (
34
+ <Box key={s.name}>
35
+ <Text
36
+ color={i === selCmdIdx ? "black" : "yellow"}
37
+ backgroundColor={i === selCmdIdx ? "yellow" : undefined}
38
+ >
39
+ /{s.name.padEnd(10)}
40
+ </Text>
41
+ <Text dimColor> {s.desc}</Text>
42
+ </Box>
43
+ ))}
44
+ </Box>
45
+ ) : (
46
+ <Text dimColor>Enter to send · /help for commands</Text>
47
+ )}
48
+ </Box>
49
+
50
+ <Box>
51
+ <Text dimColor>{formatTokenUsage(usage.in, model.contextWindow)}</Text>
52
+ <Text dimColor> │ </Text>
53
+ <Text dimColor>{model.id}</Text>
54
+ {busy && <Text dimColor> │ Esc to stop</Text>}
55
+ </Box>
56
+ </Box>
57
+ )
58
+ }
@@ -1,21 +0,0 @@
1
- import{a as e,c as t,d as n,f as r,g as i,h as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,s as p,t as m,u as h}from"./main.mjs";import g from"chalk";import{Box as _,Static as v,Text as y,render as b,useInput as ee}from"ink";import{useCallback as x,useEffect as S,useRef as C,useState as w}from"react";import{jsx as T,jsxs as E}from"react/jsx-runtime";function D(e){return typeof e.content==`string`?e.content:e.content.filter(e=>e.type===`text`).map(e=>e.type===`text`?e.text:``).join(``)}function O(e,t){return e.role!==`tool_result`||!(`tool`in e)||e.tool!==t?[]:D(e).split(`
2
- `).filter(e=>e.trim().length>0)}function k(e,t){return c(e)>t*.8}async function A(e,t,n,r,i,a){if(!k(n,r.contextWindow))return{compacted:!1,msgsRemoved:0};let o=n.slice(0,-10);if(o.length===0)return{compacted:!1,msgsRemoved:0};let s=await j(o.map(e=>e.role===`user`?`User: ${D(e)}`:e.role===`assistant`?`Assistant: ${D(e)}`:e.role===`tool_result`&&`tool`in e?`Tool(${e.tool}): ${D(e).slice(0,200)}`:``).join(`
3
-
4
- `),r,i,a);if(!s)return{compacted:!1,msgsRemoved:0};let c=[],l=[];for(let e of o)c.push(...O(e,`read`)),c.push(...O(e,`glob`)),l.push(...O(e,`write`)),l.push(...O(e,`edit`));let u=o.length;e.saveCompaction(t,s,[...new Set(c)],[...new Set(l)],u),e.truncateBeforeSeq(t,u+1);let d={role:`user`,content:`[Prior context summary]\n${s}`,ts:Date.now()};return e.append(t,d),{compacted:!0,summary:s,msgsRemoved:o.length}}async function j(e,t,n,r){let a=d(t.provider);if(!a)return null;let o=i({api:a.api,model:t,apiKey:n,baseUrl:r,system:`Summarize this coding session concisely. Cover: what was asked, files touched, what was done, key decisions. Keep it under 300 words.`,messages:[{role:`user`,content:e,ts:Date.now()}],tools:[]}),s=``;for await(let e of o)e.type===`text_delta`&&e.text&&(s+=e.text);return s.trim()||null}async function M(e,t,n){let r=await A(t,n,e.messages,e.model,e.apiKey,e.baseUrl);if(r.compacted){let i=t.messages(n);return e.setMessages(i),g.green(`✓ Context compacted (${r.msgsRemoved} messages removed)`)}return g.yellow(`Context is small enough, no compaction needed.`)}async function N(e,r,i){let a=await t(),o=await p();if(e)return await P(e.trim(),r);if(!i)return g.red(`Prompts not available in this context`);let s=[];for(let e of n){let t=e.id===a.model&&e.provider===a.provider,n=d(e.provider);n&&o.apiKeys[e.provider]&&s.push({value:`${e.provider}:${e.id}`,label:`${t?g.green(`●`):`○`} ${e.id.padEnd(20)} ${F(e.contextWindow).padEnd(8)}`,hint:n.name})}if(!s.length)return g.yellow(`No models available. Use /providers to add a provider API key.`);let c=await i.select({message:`Model`,options:s});if(!c)return``;let[l,u]=c.split(`:`),f=n.find(e=>e.provider===l&&e.id===u),m=d(l);return!f||!m?g.red(`Error: Model or provider not found`):(a.provider=l,a.model=u,await h(a),r.updateConfig({api:m.api,model:f,apiKey:o.apiKeys[l]??``,baseUrl:m.baseUrl}),g.green(`✓ Switched to ${u}`))}async function P(e,r){let i=await t(),a=await p(),o=n.find(t=>t.id===e);if(!o)return g.yellow(`"${e}" not found. Use /models`);let s=o.provider;if(!a.apiKeys[s])return g.yellow(`No API key configured for ${s}. Use /providers`);let c=d(s);return c?(i.provider=s,i.model=e,await h(i),r.updateConfig({api:c.api,model:o,apiKey:a.apiKeys[s],baseUrl:c.baseUrl}),g.green(`✓ Switched to ${e}`)):g.red(`Error: Provider not found`)}const F=e=>e>=1e6?`${e/1e6}M`:`${e/1e3}K`;async function I(e,i){if(!i)return g.red(`Prompts not available in this context`);let a=await t(),o=await p(),s=r.filter(e=>!!o.apiKeys[e.id]),c=s.length===0?g.dim(`No providers configured. Use 'Add Provider' below.`):s.map(e=>{let t=e.id===a.provider,r=t?g.green(` ●`):``,i=t?a.model:n.find(t=>t.provider===e.id)?.id??``;return` ✅ ${e.name.padEnd(24)} ${i}${r}`}).join(`
5
- `),l=await i.select({message:`Action`,header:c,options:[{value:`add`,label:`Add Provider`},{value:`update`,label:`Update API Key`},{value:`remove`,label:`Remove API Key`},{value:`default`,label:`Set Default Provider`},{value:`back`,label:`Back`}]});return!l||l===`back`?``:l===`add`?L(e,i):l===`update`?R(e,i):l===`remove`?z(e,i):l===`default`?B(e,i):``}async function L(e,i){let a=await p(),o=await t(),c=r.filter(e=>!a.apiKeys[e.id]);if(c.length===0)return g.yellow(`All providers already have API keys configured.`);let l=await i.select({message:`Add Provider`,options:c.map(e=>({value:e.id,label:e.name}))});if(!l)return``;let u=d(l);if(!u)return g.red(`Error: Provider not found`);let f=await i.password({message:`${u.name} API Key`,validate:e=>!e||e.length<8?`Enter a valid key`:void 0});if(!f)return``;if(a.apiKeys[u.id]=f,await s(a),!o.provider){o.provider=u.id;let t=n.find(e=>e.provider===u.id);t&&(o.model=t.id),await h(o),e.updateConfig({api:u.api,model:n.find(e=>e.id===o.model),apiKey:f,baseUrl:u.baseUrl})}return g.green(`✓ ${u.name} configured`)}async function R(e,i){let a=await p(),o=r.filter(e=>!!a.apiKeys[e.id]);if(o.length===0)return g.yellow(`No providers configured. Use 'Add Provider' first.`);let c=await i.select({message:`Update API Key`,options:o.map(e=>({value:e.id,label:e.name}))});if(!c)return``;let l=d(c);if(!l)return g.red(`Error: Provider not found`);let u=await i.password({message:`New key for ${l.name}`});if(!u)return``;a.apiKeys[l.id]=u,await s(a);let f=await t();if(f.provider===l.id){let t=n.find(e=>e.id===f.model&&e.provider===f.provider);t&&e.updateConfig({api:l.api,model:t,apiKey:u,baseUrl:l.baseUrl})}return g.green(`✓ Key updated`)}async function z(e,i){let a=await p(),o=await t(),c=r.filter(e=>!!a.apiKeys[e.id]);if(c.length===0)return g.yellow(`No configured providers to remove.`);let l=await i.select({message:`Remove API Key`,options:c.map(e=>({value:e.id,label:e.name}))});if(!l||!await i.confirm({message:`Are you sure you want to remove the API key for ${l}?`}))return``;if(delete a.apiKeys[l],await s(a),o.provider===l){o.provider=``,o.model=``;let t=Object.keys(a.apiKeys)[0];if(t){let r=d(t),i=n.find(e=>e.provider===t);r&&i&&(o.provider=t,o.model=i.id,e.updateConfig({api:r.api,model:i,apiKey:a.apiKeys[t],baseUrl:r.baseUrl}))}await h(o)}return g.green(`✓ Removed API key for ${l}`)}async function B(e,i){let a=await t(),o=await p(),s=await i.select({message:`Default Provider`,options:r.map(e=>({value:e.id,label:`${o.apiKeys[e.id]?`✅`:`❌`} ${e.name}`}))});if(!s)return``;if(!o.apiKeys[s])return g.yellow(`No API key for ${s}. Please set one first.`);let c=d(s),l=n.find(e=>e.provider===s);return!c||!l?g.red(`Error: Provider or model not found`):(a.provider=s,a.model=l.id,await h(a),e.updateConfig({api:c.api,model:l,apiKey:o.apiKeys[s],baseUrl:c.baseUrl}),g.green(`✓ Default set to ${c.name} (${l.id})`))}const V=[{name:`models`,desc:`Switch model`,aliases:[`model`]},{name:`providers`,desc:`Manage providers`,aliases:[`prov`,`config`,`cfg`]},{name:`compact`,desc:`Compact context`},{name:`update`,desc:`Update novacode`},{name:`help`,desc:`Show help`},{name:`clear`,desc:`Clear screen`},{name:`quit`,desc:`Exit (Ctrl+D)`,aliases:[`exit`]}],H=`
6
- ${g.bold(`Commands:`)}
7
- ${V.map(e=>` /${e.name.padEnd(12)} ${e.desc}`).join(`
8
- `)}
9
-
10
- ${g.bold(`CLI:`)}
11
- nova update Update to latest version
12
- nova session ls List sessions
13
-
14
- ${g.dim(`Keys:`)}
15
- Esc Abort
16
- ↑ / ↓ History
17
- `;async function te(e,t,n,r,i){let[a,...o]=e.slice(1).split(` `),s=o.join(` `);switch(a){case`models`:case`model`:return N(s,t,i);case`providers`:case`prov`:case`config`:case`cfg`:return I(t,i);case`compact`:return!n||!r?g.red(`Session store not available`):M(t,n,r);case`update`:return U();case`help`:return H;case`clear`:return console.clear(),``;case`quit`:return process.exit(0),null;case`exit`:return process.exit(0),null;default:return g.yellow(`Unknown: /${a}. Type /help`)}}async function U(){let e=await m();return e?e.hasUpdate?(console.log(g.yellow(`\n⚡ Updating novacode to v${e.latest}...`)),await f(!0)?g.green(`✓ Successfully updated to v${e.latest}! Please restart nova to apply changes.`):g.red(`✗ Update failed. Please try running 'nova update' manually in your terminal.`)):g.green(`✓ Already up to date (v${e.current})`):g.yellow(`Could not check for updates.`)}var W=class{#e=!1;#t=``;renderLine(e){if(e.startsWith("```"))return this.#e?(this.#e=!1,g.dim(`└${`─`.repeat(50)}`)):(this.#e=!0,this.#t=e.slice(3).trim(),g.dim(`┌`+`─`.repeat(10)+` [Code: ${this.#t||`text`}] `+`─`.repeat(40-(this.#t?.length||4))));if(this.#e)return g.cyan(`│ ${e}`);if(e.startsWith(`#`)){let t=e.match(/^(#{1,6})\s+(.*)$/);if(t?.[1]&&t[2]){let e=t[1].length,n=t[2];return e===1?g.bold.magenta.underline(n):e===2?g.bold.blue(n):g.bold.cyan(n)}}let t=e;return(t.startsWith(`- `)||t.startsWith(`* `))&&(t=` ${g.yellow(`•`)} ${t.slice(2)}`),t=t.replace(/`([^`]+)`/g,(e,t)=>g.yellow(t)),t=t.replace(/\*\*([^*]+)\*\*/g,(e,t)=>g.bold(t)),t=t.replace(/__([^_]+)__/g,(e,t)=>g.bold(t)),t=t.replace(/\*([^*]+)\*/g,(e,t)=>g.italic(t)),t=t.replace(/_([^_]+)_/g,(e,t)=>g.italic(t)),t=t.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(e,t,n)=>`${g.blue(t)} ${g.dim(`(${n})`)}`),t}};function G(e){let t=new W;return e.split(`
18
- `).map(e=>t.renderLine(e)).join(`
19
- `)}async function K(e,t,n){process.stdout.write(`\x1B[?25l`);let r=await l();process.stdout.write(`${g.cyan.bold(`⚡ novacode`)} ${g.gray(`v${r}`)}\n`);try{let{waitUntilExit:r}=b(T(Y,{agent:e,store:t,sessionId:n}));await r()}finally{process.stdout.write(`\x1B[?25h`)}}const q=[`⠋`,`⠙`,`⠹`,`⠸`,`⠼`,`⠴`,`⠦`,`⠧`,`⠇`,`⠏`];function ne(){let[e,t]=w(0);return S(()=>{let e=setInterval(()=>{t(e=>(e+1)%q.length)},80);return()=>clearInterval(e)},[]),T(y,{color:`yellow`,children:q[e]})}function J(){let[e,t]=w(!0);return S(()=>{let e=setInterval(()=>t(e=>!e),530);return()=>clearInterval(e)},[]),T(y,{color:`green`,children:e?`│`:` `})}function Y({agent:t,store:n,sessionId:r}){let[i,a]=w(t),[s,c]=w(t.messages),[l,d]=w(``),[f,p]=w(``),[h,b]=w(!1),[D,O]=w(``),[k,A]=w(``),[j,M]=w({in:0,out:0}),[N,P]=w(0),[F,I]=w({type:`chat`}),L=C(null),R=C([]),z=C(-1),B=C(null),[H,U]=w(null);S(()=>{(async()=>{let e=await m();e?.hasUpdate&&U({current:e.current,latest:e.latest})})()},[]);let W=D.startsWith(`/`)&&!D.includes(` `),K=W?V.filter(e=>e.name.startsWith(D.slice(1).toLowerCase())||e.aliases?.some(e=>e.startsWith(D.slice(1).toLowerCase()))):[],q={select:x(e=>new Promise(t=>{L.current=t,I({type:`select`,...e})}),[]),password:x(e=>new Promise(t=>{L.current=t,I({type:`password`,...e})}),[]),confirm:x(e=>new Promise(t=>{L.current=t,I({type:`confirm`,...e})}),[])};function Y(e){let t=L.current;L.current=null,I({type:`chat`}),t?.(e)}function X(e){c(t=>[...t,e]),i.setMessages([...i.messages,e]),n.append(r,e)}S(()=>{P(0)},[D]),ee((e,t)=>{if(F.type!==`chat`)return;if(t.escape){B.current&&=(B.current.abort(),null);return}if(t.upArrow){if(W&&K.length>0){P(e=>e>0?e-1:K.length-1);return}R.current.length>0&&(z.current=Math.min(z.current+1,R.current.length-1),O(R.current[z.current]??``));return}if(t.downArrow){if(W&&K.length>0){P(e=>e<K.length-1?e+1:0);return}z.current=Math.max(z.current-1,-1),O(z.current>=0?R.current[z.current]??``:``);return}if(t.tab){if(W&&K.length>0){let e=K[N];e&&O(`/${e.name} `)}return}if(!t.return){O(n=>t.backspace||t.delete?n.slice(0,-1):n+(e||``));return}if(h)return;let a=D.trim();if(a){if(W&&K.length>0){let e=K[N];e&&(a=`/${e.name}`)}if(O(``),R.current.unshift(a),z.current=-1,a.startsWith(`/`)){te(a,i,n,r,q).then(e=>{e&&X({role:`assistant`,content:[{type:`text`,text:e}],model:`system`,provider:`system`,usage:{in:0,out:0},stop:`stop`,ts:Date.now()})});return}X({role:`user`,content:a,ts:Date.now()}),B.current=new AbortController,Z(i.prompt(a,B.current.signal))}});async function Z(e){try{for await(let t of e)switch(t.type){case`start`:b(!0),d(``),p(``),A(``);break;case`text_delta`:t.text&&d(e=>e+t.text);break;case`thinking_delta`:t.text&&p(e=>e+t.text);break;case`assistant_msg`:X(t.msg),setTimeout(()=>{d(``),p(``)},0);break;case`tool_call`:A(g.dim(`⏳ ${t.call.name}…`));break;case`tool_result`:X(t.result),A(t.result.isError?g.red(`✗ ${t.result.tool}`):g.green(`✓ ${t.result.tool}`));break;case`turn_end`:A(``);break;case`usage`:t.usage&&M(t.usage)}}catch(e){X({role:`assistant`,model:`system`,provider:`system`,content:[{type:`text`,text:g.red(`Error: ${e.message}`)}],usage:{in:0,out:0},stop:`error`,ts:Date.now()})}finally{B.current=null,b(!1),d(``),p(``),A(``)}}if(F.type===`select`)return T(u,{message:F.message,options:F.options,header:F.header,onSelect:Y});if(F.type===`password`)return T(e,{message:F.message,validate:F.validate,onSubmit:Y});if(F.type===`confirm`)return T(o,{message:F.message,onConfirm:Y});let Q=s.filter(ie),$=!!(l||f||h);return E(_,{flexDirection:`column`,paddingX:1,children:[T(v,{items:Q,children:(e,t)=>T(ae,{msg:e,isFirst:t===0},`${e.ts}-${t}`)}),$&&E(_,{flexDirection:`column`,marginTop:+(Q.length>0),children:[f&&T(y,{dimColor:!0,italic:!0,children:f}),l&&T(_,{flexDirection:`row`,children:T(_,{flexGrow:1,flexShrink:1,children:E(y,{children:[G(l),!D&&T(J,{})]})})}),h&&!l&&E(_,{flexDirection:`row`,children:[T(_,{marginRight:1,children:T(ne,{})}),T(y,{dimColor:!0,children:k?k.replace(`⏳ `,``):g.yellow(`working…`)})]})]}),E(_,{flexDirection:`column`,marginTop:Q.length>0||$?1:0,children:[H&&E(_,{borderStyle:`round`,borderColor:`yellow`,paddingX:1,marginBottom:1,flexDirection:`column`,children:[E(y,{color:`yellow`,bold:!0,children:[`⬆ Update Available (v`,H.current,` → v`,H.latest,`)`]}),E(y,{dimColor:!0,children:[`Run `,T(y,{color:`cyan`,children:`/update`}),` or `,T(y,{color:`cyan`,children:`nova update`}),` to upgrade.`]})]}),E(_,{flexDirection:`row`,children:[T(_,{flexShrink:0,marginRight:1,children:T(y,{bold:!0,color:`green`,children:`>`})}),T(_,{flexGrow:1,flexShrink:1,children:E(y,{children:[D,T(J,{})]})})]}),E(_,{justifyContent:`space-between`,children:[T(_,{children:K.length>0?T(_,{flexDirection:`column`,marginLeft:2,children:K.map((e,t)=>E(_,{children:[E(y,{color:t===N?`black`:`yellow`,backgroundColor:t===N?`yellow`:void 0,children:[`/`,e.name.padEnd(10)]}),E(y,{dimColor:!0,children:[` `,e.desc]})]},e.name))}):T(y,{dimColor:!0,children:`Enter to send · /help for commands`})}),E(_,{children:[T(y,{dimColor:!0,children:re(j.in,i.model.contextWindow)}),T(y,{dimColor:!0,children:` │ `}),T(y,{dimColor:!0,children:i.model.id}),h&&T(y,{dimColor:!0,children:` │ Esc to stop`})]})]})]})]})}function X(e){let t=e/1e3;return t>=100?`${Math.round(t)}K`:`${t.toFixed(1)}K`}function re(e,t){if(e===0)return`0/${X(t)}`;let n=Math.round(e/t*100);return`${X(e)}/${X(t)} (${n}%)`}const Z={read:`blue`,write:`magenta`,edit:`yellow`,bash:`cyan`,glob:`green`,find:`green`,grep:`green`,tree:`green`};function ie(e){return e.role===`user`||e.role===`tool_result`?!0:e.role===`assistant`?e.model===`system`?!0:e.content.some(e=>e.type===`thinking`||e.type===`text`?e.text.trim().length>0:!1):!1}function ae({msg:e,isFirst:t}){if(e.role===`user`)return E(_,{marginTop:+!t,flexDirection:`row`,children:[T(_,{flexShrink:0,marginRight:1,children:T(y,{bold:!0,color:`green`,children:`>`})}),T(_,{flexGrow:1,flexShrink:1,children:T(y,{children:typeof e.content==`string`?e.content:e.content.map(e=>e.type===`text`?e.text:``).join(``)})})]});if(e.role===`assistant`)return e.model===`system`?T(_,{flexDirection:`column`,marginTop:0,children:e.content.map((e,t)=>e.type===`text`?T(y,{children:G(e.text)},t):null)}):e.content.some(e=>e.type===`text`||e.type===`thinking`)?T(_,{flexDirection:`column`,marginTop:0,children:e.content.map((e,t)=>e.type===`thinking`?T(y,{dimColor:!0,italic:!0,children:e.text},t):e.type===`text`?T(y,{children:G(e.text)},t):null)}):null;if(e.role===`tool_result`){let t=e.args?a(e.args,!0):``,n=e.content.map(e=>e.type===`text`?e.text:``).join(``).trim(),r=e.tool===`read`,i=r?n.split(`
20
- `).length:0,o=Z[e.tool]||`white`;return E(_,{flexDirection:`row`,children:[E(y,{color:e.isError?`red`:`green`,children:[e.isError?`✗`:`✓`,` `]}),T(y,{color:o,bold:!0,children:e.tool}),t&&E(y,{children:[` `,t]}),r&&!e.isError&&E(y,{dimColor:!0,children:[` (`,i,` lines)`]}),e.isError&&n&&E(y,{color:`red`,children:[` `,n.slice(0,80)]})]})}return null}export{K as interactive};
21
- //# sourceMappingURL=app-BZ42XPxw.mjs.map