ompchamber 1.1.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.
Files changed (192) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/static/css/index.b938f4a693.css +2 -0
  3. package/dist/client/static/js/77.8022a800b5.js +139 -0
  4. package/dist/client/static/js/async/{1k.5974df3b87.js → 1k.a411fe5b43.js} +2 -2
  5. package/dist/client/static/js/async/{3y.71f2ccf3b6.js → 3y.3186ecdd63.js} +5 -5
  6. package/dist/client/static/js/async/5b.d88304bc13.js +1 -0
  7. package/dist/client/static/js/async/66.86544ad741.js +93 -0
  8. package/dist/client/static/js/async/7g.db82aca7ba.js +1 -0
  9. package/dist/client/static/js/async/7l.fadbbc0583.js +1 -0
  10. package/dist/client/static/js/async/{dc.856fda6560.js → ar.ad5c8822a8.js} +2 -2
  11. package/dist/client/static/js/async/cx.9c8268d542.js +226 -0
  12. package/dist/client/static/js/async/e.6443e739a2.js +79 -0
  13. package/dist/client/static/js/async/f7.46971e9d6d.js +2 -0
  14. package/dist/client/static/js/async/g.ee41bfed7f.js +2 -0
  15. package/dist/client/static/js/async/h1.d7091cef78.js +1 -0
  16. package/dist/client/static/js/async/hs.e7c59d8090.js +41 -0
  17. package/dist/client/static/js/async/jl.4911fa3383.js +1 -0
  18. package/dist/client/static/js/async/{k.7466330d1a.js → k.d16cd09ae7.js} +2 -2
  19. package/dist/client/static/js/async/{m.c038c71b9f.js → m.2e1ea3db9e.js} +2 -2
  20. package/dist/client/static/js/async/m3.d2b05f9604.js +2 -0
  21. package/dist/client/static/js/async/mt.853be2b5aa.js +1 -0
  22. package/dist/client/static/js/async/{oc.9c060c9779.js → oc.eb22093f05.js} +1 -1
  23. package/dist/client/static/js/async/{og.634fba99ac.js → og.53f068804c.js} +1 -1
  24. package/dist/client/static/js/async/{5b.a5ccbc19dc.js → q.b453193538.js} +10 -10
  25. package/dist/client/static/js/async/q5.88b50ffa95.js +286 -0
  26. package/dist/client/static/js/async/q8.7f1b48e9cd.js +21 -0
  27. package/dist/client/static/js/async/{sy.d16be843a7.js → s.aad2ecebc6.js} +1 -1
  28. package/dist/client/static/js/async/{tr.e878658140.js → tr.2a0d2b7600.js} +1 -1
  29. package/dist/client/static/js/async/vt.20406a5604.js +1 -0
  30. package/dist/client/static/js/async/x1.fe8af17016.js +90 -0
  31. package/dist/client/static/js/async/yg.5a94e1fc8a.js +149 -0
  32. package/dist/client/static/js/async/yx.af2936c368.js +10 -0
  33. package/dist/client/static/js/index.57b39ffa7f.js +34 -0
  34. package/package.json +1 -1
  35. package/src/client/components/common/file-icon/index.tsx +4 -1
  36. package/src/client/components/common/image-viewer/index.tsx +224 -0
  37. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +57 -43
  38. package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +31 -24
  39. package/src/client/components/settings/Modal.tsx +1 -0
  40. package/src/client/components/settings/categories/ChatSettings.tsx +6 -2
  41. package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +99 -0
  42. package/src/client/components/workspace/btw-panel/AskCard.tsx +155 -0
  43. package/src/client/components/workspace/btw-panel/Composer.tsx +181 -0
  44. package/src/client/components/workspace/btw-panel/Messages.tsx +100 -0
  45. package/src/client/components/workspace/btw-panel/TopicMenu.tsx +106 -0
  46. package/src/client/components/workspace/btw-panel/attachments.ts +80 -0
  47. package/src/client/components/workspace/btw-panel/index.tsx +83 -0
  48. package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +37 -21
  49. package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
  50. package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +10 -6
  51. package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +2 -1
  52. package/src/client/components/workspace/chat-timeline/chat-input/DropOverlay.tsx +17 -0
  53. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +44 -40
  54. package/src/client/components/workspace/chat-timeline/index.tsx +11 -9
  55. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +25 -7
  56. package/src/client/components/workspace/editor/Toolbar.tsx +32 -23
  57. package/src/client/components/workspace/editor/index.tsx +45 -36
  58. package/src/client/hooks/chat/btw/index.ts +180 -0
  59. package/src/client/hooks/chat/btw/intercept.ts +43 -0
  60. package/src/client/hooks/chat/btw/mode.ts +179 -0
  61. package/src/client/hooks/chat/composer/attachments.ts +198 -0
  62. package/src/client/hooks/chat/composer/drop-entries.ts +204 -0
  63. package/src/client/hooks/chat/composer/drop-payload.test.ts +97 -0
  64. package/src/client/hooks/chat/composer/drop-payload.ts +130 -0
  65. package/src/client/hooks/chat/composer/drop-references.ts +106 -0
  66. package/src/client/hooks/chat/composer/file-drop.test.ts +194 -0
  67. package/src/client/hooks/chat/composer/file-drop.ts +226 -0
  68. package/src/client/hooks/chat/composer/file-reads.test.ts +211 -0
  69. package/src/client/hooks/chat/composer/file-reads.ts +238 -0
  70. package/src/client/hooks/chat/composer/index.ts +7 -1
  71. package/src/client/hooks/chat/composer/pipeline.ts +181 -0
  72. package/src/client/hooks/chat/omp/index.ts +3 -3
  73. package/src/client/hooks/chat/omp/prompt-send.ts +5 -5
  74. package/src/client/hooks/chat/timeline/actions.ts +16 -1
  75. package/src/client/hooks/chat/timeline/index.ts +16 -8
  76. package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
  77. package/src/client/hooks/chat/timeline/send.ts +29 -65
  78. package/src/client/hooks/chat/timeline/session-load.ts +2 -1
  79. package/src/client/hooks/chat/timeline/session-pagination.ts +161 -14
  80. package/src/client/hooks/chat/timeline/user-turns.ts +149 -0
  81. package/src/client/hooks/editor/file-actions.ts +49 -0
  82. package/src/client/hooks/editor/use-file-editor.ts +38 -27
  83. package/src/client/hooks/editor/use-file-transfer.ts +78 -0
  84. package/src/client/tailwind.css +22 -0
  85. package/src/server/db.server.ts +21 -287
  86. package/src/server/lib/btw/frames.test.ts +68 -0
  87. package/src/server/lib/btw/frames.ts +56 -0
  88. package/src/server/lib/btw/prompt.ts +28 -0
  89. package/src/server/lib/btw/registry.server.ts +141 -0
  90. package/src/server/lib/btw/runtime.server.ts +324 -0
  91. package/src/server/lib/btw/schema.server.ts +44 -0
  92. package/src/server/lib/btw/service.server.ts +171 -0
  93. package/src/server/lib/btw/session-copy.server.ts +213 -0
  94. package/src/server/lib/btw/session-copy.test.ts +166 -0
  95. package/src/server/lib/btw/store.server.ts +240 -0
  96. package/src/server/lib/db/schema.ts +110 -0
  97. package/src/server/lib/db/seed.ts +160 -0
  98. package/src/server/lib/db/workspace-sync.ts +70 -0
  99. package/src/server/lib/fs/root.ts +37 -0
  100. package/src/server/lib/omp/rpc/frame-fold.ts +207 -0
  101. package/src/server/lib/omp/rpc/manager.ts +27 -123
  102. package/src/server/lib/omp/session/auto-title.server.test.ts +137 -0
  103. package/src/server/lib/omp/session/auto-title.server.ts +152 -0
  104. package/src/server/lib/omp/session/blobs.server.test.ts +100 -0
  105. package/src/server/lib/omp/session/blobs.server.ts +76 -0
  106. package/src/server/lib/omp/session/file-scan.ts +174 -0
  107. package/src/server/lib/omp/session/files.test.ts +147 -0
  108. package/src/server/lib/omp/session/files.ts +52 -35
  109. package/src/server/lib/omp/session/locator.ts +2 -2
  110. package/src/server/lib/omp/session/merge-stored.test.ts +118 -0
  111. package/src/server/lib/omp/session/merge-stored.ts +171 -0
  112. package/src/server/lib/omp/session/messages.ts +34 -0
  113. package/src/server/lib/omp/session/reader.ts +2 -2
  114. package/src/server/lib/omp/session/subagent-presence.ts +9 -6
  115. package/src/server/lib/queue/delivery.server.test.ts +57 -0
  116. package/src/server/lib/queue/delivery.server.ts +25 -3
  117. package/src/server/plugins/ssr.ts +29 -2
  118. package/src/server/routes/btw/events.ts +70 -0
  119. package/src/server/routes/btw/index.ts +8 -0
  120. package/src/server/routes/btw/topics.ts +87 -0
  121. package/src/server/routes/btw/ws.ts +136 -0
  122. package/src/server/routes/chat/index.ts +2 -0
  123. package/src/server/routes/chat/session.ts +1 -115
  124. package/src/server/routes/chat/turns.ts +87 -0
  125. package/src/server/routes/fs/index.ts +4 -1
  126. package/src/server/routes/fs/raw.ts +85 -0
  127. package/src/server/routes/fs/read.ts +58 -3
  128. package/src/server/routes/index.ts +7 -1
  129. package/src/shared/lib/chat/attachments.test.ts +208 -0
  130. package/src/shared/lib/chat/attachments.ts +200 -15
  131. package/src/shared/lib/chat/btw/client.ts +94 -0
  132. package/src/shared/lib/chat/composer/client.test.ts +48 -0
  133. package/src/shared/lib/chat/composer/client.ts +6 -1
  134. package/src/shared/lib/chat/composer/composer.test.ts +39 -1
  135. package/src/shared/lib/chat/composer/trigger.ts +45 -1
  136. package/src/shared/lib/chat/omp/agent-events.ts +33 -80
  137. package/src/shared/lib/chat/omp/socket.ts +11 -6
  138. package/src/shared/lib/chat/omp/sse.ts +11 -6
  139. package/src/shared/lib/chat/omp/tool-results.ts +102 -0
  140. package/src/shared/lib/chat/omp/transport.ts +19 -3
  141. package/src/shared/lib/chat/text-file.ts +148 -0
  142. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -32
  143. package/src/shared/lib/chat/timeline/stream-coalescer.ts +66 -0
  144. package/src/shared/lib/chat/timeline/turns.ts +7 -0
  145. package/src/shared/lib/fs/file-kind.ts +50 -0
  146. package/src/shared/lib/fs/paths.ts +19 -1
  147. package/src/shared/lib/markdown/math.test.ts +20 -3
  148. package/src/shared/lib/omp/session/inlined-attachments.test.ts +82 -0
  149. package/src/shared/lib/omp/session/mapper.ts +24 -4
  150. package/src/shared/lib/omp/session/messages-map.ts +16 -3
  151. package/src/shared/lib/omp/session/messages-parse.ts +2 -2
  152. package/src/shared/lib/omp/session/parse-message-blocks.ts +85 -5
  153. package/src/shared/types/btw.ts +65 -0
  154. package/src/shared/types/chat.ts +39 -1
  155. package/src/shared/types/index.ts +1 -0
  156. package/src/shared/types/omp/agent.ts +14 -1
  157. package/src/shared/types/omp/session.ts +4 -1
  158. package/src/shared/types/settings/state.ts +5 -0
  159. package/dist/client/static/css/index.b859cc1cc5.css +0 -2
  160. package/dist/client/static/js/async/5bh.8b2363424f.js +0 -1
  161. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  162. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  163. package/dist/client/static/js/async/7l.7a6a377ecc.js +0 -1
  164. package/dist/client/static/js/async/cx.ca5feec570.js +0 -226
  165. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  166. package/dist/client/static/js/async/g.a06ca3b3e9.js +0 -2
  167. package/dist/client/static/js/async/h1.e8d5aa8e80.js +0 -1
  168. package/dist/client/static/js/async/hs.6d480d0ded.js +0 -41
  169. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  170. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  171. package/dist/client/static/js/async/nam.b5663e919f.js +0 -10
  172. package/dist/client/static/js/async/q.23af2a358f.js +0 -21
  173. package/dist/client/static/js/async/q5.e683f7c26e.js +0 -286
  174. package/dist/client/static/js/async/s.222ee974f6.js +0 -2
  175. package/dist/client/static/js/async/uf.46ad6209fa.js +0 -2
  176. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  177. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  178. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  179. package/dist/client/static/js/index.6e065017eb.js +0 -33
  180. package/dist/client/static/js/jp.50817896f3.js +0 -139
  181. /package/dist/client/static/js/{jp.50817896f3.js.LICENSE.txt → 77.8022a800b5.js.LICENSE.txt} +0 -0
  182. /package/dist/client/static/js/async/{1k.5974df3b87.js.LICENSE.txt → 1k.a411fe5b43.js.LICENSE.txt} +0 -0
  183. /package/dist/client/static/js/async/{3y.71f2ccf3b6.js.LICENSE.txt → 3y.3186ecdd63.js.LICENSE.txt} +0 -0
  184. /package/dist/client/static/js/async/{dc.856fda6560.js.LICENSE.txt → ar.ad5c8822a8.js.LICENSE.txt} +0 -0
  185. /package/dist/client/static/js/async/{m.c038c71b9f.js.LICENSE.txt → f7.46971e9d6d.js.LICENSE.txt} +0 -0
  186. /package/dist/client/static/js/async/{5b.a5ccbc19dc.js.LICENSE.txt → g.ee41bfed7f.js.LICENSE.txt} +0 -0
  187. /package/dist/client/static/js/async/{q.23af2a358f.js.LICENSE.txt → m.2e1ea3db9e.js.LICENSE.txt} +0 -0
  188. /package/dist/client/static/js/async/{s.222ee974f6.js.LICENSE.txt → m3.d2b05f9604.js.LICENSE.txt} +0 -0
  189. /package/dist/client/static/js/async/{g.a06ca3b3e9.js.LICENSE.txt → q.b453193538.js.LICENSE.txt} +0 -0
  190. /package/dist/client/static/js/async/{uf.46ad6209fa.js.LICENSE.txt → q8.7f1b48e9cd.js.LICENSE.txt} +0 -0
  191. /package/dist/client/static/js/async/{nam.b5663e919f.js.LICENSE.txt → yx.af2936c368.js.LICENSE.txt} +0 -0
  192. /package/dist/client/static/js/{index.6e065017eb.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,207 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Folds one omp RPC frame into the session wrapper's runtime state, and runs
8
+ * the settle-time side effects a terminal run triggers.
9
+ *
10
+ * Mirrors the client's `foldAgentEvent` (shared/lib/chat/omp/agent-events.ts):
11
+ * the same shape of problem — one frame, many state flags — solved the same way
12
+ * so both ends of the transport read alike. Split out of rpc-manager.ts so the
13
+ * wrapper owns only process lifecycle.
14
+ *
15
+ * The state machine is the load-bearing part. `turn_end` fires for EVERY turn
16
+ * in a multi-turn run, `agent_end` is the only frame that knows whether the run
17
+ * is terminal, and a `prompt` response that failed must clear the same flags a
18
+ * successful settle does — a missed clear strands the session as "running" and
19
+ * the idle reclaim can never take it back.
20
+ */
21
+
22
+ import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
23
+ import {
24
+ consumeAutoTitleOutput,
25
+ triggerAutoSessionTitle,
26
+ type AutoTitleHost,
27
+ } from '@/server/lib/omp/session/auto-title.server';
28
+ import { scheduleQueueDelivery, type QueueDeliveryHost } from '@/server/lib/queue/delivery.server';
29
+ import { loadStreamStatuses, markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
30
+ import { NON_TERMINAL_CONTINUATION_GRACE_MS, type AgentEvent } from '@/server/lib/omp/rpc/constants';
31
+
32
+ /**
33
+ * Wrapper surface the fold reads and writes. Every field is owned by the
34
+ * wrapper; the fold mutates them in place exactly as the inline switch did.
35
+ * Composes the two host contracts the settle path hands the wrapper to
36
+ * (auto-title, queue delivery) so the wrapper itself satisfies this whole type.
37
+ */
38
+ export interface SessionFrameHost extends AutoTitleHost, QueueDeliveryHost {
39
+ promptRunning: boolean;
40
+ streaming: boolean;
41
+ compacting: boolean;
42
+ awaitingAgentStart: boolean;
43
+ awaitingAgentStartDeadline: number;
44
+ continuationGraceUntil: number;
45
+ emit(event: AgentEvent): void;
46
+ /** Remember a blocking ask/approval dialog so a reattaching client can answer it. */
47
+ trackUiDialog(frame: AgentEvent): void;
48
+ /** Fold a subagent frame into the liveness roster. */
49
+ observeSubagent(frame: AgentEvent, now: number): void;
50
+ }
51
+
52
+ /** What the caller must do after the fold. */
53
+ export interface FrameFoldResult {
54
+ /** The session list is stale and needs a rebuild. */
55
+ refreshSessionList: boolean;
56
+ /** The fold already handled this frame's user-visible effect and the caller
57
+ * must NOT forward it: a failed `prompt` response has emitted its own
58
+ * `prompt_error`, and a `command_output` belonging to the chamber's own
59
+ * background rename must stay out of the timeline. */
60
+ suppressForward: boolean;
61
+ }
62
+
63
+ /** True when the ending turn was a user abort rather than a natural stop. */
64
+ function endedAborted(messages: unknown): boolean {
65
+ return (
66
+ Array.isArray(messages) &&
67
+ messages.some((entry) => {
68
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
69
+ return (entry as Record<string, unknown>).stopReason === 'aborted';
70
+ })
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Write the terminal stream badge from the ending turn's own stopReason.
76
+ *
77
+ * The `abort` command already wrote `abort` at dispatch time, but the
78
+ * `agent_end` frame arrives later and previously flattened it to `finish`.
79
+ * Re-check the current row and only upgrade `stream` rows, so a `finish`
80
+ * written here can never clobber a newer run's live `stream`/`abort`.
81
+ */
82
+ async function markEndStatus(sessionId: string, messages: unknown): Promise<void> {
83
+ if (!sessionId) return;
84
+ const current = await loadStreamStatuses();
85
+ if (current[sessionId] !== 'stream') return;
86
+ await markStreamStatus(sessionId, endedAborted(messages) ? 'abort' : 'finish');
87
+ }
88
+
89
+ /** Apply one frame to the wrapper's runtime state. */
90
+ export function foldSessionFrame(host: SessionFrameHost, event: AgentEvent): FrameFoldResult {
91
+ const result: FrameFoldResult = { refreshSessionList: false, suppressForward: false };
92
+
93
+ switch (event.type) {
94
+ case 'agent_start':
95
+ host.promptRunning = true;
96
+ host.streaming = true;
97
+ host.awaitingAgentStart = false;
98
+ host.awaitingAgentStartDeadline = 0;
99
+ host.continuationGraceUntil = 0;
100
+ clearSessionFileCaches();
101
+ result.refreshSessionList = true;
102
+ if (host.sessionId) void markStreamStatus(host.sessionId, 'stream');
103
+ break;
104
+ case 'turn_start':
105
+ // Redundant with agent_start in the happy path, but the run-level
106
+ // counterpart can be missed (e.g. the state was cleared by a command
107
+ // between frames). The turn is proof enough the session is live.
108
+ if (host.sessionId) void markStreamStatus(host.sessionId, 'stream');
109
+ break;
110
+ case 'message_start':
111
+ // Same recovery as turn_start: a message frame is only emitted inside a
112
+ // live run, so its arrival re-arms the `stream` row no matter what stale
113
+ // status sits there (upsert overwrites any terminal badge).
114
+ if (host.sessionId) void markStreamStatus(host.sessionId, 'stream');
115
+ break;
116
+ case 'turn_end': {
117
+ // A multi-turn run emits turn_end for EVERY turn — intermediates end with
118
+ // `toolUse`/`stop` while the run keeps going (verified against omp
119
+ // 18.2.8: agent_start → turn_start → turn_end('toolUse') → turn_start →
120
+ // turn_end('stop') → agent_end, and the frame carries no isTerminal
121
+ // field). Only `aborted` is unambiguous here: the run is over, so the
122
+ // abort badge can be written before agent_end arrives. Everything else
123
+ // waits for agent_end, which knows isTerminal.
124
+ if (!host.sessionId) break;
125
+ const turn = event.message as Record<string, unknown> | undefined;
126
+ if (turn?.stopReason === 'aborted') void markStreamStatus(host.sessionId, 'abort');
127
+ break;
128
+ }
129
+ case 'agent_end':
130
+ if (event.isTerminal !== false) {
131
+ host.streaming = false;
132
+ host.promptRunning = false;
133
+ host.awaitingAgentStart = false;
134
+ host.awaitingAgentStartDeadline = 0;
135
+ host.continuationGraceUntil = 0;
136
+ clearSessionFileCaches();
137
+ const aborted = endedAborted(event.messages);
138
+ if (host.sessionId) void markEndStatus(host.sessionId, event.messages);
139
+ // A settled run is the moment a session becomes nameable: omp's own
140
+ // first-message titling is suppressed under `--mode rpc-ui`
141
+ // (PI_NO_TITLE), so the chamber asks for the same title itself. The
142
+ // trigger is a no-op once the session carries a name — see
143
+ // triggerAutoSessionTitle for why that check is the safety property.
144
+ if (!aborted) void triggerAutoSessionTitle(host);
145
+ // The run truly ended — the server, not the browser, decides whether a
146
+ // queued follow-up goes out next. A user-aborted run holds the queue
147
+ // (stop-all semantics): the next run end or an explicit send picks it up.
148
+ if (!aborted) scheduleQueueDelivery(host);
149
+ } else {
150
+ host.continuationGraceUntil = Date.now() + NON_TERMINAL_CONTINUATION_GRACE_MS;
151
+ }
152
+ break;
153
+ case 'prompt_result':
154
+ host.promptRunning = false;
155
+ host.awaitingAgentStart = false;
156
+ host.awaitingAgentStartDeadline = 0;
157
+ break;
158
+ case 'auto_compaction_start':
159
+ host.compacting = true;
160
+ break;
161
+ case 'auto_compaction_end':
162
+ host.compacting = false;
163
+ clearSessionFileCaches();
164
+ break;
165
+ case 'command_output':
166
+ // A `command_output` for a command the OPERATOR typed renders as a notice
167
+ // row. But the chamber fires its own `/rename` after a settled run, and
168
+ // omp answers it on the same frame — "Session renamed to …", or "Could not
169
+ // generate a session title" for a low-signal first message. Neither was
170
+ // asked for, and the second would be pure noise. The frame carries no
171
+ // correlation id, so attribution is by the request window.
172
+ result.suppressForward = consumeAutoTitleOutput(host);
173
+ break;
174
+ case 'session_info_update':
175
+ // A rename rewrote the fixed-width title slot in place; the scan cache is
176
+ // keyed on file mtime, which that write cannot move, so the list must be
177
+ // rebuilt explicitly.
178
+ clearSessionFileCaches();
179
+ result.refreshSessionList = true;
180
+ break;
181
+ case 'extension_ui_request':
182
+ host.trackUiDialog(event);
183
+ break;
184
+ // Subagent frames carry no turn state, but they are the only proof that
185
+ // work is still running once the parent turn has ended.
186
+ case 'subagent_lifecycle':
187
+ case 'subagent_progress':
188
+ case 'subagent_event':
189
+ host.observeSubagent(event, Date.now());
190
+ break;
191
+ case 'response': {
192
+ if (event.success === false && event.command === 'prompt') {
193
+ host.promptRunning = false;
194
+ host.awaitingAgentStart = false;
195
+ host.awaitingAgentStartDeadline = 0;
196
+ host.emit({ type: 'prompt_error', errorMessage: (event.error as string) ?? 'Prompt failed' });
197
+ // The caller's trailing notify covers this: promptRunning just flipped,
198
+ // so the running snapshot changes and the listeners fire. Suppress only
199
+ // the forward, or the error would land twice.
200
+ result.suppressForward = true;
201
+ }
202
+ break;
203
+ }
204
+ }
205
+
206
+ return result;
207
+ }