@wenathlan/devthink 1.1.8 → 1.1.10

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 CHANGED
@@ -1,95 +1,14 @@
1
- # Changelog
1
+ # DevThink release notes
2
2
 
3
- ## 1.1.8 — Workflow trust boundaries and package adapters
4
-
5
- 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.
6
-
7
- | Area | Change |
8
- | --- | --- |
9
- | CodeQL remediation | Removes untrusted `workflow_run.head_sha` checkouts from npm, GitHub Packages, extension, container, GHCR and mobile publication workflows. |
10
- | Maven | Adds a Java 17 process adapter and a source-backed Maven publication workflow for GitHub Packages. |
11
- | NuGet | Adds a .NET 8 process adapter, package README and a source-backed NuGet publication workflow for GitHub Packages. |
12
- | RubyGems | Adds a Ruby process adapter and a source-backed RubyGems publication workflow for GitHub Packages. |
13
- | Release metadata | Aligns the CLI, static workbench, mobile shell, extension and adapter packages on version 1.1.8. |
14
-
15
- ## 1.1.7 — Security remediation
16
-
17
- 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.
18
-
19
- | Area | Change |
20
- | --- | --- |
21
- | Container hardening | Adds a native `HEALTHCHECK` instruction that executes `devthink --help` without requiring a shell in the distroless runtime. |
22
- | Gateway reliability | Requests an operating-system-assigned loopback port by default, avoiding HTTP-client restricted ports during concurrent test execution in CI. |
23
- | Dependency review | Confirms the workbench audit has no known vulnerabilities at low, moderate, high or critical severity. |
24
- | Mobile dependency graph | Retains the `uuid` 11.1.1 override that removes the previously vulnerable transitive UUID version. |
25
- | Release metadata | Aligns the CLI, static workbench, mobile shell and browser extension on version 1.1.7. |
26
-
27
- ## 1.1.6 — Mobile artifact activation
28
-
29
- This release corrects the mobile workflow's source detector for the JSON Capacitor configuration. Android development APK/AAB and iOS simulator application packaging now execute whenever the source is present.
30
-
31
- ## 1.1.5 — Minimal container runtime
32
-
33
- This release replaces the container runtime with a nonroot distroless compatibility image. The image contains only the compiled DevThink binary and its required C/C++ runtime libraries, reducing the operating-system package surface scanned before GHCR publication.
34
-
35
- ## 1.1.4 — Container runtime compatibility
36
-
37
- This release runs the Bun-compiled CLI in a Bun runtime image, avoiding dynamic-library mismatches during GHCR smoke tests. Security workflows retain Trivy SARIF evidence even when the configured gate detects a high-severity issue.
38
-
39
- ## 1.1.3 — Distribution repair
40
-
41
- This release corrects the mobile Capacitor configuration and resolves the vulnerable UUID transitively selected by the initial mobile lockfile. It also moves the Bun container runtime to a glibc-compatible base, so the compiled CLI runs in the released image, and limits Maven, NuGet, and RubyGems publication to manual source-backed dispatches.
42
-
43
- ## 1.1.2 — Distribution surfaces and artifacts
44
-
45
- This release keeps `@wenathlan/devthink` as the only npm package and adds source-backed distribution surfaces for a browser extension, a Bun-compiled container, Android development artifacts, and an iOS simulator artifact. Maven, NuGet and RubyGems remain source-gated because the repository does not contain Java, .NET, or Ruby package sources.
46
-
47
- | Area | Change |
48
- | --- | --- |
49
- | npm | Publishes only the DevThink CLI package. |
50
- | Browser extension | Packages the static workbench as a Manifest V3 release ZIP with explicit local-gateway host permissions. |
51
- | Container | Compiles the CLI with Bun in a multi-stage image and publishes a container release archive and GHCR image when triggered. |
52
- | Mobile | Uses Capacitor source with Bun-built web assets to produce Android development artifacts and an unsigned iOS simulator application. |
53
-
54
- ## 1.1.1 — DevThink publication and workflow reconciliation
55
-
56
- This release aligns the CLI and static workbench at version 1.1.1, refreshes direct web dependencies, and updates the GitHub Actions toolchain. The workbench remains a static deployment surface and is not published as a separate npm package.
57
-
58
- | Area | Change |
59
- | --- | --- |
60
- | Distribution | Publishes only `@wenathlan/devthink` through the credential-safe npm workflow. |
61
- | Workflows | Updates Actions runtimes and adds source-aware guards for optional ecosystem packages. |
62
- | Compatibility | Raises the declared Node.js baseline to 26.7.0 and refreshes direct web dependencies to their current releases. |
63
- | Static web | Preserves the deployable `web/` artifact for GitHub Pages, Vercel and Netlify. |
64
-
65
- ## Embedded gateway and web workbench implementation
66
-
67
- This implementation turns the audited repository context into the DevThink feature map and expands the DevThink CLI with provider-owned endpoints, normalized streaming and a local embedded gateway.
68
-
69
- | Area | Change |
70
- | --- | --- |
71
- | Documentation | Adds the DevThink feature-map collection, correction specification, canonical audit coverage, provider families, known regressions, terminal design and web plan |
72
- | Gateway | Adds provider-specific endpoint overrides, embedded streaming diagnostics and secret redaction without a separate proxy subsystem |
73
- | Providers | Adds registry entries for DeepSeek, Groq, Mistral, xAI and Ollama while retaining OpenAI, Z.AI, Anthropic, Google, OpenRouter and Qwen |
74
- | CLI | Adds `gateway status`, explicit `auth login`, canonical `devthink.json` persistence and the terminal logo from `docs/logo.md` |
75
- | Tests | Adds embedded-gateway, nested credential-redaction and configuration-path coverage |
76
- | Web | Adds the DevThink browser-like workbench directly under `web/`, including tabs, provider rail, stream canvas, route inspector, command palette, responsive layout and canonical ANSI branding |
77
- | Security | Keeps cookie capture, CAPTCHA solving, stealth automation and anti-bot evasion outside the product core |
78
-
79
- ## 1.0.0 — 2026-08-19
3
+ ## 1.1.10 — Entry and browser-workspace redesign
80
4
 
