@oh-my-pi/pi-tui 18.4.0 → 18.4.2
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 +42 -0
- package/dist/types/chat/image-loading.d.ts +24 -0
- package/dist/types/chat/transcript-entry.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +24 -3
- package/dist/types/overlays/agents-hub.d.ts +7 -4
- package/dist/types/overlays/model-browser.d.ts +15 -0
- package/dist/types/overlays/rewind-selector.d.ts +4 -3
- package/dist/types/overlays/usage-dashboard.d.ts +9 -2
- package/dist/types/prompt/composer-attachments.d.ts +7 -0
- package/dist/types/prompt/composer-cache.d.ts +34 -16
- package/dist/types/prompt/composer.d.ts +15 -18
- package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
- package/dist/types/prompt/welcome.d.ts +5 -2
- package/dist/types/render/width-aware-text.d.ts +6 -0
- package/dist/types/status-line/component.d.ts +13 -4
- package/dist/types/status-line/metrics.d.ts +0 -1
- package/dist/types/status-line/startup.d.ts +38 -0
- package/dist/types/status-line/types.d.ts +0 -2
- package/dist/types/terminal-capabilities.d.ts +10 -6
- package/dist/types/terminal.d.ts +7 -0
- package/dist/types/theme/color.d.ts +1 -0
- package/dist/types/theme/session-color.d.ts +3 -3
- package/dist/types/theme/shimmer.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +1 -1
- package/dist/types/tools/output-meta.d.ts +2 -1
- package/dist/types/tools/vibe.d.ts +4 -0
- package/dist/types/tui.d.ts +3 -2
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +27 -21
- package/src/chat/image-loading.ts +81 -0
- package/src/chat/tool-execution.ts +53 -17
- package/src/chat/transcript-entry.ts +16 -0
- package/src/chrome/transcript-container.ts +282 -86
- package/src/components/editor.ts +28 -6
- package/src/components/image.ts +25 -9
- package/src/components/loader.ts +37 -17
- package/src/overlays/agent-hub.ts +16 -6
- package/src/overlays/agents-hub.ts +7 -18
- package/src/overlays/copy-selector.ts +8 -31
- package/src/overlays/model-browser.ts +106 -25
- package/src/overlays/model-hub.ts +49 -16
- package/src/overlays/rewind-selector.ts +60 -13
- package/src/overlays/usage-dashboard.ts +47 -6
- package/src/prompt/composer-attachments.ts +54 -7
- package/src/prompt/composer-cache.ts +217 -196
- package/src/prompt/composer.ts +45 -72
- package/src/prompt/custom-editor.ts +24 -6
- package/src/prompt/model-mention-autocomplete.ts +22 -10
- package/src/prompt/welcome.ts +31 -28
- package/src/render/width-aware-text.ts +9 -0
- package/src/status-line/component.ts +286 -182
- package/src/status-line/metrics.ts +3 -21
- package/src/status-line/segments.ts +68 -79
- package/src/status-line/startup.ts +181 -0
- package/src/status-line/types.ts +0 -2
- package/src/terminal-capabilities.ts +12 -6
- package/src/terminal.ts +75 -20
- package/src/theme/color.ts +15 -0
- package/src/theme/session-color.ts +26 -4
- package/src/theme/shimmer.ts +55 -76
- package/src/theme/theme-class.ts +29 -12
- package/src/tools/output-meta.ts +11 -3
- package/src/tools/vibe.ts +15 -2
- package/src/tui.ts +208 -154
- package/src/utils.ts +15 -0
|
@@ -1,14 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speculative composer state for the next first frame, kept in one SQLite store
|
|
3
|
+
* (`~/.omp/agent/cache/composer.db`).
|
|
4
|
+
*
|
|
5
|
+
* Each row is one JSON payload keyed by project (the resolved cwd) and kind.
|
|
6
|
+
* Settings-derived kinds (theme/composer preferences, welcome model labels,
|
|
7
|
+
* status-bar inputs) are also written under the empty project, so a folder that
|
|
8
|
+
* never ran omp still paints with the user's theme and status bar: those are
|
|
9
|
+
* rarely project-specific, and path/branch render live. Recent sessions and LSP
|
|
10
|
+
* rows stay per project.
|
|
11
|
+
*
|
|
12
|
+
* ```text
|
|
13
|
+
* entries (project TEXT, kind TEXT, value TEXT JSON, PRIMARY KEY (project, kind))
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Payload formats are versioned by `PRAGMA user_version`; a mismatch clears the
|
|
17
|
+
* store, which only ever holds speculation.
|
|
18
|
+
*/
|
|
19
|
+
import type { Database, Statement } from "bun:sqlite";
|
|
1
20
|
import * as fs from "node:fs";
|
|
2
21
|
import * as path from "node:path";
|
|
3
|
-
import "@oh-my-pi/pi-utils/
|
|
4
|
-
import {
|
|
22
|
+
import { getComposerCacheDbPath } from "@oh-my-pi/pi-utils/dirs";
|
|
23
|
+
import { isBunTestRuntime } from "@oh-my-pi/pi-utils/env";
|
|
24
|
+
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
25
|
+
import * as postmortem from "@oh-my-pi/pi-utils/postmortem";
|
|
26
|
+
import { openSqliteDatabaseSync } from "@oh-my-pi/pi-utils/sqlite";
|
|
27
|
+
import { isRecord } from "@oh-my-pi/pi-utils/type-guards";
|
|
5
28
|
import type { LspServerInfo, RecentSession } from "./welcome";
|
|
6
|
-
import type { ComposerPreferences,
|
|
29
|
+
import type { ComposerPreferences, ComposerStatusCache } from "./composer";
|
|
30
|
+
import { readStatusLineStartupData } from "../status-line/startup";
|
|
7
31
|
import type { SymbolPreset } from "../theme/theme";
|
|
8
32
|
import { isWordCompletionMethod } from "./word-completion";
|
|
9
33
|
|
|
10
|
-
|
|
11
|
-
const
|
|
34
|
+
/** Bump whenever any payload format changes; older stores are cleared on open. */
|
|
35
|
+
const FORMAT_VERSION = 1;
|
|
36
|
+
/** Project key of rows that serve every project lacking its own. */
|
|
37
|
+
const ANY_PROJECT = "";
|
|
38
|
+
const RECENT_SESSION_LIMIT = 4;
|
|
39
|
+
|
|
40
|
+
const SCHEMA = `
|
|
41
|
+
PRAGMA journal_mode=WAL;
|
|
42
|
+
PRAGMA synchronous=NORMAL;
|
|
43
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
44
|
+
project TEXT NOT NULL,
|
|
45
|
+
kind TEXT NOT NULL,
|
|
46
|
+
value TEXT NOT NULL,
|
|
47
|
+
PRIMARY KEY (project, kind)
|
|
48
|
+
) WITHOUT ROWID;
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
type EntryKind = "ui" | "welcome" | "recent-sessions" | "lsp-servers" | "status";
|
|
52
|
+
/** Kinds mirrored under {@link ANY_PROJECT} as the fallback for projects without their own row. */
|
|
53
|
+
type SharedEntryKind = "ui" | "welcome" | "status";
|
|
54
|
+
|
|
12
55
|
/** Theme inputs cached from the last resolved settings load for stable prepaint colors. */
|
|
13
56
|
export interface ComposerThemePreferences {
|
|
14
57
|
readonly symbolPreset?: SymbolPreset;
|
|
@@ -29,72 +72,44 @@ export interface ComposerStartupCache {
|
|
|
29
72
|
readonly theme?: ComposerThemePreferences;
|
|
30
73
|
readonly welcome?: ComposerWelcomeCache;
|
|
31
74
|
readonly recentSessions: RecentSession[];
|
|
32
|
-
|
|
33
|
-
readonly
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function projectCacheDir(cwd: string): string {
|
|
37
|
-
const key = Bun.hash.wyhash(path.resolve(cwd)).toString(16).padStart(16, "0");
|
|
38
|
-
return path.join(getComposerCacheDir(), key);
|
|
75
|
+
/** `null` when the last run had LSP disabled. */
|
|
76
|
+
readonly lspServers: LspServerInfo[] | null;
|
|
77
|
+
readonly status?: ComposerStatusCache;
|
|
39
78
|
}
|
|
40
79
|
|
|
41
|
-
function
|
|
80
|
+
function parseJson(value: string | undefined): unknown {
|
|
81
|
+
if (value === undefined) return undefined;
|
|
42
82
|
try {
|
|
43
|
-
return
|
|
83
|
+
return JSON.parse(value);
|
|
44
84
|
} catch {
|
|
45
85
|
return undefined;
|
|
46
86
|
}
|
|
47
87
|
}
|
|
48
88
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function readRecentSessions(file: string): RecentSession[] {
|
|
54
|
-
const content = readFile(file);
|
|
55
|
-
if (!content) return [];
|
|
56
|
-
let parsed: unknown;
|
|
57
|
-
try {
|
|
58
|
-
parsed = Bun.JSONL.parse(content);
|
|
59
|
-
} catch {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
if (!Array.isArray(parsed)) return [];
|
|
89
|
+
function parseRecentSessions(value: unknown): RecentSession[] {
|
|
90
|
+
if (!Array.isArray(value)) return [];
|
|
63
91
|
const sessions: RecentSession[] = [];
|
|
64
|
-
for (const
|
|
65
|
-
if (
|
|
66
|
-
const name =
|
|
67
|
-
const timeAgo = field(value, "timeAgo");
|
|
92
|
+
for (const item of value) {
|
|
93
|
+
if (!isRecord(item)) continue;
|
|
94
|
+
const { name, timeAgo } = item;
|
|
68
95
|
if (typeof name === "string" && typeof timeAgo === "string") sessions.push({ name, timeAgo });
|
|
69
|
-
if (sessions.length ===
|
|
96
|
+
if (sessions.length === RECENT_SESSION_LIMIT) break;
|
|
70
97
|
}
|
|
71
98
|
return sessions;
|
|
72
99
|
}
|
|
73
100
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
if (!
|
|
77
|
-
let parsed: unknown;
|
|
78
|
-
try {
|
|
79
|
-
parsed = JSON.parse(content);
|
|
80
|
-
} catch {
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
83
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return [];
|
|
84
|
-
if (field(parsed, "version") !== CACHE_VERSION) return [];
|
|
85
|
-
const values = field(parsed, "servers");
|
|
86
|
-
if (!Array.isArray(values)) return [];
|
|
101
|
+
function parseLspServers(value: unknown): LspServerInfo[] | null {
|
|
102
|
+
if (value === null) return null;
|
|
103
|
+
if (!Array.isArray(value)) return [];
|
|
87
104
|
const servers: LspServerInfo[] = [];
|
|
88
|
-
for (const
|
|
89
|
-
if (
|
|
90
|
-
const name =
|
|
91
|
-
const status = field(value, "status");
|
|
92
|
-
const fileTypes = field(value, "fileTypes");
|
|
105
|
+
for (const item of value) {
|
|
106
|
+
if (!isRecord(item)) continue;
|
|
107
|
+
const { name, status, fileTypes } = item;
|
|
93
108
|
if (
|
|
94
109
|
typeof name !== "string" ||
|
|
95
110
|
(status !== "ready" && status !== "error" && status !== "connecting" && status !== "available") ||
|
|
96
111
|
!Array.isArray(fileTypes) ||
|
|
97
|
-
!fileTypes.every(
|
|
112
|
+
!fileTypes.every(fileType => typeof fileType === "string")
|
|
98
113
|
) {
|
|
99
114
|
continue;
|
|
100
115
|
}
|
|
@@ -103,96 +118,40 @@ function readLspServers(file: string): LspServerInfo[] {
|
|
|
103
118
|
return servers;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let parsed: unknown;
|
|
110
|
-
try {
|
|
111
|
-
parsed = JSON.parse(content);
|
|
112
|
-
} catch {
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
|
116
|
-
if (field(parsed, "version") !== CACHE_VERSION) return undefined;
|
|
117
|
-
const modelName = field(parsed, "modelName");
|
|
118
|
-
const providerName = field(parsed, "providerName");
|
|
121
|
+
function parseWelcome(value: unknown): ComposerWelcomeCache | undefined {
|
|
122
|
+
if (!isRecord(value)) return undefined;
|
|
123
|
+
const { modelName, providerName } = value;
|
|
119
124
|
return typeof modelName === "string" && typeof providerName === "string" ? { modelName, providerName } : undefined;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (field(parsed, "version") !== CACHE_VERSION) return undefined;
|
|
133
|
-
if (field(parsed, "statusVersion") !== STATUS_CACHE_VERSION) return undefined;
|
|
134
|
-
const shape = field(parsed, "shape");
|
|
135
|
-
const rawBorderColor = field(parsed, "borderColor");
|
|
136
|
-
const rawTopBorder = field(parsed, "topBorder");
|
|
137
|
-
const bottomLines = field(parsed, "bottomLines");
|
|
138
|
-
if (
|
|
139
|
-
typeof shape !== "string" ||
|
|
140
|
-
!Array.isArray(bottomLines) ||
|
|
141
|
-
!bottomLines.every(line => typeof line === "string")
|
|
142
|
-
) {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
let borderColor: ComposerStatusSnapshot["borderColor"];
|
|
146
|
-
if (rawBorderColor !== undefined) {
|
|
147
|
-
if (typeof rawBorderColor !== "object" || rawBorderColor === null || Array.isArray(rawBorderColor)) {
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
const prefix = field(rawBorderColor, "prefix");
|
|
151
|
-
const suffix = field(rawBorderColor, "suffix");
|
|
152
|
-
if (typeof prefix !== "string" || typeof suffix !== "string") return undefined;
|
|
153
|
-
borderColor = { prefix, suffix };
|
|
154
|
-
}
|
|
155
|
-
if (rawTopBorder === undefined) return { shape, borderColor, bottomLines };
|
|
156
|
-
if (typeof rawTopBorder !== "object" || rawTopBorder === null || Array.isArray(rawTopBorder)) return undefined;
|
|
157
|
-
const borderContent = field(rawTopBorder, "content");
|
|
158
|
-
const borderWidth = field(rawTopBorder, "width");
|
|
159
|
-
if (typeof borderContent !== "string" || typeof borderWidth !== "number") return undefined;
|
|
160
|
-
return { shape, borderColor, topBorder: { content: borderContent, width: borderWidth }, bottomLines };
|
|
127
|
+
function parseStatus(value: unknown): ComposerStatusCache | undefined {
|
|
128
|
+
if (!isRecord(value)) return undefined;
|
|
129
|
+
const statusLine = readStatusLineStartupData(value.statusLine);
|
|
130
|
+
if (!statusLine) return undefined;
|
|
131
|
+
const rawBorderColor = value.borderColor;
|
|
132
|
+
if (rawBorderColor === undefined) return { statusLine };
|
|
133
|
+
if (!isRecord(rawBorderColor)) return undefined;
|
|
134
|
+
const { prefix, suffix } = rawBorderColor;
|
|
135
|
+
if (typeof prefix !== "string" || typeof suffix !== "string") return undefined;
|
|
136
|
+
return { borderColor: { prefix, suffix }, statusLine };
|
|
161
137
|
}
|
|
162
138
|
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
Array.isArray(rawPreferences) ||
|
|
180
|
-
typeof rawTheme !== "object" ||
|
|
181
|
-
rawTheme === null ||
|
|
182
|
-
Array.isArray(rawTheme)
|
|
183
|
-
) {
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
186
|
-
const quiet = field(rawPreferences, "quiet");
|
|
187
|
-
const composerShape = field(rawPreferences, "composerShape");
|
|
188
|
-
const showHardwareCursor = field(rawPreferences, "showHardwareCursor");
|
|
189
|
-
const maxInlineImages = field(rawPreferences, "maxInlineImages");
|
|
190
|
-
const resizeScrollback = field(rawPreferences, "resizeScrollback");
|
|
191
|
-
const imeSafeCursor = field(rawPreferences, "imeSafeCursor");
|
|
192
|
-
const autocompleteMaxVisible = field(rawPreferences, "autocompleteMaxVisible");
|
|
193
|
-
const spellingTypoDetection = field(rawPreferences, "spellingTypoDetection");
|
|
194
|
-
const spellingAutocomplete = field(rawPreferences, "spellingAutocomplete");
|
|
195
|
-
const spellingAutocorrect = field(rawPreferences, "spellingAutocorrect");
|
|
139
|
+
function parseUiState(
|
|
140
|
+
value: unknown,
|
|
141
|
+
): { preferences: ComposerPreferences; theme: ComposerThemePreferences } | undefined {
|
|
142
|
+
if (!isRecord(value) || !isRecord(value.preferences) || !isRecord(value.theme)) return undefined;
|
|
143
|
+
const {
|
|
144
|
+
quiet,
|
|
145
|
+
composerShape,
|
|
146
|
+
showHardwareCursor,
|
|
147
|
+
maxInlineImages,
|
|
148
|
+
resizeScrollback,
|
|
149
|
+
imeSafeCursor,
|
|
150
|
+
autocompleteMaxVisible,
|
|
151
|
+
spellingTypoDetection,
|
|
152
|
+
spellingAutocomplete,
|
|
153
|
+
spellingAutocorrect,
|
|
154
|
+
} = value.preferences;
|
|
196
155
|
if (
|
|
197
156
|
typeof quiet !== "boolean" ||
|
|
198
157
|
typeof composerShape !== "string" ||
|
|
@@ -210,10 +169,7 @@ function readUiState(file: string): { preferences: ComposerPreferences; theme: C
|
|
|
210
169
|
) {
|
|
211
170
|
return undefined;
|
|
212
171
|
}
|
|
213
|
-
const symbolPreset
|
|
214
|
-
const colorBlindMode = field(rawTheme, "colorBlindMode");
|
|
215
|
-
const darkTheme = field(rawTheme, "darkTheme");
|
|
216
|
-
const lightTheme = field(rawTheme, "lightTheme");
|
|
172
|
+
const { symbolPreset, colorBlindMode, darkTheme, lightTheme } = value.theme;
|
|
217
173
|
if (
|
|
218
174
|
(symbolPreset !== undefined &&
|
|
219
175
|
symbolPreset !== "unicode" &&
|
|
@@ -231,10 +187,7 @@ function readUiState(file: string): { preferences: ComposerPreferences; theme: C
|
|
|
231
187
|
composerShape,
|
|
232
188
|
showHardwareCursor,
|
|
233
189
|
maxInlineImages,
|
|
234
|
-
resizeScrollback:
|
|
235
|
-
resizeScrollback === "append" || resizeScrollback === "rebuild" || resizeScrollback === "preserve"
|
|
236
|
-
? resizeScrollback
|
|
237
|
-
: "rebuild",
|
|
190
|
+
resizeScrollback: resizeScrollback ?? "rebuild",
|
|
238
191
|
imeSafeCursor,
|
|
239
192
|
autocompleteMaxVisible,
|
|
240
193
|
spellingTypoDetection,
|
|
@@ -245,61 +198,129 @@ function readUiState(file: string): { preferences: ComposerPreferences; theme: C
|
|
|
245
198
|
};
|
|
246
199
|
}
|
|
247
200
|
|
|
248
|
-
|
|
249
|
-
export function readComposerStartupCache(cwd: string): ComposerStartupCache {
|
|
250
|
-
const dir = projectCacheDir(cwd);
|
|
251
|
-
const ui = readUiState(path.join(dir, "ui.json"));
|
|
252
|
-
return {
|
|
253
|
-
preferences: ui?.preferences,
|
|
254
|
-
theme: ui?.theme,
|
|
255
|
-
welcome: readWelcome(path.join(dir, "welcome.json")),
|
|
256
|
-
recentSessions: readRecentSessions(path.join(dir, "recent-sessions.jsonl")),
|
|
257
|
-
lspServers: readLspServers(path.join(dir, "lsp-servers.json")),
|
|
258
|
-
status: readStatus(path.join(dir, "status.json")),
|
|
259
|
-
};
|
|
260
|
-
}
|
|
201
|
+
let shared: ComposerCache | null | undefined;
|
|
261
202
|
|
|
262
|
-
/**
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Process-wide store at {@link getComposerCacheDbPath}, opened on first use and
|
|
205
|
+
* closed at exit. `undefined` when it cannot be opened (logged once; startup
|
|
206
|
+
* paints without speculation) and under the test runner, so tests never read
|
|
207
|
+
* or clobber the user's cache; tests open {@link ComposerCache.open} explicitly.
|
|
208
|
+
*/
|
|
209
|
+
export function sharedComposerCache(): ComposerCache | undefined {
|
|
210
|
+
if (isBunTestRuntime()) return undefined;
|
|
211
|
+
if (shared === undefined) {
|
|
212
|
+
try {
|
|
213
|
+
const cache = ComposerCache.open();
|
|
214
|
+
postmortem.register("composer-cache", () => cache.close(), { exitOnly: true });
|
|
215
|
+
shared = cache;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.debug("composer cache unavailable", { error: String(error) });
|
|
218
|
+
shared = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return shared ?? undefined;
|
|
272
222
|
}
|
|
273
223
|
|
|
274
|
-
/**
|
|
275
|
-
export
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
);
|
|
280
|
-
}
|
|
224
|
+
/** SQLite store of the composer state the next launch paints before its session exists. */
|
|
225
|
+
export class ComposerCache {
|
|
226
|
+
readonly #db: Database;
|
|
227
|
+
readonly #select: Statement<{ project: string; kind: EntryKind; value: string }, [string, string]>;
|
|
228
|
+
readonly #upsert: Statement<unknown, [string, EntryKind, string]>;
|
|
281
229
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
230
|
+
private constructor(db: Database) {
|
|
231
|
+
this.#db = db;
|
|
232
|
+
db.run(SCHEMA);
|
|
233
|
+
const version = db.query<{ user_version: number }, []>("PRAGMA user_version").get()?.user_version;
|
|
234
|
+
if (version !== FORMAT_VERSION) {
|
|
235
|
+
db.run("DELETE FROM entries");
|
|
236
|
+
db.run(`PRAGMA user_version = ${FORMAT_VERSION}`);
|
|
237
|
+
}
|
|
238
|
+
this.#select = db.prepare("SELECT project, kind, value FROM entries WHERE project IN (?, ?)");
|
|
239
|
+
this.#upsert = db.prepare(
|
|
240
|
+
"INSERT INTO entries (project, kind, value) VALUES (?, ?, ?) ON CONFLICT (project, kind) DO UPDATE SET value = excluded.value",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
289
243
|
|
|
290
|
-
/**
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
.
|
|
296
|
-
|
|
297
|
-
}
|
|
244
|
+
/**
|
|
245
|
+
* Open (creating if needed) the store at `dbPath`, quarantining a corrupt one once.
|
|
246
|
+
* @throws when the directory or database cannot be created.
|
|
247
|
+
*/
|
|
248
|
+
static open(dbPath: string = getComposerCacheDbPath()): ComposerCache {
|
|
249
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
250
|
+
return openSqliteDatabaseSync(dbPath, db => new ComposerCache(db), { recoverCorruption: true });
|
|
251
|
+
}
|
|
298
252
|
|
|
299
|
-
/**
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
253
|
+
/** Everything cached for `cwd`, with any-project rows as fallback for shared kinds. Never throws. */
|
|
254
|
+
read(cwd: string): ComposerStartupCache {
|
|
255
|
+
const project = path.resolve(cwd);
|
|
256
|
+
const own: Partial<Record<EntryKind, string>> = {};
|
|
257
|
+
const anyProject: Partial<Record<EntryKind, string>> = {};
|
|
258
|
+
try {
|
|
259
|
+
for (const row of this.#select.all(project, ANY_PROJECT)) {
|
|
260
|
+
(row.project === project ? own : anyProject)[row.kind] = row.value;
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logger.debug("composer cache read failed", { error: String(error) });
|
|
264
|
+
}
|
|
265
|
+
const ui = parseUiState(parseJson(own.ui)) ?? parseUiState(parseJson(anyProject.ui));
|
|
266
|
+
return {
|
|
267
|
+
preferences: ui?.preferences,
|
|
268
|
+
theme: ui?.theme,
|
|
269
|
+
welcome: parseWelcome(parseJson(own.welcome)) ?? parseWelcome(parseJson(anyProject.welcome)),
|
|
270
|
+
recentSessions: parseRecentSessions(parseJson(own["recent-sessions"])),
|
|
271
|
+
lspServers: own["lsp-servers"] === undefined ? [] : parseLspServers(parseJson(own["lsp-servers"])),
|
|
272
|
+
status: parseStatus(parseJson(own.status)) ?? parseStatus(parseJson(anyProject.status)),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Resolved theme and composer settings for the next prepaint. */
|
|
277
|
+
writeUi(cwd: string, preferences: ComposerPreferences, theme: ComposerThemePreferences): void {
|
|
278
|
+
this.#putShared(cwd, "ui", { preferences, theme });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Authoritative model/provider labels for the next welcome prepaint. */
|
|
282
|
+
writeWelcome(cwd: string, welcome: ComposerWelcomeCache): void {
|
|
283
|
+
this.#putShared(cwd, "welcome", welcome);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The latest recent-session rows (first four). */
|
|
287
|
+
writeRecentSessions(cwd: string, sessions: readonly RecentSession[]): void {
|
|
288
|
+
this.#put(cwd, "recent-sessions", sessions.slice(0, RECENT_SESSION_LIMIT));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The latest detected project LSP rows; `null` records that LSP is disabled. */
|
|
292
|
+
writeLspServers(cwd: string, servers: readonly LspServerInfo[] | null): void {
|
|
293
|
+
this.#put(cwd, "lsp-servers", servers);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Status-bar inputs for the next prepaint's startup status line. */
|
|
297
|
+
writeStatus(cwd: string, status: ComposerStatusCache): void {
|
|
298
|
+
this.#putShared(cwd, "status", status);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
close(): void {
|
|
302
|
+
this.#db.close();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Best-effort upsert: a failed write only costs the next launch its speculation. */
|
|
306
|
+
#put(cwd: string, kind: EntryKind, value: unknown): void {
|
|
307
|
+
try {
|
|
308
|
+
this.#upsert.run(path.resolve(cwd), kind, JSON.stringify(value));
|
|
309
|
+
} catch (error) {
|
|
310
|
+
logger.debug("composer cache write failed", { kind, error: String(error) });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** {@link #put} for this project plus the any-project fallback row, atomically. */
|
|
315
|
+
#putShared(cwd: string, kind: SharedEntryKind, value: unknown): void {
|
|
316
|
+
const json = JSON.stringify(value);
|
|
317
|
+
try {
|
|
318
|
+
this.#db.transaction(() => {
|
|
319
|
+
this.#upsert.run(path.resolve(cwd), kind, json);
|
|
320
|
+
this.#upsert.run(ANY_PROJECT, kind, json);
|
|
321
|
+
})();
|
|
322
|
+
} catch (error) {
|
|
323
|
+
logger.debug("composer cache write failed", { kind, error: String(error) });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
305
326
|
}
|