@wenathlan/devthink 1.1.10 → 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,4 +1,16 @@
1
1
  # DevThink release notes
2
+ ## 1.1.11 — Web-CLI parity and visual renewal
3
+
4
+ ### Added
5
+
6
+ | Area | Addition |
7
+ | --- | --- |
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. |
2
14
 
3
15
  ## 1.1.10 — Entry and browser-workspace redesign
4
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenathlan/devthink",
3
- "version": "1.1.10",
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,15 +1,17 @@
1
- /** Design: DevThink v1.1.10 — Ink renders the same landing-to-browser-workspace progression as the React workbench. */
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: "◉" };
10
12
  const entryArt = [" +++++", " +++++++++++++++", " ++++++ ++++++", " +++++ +++++", " +++++ +++++", " +++++ +++++", " ++++++ ++++++", " +++++++++++++", " +++++"];
11
13
 
12
- type Runtime = { config: DevThinkConfig };
14
+ type Runtime = { config: DevThinkConfig; paths: DevThinkPaths };
13
15
  type Execute = (prompt: string, current: Session | undefined, onEvent: (event: ChatEvent) => void) => Promise<Session | undefined>;
14
16
  type Entry = { role: "user" | "devthink" | "error"; title: string; body: string };
15
17
 
@@ -20,23 +22,37 @@ function clip(value: string, width: number): string {
20
22
  function TerminalEntry({ draft, provider, width }: { draft: string; provider: string; width: number }) {
21
23
  return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={2} paddingY={1} justifyContent="space-between">
22
24
  <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>
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>
31
26
  <Text color="#746f66">enter opens a local workspace · esc exits</Text>
32
27
  </Box>;
33
28
  }
34
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
+
35
50
  function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; execute: Execute; version: string }) {
36
51
  const { exit } = useApp();
37
52
  const { columns } = useWindowSize();
38
53
  const [entered, setEntered] = useState(false);
39
54
  const [active, setActive] = useState<(typeof categories)[number]>("all");
55
+ const [view, setView] = useState<WorkspaceDestination>("chat");
40
56
  const [entries, setEntries] = useState<Entry[]>([]);
41
57
  const [session, setSession] = useState<Session>();
42
58
  const [draft, setDraft] = useState("");
@@ -44,26 +60,32 @@ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; ex
44
60
  const provider = runtime.config.activeProvider || runtime.config.provider || "local";
45
61
  const model = runtime.config.activeModel || runtime.config.model || "not configured";
46
62
  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
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
+ }
49
71
 
50
72
  async function submit() {
51
73
  const prompt = draft.trim();
52
74
  if (!prompt || pending) return;
53
75
  if (prompt === "/exit" || prompt === "/quit") return exit();
54
76
  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." }]);
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." }]);
56
78
  setDraft("");
57
79
  setEntered(true);
58
80
  return;
59
81
  }
60
82
  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
- }
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");
67
89
  setEntries((current) => [...current, { role: "user", title: active, body: prompt }, { role: "devthink", title: "connecting", body: "waiting for local gateway events…" }]);
68
90
  setDraft("");
69
91
  setPending(true);
@@ -84,25 +106,27 @@ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; ex
84
106
  if (key.escape || (key.ctrl && input === "c")) return exit();
85
107
  if (key.return) return void submit();
86
108
  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]);
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]);
89
113
  if (!key.ctrl && !key.meta && input) setDraft((current) => current + input);
90
114
  });
91
115
 
92
116
  if (!entered) return <TerminalEntry draft={draft} provider={provider} width={terminalWidth} />;
93
117
 
94
118
  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>
119
+ <Box justifyContent="space-between"><Text color="#a19a91">✦ DEVTHINK <Text color="#746f66">/ {workspaceDestination(view).label}</Text></Text><Text color="#8ab4f8">◉ {provider.toUpperCase()}</Text></Box>
96
120
  <Text color="#282724">{"─".repeat(terminalWidth)}</Text>
97
121
  <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>}
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>}
101
125
  </Box>
102
126
  <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>
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>
104
128
  <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>
129
+ <Text color="#746f66">↑↓ destinations · ←→ categories · /help commands · v{version} · esc exit</Text>
106
130
  </Box>;
107
131
  }
108
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
+ }