81
5
  ### Added
82
6
 
83
- DevThink v1.0.0 introduces a compact AI development CLI with a provider-neutral transport layer, streaming responses, local sessions, internal memory, 20 operational modes, local plugins, ANSI terminal design, and a loopback HTTP API.
84
-
85
- The first release supports OpenAI-compatible providers, Z.AI through its documented API endpoint, Anthropic Messages streaming, Google Gemini `streamGenerateContent`, OpenRouter, and a configurable Qwen-compatible transport. Credentials are resolved from environment variables or local configuration and are redacted from status output, sessions, logs, and release artifacts.
86
-
87
- The CLI includes `chat`, `providers`, `models`, `modes`, `config`, `auth`, `sessions`, `serve`, `init`, and `interactive` commands. Machine-readable JSON output is available for scripted calls, while the interactive terminal includes slash commands, a status panel, a spinner vocabulary, and a consistent Nord-inspired color system.
88
-
89
- ### Security
90
-
91
- This release deliberately excludes browser-cookie capture, undocumented web-session automation, CAPTCHA solving, anti-bot evasion, device-fingerprint forgery, and credential extraction. Provider failures are surfaced explicitly, and the release workflow scans product files for common embedded-secret patterns before publishing.
92
-
93
- ### Distribution
94
-
95
- GitHub Actions builds self-contained Bun binaries for Linux x64, macOS arm64, and Windows x64 from version tags. The workflow reads the release version from `package.json`, runs tests, generates SHA-256 checksums, and attaches artifacts to the GitHub release.
7
+ | Area | Addition |
8
+ | --- | --- |
9
+ | Landing entry | A first-command introduction with the canonical DevThink ANSI mark behind an oversized wordmark and one focused command field. |
10
+ | First-command transition | A submitted natural-language intention creates a local identity, records the first session intent and opens the workspace. |
11
+ | Browser workspace | A two-level navigation model with browser-shaped session chrome, an active local tab, task-category pills and a persistent command rail. |
12
+ | Visual system | Original black editorial surfaces, layered charcoal browser material, intentional orange states and orb-blue local connection signals. |
13
+ | CLI parity | The Ink renderer now starts on the same command-first entry surface and opens the same local workspace semantics after the first command. |
14
+ | Release packaging | Locked CLI dependencies are installed before Bun binary compilation and inside the container build so the React Ink renderer is included consistently. |
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
- if (!json) {
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.8",
4
- "description": "DevThink is an AI development workbench with provider-owned endpoints, normalized streaming, sessions, memory, an embedded gateway and a terminal/web interface.",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "devthink": "./devthink.ts"
3
+ "version": "1.1.10",
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
- "scripts": {
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
- "repository": {
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
  }
@@ -0,0 +1,112 @@
1
+ /** Design: DevThink v1.1.10 — Ink renders the same landing-to-browser-workspace progression as the React workbench. */
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
+ const entryArt = [" +++++", " +++++++++++++++", " ++++++ ++++++", " +++++ +++++", " +++++ +++++", " +++++ +++++", " ++++++ ++++++", " +++++++++++++", " +++++"];
11
+
12
+ type Runtime = { config: DevThinkConfig };
13
+ type Execute = (prompt: string, current: Session | undefined, onEvent: (event: ChatEvent) => void) => Promise<Session | undefined>;
14
+ type Entry = { role: "user" | "devthink" | "error"; title: string; body: string };
15
+
16
+ function clip(value: string, width: number): string {
17
+ return value.length > width ? `${value.slice(0, Math.max(0, width - 1))}…` : value;
18
+ }
19
+
20
+ function TerminalEntry({ draft, provider, width }: { draft: string; provider: string; width: number }) {
21
+ return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={2} paddingY={1} justifyContent="space-between">
22
+ <Box justifyContent="space-between"><Text color="#807970">✦ DEVTHINK</Text><Text color="#8ab4f8">LOCAL-FIRST WORKBENCH</Text></Box>
23
+ <Box flexDirection="column" alignItems="center">
24
+ <Box flexDirection="column" alignItems="center">{entryArt.map((line, index) => <Text key={`${line}-${index}`} color="#382c23">{line}</Text>)}</Box>
25
+ <Box marginTop={-1}><Text color="#ff5f00" bold>DEVTHINK</Text></Box>
26
+ <Box marginTop={1}><Text color="#746f66">begin with one intention</Text></Box>
27
+ <Box marginTop={2}><Text color="#282724">┌{"─".repeat(Math.max(16, width - 2))}┐</Text></Box>
28
+ <Text color="#e8e1d7">│ <Text color="#ff5f00">›_ </Text><Text color={draft ? "#e8e1d7" : "#746f66"}>{clip(draft || "Ask anything…", Math.max(12, width - 22))}</Text><Text color="#8ab4f8"> {provider}</Text><Text color="#ff5f00"> [open]</Text> │</Text>
29
+ <Text color="#282724">└{"─".repeat(Math.max(16, width - 2))}┘</Text>
30
+ </Box>
31
+ <Text color="#746f66">enter opens a local workspace · esc exits</Text>
32
+ </Box>;
33
+ }
34
+
35
+ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; execute: Execute; version: string }) {
36
+ const { exit } = useApp();
37
+ const { columns } = useWindowSize();
38
+ const [entered, setEntered] = useState(false);
39
+ const [active, setActive] = useState<(typeof categories)[number]>("all");
40
+ const [entries, setEntries] = useState<Entry[]>([]);
41
+ const [session, setSession] = useState<Session>();
42
+ const [draft, setDraft] = useState("");
43
+ const [pending, setPending] = useState(false);
44
+ const provider = runtime.config.activeProvider || runtime.config.provider || "local";
45
+ const model = runtime.config.activeModel || runtime.config.model || "not configured";
46
+ const compact = columns < 100;
47
+ const tabLine = compact ? categories.map((category) => categoryGlyphs[category]).join(" ") : categories.map((category) => `${category === active ? "[" : ""}${categoryGlyphs[category]} ${category}${category === active ? "]" : ""}`).join(" ");
48
+ const terminalWidth = Math.max(38, columns - (compact ? 2 : 6));
49
+
50
+ async function submit() {
51
+ const prompt = draft.trim();
52
+ if (!prompt || pending) return;
53
+ if (prompt === "/exit" || prompt === "/quit") return exit();
54
+ if (!entered) {
55
+ setEntries([{ role: "user", title: "first intention", body: prompt }, { role: "devthink", title: "local workspace ready", body: "The first command opened a local DevThink session. Configure a provider whenever the work needs a model." }]);
56
+ setDraft("");
57
+ setEntered(true);
58
+ return;
59
+ }
60
+ if (prompt === "/clear") { setEntries([]); setDraft(""); return; }
61
+ if (prompt === "/new") { setSession(undefined); setEntries([]); setDraft(""); return; }
62
+ if (prompt === "/help") {
63
+ setEntries((current) => [...current, { role: "devthink", title: "command reference", body: "/new /clear /models /modes /sessions /exit" }]);
64
+ setDraft("");
65
+ return;
66
+ }
67
+ setEntries((current) => [...current, { role: "user", title: active, body: prompt }, { role: "devthink", title: "connecting", body: "waiting for local gateway events…" }]);
68
+ setDraft("");
69
+ setPending(true);
70
+ let partial = "";
71
+ try {
72
+ const next = await execute(prompt, session, (event) => {
73
+ if (event.type === "text" || event.type === "reasoning") partial += event.text;
74
+ setEntries((current) => current.map((entry, index) => index === current.length - 1 ? { ...entry, title: event.type === "start" ? "streaming" : "devthink", body: partial || "connecting to local provider…" } : entry));
75
+ });
76
+ setSession(next);
77
+ 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));
78
+ } catch (error) {
79
+ 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));
80
+ } finally { setPending(false); }
81
+ }
82
+
83
+ useInput((input, key) => {
84
+ if (key.escape || (key.ctrl && input === "c")) return exit();
85
+ if (key.return) return void submit();
86
+ if (key.backspace || key.delete) return setDraft((current) => current.slice(0, -1));
87
+ if (entered && (key.leftArrow || key.tab)) return setActive((current) => categories[(categories.indexOf(current) + categories.length - 1) % categories.length]);
88
+ if (entered && key.rightArrow) return setActive((current) => categories[(categories.indexOf(current) + 1) % categories.length]);
89
+ if (!key.ctrl && !key.meta && input) setDraft((current) => current + input);
90
+ });
91
+
92
+ if (!entered) return <TerminalEntry draft={draft} provider={provider} width={terminalWidth} />;
93
+
94
+ return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={compact ? 1 : 3} paddingY={1}>
95
+ <Box justifyContent="space-between"><Text color="#a19a91">✦ DEVTHINK <Text color="#746f66">/ local session</Text></Text><Text color="#8ab4f8">◉ {provider.toUpperCase()}</Text></Box>
96
+ <Text color="#282724">{"─".repeat(terminalWidth)}</Text>
97
+ <Text color="#746f66">{clip(tabLine, terminalWidth)}</Text>
98
+ <Box marginTop={1}><Text color="#e8e1d7">{categoryGlyphs[active]} {active.toUpperCase()} <Text color="#746f66">· {entries.length} entries · {clip(model, 22)}</Text></Text></Box>
99
+ <Box flexDirection="column" flexGrow={1} justifyContent={entries.length ? "flex-start" : "center"} alignItems={entries.length ? "flex-start" : "center"}>
100
+ {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">Nothing is active yet.</Text></Box><Box><Text color="#625c54">Describe the next feature, bug or piece of work below.</Text></Box><Box marginTop={2}><Text color="#ff5f00" bold>DEVTHINK</Text></Box></Box>}
101
+ </Box>
102
+ <Text color="#282724">┌{"─".repeat(Math.max(16, terminalWidth - 2))}┐</Text>
103
+ <Text color="#e8e1d7">│ <Text color="#ff5f00">›_ </Text><Text color={draft ? "#e8e1d7" : "#746f66"}>{clip(draft || `Ask DevThink about ${active === "all" ? "the work…" : `${active}…`}`, Math.max(12, terminalWidth - 20))}</Text><Text color="#8ab4f8"> {provider}</Text><Text color="#ff5f00"> [run]</Text> │</Text>
104
+ <Text color="#282724">└{"─".repeat(Math.max(16, terminalWidth - 2))}┘</Text>
105
+ <Text color="#746f66">provider credentials stay in the local cli · v{version} · esc exit</Text>
106
+ </Box>;
107
+ }
108
+
109
+ export async function startTerminalWorkspace(runtime: Runtime, version: string, execute: Execute): Promise<void> {
110
+ const app = render(<TerminalWorkspace runtime={runtime} version={version} execute={execute} />, { exitOnCtrlC: false });
111
+ await app.waitUntilExit();
112
+ }
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;236m",
5
- panel: "\u001b[38;5;238m",
6
- text: "\u001b[38;5;252m",
7
- muted: "\u001b[38;5;245m",
8
- cyan: "\u001b[38;5;116m",
9
- blue: "\u001b[38;5;110m",
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
- return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
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.yellow, line)).join("\n");
86
- const title = color(colors.yellow + colors.bold, "DEVTHINK");
87
- return [mark, `${title} ${color(colors.muted, `v${version}`)}`, color(colors.muted, "Embedded gateway · terminal workbench · user-owned configuration")].join("\n");
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.cyan, title)} ${"─".repeat(Math.max(0, innerWidth - title.length - 3))}╮`;
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
- return color(colors.muted, `[${provider || "not configured"}] [${model || "not configured"}] [${mode}]`);
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.cyan, frames[index % frames.length])} ${message}`;
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.muted, `Connected to ${event.provider} / ${event.model}`);
111
- return color(colors.blue, `${event.name}: ${JSON.stringify(event.input)}`);
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[] {