@wenathlan/devthink 1.1.16 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1374 -0
- package/README.md +6 -0
- package/bridge.d.ts +698 -0
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts +8 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts +187 -0
- package/cli.d.ts.map +1 -0
- package/cli.js +18197 -0
- package/companion.d.ts +19 -0
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts +277 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts +273 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts +11 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts +81 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts +114 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts +636 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts +40 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/manifest.json +199 -0
- package/mcp.d.ts +501 -0
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts +2397 -0
- package/memory.d.ts.map +1 -0
- package/memory.js +5782 -0
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts +8 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts +211 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +111 -51
- package/policy.d.ts +1854 -0
- package/policy.d.ts.map +1 -0
- package/policy.js +12580 -0
- package/progress.d.ts +390 -0
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts +2336 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts +435 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts +6 -0
- package/umd.d.ts.map +1 -0
- package/build.ts +0 -41
- package/check-secrets.ts +0 -52
- package/compatibility.ts +0 -37
- package/config.ts +0 -246
- package/devthink.ts +0 -397
- package/identity.ts +0 -156
- package/ids.ts +0 -22
- package/memory.ts +0 -95
- package/modes.ts +0 -43
- package/plugin.ts +0 -68
- package/providers.ts +0 -179
- package/server.ts +0 -264
- package/session.ts +0 -155
- package/storage.ts +0 -82
- package/stream.ts +0 -130
- package/sync.ts +0 -27
- package/terminal-ui.tsx +0 -155
- package/ui.ts +0 -143
- package/workspace.ts +0 -20
package/session.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/** DevThink session model: one stable identity contract for CLI, gateway, local database, and URL-addressable web state. */
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { ensurePaths, type DevThinkPaths } from "./config.ts";
|
|
5
|
-
import type { ChatMessage } from "./providers.ts";
|
|
6
|
-
import { createCompactId } from "./ids.ts";
|
|
7
|
-
import { mirrorSession, type StoredSession, type StoredWorkspace } from "./storage.ts";
|
|
8
|
-
|
|
9
|
-
export type SessionSection = "chat" | "inspector" | "settings" | "memory" | "providers" | "projects" | "routes" | "usage";
|
|
10
|
-
export type SessionTab = { id: string; sessionId: string; workspaceId: string; label: string; provider?: string; sectionId: SessionSection; createdAt: string; updatedAt: string };
|
|
11
|
-
export type SessionMessage = ChatMessage & { id: string; sessionId: string; workspaceId: string; tabId: string; sectionId: SessionSection; createdAt: string };
|
|
12
|
-
export type Workspace = { id: string; title: string; createdAt: string; updatedAt: string };
|
|
13
|
-
|
|
14
|
-
export type Session = {
|
|
15
|
-
id: string;
|
|
16
|
-
workspaceId: string;
|
|
17
|
-
title: string;
|
|
18
|
-
mode: string;
|
|
19
|
-
model?: string;
|
|
20
|
-
provider?: string;
|
|
21
|
-
createdAt: string;
|
|
22
|
-
updatedAt: string;
|
|
23
|
-
activeTabId: string;
|
|
24
|
-
tabs: SessionTab[];
|
|
25
|
-
messages: SessionMessage[];
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function sessionPath(paths: DevThinkPaths, id: string): string {
|
|
29
|
-
return join(paths.sessions, `${id}.json`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function safeTitle(messages: SessionMessage[]): string {
|
|
33
|
-
const first = messages.find((message) => message.role === "user");
|
|
34
|
-
return first?.content.slice(0, 80) || "Untitled session";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function workspacePath(paths: DevThinkPaths, id: string): string {
|
|
38
|
-
return join(paths.workspaces, `${id}.json`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function atomicWrite(path: string, value: unknown): void {
|
|
42
|
-
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
43
|
-
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
44
|
-
renameSync(temporary, path);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function newWorkspace(id = createCompactId("w"), title = "Untitled workspace", now = new Date().toISOString()): Workspace {
|
|
48
|
-
return { id, title, createdAt: now, updatedAt: now };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function newTab(sessionId: string, workspaceId: string, provider: string | undefined, label: string, sectionId: SessionSection, now: string, id = createCompactId("t")): SessionTab {
|
|
52
|
-
return { id, sessionId, workspaceId, provider, label, sectionId, createdAt: now, updatedAt: now };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function normalizeSession(raw: Session): Session {
|
|
56
|
-
const now = raw.updatedAt || new Date().toISOString();
|
|
57
|
-
const workspaceId = raw.workspaceId || createCompactId("w");
|
|
58
|
-
const tabs = raw.tabs?.length ? raw.tabs.map((tab) => ({ ...tab, sessionId: raw.id, workspaceId, sectionId: tab.sectionId || "chat" as SessionSection })) : [newTab(raw.id, workspaceId, raw.provider, raw.title || "Untitled session", "chat", now)];
|
|
59
|
-
const activeTabId = tabs.some((tab) => tab.id === raw.activeTabId) ? raw.activeTabId : tabs[0].id;
|
|
60
|
-
const messages = (raw.messages || []).map((message) => ({ ...message, id: message.id || createCompactId("m"), sessionId: raw.id, workspaceId, tabId: message.tabId || activeTabId, sectionId: message.sectionId || "chat" as SessionSection, createdAt: message.createdAt || now }));
|
|
61
|
-
return { ...raw, workspaceId, activeTabId, tabs, messages };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function readSession(path: string): Session | undefined {
|
|
65
|
-
try {
|
|
66
|
-
if (!existsSync(path)) return undefined;
|
|
67
|
-
return normalizeSession(JSON.parse(readFileSync(path, "utf8")) as Session);
|
|
68
|
-
} catch {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function workspaceFor(session: Session): Workspace {
|
|
74
|
-
const title = session.title === "Untitled session" ? "Untitled workspace" : session.title;
|
|
75
|
-
return newWorkspace(session.workspaceId, title, session.createdAt);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function saveSession(paths: DevThinkPaths, session: Session): Session {
|
|
79
|
-
ensurePaths(paths);
|
|
80
|
-
const normalized = normalizeSession(session);
|
|
81
|
-
const existing = loadWorkspace(paths, normalized.workspaceId);
|
|
82
|
-
const workspace = { ...(existing || workspaceFor(normalized)), title: normalized.title === "Untitled session" ? existing?.title || "Untitled workspace" : normalized.title, updatedAt: normalized.updatedAt };
|
|
83
|
-
atomicWrite(sessionPath(paths, normalized.id), normalized);
|
|
84
|
-
atomicWrite(workspacePath(paths, workspace.id), workspace);
|
|
85
|
-
mirrorSession(paths, workspace as StoredWorkspace, normalized as StoredSession);
|
|
86
|
-
return normalized;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function createWorkspace(paths: DevThinkPaths, title = "Untitled workspace", id = createCompactId("w")): Workspace {
|
|
90
|
-
ensurePaths(paths);
|
|
91
|
-
const workspace = newWorkspace(id, title);
|
|
92
|
-
atomicWrite(workspacePath(paths, id), workspace);
|
|
93
|
-
return workspace;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function loadWorkspace(paths: DevThinkPaths, id: string): Workspace | undefined {
|
|
97
|
-
try {
|
|
98
|
-
if (!existsSync(workspacePath(paths, id))) return undefined;
|
|
99
|
-
return JSON.parse(readFileSync(workspacePath(paths, id), "utf8")) as Workspace;
|
|
100
|
-
} catch {
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function createSession(paths: DevThinkPaths, metadata: Pick<Session, "mode" | "model" | "provider"> & { workspaceId?: string; tabId?: string; sectionId?: SessionSection; title?: string }): Session {
|
|
106
|
-
const now = new Date().toISOString();
|
|
107
|
-
const id = createCompactId("s");
|
|
108
|
-
const workspace = metadata.workspaceId ? loadWorkspace(paths, metadata.workspaceId) || createWorkspace(paths, "Untitled workspace", metadata.workspaceId) : createWorkspace(paths);
|
|
109
|
-
const tab = newTab(id, workspace.id, metadata.provider, metadata.title || "Untitled session", metadata.sectionId || "chat", now, metadata.tabId);
|
|
110
|
-
const session: Session = { id, workspaceId: workspace.id, title: metadata.title || "Untitled session", mode: metadata.mode, model: metadata.model, provider: metadata.provider, createdAt: now, updatedAt: now, activeTabId: tab.id, tabs: [tab], messages: [] };
|
|
111
|
-
return saveSession(paths, session);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function loadSession(paths: DevThinkPaths, id: string): Session | undefined {
|
|
115
|
-
const session = readSession(sessionPath(paths, id));
|
|
116
|
-
return session ? saveSession(paths, session) : undefined;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function createTab(paths: DevThinkPaths, session: Session, input: { id?: string; label?: string; provider?: string; sectionId?: SessionSection }): Session {
|
|
120
|
-
const now = new Date().toISOString();
|
|
121
|
-
const tab = newTab(session.id, session.workspaceId, input.provider || session.provider, input.label || "New session", input.sectionId || "chat", now, input.id);
|
|
122
|
-
return saveSession(paths, { ...session, activeTabId: tab.id, updatedAt: now, tabs: [...session.tabs, tab] });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function updateTab(paths: DevThinkPaths, session: Session, tabId: string, input: Partial<Pick<SessionTab, "label" | "provider" | "sectionId">>): Session {
|
|
126
|
-
const now = new Date().toISOString();
|
|
127
|
-
const tabs = session.tabs.map((tab) => tab.id === tabId ? { ...tab, ...input, updatedAt: now } : tab);
|
|
128
|
-
if (!tabs.some((tab) => tab.id === tabId)) throw new Error(`Tab not found: ${tabId}`);
|
|
129
|
-
return saveSession(paths, { ...session, activeTabId: tabId, updatedAt: now, tabs });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function appendMessage(paths: DevThinkPaths, session: Session, message: ChatMessage, context?: { tabId?: string; sectionId?: SessionSection; id?: string }): Session {
|
|
133
|
-
const now = new Date().toISOString();
|
|
134
|
-
const tabId = context?.tabId || session.activeTabId;
|
|
135
|
-
if (!session.tabs.some((tab) => tab.id === tabId)) throw new Error(`Tab not found: ${tabId}`);
|
|
136
|
-
const nextMessages = [...session.messages, { ...message, id: context?.id || createCompactId("m"), sessionId: session.id, workspaceId: session.workspaceId, tabId, sectionId: context?.sectionId || "chat", createdAt: now }];
|
|
137
|
-
return saveSession(paths, { ...session, title: safeTitle(nextMessages), updatedAt: now, messages: nextMessages });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function listSessions(paths: DevThinkPaths): Session[] {
|
|
141
|
-
if (!existsSync(paths.sessions)) return [];
|
|
142
|
-
return readdirSync(paths.sessions).filter((file) => file.endsWith(".json") && !file.endsWith(".memory.json")).map((file) => readSession(join(paths.sessions, file))).filter((session): session is Session => Boolean(session)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function renderSessionMarkdown(session: Session): string {
|
|
146
|
-
const lines = [`# ${session.title}`, "", `- Workspace: ${session.workspaceId}`, `- Session: ${session.id}`, `- Active tab: ${session.activeTabId}`, `- Mode: ${session.mode}`, `- Provider: ${session.provider || "not configured"}`, `- Model: ${session.model || "not configured"}`, ""];
|
|
147
|
-
for (const message of session.messages) lines.push(`## ${message.role}`, "", message.content, "");
|
|
148
|
-
return `${lines.join("\n")}\n`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function exportSession(paths: DevThinkPaths, id: string, format: "json" | "markdown"): string {
|
|
152
|
-
const session = loadSession(paths, id);
|
|
153
|
-
if (!session) throw new Error(`Session not found: ${id}`);
|
|
154
|
-
return format === "json" ? `${JSON.stringify(session, null, 2)}\n` : renderSessionMarkdown(session);
|
|
155
|
-
}
|
package/storage.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/** DevThink local store: mirrors credential-free workbench records into a physical SQLite database. */
|
|
2
|
-
import type { DevThinkPaths } from "./config.ts";
|
|
3
|
-
|
|
4
|
-
type Statement = { run: (...values: unknown[]) => unknown; all: (...values: unknown[]) => unknown[] };
|
|
5
|
-
type Database = { exec: (sql: string) => unknown; prepare: (sql: string) => Statement; close: () => void };
|
|
6
|
-
type DatabaseConstructor = new (path: string) => Database;
|
|
7
|
-
|
|
8
|
-
const sqliteModule = await import(process.versions.bun ? "bun:sqlite" : "node:sqlite");
|
|
9
|
-
const DatabaseDriver = (process.versions.bun ? sqliteModule.Database : sqliteModule.DatabaseSync) as unknown as DatabaseConstructor;
|
|
10
|
-
|
|
11
|
-
export type StoredWorkspace = { id: string; title: string; createdAt: string; updatedAt: string };
|
|
12
|
-
export type StoredTab = { id: string; sessionId: string; workspaceId: string; label: string; provider?: string; sectionId: string; createdAt: string; updatedAt: string };
|
|
13
|
-
export type StoredMessage = { id: string; sessionId: string; workspaceId: string; tabId: string; sectionId: string; role: string; content: string; createdAt: string };
|
|
14
|
-
export type StoredSession = { id: string; workspaceId: string; title: string; mode: string; model?: string; provider?: string; activeTabId: string; createdAt: string; updatedAt: string; tabs: StoredTab[]; messages: StoredMessage[] };
|
|
15
|
-
export type StoredPreference = { key: string; value: string; updatedAt: string };
|
|
16
|
-
|
|
17
|
-
function initialize(database: Database): void {
|
|
18
|
-
database.exec(`
|
|
19
|
-
PRAGMA journal_mode = WAL;
|
|
20
|
-
PRAGMA foreign_keys = ON;
|
|
21
|
-
CREATE TABLE IF NOT EXISTS workspaces (id TEXT PRIMARY KEY, title TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
22
|
-
CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, title TEXT NOT NULL, mode TEXT NOT NULL, model TEXT, provider TEXT, active_tab_id TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
23
|
-
CREATE TABLE IF NOT EXISTS tabs (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, workspace_id TEXT NOT NULL, label TEXT NOT NULL, provider TEXT, section_id TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
24
|
-
CREATE TABLE IF NOT EXISTS messages (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, workspace_id TEXT NOT NULL, tab_id TEXT NOT NULL, section_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL);
|
|
25
|
-
CREATE TABLE IF NOT EXISTS preferences (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
26
|
-
CREATE INDEX IF NOT EXISTS sessions_workspace_updated ON sessions(workspace_id, updated_at DESC);
|
|
27
|
-
CREATE INDEX IF NOT EXISTS tabs_session_updated ON tabs(session_id, updated_at DESC);
|
|
28
|
-
CREATE INDEX IF NOT EXISTS messages_session_created ON messages(session_id, created_at ASC);
|
|
29
|
-
`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Executes a local SQLite transaction without allowing database failures to erase JSON-compatible records. */
|
|
33
|
-
export function mirrorSession(paths: DevThinkPaths, workspace: StoredWorkspace, session: StoredSession): void {
|
|
34
|
-
let database: Database | undefined;
|
|
35
|
-
try {
|
|
36
|
-
database = new DatabaseDriver(paths.database);
|
|
37
|
-
initialize(database);
|
|
38
|
-
database.exec("BEGIN IMMEDIATE");
|
|
39
|
-
database.prepare("INSERT INTO workspaces (id, title, created_at, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET title = excluded.title, updated_at = excluded.updated_at").run(workspace.id, workspace.title, workspace.createdAt, workspace.updatedAt);
|
|
40
|
-
database.prepare("INSERT INTO sessions (id, workspace_id, title, mode, model, provider, active_tab_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET workspace_id = excluded.workspace_id, title = excluded.title, mode = excluded.mode, model = excluded.model, provider = excluded.provider, active_tab_id = excluded.active_tab_id, updated_at = excluded.updated_at").run(session.id, session.workspaceId, session.title, session.mode, session.model ?? null, session.provider ?? null, session.activeTabId, session.createdAt, session.updatedAt);
|
|
41
|
-
database.prepare("DELETE FROM tabs WHERE session_id = ?").run(session.id);
|
|
42
|
-
database.prepare("DELETE FROM messages WHERE session_id = ?").run(session.id);
|
|
43
|
-
const addTab = database.prepare("INSERT INTO tabs (id, session_id, workspace_id, label, provider, section_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
44
|
-
const addMessage = database.prepare("INSERT INTO messages (id, session_id, workspace_id, tab_id, section_id, role, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
45
|
-
for (const tab of session.tabs) addTab.run(tab.id, tab.sessionId, tab.workspaceId, tab.label, tab.provider ?? null, tab.sectionId, tab.createdAt, tab.updatedAt);
|
|
46
|
-
for (const message of session.messages) addMessage.run(message.id, message.sessionId, message.workspaceId, message.tabId, message.sectionId, message.role, message.content, message.createdAt);
|
|
47
|
-
database.exec("COMMIT");
|
|
48
|
-
} catch {
|
|
49
|
-
try { database?.exec("ROLLBACK"); } catch { /* JSON compatibility record remains the authoritative fallback. */ }
|
|
50
|
-
} finally {
|
|
51
|
-
database?.close();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Reads non-sensitive workbench preferences from the CLI-owned local database. */
|
|
56
|
-
export function readPreferences(paths: DevThinkPaths): Record<string, StoredPreference> {
|
|
57
|
-
let database: Database | undefined;
|
|
58
|
-
try {
|
|
59
|
-
database = new DatabaseDriver(paths.database);
|
|
60
|
-
initialize(database);
|
|
61
|
-
const rows = database.prepare("SELECT key, value, updated_at AS updatedAt FROM preferences ORDER BY key ASC").all() as StoredPreference[];
|
|
62
|
-
return Object.fromEntries(rows.map((row) => [row.key, row]));
|
|
63
|
-
} catch {
|
|
64
|
-
return {};
|
|
65
|
-
} finally {
|
|
66
|
-
database?.close();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Persists an allowlisted interface preference without mixing it with provider credentials. */
|
|
71
|
-
export function savePreference(paths: DevThinkPaths, key: string, value: string): StoredPreference {
|
|
72
|
-
const preference = { key, value, updatedAt: new Date().toISOString() };
|
|
73
|
-
let database: Database | undefined;
|
|
74
|
-
try {
|
|
75
|
-
database = new DatabaseDriver(paths.database);
|
|
76
|
-
initialize(database);
|
|
77
|
-
database.prepare("INSERT INTO preferences (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at").run(preference.key, preference.value, preference.updatedAt);
|
|
78
|
-
} finally {
|
|
79
|
-
database?.close();
|
|
80
|
-
}
|
|
81
|
-
return preference;
|
|
82
|
-
}
|
package/stream.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
export type ChatEvent =
|
|
2
|
-
| { type: "start"; provider: string; model: string }
|
|
3
|
-
| { type: "text"; text: string }
|
|
4
|
-
| { type: "reasoning"; text: string }
|
|
5
|
-
| { type: "tool"; name: string; input: unknown }
|
|
6
|
-
| { type: "error"; message: string }
|
|
7
|
-
| { type: "finish"; reason?: string };
|
|
8
|
-
|
|
9
|
-
export type StreamOptions = {
|
|
10
|
-
provider: string;
|
|
11
|
-
model: string;
|
|
12
|
-
signal?: AbortSignal;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function decodeJson(value: string): Record<string, unknown> | null {
|
|
16
|
-
try {
|
|
17
|
-
const parsed: unknown = JSON.parse(value);
|
|
18
|
-
return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null;
|
|
19
|
-
} catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function extractSseFrames(buffer: { value: string }, incoming: string): string[] {
|
|
25
|
-
buffer.value += incoming;
|
|
26
|
-
const frames = buffer.value.split(/\r?\n\r?\n/);
|
|
27
|
-
buffer.value = frames.pop() || "";
|
|
28
|
-
return frames;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function extractData(frame: string): string | null {
|
|
32
|
-
const lines = frame.split(/\r?\n/).filter((line) => line.startsWith("data:"));
|
|
33
|
-
if (lines.length === 0) return null;
|
|
34
|
-
const value = lines.map((line) => line.slice(5).trimStart()).join("\n").trim();
|
|
35
|
-
return value && value !== "[DONE]" ? value : null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function textAt(value: unknown): string {
|
|
39
|
-
return typeof value === "string" ? value : "";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function normalizeOpenAi(frame: Record<string, unknown>): ChatEvent[] {
|
|
43
|
-
const choices = Array.isArray(frame.choices) ? frame.choices : [];
|
|
44
|
-
const choice = choices[0] as Record<string, unknown> | undefined;
|
|
45
|
-
const delta = (choice?.delta || {}) as Record<string, unknown>;
|
|
46
|
-
const events: ChatEvent[] = [];
|
|
47
|
-
const reasoning = textAt(delta.reasoning_content || delta.reasoning);
|
|
48
|
-
const text = textAt(delta.content);
|
|
49
|
-
if (reasoning) events.push({ type: "reasoning", text: reasoning });
|
|
50
|
-
if (text) events.push({ type: "text", text });
|
|
51
|
-
const finish = choice?.finish_reason;
|
|
52
|
-
if (finish) events.push({ type: "finish", reason: textAt(finish) });
|
|
53
|
-
return events;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizeAnthropic(frame: Record<string, unknown>): ChatEvent[] {
|
|
57
|
-
const type = textAt(frame.type);
|
|
58
|
-
if (type === "content_block_delta") {
|
|
59
|
-
const delta = (frame.delta || {}) as Record<string, unknown>;
|
|
60
|
-
const text = textAt(delta.text);
|
|
61
|
-
return text ? [{ type: "text", text }] : [];
|
|
62
|
-
}
|
|
63
|
-
if (type === "message_stop") return [{ type: "finish", reason: "stop" }];
|
|
64
|
-
if (type === "error") {
|
|
65
|
-
const error = (frame.error || {}) as Record<string, unknown>;
|
|
66
|
-
return [{ type: "error", message: textAt(error.message) || "Anthropic returned an error." }];
|
|
67
|
-
}
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function normalizeGoogle(frame: Record<string, unknown>): ChatEvent[] {
|
|
72
|
-
const candidates = Array.isArray(frame.candidates) ? frame.candidates : [];
|
|
73
|
-
const content = ((candidates[0] as Record<string, unknown> | undefined)?.content || {}) as Record<string, unknown>;
|
|
74
|
-
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
75
|
-
const events: ChatEvent[] = [];
|
|
76
|
-
for (const part of parts) {
|
|
77
|
-
const item = part as Record<string, unknown>;
|
|
78
|
-
const text = textAt(item.text);
|
|
79
|
-
if (text) events.push({ type: "text", text });
|
|
80
|
-
}
|
|
81
|
-
const finish = textAt((candidates[0] as Record<string, unknown> | undefined)?.finishReason);
|
|
82
|
-
if (finish) events.push({ type: "finish", reason: finish });
|
|
83
|
-
return events;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function normalizeFrame(provider: string, frame: Record<string, unknown>): ChatEvent[] {
|
|
87
|
-
const lower = provider.toLowerCase();
|
|
88
|
-
if (lower === "anthropic") return normalizeAnthropic(frame);
|
|
89
|
-
if (lower === "google") return normalizeGoogle(frame);
|
|
90
|
-
const data = (frame.data || frame) as Record<string, unknown>;
|
|
91
|
-
if (data !== frame && textAt(data.delta_content)) return [{ type: "text", text: textAt(data.delta_content) }];
|
|
92
|
-
if (data !== frame && textAt(data.reasoning_content)) return [{ type: "reasoning", text: textAt(data.reasoning_content) }];
|
|
93
|
-
if (data.error || frame.error) return [{ type: "error", message: textAt(data.error || frame.error) || "The provider returned an error." }];
|
|
94
|
-
return normalizeOpenAi(frame);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function* parseEventStream(response: Response, options: StreamOptions): AsyncGenerator<ChatEvent> {
|
|
98
|
-
if (!response.body) throw new Error("The provider returned an empty stream.");
|
|
99
|
-
yield { type: "start", provider: options.provider, model: options.model };
|
|
100
|
-
const reader = response.body.getReader();
|
|
101
|
-
const decoder = new TextDecoder();
|
|
102
|
-
const buffer = { value: "" };
|
|
103
|
-
try {
|
|
104
|
-
while (true) {
|
|
105
|
-
const { done, value } = await reader.read();
|
|
106
|
-
if (done) break;
|
|
107
|
-
for (const frame of extractSseFrames(buffer, decoder.decode(value, { stream: true }))) {
|
|
108
|
-
const payload = extractData(frame);
|
|
109
|
-
if (!payload) continue;
|
|
110
|
-
const parsed = decodeJson(payload);
|
|
111
|
-
if (!parsed) continue;
|
|
112
|
-
for (const event of normalizeFrame(options.provider, parsed)) yield event;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} finally {
|
|
116
|
-
reader.releaseLock();
|
|
117
|
-
}
|
|
118
|
-
yield { type: "finish", reason: "stop" };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export async function collectEvents(events: AsyncIterable<ChatEvent>): Promise<{ text: string; reasoning: string }> {
|
|
122
|
-
let text = "";
|
|
123
|
-
let reasoning = "";
|
|
124
|
-
for await (const event of events) {
|
|
125
|
-
if (event.type === "text") text += event.text;
|
|
126
|
-
if (event.type === "reasoning") reasoning += event.text;
|
|
127
|
-
if (event.type === "error") throw new Error(event.message);
|
|
128
|
-
}
|
|
129
|
-
return { text, reasoning };
|
|
130
|
-
}
|
package/sync.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { getIdentity } from "./identity.ts";
|
|
2
|
-
import { listSessions, loadWorkspace, type Session, type Workspace } from "./session.ts";
|
|
3
|
-
import { isSecureRemoteEndpoint } from "./compatibility.ts";
|
|
4
|
-
import type { DevThinkConfig, DevThinkPaths } from "./config.ts";
|
|
5
|
-
|
|
6
|
-
export type SyncSnapshot = { version: 1; userId: string; deviceId: string; exportedAt: string; workspaces: Workspace[]; sessions: Session[] };
|
|
7
|
-
export type RemoteSyncStatus = { enabled: boolean; endpoint?: string; ready: boolean; reason?: string };
|
|
8
|
-
|
|
9
|
-
export function exportLocalSnapshot(paths: DevThinkPaths): SyncSnapshot {
|
|
10
|
-
const identity = getIdentity(paths);
|
|
11
|
-
const sessions = listSessions(paths);
|
|
12
|
-
const workspaces = [...new Set(sessions.map((session) => session.workspaceId))].flatMap((workspaceId) => {
|
|
13
|
-
const workspace = loadWorkspace(paths, workspaceId);
|
|
14
|
-
return workspace ? [workspace] : [];
|
|
15
|
-
});
|
|
16
|
-
return { version: 1, userId: identity.userId, deviceId: identity.deviceId, exportedAt: new Date().toISOString(), workspaces, sessions };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function remoteSyncStatus(config: DevThinkConfig): RemoteSyncStatus {
|
|
20
|
-
const remote = config.web?.remoteSync;
|
|
21
|
-
if (!remote?.enabled) return { enabled: false, ready: false, reason: "Remote synchronization is disabled; local pairing remains available." };
|
|
22
|
-
if (!remote.endpoint) return { enabled: true, ready: false, reason: "Set web.remoteSync.endpoint to a user-operated HTTPS API that implements the DevThink sync contract." };
|
|
23
|
-
if (!isSecureRemoteEndpoint(remote.endpoint)) {
|
|
24
|
-
return { enabled: true, endpoint: remote.endpoint, ready: false, reason: "Remote synchronization endpoint is not a valid HTTPS URL." };
|
|
25
|
-
}
|
|
26
|
-
return { enabled: true, endpoint: new URL(remote.endpoint).toString(), ready: true };
|
|
27
|
-
}
|
package/terminal-ui.tsx
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/** Design: DevThink v1.1.15 — Ink shares the compact local workspace grammar, identity, destinations, settings and command language with the paired React workbench. */
|
|
2
|
-
import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
4
|
-
import { listProviders } from "./providers.ts";
|
|
5
|
-
import { listSessions, type Session } from "./session.ts";
|
|
6
|
-
import { readPreferences, savePreference } from "./storage.ts";
|
|
7
|
-
import { getIdentity, pairingStatus } from "./identity.ts";
|
|
8
|
-
import type { DevThinkConfig, DevThinkPaths } from "./config.ts";
|
|
9
|
-
import type { ChatEvent } from "./stream.ts";
|
|
10
|
-
import { isWorkspaceDestination, workspaceDestination, workspaceDestinations, type WorkspaceDestination } from "./workspace.ts";
|
|
11
|
-
|
|
12
|
-
const categories = ["features", "bugs", "refactor", "snippets", "tasks", "notes", "all"] as const;
|
|
13
|
-
const categoryGlyphs: Record<(typeof categories)[number], string> = { features: "ϟ", bugs: "⊗", refactor: "◌", snippets: "◫", tasks: "☑", notes: "□", all: "◉" };
|
|
14
|
-
const entryArt = [" +++++", " +++++++++++++++", " ++++++ ++++++", " +++++ +++++", " +++++ +++++", " +++++ +++++", " ++++++ ++++++", " +++++++++++++", " +++++"];
|
|
15
|
-
|
|
16
|
-
type Runtime = { config: DevThinkConfig; paths: DevThinkPaths };
|
|
17
|
-
type Execute = (prompt: string, current: Session | undefined, onEvent: (event: ChatEvent) => void) => Promise<Session | undefined>;
|
|
18
|
-
type Entry = { role: "user" | "devthink" | "error"; title: string; body: string };
|
|
19
|
-
|
|
20
|
-
function clip(value: string, width: number): string {
|
|
21
|
-
return value.length > width ? `${value.slice(0, Math.max(0, width - 1))}…` : value;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function TerminalEntry({ draft, provider, userId, width }: { draft: string; provider: string; userId: string; width: number }) {
|
|
25
|
-
return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={2} paddingY={1} justifyContent="space-between">
|
|
26
|
-
<Box justifyContent="space-between"><Text color="#807970">✦ DEVTHINK</Text><Text color="#8ab4f8">{userId}</Text></Box>
|
|
27
|
-
<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>
|
|
28
|
-
<Text color="#746f66">enter opens a local workspace · esc exits</Text>
|
|
29
|
-
</Box>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function projectRows(runtime: Runtime): string[] {
|
|
33
|
-
const projects = new Map<string, { title: string; sessions: number }>();
|
|
34
|
-
for (const session of listSessions(runtime.paths)) projects.set(session.workspaceId, { title: session.title, sessions: (projects.get(session.workspaceId)?.sessions || 0) + 1 });
|
|
35
|
-
return [...projects.entries()].map(([id, project]) => `${id} ${project.sessions} ${project.sessions === 1 ? "session" : "sessions"} ${project.title}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function viewRows(view: WorkspaceDestination, runtime: Runtime, session: Session | undefined): string[] {
|
|
39
|
-
if (view === "history") return listSessions(runtime.paths).map((item) => `${item.id} ${item.title}`) || ["No saved sessions."];
|
|
40
|
-
if (view === "projects") return projectRows(runtime) || ["No local projects."];
|
|
41
|
-
if (view === "providers") return listProviders().map((provider) => `${provider.id} ${provider.protocol} ${provider.env}`);
|
|
42
|
-
if (view === "usage") {
|
|
43
|
-
const sessions = listSessions(runtime.paths);
|
|
44
|
-
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)}`];
|
|
45
|
-
}
|
|
46
|
-
if (view === "routes") return ["GET /health", "GET /providers", "GET /workspaces", "GET /usage", "GET /preferences", "PATCH /preferences", "POST /sessions", "POST /chat"];
|
|
47
|
-
if (view === "settings") {
|
|
48
|
-
const preferences = Object.fromEntries(Object.entries(readPreferences(runtime.paths)).map(([key, preference]) => [key, preference.value]));
|
|
49
|
-
const identity = getIdentity(runtime.paths);
|
|
50
|
-
const pairing = pairingStatus(runtime.paths);
|
|
51
|
-
const sessions = listSessions(runtime.paths);
|
|
52
|
-
return [`userId ${identity.userId}`, `deviceId ${identity.deviceId}`, `browser sessions ${pairing.activeSessions}`, `database cli-owned-sqlite · ${new Set(sessions.map((session) => session.workspaceId)).size} workspaces · ${sessions.length} sessions`, `provider ${runtime.config.activeProvider || runtime.config.provider || "not configured"}`, `theme ${preferences.theme || "dark"}`, `railMode ${preferences.railMode || "auto"}`, `interfaceZoom ${preferences.interfaceZoom || "100"}`, "inspect all: devthink config settings", "edit identity: devthink identity --id <10-15 lowercase chars>", "edit: /settings set theme dark|light", "edit: /settings set railMode always|auto|off", "edit: /settings set interfaceZoom 80-150", "credentials remain in ~/.config/devthink/auth.json"];
|
|
53
|
-
}
|
|
54
|
-
if (session) return [`workspace ${session.workspaceId}`, `session ${session.id}`, `tab ${session.activeTabId}`];
|
|
55
|
-
return ["Describe the next feature, bug or piece of work below."];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; execute: Execute; version: string }) {
|
|
59
|
-
const { exit } = useApp();
|
|
60
|
-
const { columns } = useWindowSize();
|
|
61
|
-
const [entered, setEntered] = useState(false);
|
|
62
|
-
const [active, setActive] = useState<(typeof categories)[number]>("all");
|
|
63
|
-
const [view, setView] = useState<WorkspaceDestination>("chat");
|
|
64
|
-
const [entries, setEntries] = useState<Entry[]>([]);
|
|
65
|
-
const [session, setSession] = useState<Session>();
|
|
66
|
-
const [draft, setDraft] = useState("");
|
|
67
|
-
const [pending, setPending] = useState(false);
|
|
68
|
-
const [preferenceRevision, setPreferenceRevision] = useState(0);
|
|
69
|
-
const provider = runtime.config.activeProvider || runtime.config.provider || "local";
|
|
70
|
-
const identity = getIdentity(runtime.paths);
|
|
71
|
-
const model = runtime.config.activeModel || runtime.config.model || "not configured";
|
|
72
|
-
const compact = columns < 100;
|
|
73
|
-
const terminalWidth = Math.max(38, columns - (compact ? 2 : 6));
|
|
74
|
-
const tabLine = compact ? workspaceDestinations.map((destination) => destination.glyph).join(" ") : workspaceDestinations.map((destination) => `${destination.id === view ? "[" : ""}${destination.glyph} ${destination.label}${destination.id === view ? "]" : ""}`).join(" ");
|
|
75
|
-
const rows = useMemo(() => viewRows(view, runtime, session), [preferenceRevision, runtime, session, view]);
|
|
76
|
-
|
|
77
|
-
function openView(next: WorkspaceDestination) {
|
|
78
|
-
setView(next);
|
|
79
|
-
setEntries((current) => [...current, { role: "devthink", title: workspaceDestination(next).label, body: rows.join(" · ") }]);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function submit() {
|
|
83
|
-
const prompt = draft.trim();
|
|
84
|
-
if (!prompt || pending) return;
|
|
85
|
-
if (prompt === "/exit" || prompt === "/quit") return exit();
|
|
86
|
-
if (!entered) {
|
|
87
|
-
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." }]);
|
|
88
|
-
setDraft("");
|
|
89
|
-
setEntered(true);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (prompt === "/clear") { setEntries([]); setDraft(""); return; }
|
|
93
|
-
if (prompt === "/new") { setSession(undefined); setEntries([]); setView("chat"); setDraft(""); return; }
|
|
94
|
-
const viewMatch = prompt.match(/^\/(?:view\s+)?(chat|history|projects|providers|usage|routes|settings)$/);
|
|
95
|
-
if (viewMatch && isWorkspaceDestination(viewMatch[1])) { openView(viewMatch[1]); setDraft(""); return; }
|
|
96
|
-
if (prompt === "/sessions") { openView("history"); setDraft(""); return; }
|
|
97
|
-
const preferenceMatch = prompt.match(/^\/(?:settings\s+)?set\s+(theme|railMode|interfaceZoom)\s+(\S+)$/);
|
|
98
|
-
if (preferenceMatch) {
|
|
99
|
-
const [, key, value] = preferenceMatch;
|
|
100
|
-
const valid = (key === "theme" && ["dark", "light"].includes(value)) || (key === "railMode" && ["always", "auto", "off"].includes(value)) || (key === "interfaceZoom" && /^(?:[8-9][0-9]|1[0-4][0-9]|150)$/.test(value));
|
|
101
|
-
if (!valid) setEntries((current) => [...current, { role: "error", title: "settings", body: "Use theme dark|light, railMode always|auto|off, or interfaceZoom 80-150." }]);
|
|
102
|
-
else { savePreference(runtime.paths, key, value); setPreferenceRevision((current) => current + 1); setView("settings"); setEntries((current) => [...current, { role: "devthink", title: "settings", body: `${key} updated to ${value}.` }]); }
|
|
103
|
-
setDraft("");
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (prompt === "/help") { setEntries((current) => [...current, { role: "devthink", title: "command reference", body: "/view chat|history|projects|providers|usage|routes|settings /settings set <key> <value> /new /clear /models /modes /sessions /exit" }]); setDraft(""); return; }
|
|
107
|
-
setView("chat");
|
|
108
|
-
setEntries((current) => [...current, { role: "user", title: active, body: prompt }, { role: "devthink", title: "connecting", body: "waiting for local gateway events…" }]);
|
|
109
|
-
setDraft("");
|
|
110
|
-
setPending(true);
|
|
111
|
-
let partial = "";
|
|
112
|
-
try {
|
|
113
|
-
const next = await execute(prompt, session, (event) => {
|
|
114
|
-
if (event.type === "text" || event.type === "reasoning") partial += event.text;
|
|
115
|
-
setEntries((current) => current.map((entry, index) => index === current.length - 1 ? { ...entry, title: event.type === "start" ? "streaming" : "devthink", body: partial || "connecting to local provider…" } : entry));
|
|
116
|
-
});
|
|
117
|
-
setSession(next);
|
|
118
|
-
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));
|
|
119
|
-
} catch (error) {
|
|
120
|
-
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));
|
|
121
|
-
} finally { setPending(false); }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
useInput((input, key) => {
|
|
125
|
-
if (key.escape || (key.ctrl && input === "c")) return exit();
|
|
126
|
-
if (key.return) return void submit();
|
|
127
|
-
if (key.backspace || key.delete) return setDraft((current) => current.slice(0, -1));
|
|
128
|
-
if (entered && key.upArrow) return setView((current) => workspaceDestinations[(workspaceDestinations.findIndex((item) => item.id === current) + workspaceDestinations.length - 1) % workspaceDestinations.length].id);
|
|
129
|
-
if (entered && key.downArrow) return setView((current) => workspaceDestinations[(workspaceDestinations.findIndex((item) => item.id === current) + 1) % workspaceDestinations.length].id);
|
|
130
|
-
if (entered && view === "chat" && (key.leftArrow || key.tab)) return setActive((current) => categories[(categories.indexOf(current) + categories.length - 1) % categories.length]);
|
|
131
|
-
if (entered && view === "chat" && key.rightArrow) return setActive((current) => categories[(categories.indexOf(current) + 1) % categories.length]);
|
|
132
|
-
if (!key.ctrl && !key.meta && input) setDraft((current) => current + input);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (!entered) return <TerminalEntry draft={draft} provider={provider} userId={identity.userId} width={terminalWidth} />;
|
|
136
|
-
|
|
137
|
-
return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={compact ? 1 : 3} paddingY={1}>
|
|
138
|
-
<Box justifyContent="space-between"><Text color="#a19a91">✦ DEVTHINK <Text color="#746f66">/ {workspaceDestination(view).label}</Text></Text><Text color="#8ab4f8">◉ {identity.userId}</Text></Box>
|
|
139
|
-
<Text color="#282724">{"─".repeat(terminalWidth)}</Text>
|
|
140
|
-
<Text color="#746f66">{clip(tabLine, terminalWidth)}</Text>
|
|
141
|
-
<Box marginTop={1}><Text color="#e8e1d7">{workspaceDestination(view).glyph} {workspaceDestination(view).label.toUpperCase()} <Text color="#746f66">· {clip(model, 22)}</Text></Text></Box>
|
|
142
|
-
<Box flexDirection="column" flexGrow={1} justifyContent={entries.length && view === "chat" ? "flex-start" : "center"} alignItems={entries.length && view === "chat" ? "flex-start" : "center"}>
|
|
143
|
-
{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>}
|
|
144
|
-
</Box>
|
|
145
|
-
<Text color="#282724">┌{"─".repeat(Math.max(16, terminalWidth - 2))}┐</Text>
|
|
146
|
-
<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>
|
|
147
|
-
<Text color="#282724">└{"─".repeat(Math.max(16, terminalWidth - 2))}┘</Text>
|
|
148
|
-
<Text color="#746f66">↑↓ destinations · ←→ categories · /help commands · v{version} · esc exit</Text>
|
|
149
|
-
</Box>;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export async function startTerminalWorkspace(runtime: Runtime, version: string, execute: Execute): Promise<void> {
|
|
153
|
-
const app = render(<TerminalWorkspace runtime={runtime} version={version} execute={execute} />, { exitOnCtrlC: false });
|
|
154
|
-
await app.waitUntilExit();
|
|
155
|
-
}
|