@wenathlan/devthink 1.1.9 → 1.1.11

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,107 +1,26 @@
1
- # Changelog
1
+ # DevThink release notes
2
+ ## 1.1.11 — Web-CLI parity and visual renewal
2
3
 
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
-
27
- ## 1.1.7 — Security remediation
28
-
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.
30
-
31
- | Area | Change |
32
- | --- | --- |
33
- | Container hardening | Adds a native `HEALTHCHECK` instruction that executes `devthink --help` without requiring a shell in the distroless runtime. |
34
- | Gateway reliability | Requests an operating-system-assigned loopback port by default, avoiding HTTP-client restricted ports during concurrent test execution in CI. |
35
- | Dependency review | Confirms the workbench audit has no known vulnerabilities at low, moderate, high or critical severity. |
36
- | Mobile dependency graph | Retains the `uuid` 11.1.1 override that removes the previously vulnerable transitive UUID version. |
37
- | Release metadata | Aligns the CLI, static workbench, mobile shell and browser extension on version 1.1.7. |
38
-
39
- ## 1.1.6 — Mobile artifact activation
40
-
41
- 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.
42
-
43
- ## 1.1.5 — Minimal container runtime
44
-
45
- 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.
46
-
47
- ## 1.1.4 — Container runtime compatibility
48
-
49
- 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.
50
-
51
- ## 1.1.3 — Distribution repair
52
-
53
- 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.
54
-
55
- ## 1.1.2 — Distribution surfaces and artifacts
56
-
57
- 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.
58
-
59
- | Area | Change |
60
- | --- | --- |
61
- | npm | Publishes only the DevThink CLI package. |
62
- | Browser extension | Packages the static workbench as a Manifest V3 release ZIP with explicit local-gateway host permissions. |
63
- | Container | Compiles the CLI with Bun in a multi-stage image and publishes a container release archive and GHCR image when triggered. |
64
- | Mobile | Uses Capacitor source with Bun-built web assets to produce Android development artifacts and an unsigned iOS simulator application. |
65
-
66
- ## 1.1.1 — DevThink publication and workflow reconciliation
67
-
68
- 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.
69
-
70
- | Area | Change |
71
- | --- | --- |
72
- | Distribution | Publishes only `@wenathlan/devthink` through the credential-safe npm workflow. |
73
- | Workflows | Updates Actions runtimes and adds source-aware guards for optional ecosystem packages. |
74
- | Compatibility | Raises the declared Node.js baseline to 26.7.0 and refreshes direct web dependencies to their current releases. |
75
- | Static web | Preserves the deployable `web/` artifact for GitHub Pages, Vercel and Netlify. |
76
-
77
- ## Embedded gateway and web workbench implementation
78
-
79
- 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.
4
+ ### Added
80
5
 
81
- | Area | Change |
6
+ | Area | Addition |
82
7
  | --- | --- |
83
- | Documentation | Adds the DevThink feature-map collection, correction specification, canonical audit coverage, provider families, known regressions, terminal design and web plan |
84
- | Gateway | Adds provider-specific endpoint overrides, embedded streaming diagnostics and secret redaction without a separate proxy subsystem |
85
- | Providers | Adds registry entries for DeepSeek, Groq, Mistral, xAI and Ollama while retaining OpenAI, Z.AI, Anthropic, Google, OpenRouter and Qwen |
86
- | CLI | Adds `gateway status`, explicit `auth login`, canonical `devthink.json` persistence and the terminal logo from `docs/logo.md` |
87
- | Tests | Adds embedded-gateway, nested credential-redaction and configuration-path coverage |
88
- | 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 |
89
- | Security | Keeps cookie capture, CAPTCHA solving, stealth automation and anti-bot evasion outside the product core |
8
+ | Shared workspace contract | A renderer-neutral TypeScript destination model aligns chat, history, projects, providers, usage, routes and settings across the React web workbench and React Ink CLI. |
9
+ | Real session navigation | The web chrome now renders actual local session tabs, creates and closes tabs, and exposes a local history view without removing direct routes. |
10
+ | CLI control views | The interactive CLI now provides keyboard and slash-command access to local history, projects, providers, usage, routes and settings. |
11
+ | Browser rail | A responsive dark local rail keeps preserved control pages discoverable from the primary workspace while retaining their bookmarkable routes. |
12
+ | Entry refinement | The canonical ANSI mark, product wordmark, primary composer and local suggestions form a quieter first-intention surface. |
13
+ | Design records | Version-scoped inventory, reference synthesis and parity contract documents make the product surface and local-only security boundary explicit. |
90
14
 
