@kabmo-labs/kabmo 0.3.1 → 0.5.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 (155) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/index.js +412 -383
  3. package/dist/client/static/js/index-3qamd33d.css +1 -0
  4. package/dist/client/static/js/{index-28z316qk.js → index-cs3b557t.js} +792 -792
  5. package/package.json +1 -1
  6. package/src/client/components/common/batch-selection/BatchActionBar.tsx +8 -1
  7. package/src/client/components/common/session-actions-menu/index.tsx +23 -2
  8. package/src/client/components/layout/{RightActivityBar.test.ts → ActivityBar.test.ts} +5 -5
  9. package/src/client/components/layout/{RightActivityBar.tsx → ActivityBar.tsx} +87 -52
  10. package/src/client/components/layout/LeftActivityBar.test.ts +201 -0
  11. package/src/client/components/layout/LeftDock.tsx +119 -0
  12. package/src/client/components/layout/desktop-layout/LeftColumn.tsx +172 -0
  13. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -3
  14. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +22 -5
  15. package/src/client/components/layout/desktop-layout/index.tsx +88 -61
  16. package/src/client/components/layout/desktop-layout/left-column.test.tsx +192 -0
  17. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +6 -0
  18. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +9 -0
  19. package/src/client/components/layout/session-sidebar/Header.tsx +2 -1
  20. package/src/client/components/layout/session-sidebar/SessionItem.tsx +42 -6
  21. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  22. package/src/client/components/layout/session-sidebar/{index.tsx → SessionsView.tsx} +48 -72
  23. package/src/client/components/layout/session-sidebar/new-session-context.test.ts +55 -0
  24. package/src/client/components/mobile/LayoutWrapper.tsx +4 -0
  25. package/src/client/components/mobile/RightSidebar.tsx +30 -36
  26. package/src/client/components/mobile/SessionSidebar.tsx +124 -101
  27. package/src/client/components/mobile/mobile-main-view/Header.tsx +3 -2
  28. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -2
  29. package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +3 -2
  30. package/src/client/components/mobile/mobile-session-sidebar/Dialogs.tsx +106 -0
  31. package/src/client/components/mobile/mobile-session-sidebar/Dock.tsx +80 -0
  32. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +11 -5
  33. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +34 -44
  34. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +9 -1
  35. package/src/client/components/mobile/mobile-session-sidebar/RevealRow.tsx +60 -0
  36. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +47 -14
  37. package/src/client/components/mobile/mobile-session-sidebar/Skeleton.tsx +2 -2
  38. package/src/client/components/mobile/mobile-session-sidebar/use-dock.ts +89 -0
  39. package/src/client/components/mobile/panel-chips.test.tsx +155 -0
  40. package/src/client/components/mobile/panel-chips.tsx +74 -0
  41. package/src/client/components/settings/categories/panel-plugins/InstalledList.tsx +2 -2
  42. package/src/client/components/settings/categories/panel-plugins/LeftDockSettings.tsx +268 -0
  43. package/src/client/components/settings/categories/panel-plugins/Section.tsx +8 -0
  44. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +69 -1
  45. package/src/client/components/workspace/chat-timeline/MessageItem.gutter.test.ts +118 -0
  46. package/src/client/components/workspace/chat-timeline/MessageItem.tsx +13 -3
  47. package/src/client/components/workspace/chat-timeline/MessageList.tsx +1 -0
  48. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +106 -0
  49. package/src/client/components/workspace/chat-timeline/SessionSkeleton.tsx +1 -1
  50. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +4 -1
  51. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +5 -2
  52. package/src/client/components/workspace/chat-timeline/ThinkingSection.tsx +6 -2
  53. package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +1 -1
  54. package/src/client/components/workspace/chat-timeline/ToolCallingSection.test.ts +5 -5
  55. package/src/client/components/workspace/chat-timeline/ToolCallingSection.tsx +5 -2
  56. package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +25 -1
  57. package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +15 -10
  58. package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.test.ts +84 -0
  59. package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.tsx +185 -0
  60. package/src/client/components/workspace/chat-timeline/chat-input/compact.test.tsx +162 -0
  61. package/src/client/components/workspace/chat-timeline/chat-input/expansion.test.ts +22 -21
  62. package/src/client/components/workspace/chat-timeline/chat-input/expansion.ts +12 -20
  63. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +54 -27
  64. package/src/client/components/workspace/chat-timeline/chat-input/props.ts +16 -0
  65. package/src/client/components/workspace/chat-timeline/index.tsx +15 -16
  66. package/src/client/components/workspace/editor/PluginRail.tsx +79 -0
  67. package/src/client/components/workspace/editor/index.tsx +1 -0
  68. package/src/client/components/workspace/editor/plugin-rail.test.tsx +126 -0
  69. package/src/client/components/workspace/model-dropdown/groups.test.ts +60 -0
  70. package/src/client/components/workspace/model-dropdown/groups.ts +7 -1
  71. package/src/client/components/workspace/model-dropdown/index.tsx +3 -3
  72. package/src/client/components/workspace/model-dropdown/trigger.test.ts +123 -0
  73. package/src/client/components/workspace/model-dropdown/use-catalog.ts +8 -2
  74. package/src/client/components/workspace/panels/builtin.tsx +8 -0
  75. package/src/client/components/workspace/plugin-panel/catalog.tsx +237 -0
  76. package/src/client/components/workspace/plugin-panel/resolve.tsx +19 -122
  77. package/src/client/hooks/chat/composer/compact.ts +119 -0
  78. package/src/client/hooks/chat/omp/pending-rows.ts +4 -0
  79. package/src/client/hooks/chat/omp/pending-sessions.ts +47 -0
  80. package/src/client/hooks/chat/omp/pending-sessions.waiting.test.ts +102 -0
  81. package/src/client/hooks/chat/omp/pending-waiting.ts +48 -0
  82. package/src/client/hooks/chat/omp/session-sidebar-controller.ts +29 -0
  83. package/src/client/hooks/chat/timeline/actions.ts +34 -35
  84. package/src/client/hooks/chat/timeline/composer-refs.ts +47 -0
  85. package/src/client/hooks/chat/timeline/enqueue-follow-up.ts +37 -0
  86. package/src/client/hooks/chat/timeline/index.ts +15 -19
  87. package/src/client/hooks/chat/timeline/local-commands.ts +29 -0
  88. package/src/client/hooks/chat/timeline/queue-rewind.test.ts +6 -0
  89. package/src/client/hooks/chat/timeline/run-identity.ts +45 -0
  90. package/src/client/hooks/chat/waiting/cancel.ts +34 -0
  91. package/src/client/hooks/chat/waiting/choice.ts +53 -0
  92. package/src/client/hooks/chat/waiting/dispatch.ts +91 -0
  93. package/src/client/hooks/chat/waiting/held.ts +42 -0
  94. package/src/client/hooks/chat/waiting/hold.test.ts +139 -0
  95. package/src/client/hooks/chat/waiting/hold.ts +156 -0
  96. package/src/client/hooks/chat/waiting/store.ts +100 -0
  97. package/src/client/hooks/ui/toasts.ts +12 -1
  98. package/src/client/hooks/workspace/left-dock.ts +132 -0
  99. package/src/client/hooks/workspace/panel-registry.ts +13 -0
  100. package/src/client/tailwind.css +22 -0
  101. package/src/server/db.server.test.ts +52 -1
  102. package/src/server/db.server.ts +49 -1
  103. package/src/server/index.ts +9 -0
  104. package/src/server/lib/assets/compressed-cache.server.test.ts +78 -0
  105. package/src/server/lib/assets/compressed-cache.server.ts +88 -0
  106. package/src/server/lib/db/schema.test.ts +1 -0
  107. package/src/server/lib/db/schema.ts +3 -0
  108. package/src/server/lib/fs/fs-path.test.ts +1 -1
  109. package/src/server/lib/omp/rpc/terminal-settle.ts +7 -0
  110. package/src/server/lib/omp/session/delete.server.ts +8 -0
  111. package/src/server/lib/omp/session/ownership.server.test.ts +45 -4
  112. package/src/server/lib/panels/registry.server.ts +2 -2
  113. package/src/server/lib/queue/delivery.server.ts +31 -39
  114. package/src/server/lib/queue/payload.ts +73 -0
  115. package/src/server/lib/realtime/signals.server.ts +10 -1
  116. package/src/server/lib/realtime/topics/catalog.server.ts +2 -0
  117. package/src/server/lib/realtime/topics/global.server.ts +5 -0
  118. package/src/server/lib/realtime/topics.server.ts +2 -0
  119. package/src/server/lib/util/with-timeout.ts +29 -0
  120. package/src/server/lib/waiting/row-clock.server.ts +37 -0
  121. package/src/server/lib/waiting/runtime.server.test.ts +278 -0
  122. package/src/server/lib/waiting/runtime.server.ts +313 -0
  123. package/src/server/lib/waiting/schema.server.ts +46 -0
  124. package/src/server/lib/waiting/settle.test.ts +84 -0
  125. package/src/server/lib/waiting/settle.ts +77 -0
  126. package/src/server/lib/waiting/store.server.test.ts +167 -0
  127. package/src/server/lib/waiting/store.server.ts +268 -0
  128. package/src/server/lib/waiting/sweep-trigger.ts +35 -0
  129. package/src/server/plugins/compress.ts +18 -6
  130. package/src/server/plugins/ssr.ts +16 -8
  131. package/src/server/plugins/static.test.ts +59 -0
  132. package/src/server/plugins/static.ts +56 -14
  133. package/src/server/routes/index.ts +2 -0
  134. package/src/server/routes/waiting/bindings.ts +14 -0
  135. package/src/server/routes/waiting/index.ts +123 -0
  136. package/src/server/routes/waiting/routes.test.ts +121 -0
  137. package/src/shared/lib/chat/timeline/gutter.test.ts +37 -0
  138. package/src/shared/lib/chat/timeline/gutter.ts +37 -0
  139. package/src/shared/lib/realtime/protocol.ts +10 -0
  140. package/src/shared/lib/ui/titlebar.test.ts +124 -0
  141. package/src/shared/lib/ui/titlebar.ts +54 -0
  142. package/src/shared/lib/workspace/left-dock-width.ts +78 -0
  143. package/src/shared/lib/workspace/left-dock.test.ts +203 -0
  144. package/src/shared/lib/workspace/left-dock.ts +197 -0
  145. package/src/shared/lib/workspace/mobile-dock.test.ts +65 -0
  146. package/src/shared/lib/workspace/mobile-dock.ts +55 -0
  147. package/src/shared/lib/workspace/panel-ids.test.ts +59 -10
  148. package/src/shared/lib/workspace/panel-ids.ts +114 -26
  149. package/src/shared/lib/workspace/sidebar-width.ts +4 -0
  150. package/src/shared/types/index.ts +1 -0
  151. package/src/shared/types/waiting.ts +54 -0
  152. package/src/shared/types/workspace.ts +8 -0
  153. package/src/test-support/isolated-db.ts +3 -4
  154. package/src/test-support/silent-socket.ts +69 -0
  155. package/dist/client/static/js/index-pdp5dwc8.css +0 -1
