@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.
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +412 -383
- package/dist/client/static/js/index-3qamd33d.css +1 -0
- package/dist/client/static/js/{index-28z316qk.js → index-cs3b557t.js} +792 -792
- package/package.json +1 -1
- package/src/client/components/common/batch-selection/BatchActionBar.tsx +8 -1
- package/src/client/components/common/session-actions-menu/index.tsx +23 -2
- package/src/client/components/layout/{RightActivityBar.test.ts → ActivityBar.test.ts} +5 -5
- package/src/client/components/layout/{RightActivityBar.tsx → ActivityBar.tsx} +87 -52
- package/src/client/components/layout/LeftActivityBar.test.ts +201 -0
- package/src/client/components/layout/LeftDock.tsx +119 -0
- package/src/client/components/layout/desktop-layout/LeftColumn.tsx +172 -0
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -3
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +22 -5
- package/src/client/components/layout/desktop-layout/index.tsx +88 -61
- package/src/client/components/layout/desktop-layout/left-column.test.tsx +192 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +6 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +9 -0
- package/src/client/components/layout/session-sidebar/Header.tsx +2 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +42 -6
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/{index.tsx → SessionsView.tsx} +48 -72
- package/src/client/components/layout/session-sidebar/new-session-context.test.ts +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +4 -0
- package/src/client/components/mobile/RightSidebar.tsx +30 -36
- package/src/client/components/mobile/SessionSidebar.tsx +124 -101
- package/src/client/components/mobile/mobile-main-view/Header.tsx +3 -2
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -2
- package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +3 -2
- package/src/client/components/mobile/mobile-session-sidebar/Dialogs.tsx +106 -0
- package/src/client/components/mobile/mobile-session-sidebar/Dock.tsx +80 -0
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +11 -5
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +34 -44
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +9 -1
- package/src/client/components/mobile/mobile-session-sidebar/RevealRow.tsx +60 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +47 -14
- package/src/client/components/mobile/mobile-session-sidebar/Skeleton.tsx +2 -2
- package/src/client/components/mobile/mobile-session-sidebar/use-dock.ts +89 -0
- package/src/client/components/mobile/panel-chips.test.tsx +155 -0
- package/src/client/components/mobile/panel-chips.tsx +74 -0
- package/src/client/components/settings/categories/panel-plugins/InstalledList.tsx +2 -2
- package/src/client/components/settings/categories/panel-plugins/LeftDockSettings.tsx +268 -0
- package/src/client/components/settings/categories/panel-plugins/Section.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +69 -1
- package/src/client/components/workspace/chat-timeline/MessageItem.gutter.test.ts +118 -0
- package/src/client/components/workspace/chat-timeline/MessageItem.tsx +13 -3
- package/src/client/components/workspace/chat-timeline/MessageList.tsx +1 -0
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +106 -0
- package/src/client/components/workspace/chat-timeline/SessionSkeleton.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +4 -1
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +5 -2
- package/src/client/components/workspace/chat-timeline/ThinkingSection.tsx +6 -2
- package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/ToolCallingSection.test.ts +5 -5
- package/src/client/components/workspace/chat-timeline/ToolCallingSection.tsx +5 -2
- package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +25 -1
- package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +15 -10
- package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.test.ts +84 -0
- package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.tsx +185 -0
- package/src/client/components/workspace/chat-timeline/chat-input/compact.test.tsx +162 -0
- package/src/client/components/workspace/chat-timeline/chat-input/expansion.test.ts +22 -21
- package/src/client/components/workspace/chat-timeline/chat-input/expansion.ts +12 -20
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +54 -27
- package/src/client/components/workspace/chat-timeline/chat-input/props.ts +16 -0
- package/src/client/components/workspace/chat-timeline/index.tsx +15 -16
- package/src/client/components/workspace/editor/PluginRail.tsx +79 -0
- package/src/client/components/workspace/editor/index.tsx +1 -0
- package/src/client/components/workspace/editor/plugin-rail.test.tsx +126 -0
- package/src/client/components/workspace/model-dropdown/groups.test.ts +60 -0
- package/src/client/components/workspace/model-dropdown/groups.ts +7 -1
- package/src/client/components/workspace/model-dropdown/index.tsx +3 -3
- package/src/client/components/workspace/model-dropdown/trigger.test.ts +123 -0
- package/src/client/components/workspace/model-dropdown/use-catalog.ts +8 -2
- package/src/client/components/workspace/panels/builtin.tsx +8 -0
- package/src/client/components/workspace/plugin-panel/catalog.tsx +237 -0
- package/src/client/components/workspace/plugin-panel/resolve.tsx +19 -122
- package/src/client/hooks/chat/composer/compact.ts +119 -0
- package/src/client/hooks/chat/omp/pending-rows.ts +4 -0
- package/src/client/hooks/chat/omp/pending-sessions.ts +47 -0
- package/src/client/hooks/chat/omp/pending-sessions.waiting.test.ts +102 -0
- package/src/client/hooks/chat/omp/pending-waiting.ts +48 -0
- package/src/client/hooks/chat/omp/session-sidebar-controller.ts +29 -0
- package/src/client/hooks/chat/timeline/actions.ts +34 -35
- package/src/client/hooks/chat/timeline/composer-refs.ts +47 -0
- package/src/client/hooks/chat/timeline/enqueue-follow-up.ts +37 -0
- package/src/client/hooks/chat/timeline/index.ts +15 -19
- package/src/client/hooks/chat/timeline/local-commands.ts +29 -0
- package/src/client/hooks/chat/timeline/queue-rewind.test.ts +6 -0
- package/src/client/hooks/chat/timeline/run-identity.ts +45 -0
- package/src/client/hooks/chat/waiting/cancel.ts +34 -0
- package/src/client/hooks/chat/waiting/choice.ts +53 -0
- package/src/client/hooks/chat/waiting/dispatch.ts +91 -0
- package/src/client/hooks/chat/waiting/held.ts +42 -0
- package/src/client/hooks/chat/waiting/hold.test.ts +139 -0
- package/src/client/hooks/chat/waiting/hold.ts +156 -0
- package/src/client/hooks/chat/waiting/store.ts +100 -0
- package/src/client/hooks/ui/toasts.ts +12 -1
- package/src/client/hooks/workspace/left-dock.ts +132 -0
- package/src/client/hooks/workspace/panel-registry.ts +13 -0
- package/src/client/tailwind.css +22 -0
- package/src/server/db.server.test.ts +52 -1
- package/src/server/db.server.ts +49 -1
- package/src/server/index.ts +9 -0
- package/src/server/lib/assets/compressed-cache.server.test.ts +78 -0
- package/src/server/lib/assets/compressed-cache.server.ts +88 -0
- package/src/server/lib/db/schema.test.ts +1 -0
- package/src/server/lib/db/schema.ts +3 -0
- package/src/server/lib/fs/fs-path.test.ts +1 -1
- package/src/server/lib/omp/rpc/terminal-settle.ts +7 -0
- package/src/server/lib/omp/session/delete.server.ts +8 -0
- package/src/server/lib/omp/session/ownership.server.test.ts +45 -4
- package/src/server/lib/panels/registry.server.ts +2 -2
- package/src/server/lib/queue/delivery.server.ts +31 -39
- package/src/server/lib/queue/payload.ts +73 -0
- package/src/server/lib/realtime/signals.server.ts +10 -1
- package/src/server/lib/realtime/topics/catalog.server.ts +2 -0
- package/src/server/lib/realtime/topics/global.server.ts +5 -0
- package/src/server/lib/realtime/topics.server.ts +2 -0
- package/src/server/lib/util/with-timeout.ts +29 -0
- package/src/server/lib/waiting/row-clock.server.ts +37 -0
- package/src/server/lib/waiting/runtime.server.test.ts +278 -0
- package/src/server/lib/waiting/runtime.server.ts +313 -0
- package/src/server/lib/waiting/schema.server.ts +46 -0
- package/src/server/lib/waiting/settle.test.ts +84 -0
- package/src/server/lib/waiting/settle.ts +77 -0
- package/src/server/lib/waiting/store.server.test.ts +167 -0
- package/src/server/lib/waiting/store.server.ts +268 -0
- package/src/server/lib/waiting/sweep-trigger.ts +35 -0
- package/src/server/plugins/compress.ts +18 -6
- package/src/server/plugins/ssr.ts +16 -8
- package/src/server/plugins/static.test.ts +59 -0
- package/src/server/plugins/static.ts +56 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/waiting/bindings.ts +14 -0
- package/src/server/routes/waiting/index.ts +123 -0
- package/src/server/routes/waiting/routes.test.ts +121 -0
- package/src/shared/lib/chat/timeline/gutter.test.ts +37 -0
- package/src/shared/lib/chat/timeline/gutter.ts +37 -0
- package/src/shared/lib/realtime/protocol.ts +10 -0
- package/src/shared/lib/ui/titlebar.test.ts +124 -0
- package/src/shared/lib/ui/titlebar.ts +54 -0
- package/src/shared/lib/workspace/left-dock-width.ts +78 -0
- package/src/shared/lib/workspace/left-dock.test.ts +203 -0
- package/src/shared/lib/workspace/left-dock.ts +197 -0
- package/src/shared/lib/workspace/mobile-dock.test.ts +65 -0
- package/src/shared/lib/workspace/mobile-dock.ts +55 -0
- package/src/shared/lib/workspace/panel-ids.test.ts +59 -10
- package/src/shared/lib/workspace/panel-ids.ts +114 -26
- package/src/shared/lib/workspace/sidebar-width.ts +4 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/waiting.ts +54 -0
- package/src/shared/types/workspace.ts +8 -0
- package/src/test-support/isolated-db.ts +3 -4
- package/src/test-support/silent-socket.ts +69 -0
- package/dist/client/static/js/index-pdp5dwc8.css +0 -1
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
|
6
|
-
* keys widths by, and resolves a component from.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
/**
|
|
21
|
-
|
|
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
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
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
|
-
|
|
36
|
-
return
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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) ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|