91
- ## 1.0.0 — 2026-08-19
15
+ ## 1.1.10 — Entry and browser-workspace redesign
92
16
 
93
17
  ### Added
94
18
 
95
- 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.
96
-
97
- 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.
98
-
99
- 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.
100
-
101
- ### Security
102
-
103
- 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.
104
-
105
- ### Distribution
106
-
107
- 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.
19
+ | Area | Addition |
20
+ | --- | --- |
21
+ | Landing entry | A first-command introduction with the canonical DevThink ANSI mark behind an oversized wordmark and one focused command field. |
22
+ | First-command transition | A submitted natural-language intention creates a local identity, records the first session intent and opens the workspace. |
23
+ | Browser workspace | A two-level navigation model with browser-shaped session chrome, an active local tab, task-category pills and a persistent command rail. |
24
+ | Visual system | Original black editorial surfaces, layered charcoal browser material, intentional orange states and orb-blue local connection signals. |
25
+ | 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. |
26
+ | 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenathlan/devthink",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/wenathlan/devthink"
package/terminal-ui.tsx CHANGED
@@ -1,14 +1,17 @@
1
- /** Style: DevThink Unified Terminal Workspace — Ink renders the same sparse categories, canvas and command rail as web/TerminalWorkspace. */
1
+ /** Design: DevThink v1.1.11 — Ink shares local workspace destinations, tab semantics and command language with the React web workbench. */
2
2
  import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
3
- import { useState } from "react";
4
- import type { DevThinkConfig } from "./config.ts";
3
+ import { useMemo, useState } from "react";
4
+ import { listProviders } from "./providers.ts";
5
+ import { listSessions, type Session } from "./session.ts";
6
+ import type { DevThinkConfig, DevThinkPaths } from "./config.ts";
5
7
  import type { ChatEvent } from "./stream.ts";
6
- import type { Session } from "./session.ts";
8
+ import { isWorkspaceDestination, workspaceDestination, workspaceDestinations, type WorkspaceDestination } from "./workspace.ts";
7
9
 
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 categories = ["features", "bugs", "refactor", "snippets", "tasks", "notes", "all"] as const;
11
+ const categoryGlyphs: Record<(typeof categories)[number], string> = { features: "ϟ", bugs: "⊗", refactor: "◌", snippets: "◫", tasks: "☑", notes: "□", all: "◉" };
12
+ const entryArt = [" +++++", " +++++++++++++++", " ++++++ ++++++", " +++++ +++++", " +++++ +++++", " +++++ +++++", " ++++++ ++++++", " +++++++++++++", " +++++"];
10
13
 
11
- type Runtime = { config: DevThinkConfig };
14
+ type Runtime = { config: DevThinkConfig; paths: DevThinkPaths };
12
15
  type Execute = (prompt: string, current: Session | undefined, onEvent: (event: ChatEvent) => void) => Promise<Session | undefined>;
13
16
  type Entry = { role: "user" | "devthink" | "error"; title: string; body: string };
14
17
 
@@ -16,10 +19,40 @@ function clip(value: string, width: number): string {
16
19
  return value.length > width ? `${value.slice(0, Math.max(0, width - 1))}…` : value;
17
20
  }
18
21
 