@@ -1,15 +1,26 @@
1
1
  /**
2
- * Panel ids: the built-in right-panel views plus the panels plugin code
3
- * contributes.
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Panel ids: the built-in views plus the panels plugin code contributes.
4
8
  *
5
- * A plugin panel's id is `plugin:<pluginId>` — one string the layout stores,
6
- * keys widths by, and resolves a component from. Keeping it a plain string
7
- * rather than a second union member is what lets the activity bar, the mobile
8
- * tab bar and the width map handle a plugin without each learning about it.
9
+ * A plugin panel's id is `plugin:<pluginId>:<slot>` — one string the layout
10
+ * stores, keys widths by, and resolves a component from. One plugin contributes
11
+ * at most one panel PER SLOT (`rightPanel`, `panel`), and a plugin may register
12
+ * both: `marketplace/plugins/session-info` ships a `rightPanel` and a `panel`.
13
+ * The slot segment is what keeps those two apart.
9
14
  *
10
- * There is no panel-id segment because a plugin contributes AT MOST ONE panel
11
- * per slot: the layout gives it one activity-bar button and one editor tab, so
12
- * a second id could never be reached.
15
+ * It was not always. The id used to carry a panel segment, then dropped it on the
16
+ * claim that a plugin contributes at most one panel — true per slot, false per
17
+ * plugin. With the editor column's slot reachable again, one id for both would
18
+ * make them one entry in the enablement set, one width and one placement, so
19
+ * moving a plugin's right panel would move its editor panel with it.
20
+ *
21
+ * The BARE form is the one that shipped and is still accepted, resolving to the
22
+ * `rightPanel` slot — see {@link migrateLegacyPanelId}. Writers emit the
23
+ * segmented form; readers accept both.
13
24
  */
