@kabmo-labs/kabmo 0.3.0 → 0.4.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-8x5j8zrr.js → index-twsk4wfa.js} +791 -791
- 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/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/SessionItem.tsx +30 -5
- 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/RightSidebar.tsx +8 -5
- package/src/client/components/mobile/SessionSidebar.tsx +6 -0
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -3
- 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 +41 -13
- package/src/client/components/mobile/mobile-session-sidebar/Skeleton.tsx +2 -2
- 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 +14 -9
- 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 +154 -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/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/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/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/git-update.server.test.ts +18 -1
- 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/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/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/dist/client/static/js/index-pdp5dwc8.css +0 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The left column draws ONE separator, and its chrome spans the WHOLE column.
|
|
8
|
+
*
|
|
9
|
+
* Both `LeftColumn` and `DesktopLayout` rendered a `ResizeHandle` after the
|
|
10
|
+
* dock, and both were mounted — so the column boundary drew two hairlines 3px
|
|
11
|
+
* apart and a reader saw a double rule between the session list and the chat.
|
|
12
|
+
* It was reproduced only when the rail was ON, because that is the layout a
|
|
13
|
+
* screenshotted report came from, though the extra handle was there in both
|
|
14
|
+
* states.
|
|
15
|
+
*
|
|
16
|
+
* A geometry test cannot catch the separator (happy-dom computes no layout), so
|
|
17
|
+
* that assertion is on the STRUCTURE: the group's direct children, in order. The
|
|
18
|
+
* second handle was a sibling — `LeftColumn`'s own sits between the dock and
|
|
19
|
+
* whatever follows — which is exactly what an order comparison shows.
|
|
20
|
+
*
|
|
21
|
+
* The chrome half is structural too, and both cases were reported. The strip
|
|
22
|
+
* lived inside the dock BODY while the rail was a sibling panel, so with the
|
|
23
|
+
* rail on it stopped at the body's left edge and left an empty strip under the
|
|
24
|
+
* rail. And the rail started at the column's top, running up past the title row
|
|
25
|
+
* and under the app's own titlebar. The column is three ROWS — title, then
|
|
26
|
+
* (rail | view), then strip — so both are asserted by the row each one lives in.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
30
|
+
import { Window } from 'happy-dom';
|
|
31
|
+
import { h, render } from 'preact';
|
|
32
|
+
import { act } from 'preact/test-utils';
|
|
33
|
+
import { Group } from '@/client/components/layout/desktop-layout/resizer';
|
|
34
|
+
import { LeftColumn } from '@/client/components/layout/desktop-layout/LeftColumn';
|
|
35
|
+
import { SessionStateProvider } from '@/client/components/common/session-state-provider/index';
|
|
36
|
+
import { primeKabmoSettings } from '@/shared/lib/settings/client';
|
|
37
|
+
import { realtimeClient, resetRealtimeClient } from '@/shared/lib/realtime/client';
|
|
38
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
39
|
+
|
|
40
|
+
let container: HTMLElement | null = null;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A socket that never dials.
|
|
44
|
+
*
|
|
45
|
+
* The rail subscribes to the Source Control topic on mount. Left to a REAL
|
|
46
|
+
* socket, its close lands after this file has restored the globals and surfaces
|
|
47
|
+
* as `Unhandled error. (ErrorEvent …)` attributed to whichever suite ran next —
|
|
48
|
+
* measured. This column's subject is its own structure, so the channel is
|
|
49
|
+
* silenced rather than served.
|
|
50
|
+
*/
|
|
51
|
+
class SilentSocket {
|
|
52
|
+
static readonly CLOSING = 2;
|
|
53
|
+
static readonly CLOSED = 3;
|
|
54
|
+
readyState = SilentSocket.CLOSED;
|
|
55
|
+
onopen: (() => void) | null = null;
|
|
56
|
+
onclose: (() => void) | null = null;
|
|
57
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
58
|
+
onerror: (() => void) | null = null;
|
|
59
|
+
send(): void {}
|
|
60
|
+
close(): void {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
installDomGlobals(new Window({ url: 'http://127.0.0.1:1' }));
|
|
65
|
+
realtimeClient.setSocketConstructor(SilentSocket as unknown as typeof WebSocket);
|
|
66
|
+
primeKabmoSettings({});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterAll(() => {
|
|
70
|
+
realtimeClient.setSocketConstructor(null);
|
|
71
|
+
restoreDomGlobals();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
// The rail's Source Control dot subscribes to a realtime topic, so the render
|
|
76
|
+
// opens a socket. Left open, its close lands in the NEXT file's globals once
|
|
77
|
+
// this one has restored them — measured as `Unhandled error. (ErrorEvent …)`
|
|
78
|
+
// between tests, attributed to whichever suite happened to run next.
|
|
79
|
+
resetRealtimeClient();
|
|
80
|
+
if (container) {
|
|
81
|
+
render(null, container);
|
|
82
|
+
container.remove();
|
|
83
|
+
container = null;
|
|
84
|
+
}
|
|
85
|
+
primeKabmoSettings({});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
async function mountColumn(railEnabled: boolean) {
|
|
89
|
+
const host = document.createElement('div');
|
|
90
|
+
document.body.appendChild(host);
|
|
91
|
+
container = host;
|
|
92
|
+
|
|
93
|
+
await act(async () => {
|
|
94
|
+
render(
|
|
95
|
+
h(SessionStateProvider, {
|
|
96
|
+
sessionId: 'new-separator',
|
|
97
|
+
children: h(Group, {
|
|
98
|
+
orientation: 'horizontal',
|
|
99
|
+
id: 'kabmo-main',
|
|
100
|
+
children: [
|
|
101
|
+
h(LeftColumn, {
|
|
102
|
+
panelRef: { current: null },
|
|
103
|
+
open: true,
|
|
104
|
+
railEnabled,
|
|
105
|
+
view: 'sessions',
|
|
106
|
+
onSelectView: () => {},
|
|
107
|
+
onCollapse: () => {},
|
|
108
|
+
width: 280,
|
|
109
|
+
minWidth: 264,
|
|
110
|
+
hasActiveContext: true,
|
|
111
|
+
refreshKey: 0,
|
|
112
|
+
onRefresh: () => {},
|
|
113
|
+
onOpenFile: () => {},
|
|
114
|
+
}),
|
|
115
|
+
h('div', { 'data-panel-id': 'main-right-stack' }),
|
|
116
|
+
],
|
|
117
|
+
}),
|
|
118
|
+
}),
|
|
119
|
+
host,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
// The dock's view is `lazy()`, so the boundary matters before it resolves:
|
|
123
|
+
// the separator is a SIBLING of the panel, not part of its content.
|
|
124
|
+
for (let i = 0; i < 5; i += 1) await act(async () => {});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The group's DIRECT children, in order — a separator appears once per entry. */
|
|
128
|
+
const children = (): string[] =>
|
|
129
|
+
[...(container!.querySelector('[data-panel-group-id="kabmo-main"]')?.children ?? [])].map((child) =>
|
|
130
|
+
child.hasAttribute('data-separator') ? 'separator' : (child.getAttribute('data-panel-id') ?? child.tagName),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
describe('left column separators', () => {
|
|
134
|
+
test('one separator with the rail on', async () => {
|
|
135
|
+
await mountColumn(true);
|
|
136
|
+
expect(children().filter((entry) => entry === 'separator')).toHaveLength(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('one separator with the rail off', async () => {
|
|
140
|
+
await mountColumn(false);
|
|
141
|
+
expect(children().filter((entry) => entry === 'separator')).toHaveLength(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('the separator is the column boundary — after the dock, not before it', async () => {
|
|
145
|
+
await mountColumn(true);
|
|
146
|
+
const kids = children();
|
|
147
|
+
expect(kids.at(-1)).toBe('main-right-stack');
|
|
148
|
+
expect(kids.at(-2)).toBe('separator');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('left column chrome', () => {
|
|
153
|
+
const panel = () => container!.querySelector('[data-panel-id="left-panel"]') as HTMLElement;
|
|
154
|
+
const rail = () => panel().querySelector('nav[aria-label="Left panel views"]');
|
|
155
|
+
const title = () => panel().querySelector('.h-10.flex-shrink-0.flex.items-center.justify-between');
|
|
156
|
+
const footer = () => panel().querySelector('button[aria-label="Settings"]');
|
|
157
|
+
|
|
158
|
+
test('the title row, the rail and the strip are three rows, in that order', async () => {
|
|
159
|
+
await mountColumn(true);
|
|
160
|
+
const column = panel().firstElementChild as HTMLElement;
|
|
161
|
+
const rows = [...column.children];
|
|
162
|
+
expect(rows).toHaveLength(3);
|
|
163
|
+
// Row 0 is the title, row 1 holds the rail beside the view, row 2 the strip.
|
|
164
|
+
expect(rows[0].contains(title())).toBe(true);
|
|
165
|
+
expect(rows[1].contains(rail())).toBe(true);
|
|
166
|
+
expect(rows[2].contains(footer())).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('the rail starts BELOW the title, never above it', async () => {
|
|
170
|
+
await mountColumn(true);
|
|
171
|
+
const column = panel().firstElementChild as HTMLElement;
|
|
172
|
+
expect(column.children[0].contains(rail())).toBe(false);
|
|
173
|
+
expect(column.children[1].contains(rail())).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('the strip spans the column, so the rail does not outlive it', async () => {
|
|
177
|
+
await mountColumn(true);
|
|
178
|
+
const column = panel().firstElementChild as HTMLElement;
|
|
179
|
+
expect(column.children[2].contains(rail())).toBe(false);
|
|
180
|
+
expect(column.children[2].contains(footer())).toBe(true);
|
|
181
|
+
// Same row container for title and strip: both are full-width rows of the
|
|
182
|
+
// column, which is what makes the strip run under the rail too.
|
|
183
|
+
expect(column.children[0].parentElement).toBe(column.children[2].parentElement);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('the strip survives a view that is not the session list', async () => {
|
|
187
|
+
await mountColumn(true);
|
|
188
|
+
// The column's furniture is rendered outside `LeftDockView` entirely, so it
|
|
189
|
+
// is present even while the lazy view is still resolving.
|
|
190
|
+
expect(footer()).not.toBeNull();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -126,9 +126,15 @@ export function CategoryHeader({
|
|
|
126
126
|
a checkbox is a second thing the click could mean. */}
|
|
127
127
|
{!batchActive && (
|
|
128
128
|
<div className="flex items-center space-x-0.5 pl-1">
|
|
129
|
+
{/* `aria-label` as well as `title`, like the phone's row: the button
|
|
130
|
+
is icon-only, so `title` alone leaves it unnamed to assistive tech
|
|
131
|
+
and to `getByRole` — and naming the FOLDER is what tells this
|
|
132
|
+
button apart from the toolbar's own New Session, which inherits
|
|
133
|
+
the current session's folder instead of naming one. */}
|
|
129
134
|
<button
|
|
130
135
|
type="button"
|
|
131
136
|
title="New Session"
|
|
137
|
+
aria-label={`New session in ${folder.name}`}
|
|
132
138
|
className="w-5 h-5 flex items-center justify-center text-ink/40 hover:text-ink hover:bg-ink/10 rounded cursor-pointer transition-colors"
|
|
133
139
|
onClick={(e) => { e.stopPropagation(); onNewSession(); }}
|
|
134
140
|
>
|
|
@@ -30,6 +30,7 @@ export function Category({
|
|
|
30
30
|
onToast,
|
|
31
31
|
batch,
|
|
32
32
|
onStartSessionSelection,
|
|
33
|
+
onCancelWait,
|
|
33
34
|
}: {
|
|
34
35
|
folder: WorkspaceFolderData;
|
|
35
36
|
activeSessionId: number | string | null;
|
|
@@ -43,6 +44,8 @@ export function Category({
|
|
|
43
44
|
batch?: BatchBinding;
|
|
44
45
|
/** Start a session selection scoped to this workspace, from its `⋯` menu. */
|
|
45
46
|
onStartSessionSelection?: (folderId: number) => void;
|
|
47
|
+
/** Drop the held prompt of one row (`waitingFor.waitId`). */
|
|
48
|
+
onCancelWait?: (waitId: string) => void;
|
|
46
49
|
}) {
|
|
47
50
|
const [isOpen, setIsOpen] = useState(folder.isExpanded || false);
|
|
48
51
|
const [showMenu, setShowMenu] = useState(false);
|
|
@@ -226,6 +229,12 @@ export function Category({
|
|
|
226
229
|
hasSubagents={hasSubagents}
|
|
227
230
|
isExpanded={isExpanded}
|
|
228
231
|
onToggleExpand={() => handleToggleSessionExpand(sessionKey)}
|
|
232
|
+
waitingFor={session.waitingFor ? { title: session.waitingFor.title } : undefined}
|
|
233
|
+
onCancelWait={
|
|
234
|
+
session.waitingFor
|
|
235
|
+
? () => onCancelWait?.(session.waitingFor!.waitId)
|
|
236
|
+
: undefined
|
|
237
|
+
}
|
|
229
238
|
selectable={sessionCheckboxes}
|
|
230
239
|
selected={Boolean(batch?.selected.includes(sessionKey))}
|
|
231
240
|
onToggleSelect={batch ? () => batch.toggle(sessionKey) : undefined}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TargetedMouseEvent } from 'preact';
|
|
2
|
-
import { Check, ChevronDown, ChevronRight, CircleQuestionMark, Loader2, MoreHorizontal } from 'lucide-preact';
|
|
2
|
+
import { Check, ChevronDown, ChevronRight, CircleQuestionMark, Hourglass, Loader2, MoreHorizontal } from 'lucide-preact';
|
|
3
3
|
import { useInlineRename } from '@/client/hooks/ui/inline-rename';
|
|
4
4
|
import { useSessionRenaming } from '@/client/hooks/workspace/renaming-sessions';
|
|
5
5
|
import { SessionActionsMenu, useRowMenu } from '@/client/components/common/session-actions-menu';
|
|
@@ -30,6 +30,14 @@ export interface SessionItemProps {
|
|
|
30
30
|
isExpanded?: boolean;
|
|
31
31
|
hasSubagents?: boolean;
|
|
32
32
|
onToggleExpand?: (e: TargetedMouseEvent<HTMLElement>) => void;
|
|
33
|
+
/**
|
|
34
|
+
* The row's prompt is held until another session finishes. A pending row has
|
|
35
|
+
* no transcript and no omp id, so this cue is the only thing on screen saying
|
|
36
|
+
* the chat exists and what it is waiting for.
|
|
37
|
+
*/
|
|
38
|
+
waitingFor?: { title: string | null };
|
|
39
|
+
/** Drop this row's held prompt, from the row's own menu. */
|
|
40
|
+
onCancelWait?: () => void;
|
|
33
41
|
/** Draw a checkbox and let the row's click toggle the selection instead of
|
|
34
42
|
* opening the session (batch mode). */
|
|
35
43
|
selectable?: boolean;
|
|
@@ -54,6 +62,8 @@ export function SessionItem({
|
|
|
54
62
|
isExpanded = false,
|
|
55
63
|
hasSubagents = false,
|
|
56
64
|
onToggleExpand,
|
|
65
|
+
waitingFor,
|
|
66
|
+
onCancelWait,
|
|
57
67
|
selectable = false,
|
|
58
68
|
selected = false,
|
|
59
69
|
onToggleSelect,
|
|
@@ -70,7 +80,8 @@ export function SessionItem({
|
|
|
70
80
|
// or expanded: it is the only affordance left worth showing, and making it
|
|
71
81
|
// hover-only would leave the roster reachable only by hovering exactly the
|
|
72
82
|
// right 16px.
|
|
73
|
-
const
|
|
83
|
+
const waiting = Boolean(waitingFor);
|
|
84
|
+
const liveRun = awaitingInput || status === 'stream' || waiting;
|
|
74
85
|
const chevronPinned = showChevron && !liveRun && !renaming;
|
|
75
86
|
const showStatus = Boolean(renaming || awaitingInput || status) && !chevronPinned;
|
|
76
87
|
const {
|
|
@@ -84,7 +95,7 @@ export function SessionItem({
|
|
|
84
95
|
} = useInlineRename(title, onRename);
|
|
85
96
|
// One trigger for every row action; also reachable by right-clicking the row.
|
|
86
97
|
const menu = useRowMenu();
|
|
87
|
-
const hasActions = Boolean(onRename || onArchive || onDelete);
|
|
98
|
+
const hasActions = Boolean(onRename || onArchive || onDelete || onCancelWait);
|
|
88
99
|
|
|
89
100
|
return (
|
|
90
101
|
<>
|
|
@@ -163,6 +174,10 @@ export function SessionItem({
|
|
|
163
174
|
</span>
|
|
164
175
|
) : awaitingInput ? (
|
|
165
176
|
<CircleQuestionMark size={12} className="animate-pulse text-ink/80" />
|
|
177
|
+
) : waiting ? (
|
|
178
|
+
<span role="status" aria-label={`Waiting for ${waitingFor?.title ?? 'another session'}`}>
|
|
179
|
+
<Hourglass size={12} className="animate-pulse text-ink/60" />
|
|
180
|
+
</span>
|
|
166
181
|
) : status === 'stream' ? (
|
|
167
182
|
<Loader2 size={12} className="animate-spin" />
|
|
168
183
|
) : (
|
|
@@ -190,8 +205,17 @@ export function SessionItem({
|
|
|
190
205
|
className="flex-1 min-w-0 bg-paper border border-ink/25 rounded px-1 py-0.5 text-xs text-ink outline-none focus:border-ink/50"
|
|
191
206
|
/>
|
|
192
207
|
) : (
|
|
193
|
-
<span className="flex-1 min-w-0
|
|
194
|
-
|
|
208
|
+
<span className="flex-1 min-w-0 flex flex-col leading-snug">
|
|
209
|
+
<span className="truncate">
|
|
210
|
+
{title.charAt(0).toUpperCase() + title.slice(1)}
|
|
211
|
+
</span>
|
|
212
|
+
{/* One muted line under the title: the row is a real chat, and this
|
|
213
|
+
is why it has not started yet. */}
|
|
214
|
+
{waiting && (
|
|
215
|
+
<span className="truncate text-[10px] text-ink/40 font-normal">
|
|
216
|
+
waiting for {waitingFor?.title ?? 'another session'}
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
195
219
|
</span>
|
|
196
220
|
)}
|
|
197
221
|
|
|
@@ -234,6 +258,7 @@ export function SessionItem({
|
|
|
234
258
|
onRenameWithAi={onRenameWithAi}
|
|
235
259
|
onArchive={onArchive}
|
|
236
260
|
onDelete={onDelete}
|
|
261
|
+
onCancelWait={onCancelWait}
|
|
237
262
|
onClose={menu.close}
|
|
238
263
|
/>
|
|
239
264
|
)}
|
|
@@ -15,6 +15,8 @@ interface SessionSidebarSessionListProps {
|
|
|
15
15
|
/** Present only while a batch selection is active. */
|
|
16
16
|
batch?: BatchBinding;
|
|
17
17
|
onStartSessionSelection?: (folderId: number) => void;
|
|
18
|
+
/** Drop a held prompt from its row's menu. */
|
|
19
|
+
onCancelWait?: (waitId: string) => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function SessionSidebarSessionList({
|
|
@@ -28,6 +30,7 @@ export function SessionSidebarSessionList({
|
|
|
28
30
|
onToast,
|
|
29
31
|
batch,
|
|
30
32
|
onStartSessionSelection,
|
|
33
|
+
onCancelWait,
|
|
31
34
|
}: SessionSidebarSessionListProps) {
|
|
32
35
|
// No fade wiring here: the app-wide scrollbar rule reveals this list's bar
|
|
33
36
|
// from the document's own capture-phase listener (`lib/scrollbar-activity`),
|
|
@@ -52,6 +55,7 @@ export function SessionSidebarSessionList({
|
|
|
52
55
|
onToast={onToast}
|
|
53
56
|
batch={batch}
|
|
54
57
|
onStartSessionSelection={onStartSessionSelection}
|
|
58
|
+
onCancelWait={onCancelWait}
|
|
55
59
|
/>
|
|
56
60
|
))
|
|
57
61
|
)}
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { useState } from 'preact/hooks';
|
|
2
2
|
import { useSearchParams } from '@/client/lib/router/search-params';
|
|
3
|
-
import {
|
|
3
|
+
import { NewWorkspaceModal } from '@/client/components/layout/session-sidebar/Modals';
|
|
4
4
|
import { SchedulerModal } from '@/client/components/layout/session-sidebar/scheduler-modal';
|
|
5
|
-
import { UpdatePopup } from '@/client/components/layout/update-popup';
|
|
6
|
-
import { SessionSidebarHeader } from '@/client/components/layout/session-sidebar/Header';
|
|
7
5
|
import { SessionSidebarToolbar } from '@/client/components/layout/session-sidebar/Toolbar';
|
|
8
|
-
import { SessionSidebarFooter } from '@/client/components/layout/session-sidebar/Footer';
|
|
9
|
-
import { useAndroidDownload } from '@/client/hooks/ui/updates/android-download';
|
|
10
6
|
import { SessionSidebarSessionList } from '@/client/components/layout/session-sidebar/SessionList';
|
|
11
7
|
import { SessionListSkeleton } from '@/client/components/layout/session-sidebar/Skeleton';
|
|
12
8
|
import { ToastStack } from '@/client/components/common/ToastStack';
|
|
13
9
|
import { spawnCwdForNewSession, triggerSessionPrewarm } from '@/shared/lib/omp/session/prewarm';
|
|
14
|
-
import { useKabmoEvent } from '@/client/hooks/ui/window-event';
|
|
15
10
|
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
16
|
-
import { useUpdates } from '@/client/hooks/ui/updates';
|
|
17
|
-
import { UPDATE_REQUEST_EVENT } from '@/shared/lib/updates/popup-state';
|
|
18
11
|
import { useSessionSidebarController } from '@/client/hooks/chat/omp/session-sidebar-controller';
|
|
12
|
+
import { useCancelWait } from '@/client/hooks/chat/waiting/cancel';
|
|
13
|
+
import { useWaitingDispatch } from '@/client/hooks/chat/waiting/dispatch';
|
|
19
14
|
import { startPendingSession } from '@/client/hooks/chat/omp/pending-sessions';
|
|
20
15
|
import { useScheduledTaskCount } from '@/client/hooks/workspace/scheduled-tasks';
|
|
21
16
|
import { cloneWorkspaceFolder, createWorkspaceFolder } from '@/client/hooks/workspace/new-workspace';
|
|
@@ -25,9 +20,37 @@ import { BatchActionBar } from '@/client/components/common/batch-selection/Batch
|
|
|
25
20
|
import { BatchDeleteModal } from '@/client/components/common/batch-selection/BatchDeleteModal';
|
|
26
21
|
import type { BatchBinding } from '@/client/components/common/batch-selection/types';
|
|
27
22
|
import { liveSelection, selectableSessionIds } from '@/shared/lib/workspace/batch-select';
|
|
28
|
-
import type { AgentStreamStatus } from '@/shared/lib/chat/omp/status';
|
|
29
23
|
|
|
30
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The session list — the left dock's own view.
|
|
26
|
+
*
|
|
27
|
+
* It used to BE the left column. The column is a dock now, and this is the view
|
|
28
|
+
* that fills it unless the user places a plugin panel there; the strip below it
|
|
29
|
+
* (Settings · About · the APK offer) is the DOCK's, not this view's, because
|
|
30
|
+
* those controls act on Kabmo rather than on the list.
|
|
31
|
+
*
|
|
32
|
+
* Three things are deliberately NOT here any more:
|
|
33
|
+
*
|
|
34
|
+
* - **The header.** It is the COLUMN's title row now, and it spans the rail too
|
|
35
|
+
* (see `LeftColumn`): a title that stopped at the dock body would leave the
|
|
36
|
+
* rail starting above it, with a gap under the app's own titlebar.
|
|
37
|
+
* - **The footer.** It is dock furniture, and a plugin view on the same dock
|
|
38
|
+
* must not take it away — moving to a plugin panel would otherwise lose the
|
|
39
|
+
* only visible way into Settings.
|
|
40
|
+
* - **The update popup and the About modal it hands over to.** Both are app
|
|
41
|
+
* level: they are about the console, not about one view, and the dock renders
|
|
42
|
+
* them so they survive a view switch.
|
|
43
|
+
*
|
|
44
|
+
* Everything else — the header's collapse control, the toolbar, the batch bar,
|
|
45
|
+
* the workspace and scheduler dialogs — belongs to the list and stays.
|
|
46
|
+
*/
|
|
47
|
+
export function SessionsView({
|
|
48
|
+
className = '',
|
|
49
|
+
appSettings = {},
|
|
50
|
+
}: {
|
|
51
|
+
className?: string;
|
|
52
|
+
appSettings?: Record<string, any>;
|
|
53
|
+
}) {
|
|
31
54
|
const {
|
|
32
55
|
folders,
|
|
33
56
|
initializing,
|
|
@@ -49,23 +72,14 @@ export function SessionSidebar({ className = '', onClose, appSettings = {}, stre
|
|
|
49
72
|
} = useSessionSidebarController(appSettings, { expandSearchMatches: true });
|
|
50
73
|
const [, setSearchParams] = useSearchParams();
|
|
51
74
|
|
|
52
|
-
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
53
|
-
const [infoOpen, setInfoOpen] = useState(false);
|
|
54
|
-
|
|
55
|
-
// Modals for new workspace and scheduler
|
|
56
75
|
const [newWorkspaceOpen, setNewWorkspaceOpen] = useState(false);
|
|
57
76
|
const [schedulerOpen, setSchedulerOpen] = useState(false);
|
|
58
77
|
const scheduledCount = useScheduledTaskCount();
|
|
59
|
-
const updates = useUpdates();
|
|
60
|
-
// The Android offer: an UPDATE inside the APK, an INSTALL on an Android
|
|
61
|
-
// browser, and nothing anywhere else — see `useAndroidDownload`.
|
|
62
|
-
const androidDownload = useAndroidDownload(
|
|
63
|
-
window.__OMP_BOOTSTRAP__?.shellVersion,
|
|
64
|
-
window.__OMP_BOOTSTRAP__?.isAndroid,
|
|
65
|
-
);
|
|
66
78
|
const { toasts, pushToast, dismissToast } = useToasts();
|
|
79
|
+
const cancelWait = useCancelWait(pushToast);
|
|
80
|
+
// Follows a held prompt to its session — in whichever tab or instance sent it.
|
|
81
|
+
useWaitingDispatch({ onNotice: pushToast });
|
|
67
82
|
|
|
68
|
-
// Sidebar inline states
|
|
69
83
|
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
|
70
84
|
|
|
71
85
|
// Batch selection: two separate modes (sessions from a workspace's `⋯` menu,
|
|
@@ -105,20 +119,6 @@ export function SessionSidebar({ className = '', onClose, appSettings = {}, stre
|
|
|
105
119
|
else batch.begin('workspace');
|
|
106
120
|
};
|
|
107
121
|
|
|
108
|
-
// The popup's "Update now" hands the run over instead of starting its own: the
|
|
109
|
-
// About modal is where an update has always been driven from and it owns the
|
|
110
|
-
// live output, so this opens it and starts the run it will draw. The popup
|
|
111
|
-
// dispatches this only after it has closed, so the two dialogs never stack.
|
|
112
|
-
useKabmoEvent(UPDATE_REQUEST_EVENT, () => {
|
|
113
|
-
setInfoOpen(true);
|
|
114
|
-
void updates.apply('kabmo').then((result) => {
|
|
115
|
-
// A null result means the run was aborted with the page; there is nobody
|
|
116
|
-
// left to tell. A `manual` answer is the command to run, not a success.
|
|
117
|
-
if (!result) return;
|
|
118
|
-
pushToast(result.message, result.success && !result.manual ? 'success' : 'error');
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
122
|
/** The folder whose context a new session will inherit — same resolution
|
|
123
123
|
* the send path uses, so the prewarmed cwd matches the spawn cwd. */
|
|
124
124
|
const prewarmForNewSession = (folderId?: number) => {
|
|
@@ -137,12 +137,15 @@ export function SessionSidebar({ className = '', onClose, appSettings = {}, stre
|
|
|
137
137
|
const currentFolder = currentSessionId
|
|
138
138
|
? folders.find(f => f.sessions?.some((s) => String(s.id) === String(currentSessionId)))
|
|
139
139
|
: undefined;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
// The folder the chat will run in: the active session's, else the one the
|
|
141
|
+
// URL already names. Dropping the URL's own `folderId` (the previous
|
|
142
|
+
// `else next.delete`) left a chat opened with `?folderId=` with NO context
|
|
143
|
+
// — its composer then stayed disabled ("Please select a workspace above")
|
|
144
|
+
// and the first send never happened.
|
|
145
|
+
const contextFolderId = currentFolder?.id ?? (prev.get('folderId') ? Number(prev.get('folderId')) : null);
|
|
146
|
+
next.set('sessionId', startPendingSession(contextFolderId));
|
|
147
|
+
if (contextFolderId != null) next.set('folderId', String(contextFolderId));
|
|
148
|
+
else next.delete('folderId');
|
|
146
149
|
return next;
|
|
147
150
|
}, { replace: true });
|
|
148
151
|
};
|
|
@@ -159,9 +162,7 @@ export function SessionSidebar({ className = '', onClose, appSettings = {}, stre
|
|
|
159
162
|
|
|
160
163
|
return (
|
|
161
164
|
<>
|
|
162
|
-
<
|
|
163
|
-
<SessionSidebarHeader streamStatus={streamStatus} onCollapse={onClose} />
|
|
164
|
-
|
|
165
|
+
<div className={`flex flex-col h-full ${className}`}>
|
|
165
166
|
<SessionSidebarToolbar
|
|
166
167
|
isSearchVisible={isSearchVisible}
|
|
167
168
|
searchQuery={searchQuery}
|
|
@@ -199,6 +200,7 @@ export function SessionSidebar({ className = '', onClose, appSettings = {}, stre
|
|
|
199
200
|
onToast={pushToast}
|
|
200
201
|
batch={batchBinding}
|
|
201
202
|
onStartSessionSelection={(folderId) => batch.begin('session', folderId)}
|
|
203
|
+
onCancelWait={cancelWait}
|
|
202
204
|
/>
|
|
203
205
|
)}
|
|
204
206
|
|
|
@@ -215,33 +217,7 @@ export function SessionSidebar({ className = '', onClose, appSettings = {}, stre
|
|
|
215
217
|
onCancel={batch.end}
|
|
216
218
|
/>
|
|
217
219
|
)}
|
|
218
|
-
|
|
219
|
-
<SessionSidebarFooter
|
|
220
|
-
onSettings={() => setSettingsOpen(true)}
|
|
221
|
-
onInfo={() => setInfoOpen(true)}
|
|
222
|
-
androidDownload={androidDownload}
|
|
223
|
-
updateAvailable={updates.hasUpdate}
|
|
224
|
-
onUpdateClick={() => {
|
|
225
|
-
setInfoOpen(true);
|
|
226
|
-
void updates.check();
|
|
227
|
-
}}
|
|
228
|
-
/>
|
|
229
|
-
</aside>
|
|
230
|
-
|
|
231
|
-
{/* Settings Modal */}
|
|
232
|
-
<SettingsModal isOpen={settingsOpen} onClose={() => setSettingsOpen(false)} appSettings={appSettings} />
|
|
233
|
-
|
|
234
|
-
{/* Info Modal */}
|
|
235
|
-
<AboutModal
|
|
236
|
-
isOpen={infoOpen}
|
|
237
|
-
onClose={() => setInfoOpen(false)}
|
|
238
|
-
updates={updates}
|
|
239
|
-
onToast={pushToast}
|
|
240
|
-
/>
|
|
241
|
-
|
|
242
|
-
{/* "Update available" announcement — once per version, gated on the check.
|
|
243
|
-
Its "Update now" hands the run to the About modal below. */}
|
|
244
|
-
<UpdatePopup updates={updates} />
|
|
220
|
+
</div>
|
|
245
221
|
|
|
246
222
|
{/* Batch delete confirmation. It stays open on a refusal so the reasons
|
|
247
223
|
can be read; a fully successful run closes itself and leaves the mode. */}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* New Session keeps the chat's FOLDER context.
|
|
8
|
+
*
|
|
9
|
+
* The bug this pins was reported as "the composer is disabled in a new chat":
|
|
10
|
+
* opening the app with `?folderId=…` and pressing New Session used to DELETE the
|
|
11
|
+
* `folderId` param (its branch only restored the folder of the session on
|
|
12
|
+
* screen, and there was none yet). The pending chat then had no context, so the
|
|
13
|
+
* composer rendered "Please select a workspace above to start prompting…" and
|
|
14
|
+
* the first send never happened — measured through the browser layer, where
|
|
15
|
+
* `panels.e2e.ts` could not reach the commit box at all.
|
|
16
|
+
*
|
|
17
|
+
* The rule is one sentence, and it is what this file asserts: the folder a new
|
|
18
|
+
* chat inherits is the ACTIVE session's folder, else the one the URL already
|
|
19
|
+
* names. It is expressed here rather than by mounting the sidebar because the
|
|
20
|
+
* component is a controller with a dozen stores behind it; the branch itself is
|
|
21
|
+
* pure, and it is the branch that was wrong.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, expect, test } from 'bun:test';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The folder id a new chat is opened in, from what the sidebar knows.
|
|
28
|
+
*
|
|
29
|
+
* Mirrors `handleNewSession` in `session-sidebar/SessionsView.tsx`.
|
|
30
|
+
*/
|
|
31
|
+
function contextFolderFor(args: {
|
|
32
|
+
sessionFolderId?: number | null;
|
|
33
|
+
urlFolderId?: string | null;
|
|
34
|
+
}): number | null {
|
|
35
|
+
const { sessionFolderId = null, urlFolderId = null } = args;
|
|
36
|
+
if (sessionFolderId != null) return sessionFolderId;
|
|
37
|
+
return urlFolderId ? Number(urlFolderId) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('new chat folder context', () => {
|
|
41
|
+
test('an active session in a folder wins', () => {
|
|
42
|
+
expect(contextFolderFor({ sessionFolderId: 3, urlFolderId: '7' })).toBe(3);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a chat opened with ?folderId= keeps that folder", () => {
|
|
46
|
+
// The reported case: no session yet, so nothing to inherit — and the URL's
|
|
47
|
+
// own folder is the context the composer needs to be enabled.
|
|
48
|
+
expect(contextFolderFor({ urlFolderId: '7' })).toBe(7);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('no session and no folder is null, not NaN', () => {
|
|
52
|
+
expect(contextFolderFor({})).toBeNull();
|
|
53
|
+
expect(contextFolderFor({ urlFolderId: null })).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -7,6 +7,7 @@ import { useResolvedRepo } from '@/client/hooks/workspace/repo-scope';
|
|
|
7
7
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
8
8
|
import { PluginMark } from '@/client/components/settings/categories/panel-plugins/PluginMark';
|
|
9
9
|
import { PANEL_MARK_SIZE } from '@/shared/lib/workspace/right-panels';
|
|
10
|
+
import { SESSIONS_VIEW_ID } from '@/shared/lib/workspace/left-dock';
|
|
10
11
|
|
|
11
12
|
interface MobileRightSidebarProps {
|
|
12
13
|
enabled?: boolean;
|
|
@@ -43,11 +44,13 @@ export function MobileRightSidebar({
|
|
|
43
44
|
// the user never asked for a search. Restoring the tab they left also keeps
|
|
44
45
|
// the phone and the desktop on the same view.
|
|
45
46
|
const [activeTab, setActiveTab] = useSessionState<string>('layout.mobileRightPanel', 'files');
|
|
46
|
-
// ONE catalog, the same one the desktop
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
47
|
+
// ONE catalog, the same one the desktop bars read, and deliberately `all`:
|
|
48
|
+
// a phone has ONE drawer, so a plugin panel the user placed on the left must
|
|
49
|
+
// stay reachable here — reading only the right column would make placing a
|
|
50
|
+
// panel on the left hide it from every phone. The dock's own session list is
|
|
51
|
+
// filtered out below: the phone already has that as its own drawer screen,
|
|
52
|
+
// and a second copy inside this one would be two lists of the same sessions.
|
|
53
|
+
const catalog = usePanelCatalog('all').filter((panel) => panel.id !== SESSIONS_VIEW_ID);
|
|
51
54
|
const activePanel = catalog.find((panel) => panel.id === activeTab);
|
|
52
55
|
// Same source the desktop activity bar uses — the Source Control view's own
|
|
53
56
|
// repo pick, read shared so switching repos moves this dot too. Polls only
|
|
@@ -11,6 +11,8 @@ import { UpdatePopup } from '@/client/components/layout/update-popup';
|
|
|
11
11
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
12
12
|
import { useKabmoEvent } from '@/client/hooks/ui/window-event';
|
|
13
13
|
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
14
|
+
import { useCancelWait } from '@/client/hooks/chat/waiting/cancel';
|
|
15
|
+
import { useWaitingDispatch } from '@/client/hooks/chat/waiting/dispatch';
|
|
14
16
|
import { useUpdates } from '@/client/hooks/ui/updates';
|
|
15
17
|
import { UPDATE_REQUEST_EVENT } from '@/shared/lib/updates/popup-state';
|
|
16
18
|
import { useSessionSidebarController } from '@/client/hooks/chat/omp/session-sidebar-controller';
|
|
@@ -80,6 +82,8 @@ export function MobileSessionSidebar({
|
|
|
80
82
|
window.__OMP_BOOTSTRAP__?.isAndroid,
|
|
81
83
|
);
|
|
82
84
|
const { toasts, pushToast, dismissToast } = useToasts();
|
|
85
|
+
const cancelWait = useCancelWait(pushToast);
|
|
86
|
+
useWaitingDispatch({ onNotice: pushToast });
|
|
83
87
|
|
|
84
88
|
// The popup's "Update now" hands the run over rather than starting its own —
|
|
85
89
|
// see the desktop sidebar. The phone must also bring the drawer forward, or
|
|
@@ -229,11 +233,13 @@ export function MobileSessionSidebar({
|
|
|
229
233
|
onToast={pushToast}
|
|
230
234
|
batch={batchBinding}
|
|
231
235
|
onStartSessionSelection={(folderId) => batch.begin('session', folderId)}
|
|
236
|
+
onCancelWait={cancelWait}
|
|
232
237
|
/>
|
|
233
238
|
)}
|
|
234
239
|
|
|
235
240
|
{batch.mode && (
|
|
236
241
|
<BatchActionBar
|
|
242
|
+
drawer
|
|
237
243
|
mode={batch.mode}
|
|
238
244
|
count={batchSelected.length}
|
|
239
245
|
total={batchAvailable.length}
|