22
+ function TerminalEntry({ draft, provider, width }: { draft: string; provider: string; width: number }) {
23
+ return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={2} paddingY={1} justifyContent="space-between">
24
+ <Box justifyContent="space-between"><Text color="#807970">✦ DEVTHINK</Text><Text color="#8ab4f8">LOCAL-FIRST WORKBENCH</Text></Box>
25
+ <Box flexDirection="column" alignItems="center"><Box flexDirection="column" alignItems="center">{entryArt.map((line, index) => <Text key={`${line}-${index}`} color="#382c23">{line}</Text>)}</Box><Box marginTop={-1}><Text color="#ff5f00" bold>DEVTHINK</Text></Box><Box marginTop={1}><Text color="#746f66">begin with one intention</Text></Box><Box marginTop={2}><Text color="#282724">┌{"─".repeat(Math.max(16, width - 2))}┐</Text></Box><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><Text color="#282724">└{"─".repeat(Math.max(16, width - 2))}┘</Text></Box>
26
+ <Text color="#746f66">enter opens a local workspace · esc exits</Text>
27
+ </Box>;
28
+ }
29
+
30
+ function projectRows(runtime: Runtime): string[] {
31
+ const projects = new Map<string, { title: string; sessions: number }>();
32
+ for (const session of listSessions(runtime.paths)) projects.set(session.workspaceId, { title: session.title, sessions: (projects.get(session.workspaceId)?.sessions || 0) + 1 });
33
+ return [...projects.entries()].map(([id, project]) => `${id} ${project.sessions} ${project.sessions === 1 ? "session" : "sessions"} ${project.title}`);
34
+ }
35
+
36
+ function viewRows(view: WorkspaceDestination, runtime: Runtime, session: Session | undefined): string[] {
37
+ if (view === "history") return listSessions(runtime.paths).map((item) => `${item.id} ${item.title}`) || ["No saved sessions."];
38
+ if (view === "projects") return projectRows(runtime) || ["No local projects."];
39
+ if (view === "providers") return listProviders().map((provider) => `${provider.id} ${provider.protocol} ${provider.env}`);
40
+ if (view === "usage") {
41
+ const sessions = listSessions(runtime.paths);
42
+ return [`workspaces ${new Set(sessions.map((item) => item.workspaceId)).size}`, `sessions ${sessions.length}`, `tabs ${sessions.reduce((total, item) => total + item.tabs.length, 0)}`, `messages ${sessions.reduce((total, item) => total + item.messages.length, 0)}`];
43
+ }
44
+ if (view === "routes") return ["GET /health", "GET /providers", "GET /workspaces", "GET /usage", "POST /sessions", "POST /chat"];
45
+ if (view === "settings") return [`provider ${runtime.config.activeProvider || runtime.config.provider || "not configured"}`, "credentials remain in ~/.config/devthink/auth.json", "pair create emits a one-time local browser invitation"];
46
+ if (session) return [`workspace ${session.workspaceId}`, `session ${session.id}`, `tab ${session.activeTabId}`];
47
+ return ["Describe the next feature, bug or piece of work below."];
48
+ }
49
+
19
50
  function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; execute: Execute; version: string }) {
20
51
  const { exit } = useApp();
21
52
  const { columns } = useWindowSize();
53
+ const [entered, setEntered] = useState(false);
22
54
  const [active, setActive] = useState<(typeof categories)[number]>("all");
55
+ const [view, setView] = useState<WorkspaceDestination>("chat");
23
56
  const [entries, setEntries] = useState<Entry[]>([]);
24
57
  const [session, setSession] = useState<Session>();
25
58
  const [draft, setDraft] = useState("");
@@ -27,20 +60,32 @@ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; ex
27
60
  const provider = runtime.config.activeProvider || runtime.config.provider || "local";
28
61
  const model = runtime.config.activeModel || runtime.config.model || "not configured";
29
62
  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
63
  const terminalWidth = Math.max(38, columns - (compact ? 2 : 6));
64
+ const tabLine = compact ? workspaceDestinations.map((destination) => destination.glyph).join(" ") : workspaceDestinations.map((destination) => `${destination.id === view ? "[" : ""}${destination.glyph} ${destination.label}${destination.id === view ? "]" : ""}`).join(" ");
65
+ const rows = useMemo(() => viewRows(view, runtime, session), [runtime, session, view]);
66
+
67
+ function openView(next: WorkspaceDestination) {
68
+ setView(next);
69
+ setEntries((current) => [...current, { role: "devthink", title: workspaceDestination(next).label, body: rows.join(" · ") }]);
70
+ }
32
71
 
33
72
  async function submit() {
34
73
  const prompt = draft.trim();
35
74
  if (!prompt || pending) return;
36
75
  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" }]);
76
+ if (!entered) {
77
+ setEntries([{ role: "user", title: "first intention", body: prompt }, { role: "devthink", title: "local workspace ready", body: "The first command opened a local DevThink session. Use arrows or /view to inspect local history, projects, providers, usage, routes and settings." }]);
41
78
  setDraft("");
79
+ setEntered(true);
42
80
  return;
43
81
  }
82
+ if (prompt === "/clear") { setEntries([]); setDraft(""); return; }
83
+ if (prompt === "/new") { setSession(undefined); setEntries([]); setView("chat"); setDraft(""); return; }
84
+ const viewMatch = prompt.match(/^\/(?:view\s+)?(chat|history|projects|providers|usage|routes|settings)$/);
85
+ if (viewMatch && isWorkspaceDestination(viewMatch[1])) { openView(viewMatch[1]); setDraft(""); return; }
86
+ if (prompt === "/sessions") { openView("history"); setDraft(""); return; }
87
+ if (prompt === "/help") { setEntries((current) => [...current, { role: "devthink", title: "command reference", body: "/view chat|history|projects|providers|usage|routes|settings /new /clear /models /modes /sessions /exit" }]); setDraft(""); return; }
88
+ setView("chat");
44
89
  setEntries((current) => [...current, { role: "user", title: active, body: prompt }, { role: "devthink", title: "connecting", body: "waiting for local gateway events…" }]);
45
90
  setDraft("");
46
91
  setPending(true);
@@ -59,24 +104,29 @@ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; ex
59
104
 
60
105
  useInput((input, key) => {
61
106
  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
107
  if (key.return) return void submit();
65
108
  if (key.backspace || key.delete) return setDraft((current) => current.slice(0, -1));
109
+ if (entered && key.upArrow) return setView((current) => workspaceDestinations[(workspaceDestinations.findIndex((item) => item.id === current) + workspaceDestinations.length - 1) % workspaceDestinations.length].id);
110
+ if (entered && key.downArrow) return setView((current) => workspaceDestinations[(workspaceDestinations.findIndex((item) => item.id === current) + 1) % workspaceDestinations.length].id);
111
+ if (entered && view === "chat" && (key.leftArrow || key.tab)) return setActive((current) => categories[(categories.indexOf(current) + categories.length - 1) % categories.length]);
112
+ if (entered && view === "chat" && key.rightArrow) return setActive((current) => categories[(categories.indexOf(current) + 1) % categories.length]);
66
113
  if (!key.ctrl && !key.meta && input) setDraft((current) => current + input);
67
114
  });
68
115
 
116
+ if (!entered) return <TerminalEntry draft={draft} provider={provider} width={terminalWidth} />;
117
+
69
118
  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>
119
+ <Box justifyContent="space-between"><Text color="#a19a91">✦ DEVTHINK <Text color="#746f66">/ {workspaceDestination(view).label}</Text></Text><Text color="#8ab4f8">◉ {provider.toUpperCase()}</Text></Box>
71
120
  <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>}
121
+ <Text color="#746f66">{clip(tabLine, terminalWidth)}</Text>
122
+ <Box marginTop={1}><Text color="#e8e1d7">{workspaceDestination(view).glyph} {workspaceDestination(view).label.toUpperCase()} <Text color="#746f66">· {clip(model, 22)}</Text></Text></Box>
123
+ <Box flexDirection="column" flexGrow={1} justifyContent={entries.length && view === "chat" ? "flex-start" : "center"} alignItems={entries.length && view === "chat" ? "flex-start" : "center"}>
124
+ {view !== "chat" ? rows.map((row, index) => <Text key={`${view}-${index}`} color={index === 0 ? "#e8e1d7" : "#746f66"}>▎ {clip(row, terminalWidth - 4)}</Text>) : 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"><Text color="#4b4842">Nothing is active yet.</Text><Text color="#625c54">Describe the next feature, bug or piece of work below.</Text><Box marginTop={2}><Text color="#ff5f00" bold>DEVTHINK</Text></Box></Box>}
75
125
  </Box>
76
126
  <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>
127
+ <Text color="#e8e1d7">│ <Text color="#ff5f00">›_ </Text><Text color={draft ? "#e8e1d7" : "#746f66"}>{clip(draft || (view === "chat" ? `Ask DevThink about ${active === "all" ? "the work…" : `${active}…`}` : `Type /view chat to return…`), Math.max(12, terminalWidth - 20))}</Text><Text color="#8ab4f8"> {provider}</Text><Text color="#ff5f00"> [run]</Text> │</Text>
78
128
  <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>
129
+ <Text color="#746f66">↑↓ destinations · ←→ categories · /help commands · v{version} · esc exit</Text>
80
130
  </Box>;
81
131
  }
82
132
 
package/workspace.ts ADDED
@@ -0,0 +1,20 @@
1
+ /** DevThink workspace contract: renderer-neutral destination names, glyphs and labels shared by web and Ink. */
2
+ export const workspaceDestinations = [
3
+ { id: "chat", label: "chat", glyph: "◉" },
4
+ { id: "history", label: "history", glyph: "◷" },
5
+ { id: "projects", label: "projects", glyph: "▦" },
6
+ { id: "providers", label: "providers", glyph: "◌" },
7
+ { id: "usage", label: "usage", glyph: "◫" },
8
+ { id: "routes", label: "routes", glyph: "⌘" },
9
+ { id: "settings", label: "settings", glyph: "⚙" },
10
+ ] as const;
11
+
12
+ export type WorkspaceDestination = (typeof workspaceDestinations)[number]["id"];
13
+
14
+ export function isWorkspaceDestination(value: string): value is WorkspaceDestination {
15
+ return workspaceDestinations.some((destination) => destination.id === value);
16
+ }
17
+
18
+ export function workspaceDestination(value: WorkspaceDestination) {
19
+ return workspaceDestinations.find((destination) => destination.id === value) || workspaceDestinations[0];
20
+ }