@wenathlan/devthink 1.1.7 → 1.1.9
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/CHANGELOG.md +24 -0
- package/devthink.ts +12 -3
- package/package.json +38 -29
- package/terminal-ui.tsx +86 -0
- package/ui.ts +39 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.9 — Unified terminal workspace
|
|
4
|
+
|
|
5
|
+
This release reconstructs DevThink as one command-first terminal workspace across its static React workbench and interactive CLI. The browser is the route-addressable React surface; the CLI is a React Ink terminal surface with the same categories, empty states, local credential boundary and command rail.
|
|
6
|
+
|
|
7
|
+
| Area | Change |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Shared information architecture | Aligns `features`, `bugs`, `refactor`, `snippets`, `tasks`, `notes`, `all` and `settings` across web and CLI. |
|
|
10
|
+
| Browser workbench | Replaces the dashboard shell with a sparse black terminal workspace, top category strip, central canvas and bottom command rail. |
|
|
11
|
+
| Interactive CLI | Adds an Ink React renderer for TTY sessions, preserving the canonical ANSI fallback for noninteractive and compatibility contexts. |
|
|
12
|
+
| Binary portability | Uses Ink rather than a native TUI dependency so Bun can bundle the CLI for Linux, macOS and Windows release targets. |
|
|
13
|
+
| Release metadata | Aligns the CLI, static workbench, mobile shell, extension and package adapters on version 1.1.9. |
|
|
14
|
+
|
|
15
|
+
## 1.1.8 — Workflow trust boundaries and package adapters
|
|
16
|
+
|
|
17
|
+
This release removes `workflow_run` as a trigger from privileged distribution workflows. Publishing jobs now check out only a release tag or a manually supplied authenticated release tag, preventing a completed workflow from selecting arbitrary source code before package or release credentials become available.
|
|
18
|
+
|
|
19
|
+
| Area | Change |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| CodeQL remediation | Removes untrusted `workflow_run.head_sha` checkouts from npm, GitHub Packages, extension, container, GHCR and mobile publication workflows. |
|
|
22
|
+
| Maven | Adds a Java 17 process adapter and a source-backed Maven publication workflow for GitHub Packages. |
|
|
23
|
+
| NuGet | Adds a .NET 8 process adapter, package README and a source-backed NuGet publication workflow for GitHub Packages. |
|
|
24
|
+
| RubyGems | Adds a Ruby process adapter and a source-backed RubyGems publication workflow for GitHub Packages. |
|
|
25
|
+
| Release metadata | Aligns the CLI, static workbench, mobile shell, extension and adapter packages on version 1.1.8. |
|
|
26
|
+
|
|
3
27
|
## 1.1.7 — Security remediation
|
|
4
28
|
|
|
5
29
|
This release resolves the Trivy Dockerfile finding `DS-0026` by declaring an executable container health check that verifies the Bun-compiled DevThink CLI is available in the nonroot distroless runtime. The security workflow keeps its active Trivy, OSV, pnpm-audit and verified-secret gates; no scanner, severity threshold or release protection is weakened.
|
package/devthink.ts
CHANGED
|
@@ -116,7 +116,7 @@ function printEvents(events: ChatEvent[], json: boolean): void {
|
|
|
116
116
|
process.stdout.write("\n");
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
async function runChat(prompt: string, parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>, current?: Session): Promise<Session | undefined> {
|
|
119
|
+
async function runChat(prompt: string, parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>, current?: Session, render?: { onEvent?: (event: ChatEvent) => void; silent?: boolean }): Promise<Session | undefined> {
|
|
120
120
|
const provider = stringFlag(parsed, "provider") || runtime.config.activeProvider || runtime.config.provider;
|
|
121
121
|
const model = stringFlag(parsed, "model") || runtime.config.activeModel || runtime.config.model;
|
|
122
122
|
const mode = stringFlag(parsed, "mode") || "chat";
|
|
@@ -137,7 +137,8 @@ async function runChat(prompt: string, parsed: ParsedArgs, runtime: ReturnType<t
|
|
|
137
137
|
events.push(event);
|
|
138
138
|
if (event.type === "text") text += event.text;
|
|
139
139
|
if (event.type === "reasoning") reasoning += event.text;
|
|
140
|
-
|
|
140
|
+
render?.onEvent?.(event);
|
|
141
|
+
if (!json && !render?.silent) {
|
|
141
142
|
const rendered = formatEvent(event);
|
|
142
143
|
if (rendered) process.stdout.write(rendered);
|
|
143
144
|
}
|
|
@@ -145,7 +146,7 @@ async function runChat(prompt: string, parsed: ParsedArgs, runtime: ReturnType<t
|
|
|
145
146
|
const withAssistant = appendMessage(runtime.paths, nextSession, { role: "assistant", content: text });
|
|
146
147
|
runtime.memory.remember({ layer: "session", key: withAssistant.id, content: text.slice(0, 2000) });
|
|
147
148
|
if (json) console.log(JSON.stringify({ workspaceId: withAssistant.workspaceId, sessionId: withAssistant.id, tabId: withAssistant.activeTabId, messageId: withAssistant.messages.at(-1)?.id, provider, model, mode: modeDefinition.id, text, reasoning }));
|
|
148
|
-
else process.stdout.write("\n");
|
|
149
|
+
else if (!render?.silent) process.stdout.write("\n");
|
|
149
150
|
return withAssistant;
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -267,6 +268,14 @@ function handleRoutes(): void {
|
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
async function interactive(runtime: ReturnType<typeof loadRuntime>): Promise<void> {
|
|
271
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.env.DEVTHINK_PLAIN !== "1") {
|
|
272
|
+
try {
|
|
273
|
+
const { startTerminalWorkspace } = await import("./terminal-ui.tsx");
|
|
274
|
+
return await startTerminalWorkspace(runtime, version(), (prompt, current, onEvent) => runChat(prompt, { flags: { mode: runtime.config.mode || "chat" }, positional: [] }, runtime, current, { onEvent, silent: true }));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
process.stderr.write(`${colors.yellow}Terminal workspace fallback: ${error instanceof Error ? error.message : "renderer unavailable"}${colors.reset}\n`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
270
279
|
const input = createInterface({ input: stdin, output: stdout, terminal: true });
|
|
271
280
|
let session: Session | undefined;
|
|
272
281
|
let mode = runtime.config.mode || "chat";
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenathlan/devthink",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
"version": "1.1.9",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/wenathlan/devthink"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"ink": "^7.1.1",
|
|
10
|
+
"react": "^19.2.8"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/react": "^19.2.18",
|
|
14
|
+
"bun-types": "^1.4.0"
|
|
9
15
|
},
|
|
10
16
|
"exports": {
|
|
11
17
|
".": "./devthink.ts",
|
|
@@ -20,27 +26,26 @@
|
|
|
20
26
|
"./stream": "./stream.ts",
|
|
21
27
|
"./sync": "./sync.ts"
|
|
22
28
|
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"devthink": "./devthink.ts"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/wenathlan/devthink/issues"
|
|
34
|
+
},
|
|
35
|
+
"description": "DevThink is an AI development workbench with provider-owned endpoints, normalized streaming, sessions, memory, an embedded gateway and a terminal/web interface.",
|
|
36
|
+
"engines": {
|
|
37
|
+
"bun": ">=1.1.1",
|
|
38
|
+
"node": ">=26.7.0"
|
|
39
|
+
},
|
|
23
40
|
"files": [
|
|
24
41
|
"*.ts",
|
|
42
|
+
"*.tsx",
|
|
25
43
|
"README.md",
|
|
26
44
|
"LICENSE",
|
|
27
45
|
"CHANGELOG.md",
|
|
28
46
|
"release-notes"
|
|
29
47
|
],
|
|
30
|
-
"
|
|
31
|
-
"start": "bun run devthink.ts",
|
|
32
|
-
"dev": "bun --watch run devthink.ts",
|
|
33
|
-
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
34
|
-
"check": "bun run check:secrets && bun run test",
|
|
35
|
-
"check:secrets": "bun run check-secrets.ts",
|
|
36
|
-
"build:binary": "bun run build.ts",
|
|
37
|
-
"build": "bun build ./devthink.ts --outdir dist --target bun",
|
|
38
|
-
"release": "bun run check && bun run build:binary"
|
|
39
|
-
},
|
|
40
|
-
"engines": {
|
|
41
|
-
"bun": ">=1.1.1",
|
|
42
|
-
"node": ">=26.7.0"
|
|
43
|
-
},
|
|
48
|
+
"homepage": "https://wenathlan.github.io/devthink/",
|
|
44
49
|
"keywords": [
|
|
45
50
|
"ai",
|
|
46
51
|
"cli",
|
|
@@ -52,16 +57,20 @@
|
|
|
52
57
|
"embedded-gateway",
|
|
53
58
|
"web-workbench"
|
|
54
59
|
],
|
|
55
|
-
"
|
|
56
|
-
"type": "git",
|
|
57
|
-
"url": "https://github.com/wenathlan/devthink"
|
|
58
|
-
},
|
|
59
|
-
"homepage": "https://wenathlan.github.io/devthink/",
|
|
60
|
-
"bugs": {
|
|
61
|
-
"url": "https://github.com/wenathlan/devthink/issues"
|
|
62
|
-
},
|
|
60
|
+
"license": "MIT",
|
|
63
61
|
"publishConfig": {
|
|
64
62
|
"access": "public",
|
|
65
63
|
"registry": "https://registry.npmjs.org"
|
|
66
|
-
}
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"start": "bun run devthink.ts",
|
|
67
|
+
"dev": "bun --watch run devthink.ts",
|
|
68
|
+
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
69
|
+
"check": "bun run check:secrets && bun run test",
|
|
70
|
+
"check:secrets": "bun run check-secrets.ts",
|
|
71
|
+
"build:binary": "bun run build.ts",
|
|
72
|
+
"build": "bun build ./devthink.ts --outdir dist --target bun",
|
|
73
|
+
"release": "bun run check && bun run build:binary"
|
|
74
|
+
},
|
|
75
|
+
"type": "module"
|
|
67
76
|
}
|
package/terminal-ui.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Style: DevThink Unified Terminal Workspace — Ink renders the same sparse categories, canvas and command rail as web/TerminalWorkspace. */
|
|
2
|
+
import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { DevThinkConfig } from "./config.ts";
|
|
5
|
+
import type { ChatEvent } from "./stream.ts";
|
|
6
|
+
import type { Session } from "./session.ts";
|
|
7
|
+
|
|
8
|
+
const categories = ["features", "bugs", "refactor", "snippets", "tasks", "notes", "all", "settings"] as const;
|
|
9
|
+
const categoryGlyphs: Record<(typeof categories)[number], string> = { features: "ϟ", bugs: "⊗", refactor: "◌", snippets: "◫", tasks: "☑", notes: "□", all: "◉", settings: "⚙" };
|
|
10
|
+
|
|
11
|
+
type Runtime = { config: DevThinkConfig };
|
|
12
|
+
type Execute = (prompt: string, current: Session | undefined, onEvent: (event: ChatEvent) => void) => Promise<Session | undefined>;
|
|
13
|
+
type Entry = { role: "user" | "devthink" | "error"; title: string; body: string };
|
|
14
|
+
|
|
15
|
+
function clip(value: string, width: number): string {
|
|
16
|
+
return value.length > width ? `${value.slice(0, Math.max(0, width - 1))}…` : value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; execute: Execute; version: string }) {
|
|
20
|
+
const { exit } = useApp();
|
|
21
|
+
const { columns } = useWindowSize();
|
|
22
|
+
const [active, setActive] = useState<(typeof categories)[number]>("all");
|
|
23
|
+
const [entries, setEntries] = useState<Entry[]>([]);
|
|
24
|
+
const [session, setSession] = useState<Session>();
|
|
25
|
+
const [draft, setDraft] = useState("");
|
|
26
|
+
const [pending, setPending] = useState(false);
|
|
27
|
+
const provider = runtime.config.activeProvider || runtime.config.provider || "local";
|
|
28
|
+
const model = runtime.config.activeModel || runtime.config.model || "not configured";
|
|
29
|
+
const compact = columns < 100;
|
|
30
|
+
const tabLine = compact ? categories.map((category) => categoryGlyphs[category]).join(" ") : categories.map((category) => `${category === active ? "[" : ""}${categoryGlyphs[category]} ${category}${category === active ? "]" : ""}`).join(" ");
|
|
31
|
+
const terminalWidth = Math.max(38, columns - (compact ? 2 : 6));
|
|
32
|
+
|
|
33
|
+
async function submit() {
|
|
34
|
+
const prompt = draft.trim();
|
|
35
|
+
if (!prompt || pending) return;
|
|
36
|
+
if (prompt === "/exit" || prompt === "/quit") return exit();
|
|
37
|
+
if (prompt === "/clear") { setEntries([]); setDraft(""); return; }
|
|
38
|
+
if (prompt === "/new") { setSession(undefined); setEntries([]); setDraft(""); return; }
|
|
39
|
+
if (prompt === "/help") {
|
|
40
|
+
setEntries((current) => [...current, { role: "devthink", title: "command reference", body: "/new /clear /models /modes /sessions /exit" }]);
|
|
41
|
+
setDraft("");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setEntries((current) => [...current, { role: "user", title: active, body: prompt }, { role: "devthink", title: "connecting", body: "waiting for local gateway events…" }]);
|
|
45
|
+
setDraft("");
|
|
46
|
+
setPending(true);
|
|
47
|
+
let partial = "";
|
|
48
|
+
try {
|
|
49
|
+
const next = await execute(prompt, session, (event) => {
|
|
50
|
+
if (event.type === "text" || event.type === "reasoning") partial += event.text;
|
|
51
|
+
setEntries((current) => current.map((entry, index) => index === current.length - 1 ? { ...entry, title: event.type === "start" ? "streaming" : "devthink", body: partial || "connecting to local provider…" } : entry));
|
|
52
|
+
});
|
|
53
|
+
setSession(next);
|
|
54
|
+
setEntries((current) => current.map((entry, index) => index === current.length - 1 ? { ...entry, title: "devthink", body: partial || next?.messages.at(-1)?.content || "the provider returned no text." } : entry));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
setEntries((current) => current.map((entry, index) => index === current.length - 1 ? { role: "error", title: "gateway error", body: error instanceof Error ? error.message : "request could not be completed" } : entry));
|
|
57
|
+
} finally { setPending(false); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
useInput((input, key) => {
|
|
61
|
+
if (key.escape || (key.ctrl && input === "c")) return exit();
|
|
62
|
+
if (key.leftArrow || key.tab) return setActive((current) => categories[(categories.indexOf(current) + categories.length - 1) % categories.length]);
|
|
63
|
+
if (key.rightArrow) return setActive((current) => categories[(categories.indexOf(current) + 1) % categories.length]);
|
|
64
|
+
if (key.return) return void submit();
|
|
65
|
+
if (key.backspace || key.delete) return setDraft((current) => current.slice(0, -1));
|
|
66
|
+
if (!key.ctrl && !key.meta && input) setDraft((current) => current + input);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={compact ? 1 : 3} paddingY={1}>
|
|
70
|
+
<Text color="#a19a91">{clip(`▣ DEVTHINK ${tabLine}`, terminalWidth - 2)} <Text color="#8ab4f8">{provider.toUpperCase()}</Text></Text>
|
|
71
|
+
<Text color="#282724">{"─".repeat(terminalWidth)}</Text>
|
|
72
|
+
<Text color="#e8e1d7">{categoryGlyphs[active]} {active.toUpperCase()} ENTRIES <Text color="#746f66">· {entries.length} entries · {clip(model, 22)}</Text></Text>
|
|
73
|
+
<Box flexDirection="column" flexGrow={1} justifyContent={entries.length ? "flex-start" : "center"} alignItems={entries.length ? "flex-start" : "center"}>
|
|
74
|
+
{entries.length ? entries.map((entry, index) => <Box key={`${entry.title}-${index}`} flexDirection="column" marginBottom={1} paddingLeft={1}><Box><Text color={entry.role === "user" ? "#ff5f00" : entry.role === "error" ? "#ff6b6b" : "#8ab4f8"}>▎ {entry.title.toUpperCase()}</Text></Box><Box><Text color="#e8e1d7">{clip(entry.body, terminalWidth - 6)}</Text></Box></Box>) : <Box flexDirection="column" alignItems="center"><Box><Text color="#4b4842">no {active === "all" ? "entries" : active} planned yet.</Text></Box><Box><Text color="#625c54">think {active === "all" ? "feature add a local provider" : `${active} …`}</Text></Box><Box marginTop={2}><Text color="#ff5f00" bold>DEVTHINK</Text></Box></Box>}
|
|
75
|
+
</Box>
|
|
76
|
+
<Text color="#282724">┌{"─".repeat(Math.max(16, terminalWidth - 2))}┐</Text>
|
|
77
|
+
<Text color="#e8e1d7">│ <Text color="#ff5f00">›_ </Text><Text color={draft ? "#e8e1d7" : "#746f66"}>{clip(draft || `think ${active === "all" ? "feature, bug, task, note…" : `${active} …`}`, Math.max(12, terminalWidth - 20))}</Text><Text color="#8ab4f8"> {provider}</Text><Text color="#ff5f00"> [run]</Text> │</Text>
|
|
78
|
+
<Text color="#282724">└{"─".repeat(Math.max(16, terminalWidth - 2))}┘</Text>
|
|
79
|
+
<Text color="#746f66">provider credentials stay in the local cli · v{version} · esc exit</Text>
|
|
80
|
+
</Box>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function startTerminalWorkspace(runtime: Runtime, version: string, execute: Execute): Promise<void> {
|
|
84
|
+
const app = render(<TerminalWorkspace runtime={runtime} version={version} execute={execute} />, { exitOnCtrlC: false });
|
|
85
|
+
await app.waitUntilExit();
|
|
86
|
+
}
|
package/ui.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
/** Style: DevThink Orbital Signal Room — terminal workspace uses canonical ANSI art, orange intent and orb-blue connection states. */
|
|
1
2
|
import type { ChatEvent } from "./stream.ts";
|
|
2
3
|
|
|
3
4
|
export const colors = {
|
|
4
|
-
background: "\u001b[38;5;
|
|
5
|
-
panel: "\u001b[38;5;
|
|
6
|
-
text: "\u001b[38;5;
|
|
7
|
-
muted: "\u001b[38;5;
|
|
8
|
-
cyan: "\u001b[38;5;
|
|
9
|
-
blue: "\u001b[38;5;
|
|
5
|
+
background: "\u001b[38;5;234m",
|
|
6
|
+
panel: "\u001b[38;5;240m",
|
|
7
|
+
text: "\u001b[38;5;255m",
|
|
8
|
+
muted: "\u001b[38;5;246m",
|
|
9
|
+
cyan: "\u001b[38;5;117m",
|
|
10
|
+
blue: "\u001b[38;5;111m",
|
|
11
|
+
orange: "\u001b[38;5;208m",
|
|
12
|
+
orangeSoft: "\u001b[38;5;215m",
|
|
10
13
|
green: "\u001b[38;5;151m",
|
|
11
14
|
red: "\u001b[38;5;167m",
|
|
12
15
|
yellow: "\u001b[38;5;222m",
|
|
@@ -14,7 +17,7 @@ export const colors = {
|
|
|
14
17
|
bold: "\u001b[1m",
|
|
15
18
|
};
|
|
16
19
|
|
|
17
|
-
const frames = ["
|
|
20
|
+
const frames = ["◐", "◓", "◑", "◒"];
|
|
18
21
|
|
|
19
22
|
/** Canonical DevThink ANSI mark transcribed from docs/logo.md. */
|
|
20
23
|
const canonicalMark = String.raw`
|
|
@@ -77,38 +80,56 @@ function color(code: string, value: string): string {
|
|
|
77
80
|
return `${code}${value}${colors.reset}`;
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
function stripAnsi(value: string): string {
|
|
84
|
+
return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
function pad(value: string, width: number): string {
|
|
81
|
-
|
|
88
|
+
const visible = stripAnsi(value);
|
|
89
|
+
return visible.length >= width ? value.slice(0, Math.max(0, width - Math.max(0, visible.length - width))) : value + " ".repeat(width - visible.length);
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
export function banner(version: string): string {
|
|
85
|
-
const mark = canonicalMark.split("\n").map((line) => color(colors.
|
|
86
|
-
const title = color(colors.
|
|
87
|
-
|
|
93
|
+
const mark = canonicalMark.split("\n").map((line) => color(colors.orange, line)).join("\n");
|
|
94
|
+
const title = color(colors.orange + colors.bold, "DEVTHINK");
|
|
95
|
+
const signal = color(colors.blue, "◆ local workspace");
|
|
96
|
+
return [
|
|
97
|
+
color(colors.muted, "╭────────────────────────────── signal workspace ──────────────────────────────╮"),
|
|
98
|
+
mark,
|
|
99
|
+
` ${title} ${color(colors.muted, `v${version}`)} ${signal}`,
|
|
100
|
+
` ${color(colors.muted, "route the work · keep provider credentials local · ctrl+p commands")}`,
|
|
101
|
+
color(colors.muted, "╰────────────────────────────────────────────────────────────────────────────────╯"),
|
|
102
|
+
].join("\n");
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
export function box(title: string, content: string, width = 76): string {
|
|
91
106
|
const innerWidth = Math.max(20, width - 4);
|
|
92
|
-
const top = `╭─ ${color(colors.
|
|
93
|
-
const body = content.split("\n").map((line) => `│ ${pad(line, innerWidth)} │`);
|
|
107
|
+
const top = `╭─ ${color(colors.orange, title)} ${color(colors.muted, "─".repeat(Math.max(0, innerWidth - title.length - 3)))}╮`;
|
|
108
|
+
const body = content.split("\n").map((line) => `│ ${color(colors.blue, "▎")} ${pad(line, innerWidth - 2)} │`);
|
|
94
109
|
return [top, ...body, `╰${"─".repeat(width - 2)}╯`].join("\n");
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
export function statusBar(provider: string, model: string, mode: string): string {
|
|
98
|
-
|
|
113
|
+
const providerLabel = provider || "not configured";
|
|
114
|
+
const modelLabel = model || "not configured";
|
|
115
|
+
return [
|
|
116
|
+
color(colors.orange, "◆"), color(colors.text, providerLabel),
|
|
117
|
+
color(colors.muted, "/"), color(colors.blue, modelLabel),
|
|
118
|
+
color(colors.muted, "·"), color(colors.cyan, mode),
|
|
119
|
+
].join(" ");
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
export function spinnerFrame(index: number, message: string): string {
|
|
102
|
-
return `${color(colors.
|
|
123
|
+
return `${color(colors.blue, frames[index % frames.length])} ${color(colors.muted, "stream")} ${message}`;
|
|
103
124
|
}
|
|
104
125
|
|
|
105
126
|
export function formatEvent(event: ChatEvent): string {
|
|
106
127
|
if (event.type === "text") return event.text;
|
|
107
|
-
if (event.type === "reasoning") return color(colors.muted, event.text)
|
|
128
|
+
if (event.type === "reasoning") return `${color(colors.blue, "·")} ${color(colors.muted, event.text)}`;
|
|
108
129
|
if (event.type === "error") return color(colors.red, event.message);
|
|
109
130
|
if (event.type === "finish") return "";
|
|
110
|
-
if (event.type === "start") return color(colors.
|
|
111
|
-
return color(colors.blue,
|
|
131
|
+
if (event.type === "start") return `${color(colors.blue, "◆")} ${color(colors.text, "connected")} ${color(colors.muted, `${event.provider} / ${event.model}`)}`;
|
|
132
|
+
return `${color(colors.orange, "→")} ${color(colors.blue, event.name)} ${color(colors.muted, JSON.stringify(event.input))}`;
|
|
112
133
|
}
|
|
113
134
|
|
|
114
135
|
export function suggestions(input: string, values: string[], limit = 8): string[] {
|