ompchamber 1.0.0 → 1.2.0
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/dist/client/index.html +1 -1
- package/dist/client/static/css/index.b938f4a693.css +2 -0
- package/dist/client/static/js/77.8022a800b5.js +139 -0
- package/dist/client/static/js/async/{1k.5974df3b87.js → 1k.a411fe5b43.js} +2 -2
- package/dist/client/static/js/async/{3y.71f2ccf3b6.js → 3y.3186ecdd63.js} +5 -5
- package/dist/client/static/js/async/5b.d88304bc13.js +1 -0
- package/dist/client/static/js/async/66.86544ad741.js +93 -0
- package/dist/client/static/js/async/7g.db82aca7ba.js +1 -0
- package/dist/client/static/js/async/7l.fadbbc0583.js +1 -0
- package/dist/client/static/js/async/{dc.856fda6560.js → ar.ad5c8822a8.js} +2 -2
- package/dist/client/static/js/async/cx.9c8268d542.js +226 -0
- package/dist/client/static/js/async/e.6443e739a2.js +79 -0
- package/dist/client/static/js/async/f7.46971e9d6d.js +2 -0
- package/dist/client/static/js/async/g.ee41bfed7f.js +2 -0
- package/dist/client/static/js/async/h1.d7091cef78.js +1 -0
- package/dist/client/static/js/async/hs.e7c59d8090.js +41 -0
- package/dist/client/static/js/async/jl.4911fa3383.js +1 -0
- package/dist/client/static/js/async/{k.7466330d1a.js → k.d16cd09ae7.js} +2 -2
- package/dist/client/static/js/async/{m.c038c71b9f.js → m.2e1ea3db9e.js} +2 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js +2 -0
- package/dist/client/static/js/async/mt.853be2b5aa.js +1 -0
- package/dist/client/static/js/async/{oc.9c060c9779.js → oc.eb22093f05.js} +1 -1
- package/dist/client/static/js/async/{og.634fba99ac.js → og.53f068804c.js} +1 -1
- package/dist/client/static/js/async/{5b.0d6ae50fe8.js → q.b453193538.js} +10 -10
- package/dist/client/static/js/async/q5.88b50ffa95.js +286 -0
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +21 -0
- package/dist/client/static/js/async/{sy.d16be843a7.js → s.aad2ecebc6.js} +1 -1
- package/dist/client/static/js/async/{tr.e878658140.js → tr.2a0d2b7600.js} +1 -1
- package/dist/client/static/js/async/vt.20406a5604.js +1 -0
- package/dist/client/static/js/async/x1.fe8af17016.js +90 -0
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +149 -0
- package/dist/client/static/js/async/yx.af2936c368.js +10 -0
- package/dist/client/static/js/index.57b39ffa7f.js +34 -0
- package/package.json +1 -1
- package/src/client/components/common/ComposerPicker.tsx +16 -4
- package/src/client/components/common/ComposerTextarea.tsx +1 -0
- package/src/client/components/common/file-icon/index.tsx +4 -1
- package/src/client/components/common/image-viewer/index.tsx +224 -0
- package/src/client/components/layout/RightActivityBar.tsx +1 -1
- package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
- package/src/client/components/layout/desktop-layout/index.tsx +25 -96
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
- package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
- package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +26 -12
- package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +57 -43
- package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +31 -24
- package/src/client/components/settings/Modal.tsx +1 -0
- package/src/client/components/settings/categories/ChatSettings.tsx +6 -2
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +99 -0
- package/src/client/components/workspace/btw-panel/AskCard.tsx +155 -0
- package/src/client/components/workspace/btw-panel/Composer.tsx +181 -0
- package/src/client/components/workspace/btw-panel/Messages.tsx +100 -0
- package/src/client/components/workspace/btw-panel/TopicMenu.tsx +106 -0
- package/src/client/components/workspace/btw-panel/attachments.ts +80 -0
- package/src/client/components/workspace/btw-panel/index.tsx +83 -0
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +39 -20
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
- package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +10 -6
- package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/chat-input/DropOverlay.tsx +17 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +67 -43
- package/src/client/components/workspace/chat-timeline/index.tsx +14 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +25 -7
- package/src/client/components/workspace/editor/Toolbar.tsx +32 -23
- package/src/client/components/workspace/editor/index.tsx +45 -36
- package/src/client/components/workspace/model-dropdown/groups.ts +35 -2
- package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
- package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
- package/src/client/hooks/chat/btw/index.ts +180 -0
- package/src/client/hooks/chat/btw/intercept.ts +43 -0
- package/src/client/hooks/chat/btw/mode.ts +179 -0
- package/src/client/hooks/chat/composer/attachments.ts +198 -0
- package/src/client/hooks/chat/composer/drop-entries.ts +204 -0
- package/src/client/hooks/chat/composer/drop-payload.test.ts +97 -0
- package/src/client/hooks/chat/composer/drop-payload.ts +130 -0
- package/src/client/hooks/chat/composer/drop-references.ts +106 -0
- package/src/client/hooks/chat/composer/file-drop.test.ts +194 -0
- package/src/client/hooks/chat/composer/file-drop.ts +226 -0
- package/src/client/hooks/chat/composer/file-reads.test.ts +211 -0
- package/src/client/hooks/chat/composer/file-reads.ts +238 -0
- package/src/client/hooks/chat/composer/index.ts +35 -9
- package/src/client/hooks/chat/composer/pipeline.ts +181 -0
- package/src/client/hooks/chat/omp/index.ts +3 -3
- package/src/client/hooks/chat/omp/prompt-send.ts +5 -5
- package/src/client/hooks/chat/timeline/actions.ts +40 -4
- package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
- package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
- package/src/client/hooks/chat/timeline/index.ts +29 -9
- package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
- package/src/client/hooks/chat/timeline/send.ts +45 -70
- package/src/client/hooks/chat/timeline/session-load.ts +2 -1
- package/src/client/hooks/chat/timeline/session-pagination.ts +161 -14
- package/src/client/hooks/chat/timeline/user-turns.ts +149 -0
- package/src/client/hooks/editor/file-actions.ts +49 -0
- package/src/client/hooks/editor/use-file-editor.ts +38 -27
- package/src/client/hooks/editor/use-file-transfer.ts +78 -0
- package/src/client/hooks/workspace/available-width.ts +33 -0
- package/src/client/hooks/workspace/panel-widths.ts +34 -37
- package/src/client/tailwind.css +22 -0
- package/src/server/db.server.ts +21 -287
- package/src/server/lib/btw/frames.test.ts +68 -0
- package/src/server/lib/btw/frames.ts +56 -0
- package/src/server/lib/btw/prompt.ts +28 -0
- package/src/server/lib/btw/registry.server.ts +141 -0
- package/src/server/lib/btw/runtime.server.ts +324 -0
- package/src/server/lib/btw/schema.server.ts +44 -0
- package/src/server/lib/btw/service.server.ts +171 -0
- package/src/server/lib/btw/session-copy.server.ts +213 -0
- package/src/server/lib/btw/session-copy.test.ts +166 -0
- package/src/server/lib/btw/store.server.ts +240 -0
- package/src/server/lib/db/schema.ts +110 -0
- package/src/server/lib/db/seed.ts +160 -0
- package/src/server/lib/db/workspace-sync.ts +70 -0
- package/src/server/lib/fs/root.ts +37 -0
- package/src/server/lib/models/preferences.server.ts +110 -0
- package/src/server/lib/models/registry.server.ts +206 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +207 -0
- package/src/server/lib/omp/rpc/manager.ts +27 -123
- package/src/server/lib/omp/rpc/utility.ts +7 -1
- package/src/server/lib/omp/session/auto-title.server.test.ts +137 -0
- package/src/server/lib/omp/session/auto-title.server.ts +152 -0
- package/src/server/lib/omp/session/blobs.server.test.ts +100 -0
- package/src/server/lib/omp/session/blobs.server.ts +76 -0
- package/src/server/lib/omp/session/file-scan.ts +174 -0
- package/src/server/lib/omp/session/files.test.ts +147 -0
- package/src/server/lib/omp/session/files.ts +52 -35
- package/src/server/lib/omp/session/locator.ts +2 -2
- package/src/server/lib/omp/session/merge-stored.test.ts +118 -0
- package/src/server/lib/omp/session/merge-stored.ts +171 -0
- package/src/server/lib/omp/session/messages.ts +34 -0
- package/src/server/lib/omp/session/reader.ts +2 -2
- package/src/server/lib/omp/session/subagent-presence.ts +9 -6
- package/src/server/lib/queue/delivery.server.test.ts +260 -0
- package/src/server/lib/queue/delivery.server.ts +63 -11
- package/src/server/plugins/ssr.ts +29 -2
- package/src/server/routes/btw/events.ts +70 -0
- package/src/server/routes/btw/index.ts +8 -0
- package/src/server/routes/btw/topics.ts +87 -0
- package/src/server/routes/btw/ws.ts +136 -0
- package/src/server/routes/chat/index.ts +2 -0
- package/src/server/routes/chat/session.ts +1 -115
- package/src/server/routes/chat/turns.ts +87 -0
- package/src/server/routes/fs/index.ts +4 -1
- package/src/server/routes/fs/raw.ts +85 -0
- package/src/server/routes/fs/read.ts +58 -3
- package/src/server/routes/index.ts +7 -1
- package/src/server/routes/models/root.ts +88 -215
- package/src/server/routes/settings/commands.ts +59 -18
- package/src/shared/lib/chat/attachments.test.ts +208 -0
- package/src/shared/lib/chat/attachments.ts +200 -15
- package/src/shared/lib/chat/btw/client.ts +94 -0
- package/src/shared/lib/chat/composer/client.test.ts +48 -0
- package/src/shared/lib/chat/composer/client.ts +23 -9
- package/src/shared/lib/chat/composer/commands.ts +290 -0
- package/src/shared/lib/chat/composer/composer.test.ts +195 -0
- package/src/shared/lib/chat/composer/filter.ts +21 -7
- package/src/shared/lib/chat/composer/trigger.ts +154 -21
- package/src/shared/lib/chat/omp/agent-events.ts +33 -80
- package/src/shared/lib/chat/omp/socket.ts +11 -6
- package/src/shared/lib/chat/omp/sse.ts +11 -6
- package/src/shared/lib/chat/omp/tool-results.ts +102 -0
- package/src/shared/lib/chat/omp/transport.ts +19 -3
- package/src/shared/lib/chat/text-file.ts +148 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -32
- package/src/shared/lib/chat/timeline/stream-coalescer.ts +66 -0
- package/src/shared/lib/chat/timeline/turns.ts +7 -0
- package/src/shared/lib/fs/file-kind.ts +50 -0
- package/src/shared/lib/fs/paths.ts +19 -1
- package/src/shared/lib/markdown/math.test.ts +20 -3
- package/src/shared/lib/models/preferences.test.ts +131 -0
- package/src/shared/lib/models/preferences.ts +88 -0
- package/src/shared/lib/models/provider-models.ts +8 -1
- package/src/shared/lib/omp/session/inlined-attachments.test.ts +82 -0
- package/src/shared/lib/omp/session/mapper.ts +24 -4
- package/src/shared/lib/omp/session/messages-map.ts +16 -3
- package/src/shared/lib/omp/session/messages-parse.ts +2 -2
- package/src/shared/lib/omp/session/parse-message-blocks.ts +85 -5
- package/src/shared/lib/omp/subagent/label.test.ts +94 -0
- package/src/shared/lib/omp/subagent/label.ts +84 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
- package/src/shared/lib/workspace/panel-widths.ts +161 -30
- package/src/shared/lib/workspace/right-panels.ts +50 -11
- package/src/shared/lib/workspace/session-state/store.ts +3 -0
- package/src/shared/types/btw.ts +65 -0
- package/src/shared/types/chat.ts +77 -66
- package/src/shared/types/index.ts +2 -0
- package/src/shared/types/models.ts +101 -0
- package/src/shared/types/omp/agent.ts +14 -1
- package/src/shared/types/omp/session.ts +4 -1
- package/src/shared/types/omp/subagent.ts +4 -0
- package/src/shared/types/settings/command.ts +13 -0
- package/src/shared/types/settings/state.ts +5 -0
- package/dist/client/static/css/index.e880517fcd.css +0 -2
- package/dist/client/static/js/async/5bh.8b2363424f.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.7a6a377ecc.js +0 -1
- package/dist/client/static/js/async/cx.ca5feec570.js +0 -226
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/g.a06ca3b3e9.js +0 -2
- package/dist/client/static/js/async/h1.e8d5aa8e80.js +0 -1
- package/dist/client/static/js/async/hs.6d480d0ded.js +0 -41
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/nam.b5663e919f.js +0 -10
- package/dist/client/static/js/async/q.23af2a358f.js +0 -21
- package/dist/client/static/js/async/q5.e683f7c26e.js +0 -286
- package/dist/client/static/js/async/s.222ee974f6.js +0 -2
- package/dist/client/static/js/async/uf.46ad6209fa.js +0 -2
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/index.0de15c5e17.js +0 -33
- package/dist/client/static/js/jp.50817896f3.js +0 -139
- /package/dist/client/static/js/{jp.50817896f3.js.LICENSE.txt → 77.8022a800b5.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.5974df3b87.js.LICENSE.txt → 1k.a411fe5b43.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.71f2ccf3b6.js.LICENSE.txt → 3y.3186ecdd63.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{dc.856fda6560.js.LICENSE.txt → ar.ad5c8822a8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.c038c71b9f.js.LICENSE.txt → f7.46971e9d6d.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{5b.0d6ae50fe8.js.LICENSE.txt → g.ee41bfed7f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.23af2a358f.js.LICENSE.txt → m.2e1ea3db9e.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.222ee974f6.js.LICENSE.txt → m3.d2b05f9604.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.a06ca3b3e9.js.LICENSE.txt → q.b453193538.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{uf.46ad6209fa.js.LICENSE.txt → q8.7f1b48e9cd.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{nam.b5663e919f.js.LICENSE.txt → yx.af2936c368.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.0de15c5e17.js.LICENSE.txt → index.57b39ffa7f.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Schema bootstrap for the chamber database: the `CREATE TABLE IF NOT EXISTS`
|
|
8
|
+
* block plus the in-place migrations that repair schemas written by older
|
|
9
|
+
* installs. Split out of `@/server/db.server` so the connection lifecycle
|
|
10
|
+
* there stays readable; the order below is load-bearing — the migrations run
|
|
11
|
+
* between the two DDL blocks, after the tables they touch exist.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { DbClient } from '@/server/lib/db/client';
|
|
15
|
+
import { migrateQueueTableFk } from '@/server/lib/queue/schema-migration.server';
|
|
16
|
+
import { ensureBtwSchema } from '@/server/lib/btw/schema.server';
|
|
17
|
+
import { migrateWorkspaceFolderColumns } from '@/shared/lib/workspace/schema-migrations';
|
|
18
|
+
|
|
19
|
+
export async function initSchema(db: DbClient): Promise<void> {
|
|
20
|
+
await db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS workspace_folders (
|
|
22
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
name TEXT NOT NULL,
|
|
24
|
+
is_expanded BOOLEAN DEFAULT 0,
|
|
25
|
+
project_path TEXT,
|
|
26
|
+
model TEXT DEFAULT 'Not selected',
|
|
27
|
+
accent_color TEXT DEFAULT '',
|
|
28
|
+
icon TEXT DEFAULT 'default',
|
|
29
|
+
custom_icon_url TEXT,
|
|
30
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
folder_id INTEGER NOT NULL,
|
|
36
|
+
title TEXT NOT NULL,
|
|
37
|
+
is_active BOOLEAN DEFAULT 0,
|
|
38
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
39
|
+
FOREIGN KEY (folder_id) REFERENCES workspace_folders (id)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS app_settings (
|
|
43
|
+
key TEXT PRIMARY KEY,
|
|
44
|
+
value TEXT NOT NULL
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
48
|
+
session_id TEXT PRIMARY KEY,
|
|
49
|
+
title TEXT,
|
|
50
|
+
messages TEXT NOT NULL,
|
|
51
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS deleted_workspaces (
|
|
55
|
+
project_path TEXT PRIMARY KEY,
|
|
56
|
+
deleted_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS archived_sessions (
|
|
60
|
+
session_id TEXT PRIMARY KEY,
|
|
61
|
+
archived_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS session_ui_state (
|
|
65
|
+
session_id TEXT PRIMARY KEY,
|
|
66
|
+
state TEXT NOT NULL DEFAULT '{}',
|
|
67
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS queued_messages (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
session_id TEXT NOT NULL,
|
|
73
|
+
position INTEGER NOT NULL,
|
|
74
|
+
message TEXT NOT NULL DEFAULT '',
|
|
75
|
+
attachments TEXT NOT NULL DEFAULT '[]',
|
|
76
|
+
provider TEXT,
|
|
77
|
+
model_id TEXT,
|
|
78
|
+
thinking_level TEXT,
|
|
79
|
+
access_mode TEXT,
|
|
80
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
81
|
+
);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_queued_messages_session ON queued_messages (session_id, position);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS session_stream_state (
|
|
85
|
+
session_id TEXT PRIMARY KEY,
|
|
86
|
+
status TEXT NOT NULL,
|
|
87
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
88
|
+
);
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
await migrateWorkspaceFolderColumns(db);
|
|
92
|
+
// Installs older `queued_messages` schemas carry an invalid FK (see the
|
|
93
|
+
// migration module); rebuild once so real-session queue inserts work.
|
|
94
|
+
await migrateQueueTableFk(db);
|
|
95
|
+
// Side-question tables live in their own module (see schema.server.ts).
|
|
96
|
+
await ensureBtwSchema(db);
|
|
97
|
+
|
|
98
|
+
await db.exec(`
|
|
99
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
100
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
101
|
+
session_id INTEGER NOT NULL,
|
|
102
|
+
parent_id INTEGER,
|
|
103
|
+
name TEXT NOT NULL,
|
|
104
|
+
type TEXT NOT NULL,
|
|
105
|
+
is_expanded BOOLEAN DEFAULT 0,
|
|
106
|
+
FOREIGN KEY (session_id) REFERENCES sessions (id),
|
|
107
|
+
FOREIGN KEY (parent_id) REFERENCES files (id)
|
|
108
|
+
);
|
|
109
|
+
`);
|
|
110
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `MOCK=true` database seeding: demo workspace folders, screenshot-matching
|
|
8
|
+
* chat sessions, the bundled sample transcripts and a synthetic file tree.
|
|
9
|
+
* Split out of `@/server/db.server` so the connection lifecycle there stays
|
|
10
|
+
* readable. Runs only in mock mode and only on a database that has not been
|
|
11
|
+
* seeded before — every branch is idempotent, so a restart re-asserts the
|
|
12
|
+
* demo rows instead of duplicating them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { DbClient } from '@/server/lib/db/client';
|
|
16
|
+
import { SAMPLE_TOOLS_SESSION_ID, getSampleToolsSession } from '@/client/data/samples/tools-session';
|
|
17
|
+
import { SAMPLE_DIALOGUE_SESSION_ID, getSampleDialogueSession } from '@/client/data/samples/dialogue-session';
|
|
18
|
+
import { SAMPLE_DEVICES_SESSION_ID, getSampleDevicesSession } from '@/client/data/samples/virtual-devices-session';
|
|
19
|
+
|
|
20
|
+
const INSERT_CHAT_SESSION =
|
|
21
|
+
'INSERT OR REPLACE INTO chat_sessions (session_id, title, messages, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)';
|
|
22
|
+
|
|
23
|
+
export async function seedMockData(db: DbClient): Promise<void> {
|
|
24
|
+
const folderCount = await db.get('SELECT COUNT(*) as count FROM workspace_folders');
|
|
25
|
+
|
|
26
|
+
if (folderCount.count === 0) {
|
|
27
|
+
await db.exec(`
|
|
28
|
+
INSERT OR IGNORE INTO workspace_folders (id, name, is_expanded) VALUES
|
|
29
|
+
(1, 'Chats', 1),
|
|
30
|
+
(2, 'Workspace', 1),
|
|
31
|
+
(3, 'drrealhandler', 1);
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await seedChatSessions(db);
|
|
36
|
+
await seedWorkspaceSessions(db, 2, [
|
|
37
|
+
'History Commit 2026-09-05 23:00',
|
|
38
|
+
'History Commit 2026-09-04 23:00',
|
|
39
|
+
'History Commit 2026-09-03 23:00',
|
|
40
|
+
'Error SMSC latency pada jns6.5 smppv2',
|
|
41
|
+
'Penyebab provider_submit_status=-1 di jns6.5 Smartfren SMPP',
|
|
42
|
+
'jns6.5 filevalidator error Mask Code di Release',
|
|
43
|
+
'Union query transaksi 202607 dan 202606',
|
|
44
|
+
]);
|
|
45
|
+
await seedWorkspaceSessions(db, 3, [
|
|
46
|
+
'Buat branch feat/add-provider-err-telco-to-dr-smpp',
|
|
47
|
+
'Update wiki v1.5.0 & testing dari add-configurabel',
|
|
48
|
+
'Konfigurasi rute DR SMPP via config lookup',
|
|
49
|
+
'drrealhandler update v1.5.0',
|
|
50
|
+
'Gitlab wiki clone isi kosong',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
await seedFillerSessions(db);
|
|
54
|
+
await seedSessionFiles(db);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Demo folders 1-3 and the five sessions the sidebar screenshots show. */
|
|
58
|
+
async function seedChatSessions(db: DbClient): Promise<void> {
|
|
59
|
+
// Ensure ompchamber, Workspace, drrealhandler folders exist
|
|
60
|
+
await db.run("INSERT OR IGNORE INTO workspace_folders (id, name, is_expanded) VALUES (1, 'ompchamber', 1)");
|
|
61
|
+
await db.run("INSERT OR IGNORE INTO workspace_folders (id, name, is_expanded) VALUES (2, 'Workspace', 1)");
|
|
62
|
+
await db.run("INSERT OR IGNORE INTO workspace_folders (id, name, is_expanded) VALUES (3, 'drrealhandler', 1)");
|
|
63
|
+
await db.run("UPDATE workspace_folders SET name = 'ompchamber' WHERE id = 1");
|
|
64
|
+
|
|
65
|
+
const existingChats = await db.all("SELECT * FROM sessions WHERE folder_id = 1");
|
|
66
|
+
const titles = [
|
|
67
|
+
'Cek stream subagent di ompweb',
|
|
68
|
+
'Fitur Diff Panel dan Git Status Files',
|
|
69
|
+
'Implementasi Saved State UI per Session ID',
|
|
70
|
+
'Review loading indicator, model & intent title',
|
|
71
|
+
'Status fitur streer dan queue',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
if (existingChats.length === 0) {
|
|
75
|
+
await db.run("INSERT INTO sessions (folder_id, title, is_active) VALUES (1, ?, 0)", [titles[0]]);
|
|
76
|
+
await db.run("INSERT INTO sessions (folder_id, title, is_active) VALUES (1, ?, 0)", [titles[1]]);
|
|
77
|
+
await db.run("INSERT INTO sessions (folder_id, title, is_active) VALUES (1, ?, 1)", [titles[2]]);
|
|
78
|
+
await db.run("INSERT INTO sessions (folder_id, title, is_active) VALUES (1, ?, 0)", [titles[3]]);
|
|
79
|
+
await db.run("INSERT INTO sessions (folder_id, title, is_active) VALUES (1, ?, 0)", [titles[4]]);
|
|
80
|
+
} else {
|
|
81
|
+
await db.run("UPDATE sessions SET title = ? WHERE id = 1", [titles[0]]);
|
|
82
|
+
await db.run("UPDATE sessions SET title = ? WHERE id = 2", [titles[1]]);
|
|
83
|
+
await db.run("UPDATE sessions SET title = ? WHERE id = 3", [titles[2]]);
|
|
84
|
+
await db.run("UPDATE sessions SET title = ? WHERE id = 4", [titles[3]]);
|
|
85
|
+
await db.run("UPDATE sessions SET title = ? WHERE id = 5", [titles[4]]);
|
|
86
|
+
await db.run("UPDATE sessions SET is_active = 0 WHERE folder_id = 1");
|
|
87
|
+
await db.run("UPDATE sessions SET is_active = 1 WHERE id = 3");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Pre-seed sample sessions data
|
|
91
|
+
try {
|
|
92
|
+
const s1 = getSampleToolsSession();
|
|
93
|
+
await db.run(INSERT_CHAT_SESSION, ['1', s1.title, JSON.stringify(s1.messages)]);
|
|
94
|
+
await db.run(INSERT_CHAT_SESSION, [SAMPLE_TOOLS_SESSION_ID, s1.title, JSON.stringify(s1.messages)]);
|
|
95
|
+
|
|
96
|
+
const s2 = getSampleDialogueSession();
|
|
97
|
+
await db.run(INSERT_CHAT_SESSION, ['2', s2.title, JSON.stringify(s2.messages)]);
|
|
98
|
+
await db.run(INSERT_CHAT_SESSION, [SAMPLE_DIALOGUE_SESSION_ID, s2.title, JSON.stringify(s2.messages)]);
|
|
99
|
+
|
|
100
|
+
const s3 = getSampleDevicesSession();
|
|
101
|
+
await db.run(INSERT_CHAT_SESSION, ['3', s3.title, JSON.stringify(s3.messages)]);
|
|
102
|
+
await db.run(INSERT_CHAT_SESSION, [SAMPLE_DEVICES_SESSION_ID, s3.title, JSON.stringify(s3.messages)]);
|
|
103
|
+
const s3Row = await db.get("SELECT id FROM sessions WHERE folder_id = 1 AND title = ?", [titles[2]]);
|
|
104
|
+
if (s3Row) {
|
|
105
|
+
await db.run(INSERT_CHAT_SESSION, [String(s3Row.id), s3.title, JSON.stringify(s3.messages)]);
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error('Failed to pre-seed sample sessions:', e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Add the given session titles to a folder when it has none yet. */
|
|
113
|
+
async function seedWorkspaceSessions(db: DbClient, folderId: number, titles: string[]): Promise<void> {
|
|
114
|
+
const existing = await db.all('SELECT * FROM sessions WHERE folder_id = ?', [folderId]);
|
|
115
|
+
if (existing.length > 0) return;
|
|
116
|
+
for (const title of titles) {
|
|
117
|
+
await db.run('INSERT INTO sessions (folder_id, title, is_active) VALUES (?, ?, 0)', [folderId, title]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Add random sessions if we don't have enough to demonstrate the UI. */
|
|
122
|
+
async function seedFillerSessions(db: DbClient): Promise<void> {
|
|
123
|
+
const sessionCount = await db.get('SELECT COUNT(*) as count FROM sessions');
|
|
124
|
+
if (sessionCount.count >= 15) return;
|
|
125
|
+
|
|
126
|
+
const titles = [
|
|
127
|
+
'Fix header styling issue', 'Implement authentication flow', 'Setup CI/CD pipeline',
|
|
128
|
+
'Debug database connection', 'Update README.md docs', 'Refactor UI components',
|
|
129
|
+
];
|
|
130
|
+
for (let i = 1; i <= 3; i++) {
|
|
131
|
+
for (let j = 0; j < 3; j++) {
|
|
132
|
+
const title = titles[Math.floor(Math.random() * titles.length)] + ' #' + Math.floor(Math.random() * 1000);
|
|
133
|
+
await db.run('INSERT INTO sessions (folder_id, title, is_active) VALUES (?, ?, 0)', [i, title]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Seed a demo file tree for every session when the files table is empty. */
|
|
139
|
+
async function seedSessionFiles(db: DbClient): Promise<void> {
|
|
140
|
+
const fileCount = await db.get('SELECT COUNT(*) as count FROM files');
|
|
141
|
+
if (fileCount.count !== 0) return;
|
|
142
|
+
|
|
143
|
+
const insertFile =
|
|
144
|
+
'INSERT INTO files (session_id, parent_id, name, type, is_expanded) VALUES (?, ?, ?, ?, ?)';
|
|
145
|
+
const allSessions = await db.all('SELECT id FROM sessions');
|
|
146
|
+
for (const session of allSessions) {
|
|
147
|
+
const src = await db.run(insertFile, [session.id, null, 'src', 'folder', 1]);
|
|
148
|
+
const components = await db.run(insertFile, [session.id, null, 'components', 'folder', 0]);
|
|
149
|
+
|
|
150
|
+
await db.run(insertFile, [session.id, src.lastID, 'index.tsx', 'file', 0]);
|
|
151
|
+
await db.run(insertFile, [session.id, src.lastID, 'App.tsx', 'file', 0]);
|
|
152
|
+
await db.run(insertFile, [session.id, src.lastID, 'styles.css', 'file', 0]);
|
|
153
|
+
|
|
154
|
+
await db.run(insertFile, [session.id, components.lastID, 'Button.tsx', 'file', 0]);
|
|
155
|
+
await db.run(insertFile, [session.id, components.lastID, 'Header.tsx', 'file', 0]);
|
|
156
|
+
|
|
157
|
+
await db.run(insertFile, [session.id, null, 'package.json', 'file', 0]);
|
|
158
|
+
await db.run(insertFile, [session.id, null, 'README.md', 'file', 0]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Additive-only folder ↔ omp project sync: creates a workspace folder for
|
|
8
|
+
* every project discovered from ~/.omp/agent (folder name = lowercase
|
|
9
|
+
* basename, bound via project_path). It never modifies or deletes existing
|
|
10
|
+
* user data — no renames, no re-binding of unbound folders — and it skips
|
|
11
|
+
* projects the user has explicitly deleted (tombstoned in
|
|
12
|
+
* `deleted_workspaces`), so a deleted workspace stays deleted.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { DbClient } from '@/server/lib/db/client';
|
|
16
|
+
import { projectPathKey } from '@/server/lib/omp/core/paths';
|
|
17
|
+
import { loadOmpSidebarData } from '@/server/lib/omp/session/reader';
|
|
18
|
+
import { orderedOmpProjects, projectDisplayName } from '@/shared/lib/omp/session/sidebar';
|
|
19
|
+
|
|
20
|
+
/** SYNC_WORKSPACE env flag (default true when unset). When enabled in real
|
|
21
|
+
* mode, workspace folders are auto-created from discovered omp projects. */
|
|
22
|
+
function isWorkspaceSyncEnabled(): boolean {
|
|
23
|
+
const raw = (Bun.env.SYNC_WORKSPACE || '').trim().toLowerCase();
|
|
24
|
+
if (raw === '') return true;
|
|
25
|
+
return raw !== 'false' && raw !== '0' && raw !== 'off' && raw !== 'no';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Real-data workspace bootstrap: sync discovered omp projects into
|
|
30
|
+
* `workspace_folders` when SYNC_WORKSPACE allows it. Discovery must never
|
|
31
|
+
* block app startup, so a failure is logged rather than thrown.
|
|
32
|
+
*/
|
|
33
|
+
export async function syncWorkspaceFoldersFromDiscovery(db: DbClient): Promise<void> {
|
|
34
|
+
if (!isWorkspaceSyncEnabled()) return;
|
|
35
|
+
try {
|
|
36
|
+
await syncWorkspaceFoldersWithOmp(db);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Discovery must never block app startup.
|
|
39
|
+
console.error('OMP workspace sync failed:', err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function syncWorkspaceFoldersWithOmp(db: DbClient): Promise<void> {
|
|
44
|
+
const data = await loadOmpSidebarData();
|
|
45
|
+
const existing = await db.all('SELECT id, name, project_path FROM workspace_folders');
|
|
46
|
+
const tombstoned = await db.all('SELECT project_path FROM deleted_workspaces');
|
|
47
|
+
|
|
48
|
+
// Compare by canonical key, not raw string: tombstones may predate the
|
|
49
|
+
// directory (unresolved symlink spelling) while project.path is realpath'd.
|
|
50
|
+
const byPath = new Set(existing.map((r) => r.project_path).filter(Boolean).map(projectPathKey));
|
|
51
|
+
const usedNames = new Set(existing.map((r) => (r.name as string).toLowerCase()));
|
|
52
|
+
const deletedPaths = new Set(tombstoned.map((r) => projectPathKey(r.project_path as string)));
|
|
53
|
+
|
|
54
|
+
for (const project of orderedOmpProjects(data)) {
|
|
55
|
+
if (deletedPaths.has(projectPathKey(project.path))) continue;
|
|
56
|
+
if (byPath.has(projectPathKey(project.path))) continue;
|
|
57
|
+
|
|
58
|
+
const name = projectDisplayName(project);
|
|
59
|
+
let candidate = name;
|
|
60
|
+
let suffix = 2;
|
|
61
|
+
while (usedNames.has(candidate)) {
|
|
62
|
+
candidate = `${name}-${suffix++}`;
|
|
63
|
+
}
|
|
64
|
+
usedNames.add(candidate);
|
|
65
|
+
await db.run(
|
|
66
|
+
'INSERT INTO workspace_folders (name, is_expanded, project_path) VALUES (?, 1, ?)',
|
|
67
|
+
[candidate, project.path],
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -28,6 +28,43 @@ export function resolveWithinRoot(root: string, rel: string): string | null {
|
|
|
28
28
|
return isWithinRoot(root, target) ? target : null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a file a drop or a link pointed at, which may live outside the active
|
|
33
|
+
* root.
|
|
34
|
+
*
|
|
35
|
+
* A dropped file is always named by an absolute path or URL — a file manager
|
|
36
|
+
* cannot know which workspace the page considers active — so
|
|
37
|
+
* {@link resolveWithinRoot} rejects exactly the references the composer needs.
|
|
38
|
+
* The allow-list that governs a client-supplied `root` governs these too: the
|
|
39
|
+
* app root, a path beneath it, or an exact registered workspace project path.
|
|
40
|
+
* Anything else resolves to null, so an arbitrary path from a page still cannot
|
|
41
|
+
* read the filesystem.
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveReferencedPath(rawPath: string): Promise<string | null> {
|
|
44
|
+
const candidate = rawPath.trim();
|
|
45
|
+
if (!candidate) return null;
|
|
46
|
+
|
|
47
|
+
const resolved = path.resolve(candidate);
|
|
48
|
+
if (resolved === APP_ROOT || resolved.startsWith(APP_ROOT + path.sep)) {
|
|
49
|
+
return (await pathExists(resolved)) ? resolved : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const db = await getDb();
|
|
54
|
+
const rows = (await db.all(
|
|
55
|
+
'SELECT project_path FROM workspace_folders WHERE project_path IS NOT NULL'
|
|
56
|
+
)) as { project_path: string }[];
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const workspaceRoot = path.resolve(row.project_path);
|
|
59
|
+
if (isWithinRoot(workspaceRoot, resolved) && (await pathExists(resolved))) return resolved;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Database unavailable — nothing outside the app root can be verified.
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
31
68
|
/**
|
|
32
69
|
* Default browsing root when no session-bound project is active. Mock/demo
|
|
33
70
|
* mode browses the bundled `examples` tree; real mode browses the app root.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Persistence for the user's favorite + last-used models.
|
|
8
|
+
*
|
|
9
|
+
* Lives apart from the model registry because a favorite is NOT a property of
|
|
10
|
+
* omp's registry: the catalog the route used to mutate (`omp_models_catalog`)
|
|
11
|
+
* is a MOCK-only demo list that real mode never reads back, so a star clicked
|
|
12
|
+
* on a real install was written into a dead row and lost on the next render.
|
|
13
|
+
* This store is the single source for both modes.
|
|
14
|
+
*
|
|
15
|
+
* Keys are composite (`provider:id`) — see `modelKey` — and stale ones are
|
|
16
|
+
* dropped on read, so a model removed from models.yml cannot hold a slot in the
|
|
17
|
+
* five-entry recent rail forever.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getDb } from '@/server/db.server';
|
|
21
|
+
import { readSettingsJson, writeSettingsJson } from '@/server/lib/db/settings-store';
|
|
22
|
+
import { isMockMode } from '@/server/mock.server';
|
|
23
|
+
import { INITIAL_MODELS_CATALOG } from '@/client/data/models/catalog';
|
|
24
|
+
import {
|
|
25
|
+
EMPTY_MODEL_PREFERENCES,
|
|
26
|
+
RECENT_MODELS_LIMIT,
|
|
27
|
+
filterKnownKeys,
|
|
28
|
+
readModelPreferences,
|
|
29
|
+
recordRecentKey,
|
|
30
|
+
toggleFavoriteKey,
|
|
31
|
+
} from '@/shared/lib/models/preferences';
|
|
32
|
+
import { modelKey } from '@/shared/lib/models/identity';
|
|
33
|
+
import type { ModelPreferences } from '@/shared/types';
|
|
34
|
+
|
|
35
|
+
const MODEL_PREFERENCES_KEY = 'omp_model_preferences';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Demo favorites/recents so a `MOCK=true` chamber still opens with the rails
|
|
39
|
+
* the shipped catalog advertises. Seeded on first read only — from then on the
|
|
40
|
+
* row is the user's, and the catalog's own flags are ignored.
|
|
41
|
+
*/
|
|
42
|
+
function mockSeedPreferences(): ModelPreferences {
|
|
43
|
+
return {
|
|
44
|
+
favorites: INITIAL_MODELS_CATALOG.filter((model) => model.isFavorite).map(modelKey),
|
|
45
|
+
recentKeys: INITIAL_MODELS_CATALOG.filter((model) => model.isRecent).map(modelKey).slice(0, RECENT_MODELS_LIMIT),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readStoredPreferences(): Promise<ModelPreferences> {
|
|
50
|
+
const db = await getDb();
|
|
51
|
+
const raw = await readSettingsJson<unknown>(db, MODEL_PREFERENCES_KEY, null);
|
|
52
|
+
if (raw === null) return isMockMode() ? mockSeedPreferences() : EMPTY_MODEL_PREFERENCES;
|
|
53
|
+
return readModelPreferences(raw);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Preferences narrowed to the models the registry actually serves right now —
|
|
58
|
+
* the shape every loader response carries.
|
|
59
|
+
*/
|
|
60
|
+
export async function readModelPreferencesFor(
|
|
61
|
+
models: readonly { provider: string; id: string }[],
|
|
62
|
+
): Promise<ModelPreferences> {
|
|
63
|
+
try {
|
|
64
|
+
const known = new Set(models.map(modelKey));
|
|
65
|
+
const stored = await readStoredPreferences();
|
|
66
|
+
return {
|
|
67
|
+
favorites: filterKnownKeys(stored.favorites, known),
|
|
68
|
+
recentKeys: filterKnownKeys(stored.recentKeys, known),
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return EMPTY_MODEL_PREFERENCES;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Persist a mutation with the stale keys already removed, so a key the registry
|
|
77
|
+
* dropped cannot sit in the row holding one of the five recent slots. Reading
|
|
78
|
+
* prunes for display only — without this the slot stayed occupied across
|
|
79
|
+
* reloads and the RECENT rail silently showed four models instead of five.
|
|
80
|
+
*/
|
|
81
|
+
async function writePreferences(known: ReadonlySet<string>, mutate: (current: ModelPreferences) => ModelPreferences): Promise<ModelPreferences> {
|
|
82
|
+
const current = await readStoredPreferences();
|
|
83
|
+
const next = mutate({
|
|
84
|
+
favorites: filterKnownKeys(current.favorites, known),
|
|
85
|
+
recentKeys: filterKnownKeys(current.recentKeys, known),
|
|
86
|
+
});
|
|
87
|
+
const db = await getDb();
|
|
88
|
+
await writeSettingsJson(db, MODEL_PREFERENCES_KEY, next);
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Flip one model's favorite flag, returning the full persisted pair. */
|
|
93
|
+
export async function toggleFavoritePreference(key: string, known: ReadonlySet<string>): Promise<ModelPreferences> {
|
|
94
|
+
return writePreferences(known, (current) => ({
|
|
95
|
+
...current,
|
|
96
|
+
favorites: toggleFavoriteKey(current.favorites, key),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Record a model as last used. Called from the selection action rather than
|
|
102
|
+
* from a separate endpoint: "the user picked this model" is one event, and a
|
|
103
|
+
* second request would only add a way for the two to disagree.
|
|
104
|
+
*/
|
|
105
|
+
export async function recordRecentPreference(key: string, known: ReadonlySet<string>): Promise<ModelPreferences> {
|
|
106
|
+
return writePreferences(known, (current) => ({
|
|
107
|
+
...current,
|
|
108
|
+
recentKeys: recordRecentKey(current.recentKeys, key),
|
|
109
|
+
}));
|
|
110
|
+
}
|