@wenathlan/devthink 1.1.13 → 1.1.15
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 +26 -0
- package/devthink.ts +21 -7
- package/identity.ts +20 -0
- package/package.json +1 -1
- package/server.ts +18 -1
- package/terminal-ui.tsx +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
# DevThink release notes
|
|
2
|
+
## 1.1.15 — Unified capabilities and Settings
|
|
3
|
+
### Added
|
|
4
|
+
| Area | Addition |
|
|
5
|
+
| --- | --- |
|
|
6
|
+
| Capability matrix | A renderer-neutral matrix now maps every safe local action across CLI commands, Ink destinations, authenticated gateway routes and web pages. |
|
|
7
|
+
| Shared Settings page | `/settings` is a dedicated browser page equivalent to `devthink config settings`, `devthink config set` and the Ink Settings view. |
|
|
8
|
+
| Settings summary | The authenticated `GET /settings` route returns identity, pairing session count, preferences, active provider summary and local database ownership metadata without secrets or filesystem paths. |
|
|
9
|
+
| Same database client | The paired browser operates as a temporary client of the CLI-owned `devthink.db` for one public ID; it does not create an independent static database. |
|
|
10
|
+
| Settings domain | Pairing controls now belong to `web/settings/`, and the common control navigation includes Settings alongside Providers, Projects, Routes and Usage. |
|
|
11
|
+
|
|
12
|
+
## 1.1.14 — Route and local identity refinement
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
| Area | Addition |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Portable Home route | Relative static builds preserve the Vite `./` base and resolve the Home route at `/` without treating `.` as a router prefix. |
|
|
19
|
+
| CLI-owned person | The browser now projects the authenticated CLI-owned public user and device identity after pairing instead of storing a second browser identity. |
|
|
20
|
+
| Shared public ID | `devthink identity --id <value>` and the paired web Settings panel update the same local public identifier, with compact 10–15 character validation. |
|
|
21
|
+
| Shared route index | Web and CLI route maps now list the authenticated `GET` and `PUT /identity` operations alongside preferences and workspace routes. |
|
|
22
|
+
| Documentation boundary | Workbench design and deployment notes moved from `/web` to `docs/`, leaving the static application tree free of planning documents. |
|
|
23
|
+
|
|
2
24
|
## 1.1.13 — Flat domain consolidation
|
|
3
25
|
|
|
4
26
|
### Added
|
|
@@ -11,6 +33,10 @@
|
|
|
11
33
|
| Compact visual grammar | Historical workspace CSS overrides were replaced with one terminal workspace system for entry, tabs, rail, command bar, pairing, controls and error states. |
|
|
12
34
|
| Control frame | Providers, projects, routes and usage share one explicitly named root control shell without a generic application directory. |
|
|
13
35
|
| Preserved parity boundary | Web and Ink retain the same safe workspace destinations, settings and local gateway interactions; provider credentials remain CLI-owned. |
|
|
36
|
+
| Portable Home route | Relative Vite builds now retain `./` as a relative base and normalize it to the Home router base, so `/` resolves the first workbench screen in portable static artifacts. |
|
|
37
|
+
| Same local person | A paired browser reads the CLI-owned public user and device IDs through the authenticated gateway instead of creating a second browser profile. |
|
|
38
|
+
| Public ID controls | `devthink identity --id <value>` and the paired web Settings panel update the same local public ID, preserving compact workspace data and temporary browser sessions. |
|
|
39
|
+
| Documentation boundary | Workbench deployment and design records now live under `docs/`, leaving `/web` exclusively for browser application artifacts. |
|
|
14
40
|
|
|
15
41
|
## 1.1.12 — Page domains and tabbed interaction
|
|
16
42
|
|
package/devthink.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { createMemoryStore, memorySummary } from "./memory.ts";
|
|
|
8
8
|
import { listModes, modePrompt, resolveMode } from "./modes.ts";
|
|
9
9
|
import { listModels, listProviders, streamChat } from "./providers.ts";
|
|
10
10
|
import { appendMessage, createSession, exportSession, listSessions, loadSession, type Session } from "./session.ts";
|
|
11
|
-
import { createPairing, createPairingLink, getIdentity, pairingStatus, revokeBrowserSessions } from "./identity.ts";
|
|
11
|
+
import { createPairing, createPairingLink, getIdentity, pairingStatus, revokeBrowserSessions, setIdentityUserId } from "./identity.ts";
|
|
12
12
|
import { readPreferences, savePreference } from "./storage.ts";
|
|
13
13
|
import type { ChatEvent } from "./stream.ts";
|
|
14
14
|
import { banner, box, colors, formatConfig, formatEvent, statusBar } from "./ui.ts";
|
|
@@ -69,12 +69,13 @@ function printHelp(): void {
|
|
|
69
69
|
" modes List the 20 operational modes",
|
|
70
70
|
" config [key] [value] Read or write local configuration",
|
|
71
71
|
" config preferences Show shared workbench preferences",
|
|
72
|
+
" config settings Show shared identity, pairing, provider and database summary",
|
|
72
73
|
" config set <key> <value> Set theme, railMode, or interfaceZoom",
|
|
73
74
|
" auth login <provider> Save a user-provided official credential",
|
|
74
75
|
" auth status Show redacted configured credential sources",
|
|
75
76
|
" auth clear <provider> Remove a stored provider credential",
|
|
76
77
|
" auth migrate Move legacy config credentials into auth.json",
|
|
77
|
-
" identity
|
|
78
|
+
" identity [--id <value>] Show or set the local public user identifier",
|
|
78
79
|
" pair create Create an automatic one-time link for the web workbench",
|
|
79
80
|
" pair revoke Revoke current paired browser sessions",
|
|
80
81
|
" sync status Show local snapshot and remote adapter readiness",
|
|
@@ -155,6 +156,17 @@ async function runChat(prompt: string, parsed: ParsedArgs, runtime: ReturnType<t
|
|
|
155
156
|
|
|
156
157
|
async function handleConfig(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): Promise<void> {
|
|
157
158
|
const action = parsed.positional[1];
|
|
159
|
+
if (action === "settings") {
|
|
160
|
+
const identity = getIdentity(runtime.paths);
|
|
161
|
+
const sessions = listSessions(runtime.paths);
|
|
162
|
+
return console.log(JSON.stringify({
|
|
163
|
+
identity,
|
|
164
|
+
pairing: pairingStatus(runtime.paths),
|
|
165
|
+
preferences: Object.fromEntries(Object.entries(readPreferences(runtime.paths)).map(([key, preference]) => [key, preference.value])),
|
|
166
|
+
provider: { activeProvider: runtime.config.activeProvider || runtime.config.provider || undefined, activeModel: runtime.config.activeModel || runtime.config.model || undefined },
|
|
167
|
+
database: { ownerUserId: identity.userId, local: true, persistence: "cli-owned-sqlite", workspaces: new Set(sessions.map((session) => session.workspaceId)).size, sessions: sessions.length },
|
|
168
|
+
}, null, 2));
|
|
169
|
+
}
|
|
158
170
|
if (action === "preferences") return console.log(JSON.stringify(Object.fromEntries(Object.entries(readPreferences(runtime.paths)).map(([key, preference]) => [key, preference.value])), null, 2));
|
|
159
171
|
if (action === "set") {
|
|
160
172
|
const key = parsed.positional[2];
|
|
@@ -212,13 +224,15 @@ async function handleAuth(parsed: ParsedArgs, runtime: ReturnType<typeof loadRun
|
|
|
212
224
|
throw new Error("Supported auth actions: login, status, clear, migrate.");
|
|
213
225
|
}
|
|
214
226
|
|
|
215
|
-
function handleIdentity(runtime: ReturnType<typeof loadRuntime>): void {
|
|
216
|
-
|
|
227
|
+
function handleIdentity(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): void {
|
|
228
|
+
const requestedUserId = stringFlag(parsed, "id");
|
|
229
|
+
const identity = requestedUserId ? setIdentityUserId(runtime.paths, requestedUserId) : getIdentity(runtime.paths);
|
|
230
|
+
console.log(JSON.stringify({ identity, pairing: pairingStatus(runtime.paths) }, null, 2));
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
function handlePairing(parsed: ParsedArgs, runtime: ReturnType<typeof loadRuntime>): void {
|
|
220
234
|
const action = parsed.positional[1] || "create";
|
|
221
|
-
if (action === "status") return handleIdentity(runtime);
|
|
235
|
+
if (action === "status") return handleIdentity(parsed, runtime);
|
|
222
236
|
if (action === "revoke") return console.log(`Revoked ${revokeBrowserSessions(runtime.paths)} paired browser session(s).`);
|
|
223
237
|
if (action !== "create") throw new Error("Supported pair actions: create, status, revoke.");
|
|
224
238
|
const pairing = createPairing(runtime.paths);
|
|
@@ -278,7 +292,7 @@ function handleUsage(runtime: ReturnType<typeof loadRuntime>): void {
|
|
|
278
292
|
}
|
|
279
293
|
|
|
280
294
|
function handleRoutes(): void {
|
|
281
|
-
console.log(["web: /", "web: /providers", "web: /projects", "web: /routes", "web: /usage", "web: /w/:workspaceId/s/:sessionId/t/:tabId/:sectionId", "gateway: GET /health", "gateway: GET /providers", "gateway: GET /workspaces", "gateway: GET /usage", "gateway: GET /preferences", "gateway: PATCH /preferences", "gateway: POST /sessions", "gateway: POST /chat"].join("\n"));
|
|
295
|
+
console.log(["web: /", "web: /providers", "web: /projects", "web: /routes", "web: /usage", "web: /settings", "web: /w/:workspaceId/s/:sessionId/t/:tabId/:sectionId", "gateway: GET /health", "gateway: GET /identity", "gateway: PUT /identity", "gateway: GET /settings", "gateway: GET /providers", "gateway: GET /workspaces", "gateway: GET /usage", "gateway: GET /preferences", "gateway: PATCH /preferences", "gateway: POST /sessions", "gateway: POST /chat"].join("\n"));
|
|
282
296
|
}
|
|
283
297
|
|
|
284
298
|
async function interactive(runtime: ReturnType<typeof loadRuntime>): Promise<void> {
|
|
@@ -351,7 +365,7 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
|
351
365
|
if (command === "modes") return console.log(listModes().map((mode) => `${mode.id} ${mode.purpose}`).join("\n"));
|
|
352
366
|
if (command === "config") return handleConfig(parsed, runtime);
|
|
353
367
|
if (command === "auth") return handleAuth(parsed, runtime);
|
|
354
|
-
if (command === "identity") return handleIdentity(runtime);
|
|
368
|
+
if (command === "identity") return handleIdentity(parsed, runtime);
|
|
355
369
|
if (command === "pair") return handlePairing(parsed, runtime);
|
|
356
370
|
if (command === "sync") return handleSync(parsed, runtime);
|
|
357
371
|
if (command === "gateway") return handleGateway(runtime);
|
package/identity.ts
CHANGED
|
@@ -13,6 +13,12 @@ const codeLength = 8;
|
|
|
13
13
|
const pairingLifetimeMs = 5 * 60 * 1000;
|
|
14
14
|
const browserSessionLifetimeMs = 15 * 60 * 1000;
|
|
15
15
|
|
|
16
|
+
export function normalizePublicUserId(value: string): string {
|
|
17
|
+
const userId = value.trim().toLowerCase();
|
|
18
|
+
if (!/^[a-z][a-z0-9]{9,14}$/.test(userId)) throw new Error("Public user ID must contain 10-15 lowercase letters or numbers and start with a letter.");
|
|
19
|
+
return userId;
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
function digest(value: string): string {
|
|
17
23
|
return createHash("sha256").update(value).digest("hex");
|
|
18
24
|
}
|
|
@@ -44,6 +50,20 @@ export function getIdentity(paths: DevThinkPaths): DevThinkIdentity {
|
|
|
44
50
|
return identity;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/** Updates the public local-person identifier and migrates active local pairing records. */
|
|
54
|
+
export function setIdentityUserId(paths: DevThinkPaths, requestedUserId: string): DevThinkIdentity {
|
|
55
|
+
const current = getIdentity(paths);
|
|
56
|
+
const userId = normalizePublicUserId(requestedUserId);
|
|
57
|
+
if (current.userId === userId) return current;
|
|
58
|
+
const next = { ...current, userId };
|
|
59
|
+
privateWrite(paths.identity, next);
|
|
60
|
+
const store = readStore(paths);
|
|
61
|
+
for (const record of store.pairings) if (record.userId === current.userId) record.userId = userId;
|
|
62
|
+
for (const session of store.sessions) if (session.userId === current.userId) session.userId = userId;
|
|
63
|
+
writeStore(paths, store);
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
function readStore(paths: DevThinkPaths): PairingStore {
|
|
48
68
|
try {
|
|
49
69
|
if (existsSync(paths.pairings)) {
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { randomInt } from "node:crypto";
|
|
|
3
3
|
import { isAllowedOrigin } from "./compatibility.ts";
|
|
4
4
|
import { listModels, listProviders, streamChat, type ChatMessage } from "./providers.ts";
|
|
5
5
|
import { ensurePaths, resolvePaths, saveConfig, type DevThinkConfig, type DevThinkPaths } from "./config.ts";
|
|
6
|
-
import { consumePairing, getIdentity, pairingStatus, revokeBrowserSessions, verifyBrowserSession } from "./identity.ts";
|
|
6
|
+
import { consumePairing, getIdentity, pairingStatus, revokeBrowserSessions, setIdentityUserId, verifyBrowserSession } from "./identity.ts";
|
|
7
7
|
import { appendMessage, createSession, createTab, listSessions, loadSession, loadWorkspace, updateTab, type SessionSection } from "./session.ts";
|
|
8
8
|
import { readPreferences, savePreference } from "./storage.ts";
|
|
9
9
|
|
|
@@ -138,6 +138,12 @@ async function route(request: IncomingMessage, response: ServerResponse, config:
|
|
|
138
138
|
const browserSession = origin ? verifyBrowserSession(paths, bearerToken(request)) : undefined;
|
|
139
139
|
if (origin && !browserSession) return writeJson(response, 401, { error: "A current browser pairing token is required." }, origin);
|
|
140
140
|
if (request.method === "GET" && url.pathname === "/identity") return writeJson(response, 200, { identity: getIdentity(paths), pairing: pairingStatus(paths) }, origin);
|
|
141
|
+
if (request.method === "PUT" && url.pathname === "/identity") {
|
|
142
|
+
const body = await readBody(request);
|
|
143
|
+
if (typeof body.userId !== "string") return writeJson(response, 400, { error: "userId is required." }, origin);
|
|
144
|
+
try { return writeJson(response, 200, { identity: setIdentityUserId(paths, body.userId), pairing: pairingStatus(paths) }, origin); }
|
|
145
|
+
catch (error) { return writeJson(response, 400, { error: error instanceof Error ? error.message : "Identity update was rejected." }, origin); }
|
|
146
|
+
}
|
|
141
147
|
if (request.method === "POST" && url.pathname === "/pairings/revoke") return writeJson(response, 200, { revoked: revokeBrowserSessions(paths) }, origin);
|
|
142
148
|
if (request.method === "GET" && url.pathname === "/sessions") return writeJson(response, 200, { sessions: listSessions(paths) }, origin);
|
|
143
149
|
if (request.method === "GET" && url.pathname === "/workspaces") {
|
|
@@ -157,6 +163,17 @@ async function route(request: IncomingMessage, response: ServerResponse, config:
|
|
|
157
163
|
if (request.method === "GET" && url.pathname === "/preferences") {
|
|
158
164
|
return writeJson(response, 200, { preferences: Object.fromEntries(Object.entries(readPreferences(paths)).map(([key, preference]) => [key, preference.value])) }, origin);
|
|
159
165
|
}
|
|
166
|
+
if (request.method === "GET" && url.pathname === "/settings") {
|
|
167
|
+
const identity = getIdentity(paths);
|
|
168
|
+
const sessions = listSessions(paths);
|
|
169
|
+
return writeJson(response, 200, {
|
|
170
|
+
identity,
|
|
171
|
+
pairing: pairingStatus(paths),
|
|
172
|
+
preferences: Object.fromEntries(Object.entries(readPreferences(paths)).map(([key, preference]) => [key, preference.value])),
|
|
173
|
+
provider: { activeProvider: config.activeProvider || config.provider || undefined, activeModel: config.activeModel || config.model || undefined },
|
|
174
|
+
database: { ownerUserId: identity.userId, local: true, persistence: "cli-owned-sqlite", workspaces: new Set(sessions.map((session) => session.workspaceId)).size, sessions: sessions.length },
|
|
175
|
+
}, origin);
|
|
176
|
+
}
|
|
160
177
|
if (request.method === "PATCH" && url.pathname === "/preferences") {
|
|
161
178
|
const body = await readBody(request);
|
|
162
179
|
const key = typeof body.key === "string" ? body.key : "";
|
package/terminal-ui.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
/** Design: DevThink v1.1.
|
|
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
2
|
import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import { listProviders } from "./providers.ts";
|
|
5
5
|
import { listSessions, type Session } from "./session.ts";
|
|
6
6
|
import { readPreferences, savePreference } from "./storage.ts";
|
|
7
|
+
import { getIdentity, pairingStatus } from "./identity.ts";
|
|
7
8
|
import type { DevThinkConfig, DevThinkPaths } from "./config.ts";
|
|
8
9
|
import type { ChatEvent } from "./stream.ts";
|
|
9
10
|
import { isWorkspaceDestination, workspaceDestination, workspaceDestinations, type WorkspaceDestination } from "./workspace.ts";
|
|
@@ -20,9 +21,9 @@ function clip(value: string, width: number): string {
|
|
|
20
21
|
return value.length > width ? `${value.slice(0, Math.max(0, width - 1))}…` : value;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function TerminalEntry({ draft, provider, width }: { draft: string; provider: string; width: number }) {
|
|
24
|
+
function TerminalEntry({ draft, provider, userId, width }: { draft: string; provider: string; userId: string; width: number }) {
|
|
24
25
|
return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={2} paddingY={1} justifyContent="space-between">
|
|
25
|
-
<Box justifyContent="space-between"><Text color="#807970">✦ DEVTHINK</Text><Text color="#8ab4f8">
|
|
26
|
+
<Box justifyContent="space-between"><Text color="#807970">✦ DEVTHINK</Text><Text color="#8ab4f8">{userId}</Text></Box>
|
|
26
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>
|
|
27
28
|
<Text color="#746f66">enter opens a local workspace · esc exits</Text>
|
|
28
29
|
</Box>;
|
|
@@ -45,7 +46,10 @@ function viewRows(view: WorkspaceDestination, runtime: Runtime, session: Session
|
|
|
45
46
|
if (view === "routes") return ["GET /health", "GET /providers", "GET /workspaces", "GET /usage", "GET /preferences", "PATCH /preferences", "POST /sessions", "POST /chat"];
|
|
46
47
|
if (view === "settings") {
|
|
47
48
|
const preferences = Object.fromEntries(Object.entries(readPreferences(runtime.paths)).map(([key, preference]) => [key, preference.value]));
|
|
48
|
-
|
|
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"];
|
|
49
53
|
}
|
|
50
54
|
if (session) return [`workspace ${session.workspaceId}`, `session ${session.id}`, `tab ${session.activeTabId}`];
|
|
51
55
|
return ["Describe the next feature, bug or piece of work below."];
|
|
@@ -63,6 +67,7 @@ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; ex
|
|
|
63
67
|
const [pending, setPending] = useState(false);
|
|
64
68
|
const [preferenceRevision, setPreferenceRevision] = useState(0);
|
|
65
69
|
const provider = runtime.config.activeProvider || runtime.config.provider || "local";
|
|
70
|
+
const identity = getIdentity(runtime.paths);
|
|
66
71
|
const model = runtime.config.activeModel || runtime.config.model || "not configured";
|
|
67
72
|
const compact = columns < 100;
|
|
68
73
|
const terminalWidth = Math.max(38, columns - (compact ? 2 : 6));
|
|
@@ -127,10 +132,10 @@ function TerminalWorkspace({ runtime, execute, version }: { runtime: Runtime; ex
|
|
|
127
132
|
if (!key.ctrl && !key.meta && input) setDraft((current) => current + input);
|
|
128
133
|
});
|
|
129
134
|
|
|
130
|
-
if (!entered) return <TerminalEntry draft={draft} provider={provider} width={terminalWidth} />;
|
|
135
|
+
if (!entered) return <TerminalEntry draft={draft} provider={provider} userId={identity.userId} width={terminalWidth} />;
|
|
131
136
|
|
|
132
137
|
return <Box flexDirection="column" width="100%" height="100%" minHeight={20} paddingX={compact ? 1 : 3} paddingY={1}>
|
|
133
|
-
<Box justifyContent="space-between"><Text color="#a19a91">✦ DEVTHINK <Text color="#746f66">/ {workspaceDestination(view).label}</Text></Text><Text color="#8ab4f8">◉ {
|
|
138
|
+
<Box justifyContent="space-between"><Text color="#a19a91">✦ DEVTHINK <Text color="#746f66">/ {workspaceDestination(view).label}</Text></Text><Text color="#8ab4f8">◉ {identity.userId}</Text></Box>
|
|
134
139
|
<Text color="#282724">{"─".repeat(terminalWidth)}</Text>
|
|
135
140
|
<Text color="#746f66">{clip(tabLine, terminalWidth)}</Text>
|
|
136
141
|
<Box marginTop={1}><Text color="#e8e1d7">{workspaceDestination(view).glyph} {workspaceDestination(view).label.toUpperCase()} <Text color="#746f66">· {clip(model, 22)}</Text></Text></Box>
|