@kabmo-labs/kabmo 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +333 -333
- package/dist/client/static/js/index-pdp5dwc8.css +1 -0
- package/dist/client/static/js/{index-4vyzz0z4.js → index-zsskckw7.js} +681 -681
- package/package.json +2 -1
- package/public/kabmo-wordmark.svg +31 -0
- package/src/cli/lib/commands/update.js +17 -2
- package/src/cli/lib/commands/update.test.js +36 -0
- package/src/client/bootstrap.d.ts +8 -0
- package/src/client/components/auth/login-screen/index.test.tsx +93 -0
- package/src/client/components/auth/login-screen/index.tsx +43 -22
- package/src/client/components/auth/login-screen/password-toggle.test.tsx +121 -0
- package/src/client/components/common/batch-selection/BatchActionBar.test.ts +132 -0
- package/src/client/components/common/batch-selection/BatchActionBar.tsx +118 -0
- package/src/client/components/common/batch-selection/BatchDeleteModal.tsx +177 -0
- package/src/client/components/common/batch-selection/SelectionCheckbox.tsx +58 -0
- package/src/client/components/common/batch-selection/folder-state.test.ts +130 -0
- package/src/client/components/common/batch-selection/folder-state.ts +101 -0
- package/src/client/components/common/batch-selection/types.ts +42 -0
- package/src/client/components/common/brand-wordmark/Mark.tsx +62 -0
- package/src/client/components/common/brand-wordmark/index.test.tsx +29 -7
- package/src/client/components/common/brand-wordmark/index.tsx +33 -11
- package/src/client/components/common/brand-wordmark/wordmark-svg.test.ts +88 -0
- package/src/client/components/common/header-controls/MoreMenu.tsx +136 -0
- package/src/client/components/common/header-controls/index.test.tsx +77 -11
- package/src/client/components/common/header-controls/index.tsx +53 -4
- package/src/client/components/common/session-state-provider/index.test.ts +22 -1
- package/src/client/components/common/workspace-options-menu/index.tsx +25 -11
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +169 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +72 -91
- package/src/client/components/layout/session-sidebar/Footer.tsx +20 -2
- package/src/client/components/layout/session-sidebar/OverflowMenu.tsx +23 -5
- package/src/client/components/layout/session-sidebar/SessionItem.batch.test.ts +162 -0
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +35 -6
- package/src/client/components/layout/session-sidebar/SessionList.tsx +10 -1
- package/src/client/components/layout/session-sidebar/Toolbar.test.ts +2 -0
- package/src/client/components/layout/session-sidebar/Toolbar.tsx +30 -34
- package/src/client/components/layout/session-sidebar/index.tsx +92 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +32 -1
- package/src/client/components/mobile/SessionSidebar.tsx +83 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +27 -5
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +21 -1
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +56 -12
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +8 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +27 -6
- package/src/client/components/mobile/mobile-session-sidebar/Toolbar.test.ts +106 -0
- package/src/client/components/mobile/mobile-session-sidebar/Toolbar.tsx +178 -93
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +9 -0
- package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +6 -0
- package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +4 -1
- package/src/client/components/workspace/chat-timeline/chat-input/DropOverlay.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/chat-input/GoalBanner.tsx +6 -1
- package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +4 -1
- package/src/client/components/workspace/chat-timeline/chat-input/VoiceNotice.tsx +2 -2
- package/src/client/components/workspace/chat-timeline/chat-input/expansion.test.ts +72 -1
- package/src/client/components/workspace/chat-timeline/chat-input/expansion.ts +28 -4
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +20 -2
- package/src/client/components/workspace/chat-timeline/chat-input/props.ts +7 -0
- package/src/client/components/workspace/chat-timeline/index.tsx +9 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/OrphanAsks.test.tsx +127 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/OrphanAsks.tsx +71 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/PluginPendingInteractions.test.ts +1 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -2
- package/src/client/components/workspace/git-panel/BranchToolbar.tsx +4 -0
- package/src/client/components/workspace/git-panel/index.test.ts +15 -0
- package/src/client/components/workspace/search-panel/Body.test.ts +149 -0
- package/src/client/components/workspace/search-panel/Body.tsx +53 -9
- package/src/client/hooks/chat/timeline/ask-frames-grouped.test.ts +46 -2
- package/src/client/hooks/chat/timeline/ask-frames.test.ts +1 -1
- package/src/client/hooks/chat/timeline/ask-frames.ts +79 -29
- package/src/client/hooks/chat/timeline/index.ts +2 -1
- package/src/client/hooks/chat/timeline/scroll-probe.test-util.ts +227 -0
- package/src/client/hooks/chat/timeline/scroll-reading-back.test.ts +67 -0
- package/src/client/hooks/chat/timeline/scroll.test.ts +84 -226
- package/src/client/hooks/chat/timeline/scroll.ts +34 -1
- package/src/client/hooks/ui/media-query.ts +41 -0
- package/src/client/hooks/ui/updates/android-download.test.ts +73 -0
- package/src/client/hooks/ui/updates/android-download.ts +106 -0
- package/src/client/hooks/workspace/batch-delete-run.ts +95 -0
- package/src/client/hooks/workspace/batch-delete.ts +63 -0
- package/src/client/hooks/workspace/batch-selection.ts +98 -0
- package/src/client/hooks/workspace/file-tabs.test.ts +165 -0
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/session-state/index.ts +24 -1
- package/src/client/hooks/workspace/toolbar-row-width.ts +43 -0
- package/src/client/lib/native/back.test.ts +108 -0
- package/src/client/lib/native/back.ts +60 -0
- package/src/client/main.tsx +6 -0
- package/src/client/tailwind.css +6 -0
- package/src/server/lib/fs/git-actions-branch.ts +18 -1
- package/src/server/lib/fs/git-actions-sync.test.ts +144 -0
- package/src/server/lib/fs/git-publish.test.ts +96 -0
- package/src/server/lib/fs/git-publish.ts +35 -3
- package/src/server/lib/fs/listing.test.ts +86 -0
- package/src/server/lib/fs/listing.ts +20 -6
- package/src/server/lib/lifecycle/restart.ts +52 -14
- package/src/server/lib/omp/session/delete-flow.server.ts +101 -0
- package/src/server/lib/updates/android-release.test.ts +92 -0
- package/src/server/lib/updates/android-release.ts +148 -0
- package/src/server/lib/updates/apply.ts +8 -0
- package/src/server/lib/updates/install-steps.ts +81 -0
- package/src/server/lib/updates/install.ts +19 -67
- package/src/server/lib/updates/installed-cli.test.ts +222 -0
- package/src/server/lib/updates/installed-cli.ts +139 -0
- package/src/server/lib/updates/omp.ts +1 -1
- package/src/server/plugins/ssr.ts +9 -2
- package/src/server/routes/folders/batch-delete.ts +68 -0
- package/src/server/routes/folders/index.ts +4 -0
- package/src/server/routes/fs/read.ts +24 -9
- package/src/server/routes/sessions/batch-delete.test.ts +142 -0
- package/src/server/routes/sessions/batch-delete.ts +60 -0
- package/src/server/routes/sessions/delete.ts +13 -51
- package/src/server/routes/sessions/index.ts +5 -0
- package/src/server/routes/updates/android.ts +22 -0
- package/src/server/routes/updates/apply.ts +1 -1
- package/src/server/routes/updates/index.ts +2 -0
- package/src/shared/lib/updates/shell-ua.test.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +36 -0
- package/src/shared/lib/workspace/batch-select.test.ts +92 -0
- package/src/shared/lib/workspace/batch-select.ts +86 -0
- package/src/shared/lib/workspace/session-state/listeners.ts +38 -0
- package/src/shared/lib/workspace/session-state/replacement.test.ts +121 -0
- package/src/shared/lib/workspace/session-state/store.ts +13 -2
- package/src/shared/lib/workspace/sidebar-toolbar.test.ts +98 -0
- package/src/shared/lib/workspace/sidebar-toolbar.ts +84 -0
- package/dist/client/static/js/index-x9cehh1a.css +0 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* source of truth.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { FolderOpen, Pin, PinOff, Trash2 } from 'lucide-preact';
|
|
16
|
+
import { FolderOpen, ListChecks, Pin, PinOff, Trash2 } from 'lucide-preact';
|
|
17
17
|
|
|
18
18
|
interface WorkspaceOptionsMenuProps {
|
|
19
19
|
variant: 'desktop' | 'mobile';
|
|
@@ -21,6 +21,8 @@ interface WorkspaceOptionsMenuProps {
|
|
|
21
21
|
/** Reveal the workspace directory in the platform file manager. Omitted for
|
|
22
22
|
* an unbound folder, which has no directory of its own. */
|
|
23
23
|
onOpenFolder?: () => void;
|
|
24
|
+
/** Enter the session batch-selection mode scoped to this workspace. */
|
|
25
|
+
onSelectSessions?: () => void;
|
|
24
26
|
confirmDelete: boolean;
|
|
25
27
|
onPin: () => void;
|
|
26
28
|
onDelete: () => void;
|
|
@@ -32,6 +34,7 @@ export function WorkspaceOptionsMenu({
|
|
|
32
34
|
variant,
|
|
33
35
|
isPinned = false,
|
|
34
36
|
onOpenFolder,
|
|
37
|
+
onSelectSessions,
|
|
35
38
|
confirmDelete,
|
|
36
39
|
onPin,
|
|
37
40
|
onDelete,
|
|
@@ -62,6 +65,12 @@ export function WorkspaceOptionsMenu({
|
|
|
62
65
|
{isPinned ? <PinOff size={13} /> : <Pin size={13} />}
|
|
63
66
|
<span>{isPinned ? 'Unpin Workspace' : 'Pin Workspace'}</span>
|
|
64
67
|
</button>
|
|
68
|
+
{onSelectSessions && (
|
|
69
|
+
<button type="button" onClick={onSelectSessions} className="w-full text-left px-3 py-2 hover:bg-ink/5 text-ink/80 flex items-center space-x-2">
|
|
70
|
+
<ListChecks size={13} />
|
|
71
|
+
<span>Select sessions</span>
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
65
74
|
<button type="button" onClick={onRequestDelete} className="w-full text-left px-3 py-2 hover:bg-error/10 text-error flex items-center space-x-2">
|
|
66
75
|
<Trash2 size={13} />
|
|
67
76
|
<span>Delete Workspace</span>
|
|
@@ -73,27 +82,32 @@ export function WorkspaceOptionsMenu({
|
|
|
73
82
|
return confirmDelete ? (
|
|
74
83
|
<>
|
|
75
84
|
<div className="px-3 py-1.5 text-xs text-ink/80 font-medium">Delete workspace?</div>
|
|
76
|
-
<
|
|
85
|
+
<button type="button" onClick={onDelete} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-error">
|
|
77
86
|
<Trash2 size={12} /><span>Yes, delete</span>
|
|
78
|
-
</
|
|
79
|
-
<
|
|
87
|
+
</button>
|
|
88
|
+
<button type="button" onClick={onCancelDelete} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-ink/70">
|
|
80
89
|
<span>Cancel</span>
|
|
81
|
-
</
|
|
90
|
+
</button>
|
|
82
91
|
</>
|
|
83
92
|
) : (
|
|
84
93
|
<>
|
|
85
94
|
{onOpenFolder && (
|
|
86
|
-
<
|
|
95
|
+
<button type="button" onClick={onOpenFolder} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-ink/80">
|
|
87
96
|
<FolderOpen size={12} /><span>Go to Explorer</span>
|
|
88
|
-
</
|
|
97
|
+
</button>
|
|
89
98
|
)}
|
|
90
|
-
<
|
|
99
|
+
<button type="button" onClick={onPin} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-ink/80">
|
|
91
100
|
{isPinned ? <PinOff size={12} /> : <Pin size={12} />}
|
|
92
101
|
<span>{isPinned ? 'Unpin Workspace' : 'Pin Workspace'}</span>
|
|
93
|
-
</
|
|
94
|
-
|
|
102
|
+
</button>
|
|
103
|
+
{onSelectSessions && (
|
|
104
|
+
<button type="button" onClick={onSelectSessions} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-ink/80">
|
|
105
|
+
<ListChecks size={12} /><span>Select sessions</span>
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
<button type="button" onClick={onRequestDelete} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-error">
|
|
95
109
|
<Trash2 size={12} /><span>Delete Workspace</span>
|
|
96
|
-
</
|
|
110
|
+
</button>
|
|
97
111
|
</>
|
|
98
112
|
);
|
|
99
113
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A workspace's header row in the desktop sidebar: the disclosure mark, the
|
|
8
|
+
* folder's name, its two actions, and — while a batch selection is active — the
|
|
9
|
+
* checkbox that selects it (workspace mode) or all of its rows (session mode).
|
|
10
|
+
*
|
|
11
|
+
* Split out of `CategoryItem` because the row now has two modes' worth of
|
|
12
|
+
* markup and the parent is also the list. The checkbox rules stay in the parent
|
|
13
|
+
* (they are about the selection, not the drawing) and arrive as `checkbox`
|
|
14
|
+
* props; this file only decides how the two states look.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ChevronDown, ChevronRight, MoreHorizontal, Pin, Plus } from 'lucide-preact';
|
|
18
|
+
import { SelectionCheckbox } from '@/client/components/common/batch-selection/SelectionCheckbox';
|
|
19
|
+
import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-options-menu';
|
|
20
|
+
import { getProjectIcon } from '@/shared/lib/workspace/project-icon';
|
|
21
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
22
|
+
|
|
23
|
+
export interface CategoryHeaderCheckbox {
|
|
24
|
+
checked: boolean;
|
|
25
|
+
indeterminate: boolean;
|
|
26
|
+
label: string;
|
|
27
|
+
onChange: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CategoryHeaderProps {
|
|
31
|
+
folder: WorkspaceFolderData;
|
|
32
|
+
isOpen: boolean;
|
|
33
|
+
onToggleFolder: () => void;
|
|
34
|
+
/** Null when this workspace is not part of the active selection. */
|
|
35
|
+
checkbox: CategoryHeaderCheckbox | null;
|
|
36
|
+
/** True while a batch selection is active — actions are hidden then. */
|
|
37
|
+
batchActive: boolean;
|
|
38
|
+
menuRef: { current: HTMLDivElement | null };
|
|
39
|
+
showMenu: boolean;
|
|
40
|
+
onToggleMenu: () => void;
|
|
41
|
+
onNewSession: () => void;
|
|
42
|
+
onOpenFolder?: () => void;
|
|
43
|
+
onSelectSessions?: () => void;
|
|
44
|
+
confirmDelete: boolean;
|
|
45
|
+
onPin: () => void;
|
|
46
|
+
onDelete: () => void;
|
|
47
|
+
onRequestDelete: () => void;
|
|
48
|
+
onCancelDelete: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function CategoryHeader({
|
|
52
|
+
folder,
|
|
53
|
+
isOpen,
|
|
54
|
+
onToggleFolder,
|
|
55
|
+
checkbox,
|
|
56
|
+
batchActive,
|
|
57
|
+
menuRef,
|
|
58
|
+
showMenu,
|
|
59
|
+
onToggleMenu,
|
|
60
|
+
onNewSession,
|
|
61
|
+
onOpenFolder,
|
|
62
|
+
onSelectSessions,
|
|
63
|
+
confirmDelete,
|
|
64
|
+
onPin,
|
|
65
|
+
onDelete,
|
|
66
|
+
onRequestDelete,
|
|
67
|
+
onCancelDelete,
|
|
68
|
+
}: CategoryHeaderProps) {
|
|
69
|
+
const ProjectIcon = getProjectIcon(folder.icon);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="group flex items-center justify-between h-7 text-xs font-semibold text-ink px-2 hover:bg-ink/5 rounded-md transition-colors select-none">
|
|
73
|
+
<div
|
|
74
|
+
className="flex-1 h-full flex items-center cursor-pointer min-w-0"
|
|
75
|
+
onClick={batchActive ? undefined : onToggleFolder}
|
|
76
|
+
>
|
|
77
|
+
{checkbox ? (
|
|
78
|
+
<>
|
|
79
|
+
<SelectionCheckbox
|
|
80
|
+
size="md"
|
|
81
|
+
checked={checkbox.checked}
|
|
82
|
+
indeterminate={checkbox.indeterminate}
|
|
83
|
+
label={checkbox.label}
|
|
84
|
+
onChange={checkbox.onChange}
|
|
85
|
+
/>
|
|
86
|
+
<span className="w-2 shrink-0" />
|
|
87
|
+
</>
|
|
88
|
+
) : (
|
|
89
|
+
<>
|
|
90
|
+
{/* One 16px slot owns both glyphs, exactly like a session row's
|
|
91
|
+
status/chevron slot: the folder mark sits there at rest and hands
|
|
92
|
+
the slot to the disclosure chevron on hover, so the icon column
|
|
93
|
+
reads as one line AND the header says what clicking it does. */}
|
|
94
|
+
<span className="relative w-4 h-4 flex items-center justify-center shrink-0">
|
|
95
|
+
<span className="absolute inset-0 flex items-center justify-center transition-opacity group-hover:opacity-0 touch-hidden">
|
|
96
|
+
{folder.customIconUrl ? (
|
|
97
|
+
<img
|
|
98
|
+
src={folder.customIconUrl}
|
|
99
|
+
alt=""
|
|
100
|
+
className="w-[15px] h-[15px] rounded-xs object-contain shrink-0"
|
|
101
|
+
referrerPolicy="no-referrer"
|
|
102
|
+
/>
|
|
103
|
+
) : (
|
|
104
|
+
<ProjectIcon
|
|
105
|
+
size={15}
|
|
106
|
+
className="text-ink/75"
|
|
107
|
+
style={{ color: folder.accentColor || undefined }}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</span>
|
|
111
|
+
<span className="absolute inset-0 flex items-center justify-center text-ink/60 opacity-0 transition-opacity group-hover:opacity-100 touch-visible">
|
|
112
|
+
{isOpen ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
|
|
113
|
+
</span>
|
|
114
|
+
</span>
|
|
115
|
+
<span className="w-2 shrink-0" />
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
{folder.isPinned && <Pin size={11} className="text-ink/60 shrink-0 mr-1.5" />}
|
|
119
|
+
<span className="truncate text-[13px] font-semibold tracking-tight">{folder.name}</span>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Workspace Actions — always drawn, matching the drawer's folder header.
|
|
123
|
+
They used to appear only on hover, which left the folder's own
|
|
124
|
+
affordances undiscoverable in a sidebar that has no other cue. In
|
|
125
|
+
batch mode they are hidden: the row is a choice, and an action beside
|
|
126
|
+
a checkbox is a second thing the click could mean. */}
|
|
127
|
+
{!batchActive && (
|
|
128
|
+
<div className="flex items-center space-x-0.5 pl-1">
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
title="New Session"
|
|
132
|
+
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
|
+
onClick={(e) => { e.stopPropagation(); onNewSession(); }}
|
|
134
|
+
>
|
|
135
|
+
<Plus size={12} />
|
|
136
|
+
</button>
|
|
137
|
+
|
|
138
|
+
<div className="relative flex items-center" ref={menuRef}>
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
title={`Options for ${folder.name}`}
|
|
142
|
+
aria-label={`Options for ${folder.name}`}
|
|
143
|
+
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"
|
|
144
|
+
onClick={(e) => { e.stopPropagation(); onToggleMenu(); }}
|
|
145
|
+
>
|
|
146
|
+
<MoreHorizontal size={12} />
|
|
147
|
+
</button>
|
|
148
|
+
|
|
149
|
+
{showMenu && (
|
|
150
|
+
<div className="absolute right-0 top-full mt-1 w-44 bg-paper border border-ink/15 rounded-md shadow-lg z-50 py-1 text-xs">
|
|
151
|
+
<WorkspaceOptionsMenu
|
|
152
|
+
variant="desktop"
|
|
153
|
+
isPinned={folder.isPinned}
|
|
154
|
+
onOpenFolder={onOpenFolder}
|
|
155
|
+
onSelectSessions={onSelectSessions}
|
|
156
|
+
confirmDelete={confirmDelete}
|
|
157
|
+
onPin={onPin}
|
|
158
|
+
onDelete={onDelete}
|
|
159
|
+
onRequestDelete={onRequestDelete}
|
|
160
|
+
onCancelDelete={onCancelDelete}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
-
import { ChevronDown,
|
|
2
|
+
import { ChevronDown, ChevronUp } from 'lucide-preact';
|
|
3
3
|
import { useSearchParams } from '@/client/lib/router/search-params';
|
|
4
4
|
import { useFetcher } from '@/client/lib/router/fetcher';
|
|
5
5
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
@@ -8,13 +8,14 @@ import { useWorkspaceFolderActions } from '@/client/hooks/workspace/workspace-fo
|
|
|
8
8
|
import { useSessionDelete } from '@/client/hooks/workspace/session-delete';
|
|
9
9
|
import { SessionDeleteModal } from '@/client/components/common/session-delete-modal';
|
|
10
10
|
import { SessionItem } from '@/client/components/layout/session-sidebar/SessionItem';
|
|
11
|
+
import { CategoryHeader } from '@/client/components/layout/session-sidebar/CategoryHeader';
|
|
11
12
|
import { SubagentList } from '@/client/components/common/subagent-list';
|
|
12
|
-
import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-options-menu';
|
|
13
13
|
import { useExpandedSessions } from '@/client/hooks/workspace/expanded-sessions';
|
|
14
|
-
import { getProjectIcon } from '@/shared/lib/workspace/project-icon';
|
|
15
|
-
import { selectVisibleSessions } from '@/shared/lib/workspace/session-visibility';
|
|
16
14
|
import { relativeTimeAgo } from '@/shared/lib/workspace/relative-time';
|
|
17
15
|
import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
16
|
+
import type { BatchBinding } from '@/client/components/common/batch-selection/types';
|
|
17
|
+
import { folderBatchState } from '@/client/components/common/batch-selection/folder-state';
|
|
18
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
18
19
|
|
|
19
20
|
export { SessionItem };
|
|
20
21
|
|
|
@@ -27,8 +28,10 @@ export function Category({
|
|
|
27
28
|
showArchived = false,
|
|
28
29
|
sessionStatus = {},
|
|
29
30
|
onToast,
|
|
31
|
+
batch,
|
|
32
|
+
onStartSessionSelection,
|
|
30
33
|
}: {
|
|
31
|
-
folder:
|
|
34
|
+
folder: WorkspaceFolderData;
|
|
32
35
|
activeSessionId: number | string | null;
|
|
33
36
|
onSelectSession: (id: number | string) => void;
|
|
34
37
|
onNewSessionForFolder: (id: number) => void;
|
|
@@ -36,6 +39,10 @@ export function Category({
|
|
|
36
39
|
showArchived?: boolean;
|
|
37
40
|
sessionStatus?: Record<string, 'stream' | 'finish' | 'abort'>;
|
|
38
41
|
onToast?: (message: string, type?: 'success' | 'error') => void;
|
|
42
|
+
/** Present only while a batch selection is active; absent is the normal list. */
|
|
43
|
+
batch?: BatchBinding;
|
|
44
|
+
/** Start a session selection scoped to this workspace, from its `⋯` menu. */
|
|
45
|
+
onStartSessionSelection?: (folderId: number) => void;
|
|
39
46
|
}) {
|
|
40
47
|
const [isOpen, setIsOpen] = useState(folder.isExpanded || false);
|
|
41
48
|
const [showMenu, setShowMenu] = useState(false);
|
|
@@ -74,7 +81,6 @@ export function Category({
|
|
|
74
81
|
});
|
|
75
82
|
|
|
76
83
|
const allSessions = folder.sessions;
|
|
77
|
-
const ProjectIcon = getProjectIcon(folder.icon);
|
|
78
84
|
|
|
79
85
|
const toggleFolder = () => {
|
|
80
86
|
const nextState = !isOpen;
|
|
@@ -113,101 +119,73 @@ export function Category({
|
|
|
113
119
|
|
|
114
120
|
const isActuallyOpen = forceExpanded || isOpen;
|
|
115
121
|
|
|
116
|
-
const visibleSessions = (allSessions || []).filter((s
|
|
122
|
+
const visibleSessions = (allSessions || []).filter((s) =>
|
|
117
123
|
showArchived ? s.is_archived === 1 : s.is_archived !== 1
|
|
118
124
|
);
|
|
119
125
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
// Batch rules live in one shared module — the phone's drawer item reads the
|
|
127
|
+
// same function, so the two layouts cannot disagree about which rows a
|
|
128
|
+
// header owns or what its checkbox reports.
|
|
129
|
+
const {
|
|
130
|
+
folderCheckbox,
|
|
131
|
+
sessionCheckboxes,
|
|
132
|
+
folderSummary,
|
|
133
|
+
toggleFolderSelection,
|
|
134
|
+
rendered: renderedSessions,
|
|
135
|
+
hiddenCount,
|
|
136
|
+
} = folderBatchState({
|
|
137
|
+
batch,
|
|
138
|
+
folder,
|
|
139
|
+
sessions: visibleSessions,
|
|
140
|
+
showArchived,
|
|
124
141
|
visibleCount,
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
activeSessionId,
|
|
143
|
+
sessionStatus,
|
|
144
|
+
});
|
|
145
|
+
// A session selection opens every folder: a checkbox on a collapsed workspace
|
|
146
|
+
// would let the user select rows they cannot see or unselect. The WORKSPACE
|
|
147
|
+
// mode leaves the expansion alone — it selects folders, not rows.
|
|
148
|
+
const listOpen = isActuallyOpen || sessionCheckboxes;
|
|
127
149
|
const hasMore = hiddenCount > 0;
|
|
128
150
|
|
|
129
151
|
return (
|
|
130
152
|
<div className="space-y-1">
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{folder.isPinned && <Pin size={11} className="text-ink/60 shrink-0 mr-1.5" />}
|
|
163
|
-
<span className="truncate text-[13px] font-semibold tracking-tight">{folder.name}</span>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
{/* Workspace Actions — always drawn, matching the drawer's folder
|
|
167
|
-
header. They used to appear only on hover, which left the folder's
|
|
168
|
-
own affordances undiscoverable in a sidebar that has no other cue. */}
|
|
169
|
-
<div className="flex items-center space-x-0.5 pl-1">
|
|
170
|
-
<button
|
|
171
|
-
type="button"
|
|
172
|
-
title="New Session"
|
|
173
|
-
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"
|
|
174
|
-
onClick={(e) => { e.stopPropagation(); onNewSessionForFolder(folder.id); }}
|
|
175
|
-
>
|
|
176
|
-
<Plus size={12} />
|
|
177
|
-
</button>
|
|
178
|
-
|
|
179
|
-
<div className="relative flex items-center" ref={menuRef}>
|
|
180
|
-
<button
|
|
181
|
-
type="button"
|
|
182
|
-
title="Workspace Options"
|
|
183
|
-
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"
|
|
184
|
-
onClick={(e) => { e.stopPropagation(); setShowMenu(!showMenu); }}
|
|
185
|
-
>
|
|
186
|
-
<MoreHorizontal size={12} />
|
|
187
|
-
</button>
|
|
188
|
-
|
|
189
|
-
{showMenu && (
|
|
190
|
-
<div className="absolute right-0 top-full mt-1 w-44 bg-paper border border-ink/15 rounded-md shadow-lg z-50 py-1 text-xs">
|
|
191
|
-
<WorkspaceOptionsMenu
|
|
192
|
-
variant="desktop"
|
|
193
|
-
isPinned={folder.isPinned}
|
|
194
|
-
onOpenFolder={onOpenFolder}
|
|
195
|
-
confirmDelete={confirmDelete}
|
|
196
|
-
onPin={onPin}
|
|
197
|
-
onDelete={onDelete}
|
|
198
|
-
onRequestDelete={requestDelete}
|
|
199
|
-
onCancelDelete={cancelDelete}
|
|
200
|
-
/>
|
|
201
|
-
</div>
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
153
|
+
<CategoryHeader
|
|
154
|
+
folder={folder}
|
|
155
|
+
isOpen={isActuallyOpen}
|
|
156
|
+
onToggleFolder={toggleFolder}
|
|
157
|
+
checkbox={
|
|
158
|
+
folderCheckbox && folderSummary
|
|
159
|
+
? {
|
|
160
|
+
checked: folderSummary.all,
|
|
161
|
+
indeterminate: folderSummary.some,
|
|
162
|
+
label: `Select ${folder.name}`,
|
|
163
|
+
onChange: toggleFolderSelection,
|
|
164
|
+
}
|
|
165
|
+
: null
|
|
166
|
+
}
|
|
167
|
+
batchActive={Boolean(batch)}
|
|
168
|
+
menuRef={menuRef}
|
|
169
|
+
showMenu={showMenu}
|
|
170
|
+
onToggleMenu={() => setShowMenu(!showMenu)}
|
|
171
|
+
onNewSession={() => onNewSessionForFolder(folder.id)}
|
|
172
|
+
onOpenFolder={onOpenFolder}
|
|
173
|
+
onSelectSessions={
|
|
174
|
+
onStartSessionSelection
|
|
175
|
+
? () => { setShowMenu(false); onStartSessionSelection(folder.id); }
|
|
176
|
+
: undefined
|
|
177
|
+
}
|
|
178
|
+
confirmDelete={confirmDelete}
|
|
179
|
+
onPin={onPin}
|
|
180
|
+
onDelete={onDelete}
|
|
181
|
+
onRequestDelete={requestDelete}
|
|
182
|
+
onCancelDelete={cancelDelete}
|
|
183
|
+
/>
|
|
206
184
|
|
|
207
185
|
{/* Sessions List */}
|
|
208
|
-
{
|
|
186
|
+
{listOpen && (
|
|
209
187
|
<div className="space-y-0.5">
|
|
210
|
-
{renderedSessions.map((session
|
|
188
|
+
{renderedSessions.map((session) => {
|
|
211
189
|
const sessionKey = String(session.id);
|
|
212
190
|
const isActive = activeSessionId !== null
|
|
213
191
|
? String(activeSessionId) === sessionKey
|
|
@@ -248,6 +226,9 @@ export function Category({
|
|
|
248
226
|
hasSubagents={hasSubagents}
|
|
249
227
|
isExpanded={isExpanded}
|
|
250
228
|
onToggleExpand={() => handleToggleSessionExpand(sessionKey)}
|
|
229
|
+
selectable={sessionCheckboxes}
|
|
230
|
+
selected={Boolean(batch?.selected.includes(sessionKey))}
|
|
231
|
+
onToggleSelect={batch ? () => batch.toggle(sessionKey) : undefined}
|
|
251
232
|
/>
|
|
252
233
|
{hasSubagents && isExpanded && (
|
|
253
234
|
<SubagentList sessionId={session.id} isActiveSession={canExpandActive} />
|
|
@@ -261,7 +242,7 @@ export function Category({
|
|
|
261
242
|
truncated for height, so after a partial reveal the user needs to
|
|
262
243
|
keep revealing AND to start over, which is what two controls say
|
|
263
244
|
and one alternating control cannot. */}
|
|
264
|
-
{(hasMore || canShowLess) && !forceExpanded && (
|
|
245
|
+
{(hasMore || canShowLess) && !forceExpanded && !sessionCheckboxes && (
|
|
265
246
|
<div className="flex items-center gap-1">
|
|
266
247
|
{hasMore && (
|
|
267
248
|
<button
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Info, Settings } from 'lucide-preact';
|
|
1
|
+
import { Download, Info, Settings } from 'lucide-preact';
|
|
2
2
|
|
|
3
3
|
interface SessionSidebarFooterProps {
|
|
4
4
|
onSettings: () => void;
|
|
5
5
|
onInfo: () => void;
|
|
6
6
|
updateAvailable?: boolean;
|
|
7
7
|
onUpdateClick?: () => void;
|
|
8
|
+
/** The newer APK to offer, or null. Only ever set inside the Android app. */
|
|
9
|
+
androidDownload?: { url: string; latest: string; releaseUrl: string | null } | null;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -17,7 +19,7 @@ interface SessionSidebarFooterProps {
|
|
|
17
19
|
* is unreachable by keyboard"). It is also what lets a browser spec address
|
|
18
20
|
* these controls by role and name instead of by their position in the DOM.
|
|
19
21
|
*/
|
|
20
|
-
export function SessionSidebarFooter({ onSettings, onInfo, updateAvailable, onUpdateClick }: SessionSidebarFooterProps) {
|
|
22
|
+
export function SessionSidebarFooter({ onSettings, onInfo, updateAvailable, onUpdateClick, androidDownload }: SessionSidebarFooterProps) {
|
|
21
23
|
return (
|
|
22
24
|
<div className="h-10 px-3 border-t border-ink/10 flex items-center justify-between text-ink/60 shrink-0">
|
|
23
25
|
<div className="flex space-x-1">
|
|
@@ -39,6 +41,22 @@ export function SessionSidebarFooter({ onSettings, onInfo, updateAvailable, onUp
|
|
|
39
41
|
>
|
|
40
42
|
<Info size={16} />
|
|
41
43
|
</button>
|
|
44
|
+
{androidDownload && (
|
|
45
|
+
// Next to About, and only when BOTH facts hold: this page is inside
|
|
46
|
+
// the Android app, and a newer APK is published. An up-to-date app
|
|
47
|
+
// shows nothing here, and a browser never does — see
|
|
48
|
+
// `useAndroidDownload`.
|
|
49
|
+
<a
|
|
50
|
+
href={androidDownload.url}
|
|
51
|
+
target="_blank"
|
|
52
|
+
rel="noopener noreferrer"
|
|
53
|
+
title={`Download Kabmo ${androidDownload.latest} for Android`}
|
|
54
|
+
aria-label={`Download Kabmo ${androidDownload.latest} for Android`}
|
|
55
|
+
className="p-1 rounded hover:text-ink hover:bg-ink/5 cursor-pointer transition-colors"
|
|
56
|
+
>
|
|
57
|
+
<Download size={16} />
|
|
58
|
+
</a>
|
|
59
|
+
)}
|
|
42
60
|
</div>
|
|
43
61
|
{updateAvailable && (
|
|
44
62
|
<button
|
|
@@ -39,6 +39,14 @@ interface ToolbarOverflowMenuProps {
|
|
|
39
39
|
sortHidden: boolean;
|
|
40
40
|
sortOption: SessionSortOption;
|
|
41
41
|
onSortChange: (option: SessionSortOption) => void;
|
|
42
|
+
/**
|
|
43
|
+
* The row this menu sits in. Both layouts share the RULE and the menu, but
|
|
44
|
+
* their controls are different sizes — the desktop row is `w-7` in a 34px
|
|
45
|
+
* step, the phone's drawer is `w-9.5` in a 46px one — so the trigger and the
|
|
46
|
+
* sort rows have to match the controls beside them. A `⋯` drawn at desktop
|
|
47
|
+
* metrics in a drawer of 38px buttons reads as a control from another screen.
|
|
48
|
+
*/
|
|
49
|
+
layout?: 'desktop' | 'mobile';
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export function ToolbarOverflowMenu({
|
|
@@ -46,7 +54,9 @@ export function ToolbarOverflowMenu({
|
|
|
46
54
|
sortHidden,
|
|
47
55
|
sortOption,
|
|
48
56
|
onSortChange,
|
|
57
|
+
layout = 'desktop',
|
|
49
58
|
}: ToolbarOverflowMenuProps) {
|
|
59
|
+
const mobile = layout === 'mobile';
|
|
50
60
|
const [open, setOpen] = useState(false);
|
|
51
61
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
52
62
|
useOnClickOutside(rootRef, () => setOpen(false));
|
|
@@ -75,19 +85,25 @@ export function ToolbarOverflowMenu({
|
|
|
75
85
|
aria-haspopup="menu"
|
|
76
86
|
aria-expanded={open}
|
|
77
87
|
onClick={() => setOpen(!open)}
|
|
78
|
-
className={`
|
|
88
|
+
className={`inline-flex items-center justify-center transition-colors cursor-pointer ${
|
|
89
|
+
mobile
|
|
90
|
+
? 'w-9.5 h-9.5 rounded-xl border active:scale-95 shadow-xs'
|
|
91
|
+
: 'w-7 h-7 rounded border'
|
|
92
|
+
} ${
|
|
79
93
|
open
|
|
80
94
|
? 'border-ink/30 bg-ink/10 text-ink'
|
|
81
95
|
: 'border-ink/15 bg-paper text-ink/70 hover:bg-ink/5 hover:text-ink'
|
|
82
96
|
}`}
|
|
83
97
|
>
|
|
84
|
-
<Ellipsis size={14} />
|
|
98
|
+
<Ellipsis size={mobile ? 16 : 14} />
|
|
85
99
|
</button>
|
|
86
100
|
|
|
87
101
|
{open && (
|
|
88
102
|
<div
|
|
89
103
|
role="menu"
|
|
90
|
-
className=
|
|
104
|
+
className={`absolute right-0 top-full mt-2 w-52 bg-paper border border-ink/15 shadow-lg z-50 py-1 text-xs ${
|
|
105
|
+
mobile ? 'rounded-xl p-2' : 'rounded-lg'
|
|
106
|
+
}`}
|
|
91
107
|
>
|
|
92
108
|
{actions.map((action) => (
|
|
93
109
|
<button
|
|
@@ -96,7 +112,9 @@ export function ToolbarOverflowMenu({
|
|
|
96
112
|
role="menuitem"
|
|
97
113
|
onClick={() => run(action)}
|
|
98
114
|
aria-pressed={action.active}
|
|
99
|
-
className={`w-full text-left
|
|
115
|
+
className={`w-full text-left flex items-center gap-2 transition-colors ${
|
|
116
|
+
mobile ? 'px-2.5 py-1.5 rounded-lg' : 'px-3 py-1.5'
|
|
117
|
+
} ${
|
|
100
118
|
action.active ? 'bg-ink/5 text-ink font-medium' : 'text-ink/75 hover:bg-ink/5 hover:text-ink'
|
|
101
119
|
}`}
|
|
102
120
|
>
|
|
@@ -107,7 +125,7 @@ export function ToolbarOverflowMenu({
|
|
|
107
125
|
|
|
108
126
|
{sortHidden && (
|
|
109
127
|
<div className="mt-1 border-t border-ink/10 pt-1">
|
|
110
|
-
<SortMenu variant=
|
|
128
|
+
<SortMenu variant={layout} sortOption={sortOption} onSortChange={onSortChange} />
|
|
111
129
|
</div>
|
|
112
130
|
)}
|
|
113
131
|
</div>
|