@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 +10 -91
- package/devthink.ts +12 -3
- package/package.json +38 -29
- package/terminal-ui.tsx +112 -0
- package/ui.ts +39 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,95 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DevThink release notes
|
|
2
2
|
|
|
3
|
-
## 1.1.
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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.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
|
-
"
|
|
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,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;
|
|
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[] {
|