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/README.md +0 -13
- package/dist/app-QfQR2FN9.mjs +21 -0
- package/dist/app-QfQR2FN9.mjs.map +1 -0
- package/dist/main.mjs +21 -21
- package/dist/main.mjs.map +1 -1
- package/package.json +1 -1
- package/src/provider/stream.ts +1 -3
- package/src/session/store.ts +27 -4
- package/src/tui/app.tsx +23 -216
- package/src/tui/components/liveArea.tsx +73 -0
- package/src/tui/components/message.tsx +113 -0
- package/src/tui/components/statusBar.tsx +58 -0
- package/dist/app-BZ42XPxw.mjs +0 -21
- package/dist/app-BZ42XPxw.mjs.map +0 -1
package/package.json
CHANGED
package/src/provider/stream.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/session/store.ts
CHANGED
|
@@ -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 =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 {
|
|
10
|
-
import {
|
|
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
|
|
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 [
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
}
|
package/dist/app-BZ42XPxw.mjs
DELETED
|
@@ -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
|