14
25
 
15
26
  import { RIGHT_PANEL_TYPES, type RightPanelType } from '@/shared/lib/workspace/right-panels';
@@ -17,43 +28,120 @@ import { RIGHT_PANEL_TYPES, type RightPanelType } from '@/shared/lib/workspace/r
17
28
  /** Prefix every plugin-contributed panel id carries. */
18
29
  export const PLUGIN_PANEL_PREFIX = 'plugin:';
19
30
 
20
- /** `plugin:<pluginId>` for a plugin's contributed panel. */
21
- export function pluginPanelKey(pluginId: string): string {
31
+ /**
32
+ * The slots a plugin can contribute a panel to.
33
+ *
34
+ * NOT `headerPanel`/`settingsSection`: those are not panels — one is a navbar
35
+ * entry, the other a settings block — and neither keys a width or a placement.
36
+ */
37
+ export const PLUGIN_PANEL_SLOTS = ['rightPanel', 'panel'] as const;
38
+
39
+ export type PluginPanelSlot = (typeof PLUGIN_PANEL_SLOTS)[number];
40
+
41
+ /** The slot a bare `plugin:<id>` names: the form that shipped. */
42
+ /** The slot a bare `plugin:<id>` names, where an INSTANCE key is expected. */
43
+ export const LEGACY_PLUGIN_SLOT: PluginPanelSlot = 'rightPanel';
44
+
45
+ /**
46
+ * `plugin:<pluginId>:<slot>` — ONE PANEL of a plugin.
47
+ *
48
+ * The key every panel-shaped store uses: the width map, the dock placement, the
49
+ * stored order and the active selection. Two panels of one plugin are two panels
50
+ * here, which is the whole point.
51
+ */
52
+ export function pluginPanelKey(pluginId: string, slot: PluginPanelSlot): string {
53
+ return `${PLUGIN_PANEL_PREFIX}${pluginId}:${slot}`;
54
+ }
55
+
56
+ /**
57
+ * `plugin:<pluginId>` — the PLUGIN, not a panel.
58
+ *
59
+ * Separate from {@link pluginPanelKey} on purpose. Enablement is per PLUGIN: the
60
+ * Settings switch says "Disable <plugin>" and the server gates that plugin's
61
+ * BUNDLE on this key, so one switch has to be one id no matter how many panels
62
+ * the plugin contributes. Making enablement per panel would leave a plugin whose
63
+ * bundle is still served while half its panels are "off".
64
+ *
65
+ * This is also the form that shipped, so every stored disabled row still names a
66
+ * plugin and needs no migration.
67
+ */
68
+ export function pluginEnablementKey(pluginId: string): string {
22
69
  return `${PLUGIN_PANEL_PREFIX}${pluginId}`;
23
70
  }
24
71
 
72
+ function isSlot(value: string): value is PluginPanelSlot {
73
+ return (PLUGIN_PANEL_SLOTS as readonly string[]).includes(value);
74
+ }
75
+
25
76
  /**
26
- * The plugin id a panel id names, or null when `value` is not one.
77
+ * The parts of a plugin panel id, or null.
78
+ *
79
+ * Two shapes are accepted, and the second is DELIBERATELY the one that shipped:
27
80
  *
28
- * The segment must be non-empty: `plugin:` is the shape a truncated write or a
29
- * half-typed value produces, and accepting it would key a width map and a
30
- * component lookup on an id no registry can ever report.
81
+ * - `plugin:<id>:<slot>` — the current form;
82
+ * - `plugin:<id>` — a stored blob, an enablement row or an order entry written
83
+ * before the slot segment existed. It resolves to `rightPanel`, because that
84
+ * was the only reachable slot when it was written.
85
+ *
86
+ * The plugin id is validated as ONE segment: an empty id, a slash or a third
87
+ * colon is a truncated write or a hand-edited blob, and accepting it would key a
88
+ * width map and a component lookup on an id no registry can report.
31
89
  */
32
- export function pluginKeyOf(value: unknown): string | null {
90
+ export function pluginPanelParts(value: unknown): { pluginId: string; slot: PluginPanelSlot } | null {
33
91
  if (typeof value !== 'string' || !value.startsWith(PLUGIN_PANEL_PREFIX)) return null;
34
92
  const rest = value.slice(PLUGIN_PANEL_PREFIX.length);
35
- if (!rest || rest.includes('/')) return null;
36
- return value;
93
+ const pieces = rest.split(':');
94
+ if (pieces.length > 2) return null;
95
+ const [pluginId, slot] = pieces;
96
+ if (!pluginId || pluginId.includes('/')) return null;
97
+ if (slot === undefined) return { pluginId, slot: LEGACY_PLUGIN_SLOT };
98
+ if (!isSlot(slot)) return null;
99
+ return { pluginId, slot };
100
+ }
101
+
102
+ /**
103
+ * The PANEL INSTANCE key a stored value names, or null.
104
+ *
105
+ * A bare `plugin:<id>` normalizes to the `rightPanel` instance, because that was
106
+ * the only reachable slot when such a row was written — the width map, the dock
107
+ * placement and the stored order all read through here, so a row from an older
108
+ * build keeps pointing at the panel it pointed at before.
109
+ */
110
+ export function pluginKeyOf(value: unknown): string | null {
111
+ const parts = pluginPanelParts(value);
112
+ return parts ? pluginPanelKey(parts.pluginId, parts.slot) : null;
113
+ }
114
+
115
+ /** Whether a stored value is the retired bare form, and what it resolves to. */
116
+ export function migrateLegacyPanelId(value: unknown): { from: string; to: string } | null {
117
+ const parts = pluginPanelParts(value);
118
+ if (!parts || typeof value !== 'string') return null;
119
+ const to = pluginPanelKey(parts.pluginId, parts.slot);
120
+ return value === to ? null : { from: value, to };
37
121
  }
38
122
 
39
123
  /** The plugin id inside a panel id, or null. */
40
124
  export function pluginIdOf(value: unknown): string | null {
41
- const key = pluginKeyOf(value);
42
- return key ? key.slice(PLUGIN_PANEL_PREFIX.length) : null;
125
+ return pluginPanelParts(value)?.pluginId ?? null;
126
+ }
127
+
128
+ /** The slot a panel id names, or null when `value` is not a plugin panel id. */
129
+ export function pluginSlotOf(value: unknown): PluginPanelSlot | null {
130
+ return pluginPanelParts(value)?.slot ?? null;
43
131
  }
44
132
 
45
133
  /**
46
- * Whether a stored `layout.activeRightPanel` may be adopted on load.
134
+ * Whether a stored `layout.activeRightPanel` / width key may be adopted on load.
47
135
  *
48
136
  * A plugin panel is accepted on the shape of its id alone: the registry is a
49
137
  * network read and the slots exist only after a bundle has been imported, so
50
- * requiring the panel to be present would drop a perfectly valid selection
51
- * every time the server is slower than the first paint. A panel whose plugin
52
- * has since been uninstalled renders its own "not installed" notice rather than
53
- * falling back silently to Files.
138
+ * requiring the panel to be present would drop a perfectly valid selection every
139
+ * time the server is slower than the first paint. A panel whose plugin has since
140
+ * been uninstalled renders its own "not installed" notice rather than falling
141
+ * back silently to Files.
54
142
  */
55
143
  export function isPanelId(value: unknown): value is string {
56
- return (RIGHT_PANEL_TYPES as readonly string[]).includes(value as string) || pluginKeyOf(value) !== null;
144
+ return (RIGHT_PANEL_TYPES as readonly string[]).includes(value as string) || pluginPanelParts(value) !== null;
57
145
  }
58
146
 
59
147
  export type { RightPanelType };
@@ -43,6 +43,10 @@ export const DEFAULT_LEFT_PANEL_WIDTH = 280;
43
43
  * `MIN_CHAT_PANEL_WIDTH` in `panel-widths.ts`: the sidebar cannot open past 500
44
44
  * and still leave the editor (320) and a browser view (320) room on a 1440px
45
45
  * display.
46
+ *
47
+ * It was raised to 720 for a second left pane and put back when that pane was
48
+ * removed: the dock holds ONE panel beside the list, and a wider ceiling would
49
+ * only invite a column nothing else in the layout budgets for.
46
50
  */
47
51
  export const MIN_LEFT_PANEL_WIDTH = 264;
48
52
  export const MAX_LEFT_PANEL_WIDTH = 500;
@@ -4,6 +4,7 @@ export * from '@/shared/types/fs';
4
4
  export * from '@/shared/types/git';
5
5
  export * from '@/shared/types/chat';
6
6
  export * from '@/shared/types/schedule';
7
+ export * from '@/shared/types/waiting';
7
8
  export * from '@/shared/types/btw';
8
9
  export * from '@/shared/types/models';
9
10
  export * from '@/shared/types/terminal';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * A prompt held until another session finishes.
8
+ *
9
+ * The row is Kabmo's own (SQLite `waiting_prompts`): omp has no notion of a
10
+ * prompt that has not been sent yet, so the wait has to live somewhere that
11
+ * outlives the tab. Its `dependsOn` session is a SESSION ID, never a snapshot of
12
+ * its state — the state is re-read at each sweep, because a session that looks
13
+ * finished can pick up a queued follow-up a moment later.
14
+ */
15
+
16
+ import type { Attachment, QueuedMessageModel } from '@/shared/types/chat';
17
+
18
+ export type WaitingPromptStatus = 'waiting' | 'dispatched' | 'cancelled' | 'error';
19
+
20
+ /** Plan/goal flags the spawn must carry (a new session has no JSONL to read). */
21
+ export interface WaitingPromptModes {
22
+ plan: boolean;
23
+ goal: boolean;
24
+ }
25
+
26
+ export interface WaitingPrompt {
27
+ id: string;
28
+ /**
29
+ * The client's pending id (`new-…`) this prompt belongs to. The dispatch
30
+ * answers with omp's real session id, and the client moves the sidebar row
31
+ * across by this pairing — the same adoption the local send path performs.
32
+ */
33
+ pendingId: string | null;
34
+ /** The session whose completion releases this prompt. */
35
+ dependsOn: string;
36
+ /** Title of `dependsOn` at creation time, for the waiting row's own label. */
37
+ dependsOnTitle: string | null;
38
+ /** Composer text as the user typed it. */
39
+ prompt: string;
40
+ attachments: Attachment[];
41
+ /** Model/thinking/access snapshot, replayed at dispatch. */
42
+ model: QueuedMessageModel | null;
43
+ modes: WaitingPromptModes | null;
44
+ /** Absolute directory the new session spawns in, resolved at creation. */
45
+ cwd: string;
46
+ folderId: number | null;
47
+ status: WaitingPromptStatus;
48
+ /** omp's real session id, once the prompt was dispatched. */
49
+ sessionId: string | null;
50
+ /** Why the dispatch failed (status `error`). */
51
+ error: string | null;
52
+ createdAt: number;
53
+ updatedAt: number;
54
+ }
@@ -30,6 +30,14 @@ export interface SessionItemData {
30
30
  * provider/model in every tab and Kabmo instances. Absent for a session
31
31
  * with no run row; callers fall back to the transcript's model entry. */
32
32
  runModel?: { provider: string; modelId: string };
33
+ /**
34
+ * Set on a PENDING row whose prompt is held until another session finishes.
35
+ *
36
+ * The row has no transcript and no omp id yet — it exists because the user
37
+ * sent a prompt that is deferred, and the cue is the only thing on screen
38
+ * saying so. Absent means the row is not waiting (the normal case).
39
+ */
40
+ waitingFor?: { waitId: string; sessionId: string; title: string | null };
33
41
  }
34
42
 
35
43
  /**
@@ -23,14 +23,14 @@ import { mkdtempSync } from 'node:fs';
23
23
  import { tmpdir } from 'node:os';
24
24
  import { join } from 'node:path';
25
25
 
26
+ import { closeDb } from '@/server/db.server';
26
27
  import { removeDir } from '@/test-support/remove-dir';
27
28
 
28
29
  let dir: string | null = null;
29
30
 
30
31
  /** Point the process at a fresh database. Call from `beforeEach`. */
31
32
  export function isolateDb(): string {
32
- globalThis.__kabmoDb?.resolved?.raw.close();
33
- globalThis.__kabmoDb = undefined;
33
+ closeDb();
34
34
 
35
35
  dir = mkdtempSync(join(tmpdir(), 'omc-db-'));
36
36
  const path = join(dir, 'db.sqlite');
@@ -40,8 +40,7 @@ export function isolateDb(): string {
40
40
 
41
41
  /** Close the handle and drop the temp directory. Call from `afterEach`. */
42
42
  export function releaseDb(): void {
43
- globalThis.__kabmoDb?.resolved?.raw.close();
44
- globalThis.__kabmoDb = undefined;
43
+ closeDb();
45
44
  delete Bun.env.KABMO_DB_PATH;
46
45
  // `removeDir`, not a bare `rmSync`: a closed SQLite handle is released by the
47
46
  // OS asynchronously on Windows, so the delete races it and fails with EBUSY —
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * A `WebSocket` that never dials, and the teardown pair that goes with it.
8
+ *
9
+ * A suite that mounts a component reading a realtime topic subscribes to the
10
+ * unified channel on mount, and `dial()` then constructs a real socket from
11
+ * `window.location`. Two failure shapes follow, and both are reported against
12
+ * whichever file runs NEXT rather than the one that caused them:
13
+ *
14
+ * - a happy-dom window created with no URL reports `about:blank`, whose origin
15
+ * is `null`, so `realtimeSocketUrl()` produces `ws:///api/realtime/ws`. Bun
16
+ * constructs that object without complaint and fails it ASYNCHRONOUSLY, after
17
+ * the suite has already restored the DOM globals — measured as
18
+ * `Unhandled error. (ErrorEvent { isTrusted: [Getter] })` between tests, and
19
+ * then as `Cannot call beforeAll() after the test run has completed` in the
20
+ * next file, because the error tears the run down mid-file.
21
+ * - a window WITH an origin dials a port nothing listens on, which fails the
22
+ * same way (a refused connection is also asynchronous).
23
+ *
24
+ * `resetRealtimeClient()` in `afterEach` is not enough on its own: it closes a
25
+ * socket that already exists, and the dial's own failure has not been delivered
26
+ * yet. Silencing the CONSTRUCTOR is what removes the failure at its source.
27
+ *
28
+ * The subject of such a suite is its own markup, never the channel, so this
29
+ * stub sends nothing and reports nothing. A suite that really drives the
30
+ * channel uses `startRealtimeTestServer` instead, which injects the runner's
31
+ * own `WebSocket` (see `pristineWebSocket`).
32
+ */
33
+
34
+ import { realtimeClient, resetRealtimeClient } from '@/shared/lib/realtime/client';
35
+
36
+ /** `WebSocket.CLOSING` / `CLOSED`, spelled out: the client reads them off the
37
+ * instance, and this object need not carry the statics. */
38
+ class SilentSocket {
39
+ static readonly CONNECTING = 0;
40
+ static readonly OPEN = 1;
41
+ static readonly CLOSING = 2;
42
+ static readonly CLOSED = 3;
43
+ readyState = SilentSocket.CLOSED;
44
+ onopen: (() => void) | null = null;
45
+ onclose: (() => void) | null = null;
46
+ onmessage: ((event: { data: string }) => void) | null = null;
47
+ onerror: (() => void) | null = null;
48
+ send(): void {}
49
+ close(): void {}
50
+ }
51
+
52
+ /**
53
+ * Point the channel at `SilentSocket`. Call it BEFORE mounting anything that
54
+ * subscribes — a dial that already happened is not retracted by this.
55
+ */
56
+ export function silenceRealtime(): void {
57
+ realtimeClient.setSocketConstructor(SilentSocket as unknown as typeof WebSocket);
58
+ }
59
+
60
+ /**
61
+ * Undo `silenceRealtime` and drop the channel's state. This is the `afterEach`
62
+ * half: the constructor goes back to `null` (the real global) so the next file
63
+ * is not left silenced, and the reset clears the topic cache the stubbed
64
+ * channel accumulated.
65
+ */
66
+ export function restoreRealtime(): void {
67
+ resetRealtimeClient();
68
+ realtimeClient.setSocketConstructor(null);
69
+ }