@saadnvd1/agent-os 0.3.611 → 0.3.612
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.
|
@@ -17,69 +17,26 @@ import {
|
|
|
17
17
|
ChevronDown,
|
|
18
18
|
ChevronLeft,
|
|
19
19
|
ChevronRight,
|
|
20
|
+
Ellipsis,
|
|
20
21
|
Circle,
|
|
21
|
-
FolderOpen,
|
|
22
|
-
GitBranch,
|
|
23
22
|
Menu,
|
|
24
23
|
Plus,
|
|
25
24
|
Search,
|
|
25
|
+
Star,
|
|
26
26
|
Share2,
|
|
27
|
-
Terminal as TerminalIcon,
|
|
28
27
|
TextSearch,
|
|
29
|
-
Users,
|
|
30
|
-
ScrollText,
|
|
31
|
-
Package,
|
|
32
28
|
} from "lucide-react";
|
|
33
29
|
import { cn } from "@/lib/utils";
|
|
34
30
|
import { useAwayQuery } from "@/data/away";
|
|
35
31
|
import { awayUntil } from "@/components/Away/time";
|
|
36
32
|
import type { Session, Project } from "@/lib/db";
|
|
37
|
-
import type { LucideIcon } from "lucide-react";
|
|
38
33
|
import { HostBadge } from "@/components/Hosts/HostBadge";
|
|
34
|
+
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
39
35
|
import { shareUiActions } from "@/stores/shareUi";
|
|
40
36
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
41
37
|
import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
|
|
42
38
|
import type { ViewMode } from "@/contexts/PaneContext";
|
|
43
|
-
|
|
44
|
-
interface ViewModeButtonProps {
|
|
45
|
-
mode: ViewMode;
|
|
46
|
-
currentMode: ViewMode;
|
|
47
|
-
icon: LucideIcon;
|
|
48
|
-
label?: string;
|
|
49
|
-
onClick: (mode: ViewMode) => void;
|
|
50
|
-
badge?: React.ReactNode;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function ViewModeButton({
|
|
54
|
-
mode,
|
|
55
|
-
currentMode,
|
|
56
|
-
icon: Icon,
|
|
57
|
-
label,
|
|
58
|
-
onClick,
|
|
59
|
-
badge,
|
|
60
|
-
}: ViewModeButtonProps) {
|
|
61
|
-
return (
|
|
62
|
-
<button
|
|
63
|
-
type="button"
|
|
64
|
-
aria-label={label}
|
|
65
|
-
aria-pressed={currentMode === mode}
|
|
66
|
-
onClick={(e) => {
|
|
67
|
-
e.stopPropagation();
|
|
68
|
-
onClick(mode);
|
|
69
|
-
}}
|
|
70
|
-
className={cn(
|
|
71
|
-
"rounded p-1.5 transition-colors",
|
|
72
|
-
badge && "flex items-center gap-0.5",
|
|
73
|
-
currentMode === mode
|
|
74
|
-
? "bg-secondary text-foreground"
|
|
75
|
-
: "text-muted-foreground"
|
|
76
|
-
)}
|
|
77
|
-
>
|
|
78
|
-
<Icon className="h-4 w-4" />
|
|
79
|
-
{badge}
|
|
80
|
-
</button>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
39
|
+
import { MobileViewPicker } from "./MobileViewPicker";
|
|
83
40
|
|
|
84
41
|
interface MobileTabBarProps {
|
|
85
42
|
session: Session | null | undefined;
|
|
@@ -205,7 +162,7 @@ export function MobileTabBar({
|
|
|
205
162
|
onClick={handlePrev}
|
|
206
163
|
onTouchEnd={(e) => e.stopPropagation()}
|
|
207
164
|
disabled={!hasPrev || isNavigating}
|
|
208
|
-
className="hover:bg-accent flex h-8 w-8 shrink-0 items-center justify-center rounded-md disabled:pointer-events-none disabled:opacity-50"
|
|
165
|
+
className="hover:bg-accent flex h-8 w-8 shrink-0 items-center justify-center rounded-md disabled:pointer-events-none disabled:opacity-50 max-[429px]:hidden"
|
|
209
166
|
>
|
|
210
167
|
<ChevronLeft className="h-4 w-4" />
|
|
211
168
|
</button>
|
|
@@ -304,7 +261,7 @@ export function MobileTabBar({
|
|
|
304
261
|
onClick={handleNext}
|
|
305
262
|
onTouchEnd={(e) => e.stopPropagation()}
|
|
306
263
|
disabled={!hasNext || isNavigating}
|
|
307
|
-
className="hover:bg-accent flex h-8 w-8 shrink-0 items-center justify-center rounded-md disabled:pointer-events-none disabled:opacity-50"
|
|
264
|
+
className="hover:bg-accent flex h-8 w-8 shrink-0 items-center justify-center rounded-md disabled:pointer-events-none disabled:opacity-50 max-[429px]:hidden"
|
|
308
265
|
>
|
|
309
266
|
<ChevronRight className="h-4 w-4" />
|
|
310
267
|
</button>
|
|
@@ -313,7 +270,7 @@ export function MobileTabBar({
|
|
|
313
270
|
{bookmarksSessionId && (
|
|
314
271
|
<ChatBookmarksButton
|
|
315
272
|
sessionId={bookmarksSessionId}
|
|
316
|
-
className="-my-1.5 h-11 min-w-11"
|
|
273
|
+
className="-my-1.5 h-11 min-w-11 max-[429px]:hidden"
|
|
317
274
|
/>
|
|
318
275
|
)}
|
|
319
276
|
{onFind && (
|
|
@@ -325,7 +282,7 @@ export function MobileTabBar({
|
|
|
325
282
|
e.stopPropagation();
|
|
326
283
|
onFind();
|
|
327
284
|
}}
|
|
328
|
-
className="-my-1.5 h-11 w-11 shrink-0"
|
|
285
|
+
className="-my-1.5 h-11 w-11 shrink-0 max-[429px]:hidden"
|
|
329
286
|
>
|
|
330
287
|
<TextSearch className="h-4 w-4" />
|
|
331
288
|
</Button>
|
|
@@ -338,11 +295,49 @@ export function MobileTabBar({
|
|
|
338
295
|
e.stopPropagation();
|
|
339
296
|
paletteActions.open();
|
|
340
297
|
}}
|
|
341
|
-
//
|
|
342
|
-
className="-my-1.5 h-11 w-11 shrink-0"
|
|
298
|
+
// 44px targets in a 32px bar row, via -my-1.5.
|
|
299
|
+
className="-my-1.5 h-11 w-11 shrink-0 max-[429px]:hidden"
|
|
343
300
|
>
|
|
344
301
|
<Search className="h-4 w-4" />
|
|
345
302
|
</Button>
|
|
303
|
+
{/* Below 430px Find, Palette and Bookmarks fold into one menu. */}
|
|
304
|
+
<DropdownMenu modal={false}>
|
|
305
|
+
<DropdownMenuTrigger asChild>
|
|
306
|
+
<Button
|
|
307
|
+
variant="ghost"
|
|
308
|
+
size="icon-sm"
|
|
309
|
+
aria-label="More"
|
|
310
|
+
onClick={(e) => e.stopPropagation()}
|
|
311
|
+
className="-my-1.5 hidden h-11 w-11 shrink-0 max-[429px]:flex"
|
|
312
|
+
>
|
|
313
|
+
<Ellipsis className="h-4 w-4" />
|
|
314
|
+
</Button>
|
|
315
|
+
</DropdownMenuTrigger>
|
|
316
|
+
<DropdownMenuContent align="end" className="min-w-48">
|
|
317
|
+
{onFind && (
|
|
318
|
+
<DropdownMenuItem className="h-11 gap-2" onSelect={onFind}>
|
|
319
|
+
<TextSearch className="h-4 w-4" />
|
|
320
|
+
Find in conversation
|
|
321
|
+
</DropdownMenuItem>
|
|
322
|
+
)}
|
|
323
|
+
<DropdownMenuItem
|
|
324
|
+
className="h-11 gap-2"
|
|
325
|
+
onSelect={() => paletteActions.open()}
|
|
326
|
+
>
|
|
327
|
+
<Search className="h-4 w-4" />
|
|
328
|
+
Command palette
|
|
329
|
+
</DropdownMenuItem>
|
|
330
|
+
{bookmarksSessionId && (
|
|
331
|
+
<DropdownMenuItem
|
|
332
|
+
className="h-11 gap-2"
|
|
333
|
+
onSelect={() => bookmarksUiActions.openChat(bookmarksSessionId)}
|
|
334
|
+
>
|
|
335
|
+
<Star className="h-4 w-4" />
|
|
336
|
+
Bookmarks
|
|
337
|
+
</DropdownMenuItem>
|
|
338
|
+
)}
|
|
339
|
+
</DropdownMenuContent>
|
|
340
|
+
</DropdownMenu>
|
|
346
341
|
<Button
|
|
347
342
|
variant="ghost"
|
|
348
343
|
size="icon-sm"
|
|
@@ -358,60 +353,15 @@ export function MobileTabBar({
|
|
|
358
353
|
<SessionPrStage sessionId={session?.id} />
|
|
359
354
|
<ViewSwitch session={session} />
|
|
360
355
|
|
|
361
|
-
{/* View mode toggle */}
|
|
362
356
|
{session?.working_directory && (
|
|
363
|
-
<
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
<ViewModeButton
|
|
372
|
-
mode="files"
|
|
373
|
-
currentMode={viewMode}
|
|
374
|
-
icon={FolderOpen}
|
|
375
|
-
onClick={onViewModeChange}
|
|
376
|
-
/>
|
|
377
|
-
)}
|
|
378
|
-
{isLocalSession && (
|
|
379
|
-
<ViewModeButton
|
|
380
|
-
mode="git"
|
|
381
|
-
currentMode={viewMode}
|
|
382
|
-
icon={GitBranch}
|
|
383
|
-
onClick={onViewModeChange}
|
|
384
|
-
/>
|
|
385
|
-
)}
|
|
386
|
-
<ViewModeButton
|
|
387
|
-
mode="artifacts"
|
|
388
|
-
currentMode={viewMode}
|
|
389
|
-
icon={Package}
|
|
390
|
-
label="Artifacts"
|
|
391
|
-
onClick={onViewModeChange}
|
|
392
|
-
/>
|
|
393
|
-
{hasLog && (
|
|
394
|
-
<ViewModeButton
|
|
395
|
-
mode="log"
|
|
396
|
-
currentMode={viewMode}
|
|
397
|
-
icon={ScrollText}
|
|
398
|
-
onClick={onViewModeChange}
|
|
399
|
-
/>
|
|
400
|
-
)}
|
|
401
|
-
{isConductor && (
|
|
402
|
-
<ViewModeButton
|
|
403
|
-
mode="workers"
|
|
404
|
-
currentMode={viewMode}
|
|
405
|
-
icon={Users}
|
|
406
|
-
onClick={onViewModeChange}
|
|
407
|
-
badge={
|
|
408
|
-
<span className="bg-primary/20 text-primary rounded px-1 text-[10px]">
|
|
409
|
-
{workerCount}
|
|
410
|
-
</span>
|
|
411
|
-
}
|
|
412
|
-
/>
|
|
413
|
-
)}
|
|
414
|
-
</div>
|
|
357
|
+
<MobileViewPicker
|
|
358
|
+
viewMode={viewMode}
|
|
359
|
+
onViewModeChange={onViewModeChange}
|
|
360
|
+
isLocalSession={isLocalSession}
|
|
361
|
+
hasLog={hasLog}
|
|
362
|
+
isConductor={isConductor}
|
|
363
|
+
workerCount={workerCount}
|
|
364
|
+
/>
|
|
415
365
|
)}
|
|
416
366
|
</div>
|
|
417
367
|
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import "@/components/Chat/composer/test-dom";
|
|
2
|
+
import { act, createElement } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { MobileViewPicker, mobileViews } from "./MobileViewPicker";
|
|
6
|
+
|
|
7
|
+
// Radix dispatches CustomEvents; the node global is not jsdom's.
|
|
8
|
+
Object.defineProperty(globalThis, "CustomEvent", {
|
|
9
|
+
value: (window as unknown as { CustomEvent: unknown }).CustomEvent,
|
|
10
|
+
configurable: true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const all = {
|
|
14
|
+
isLocalSession: true,
|
|
15
|
+
hasLog: true,
|
|
16
|
+
isConductor: true,
|
|
17
|
+
workerCount: 3,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("mobileViews", () => {
|
|
21
|
+
it("lists every view, Artifacts included", () => {
|
|
22
|
+
expect(mobileViews(all).map((v) => v.label)).toEqual([
|
|
23
|
+
"Chat",
|
|
24
|
+
"Files",
|
|
25
|
+
"Git",
|
|
26
|
+
"Artifacts",
|
|
27
|
+
"Log",
|
|
28
|
+
"Workers",
|
|
29
|
+
]);
|
|
30
|
+
expect(mobileViews(all).at(-1)?.count).toBe(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("hides views that do not apply", () => {
|
|
34
|
+
expect(
|
|
35
|
+
mobileViews({
|
|
36
|
+
isLocalSession: false,
|
|
37
|
+
isConductor: false,
|
|
38
|
+
workerCount: 0,
|
|
39
|
+
}).map((v) => v.label)
|
|
40
|
+
).toEqual(["Chat", "Artifacts"]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("MobileViewPicker", () => {
|
|
45
|
+
async function open(viewMode: "terminal" | "artifacts", onChange = vi.fn()) {
|
|
46
|
+
const el = document.createElement("div");
|
|
47
|
+
document.body.appendChild(el);
|
|
48
|
+
const root = createRoot(el);
|
|
49
|
+
await act(async () => {
|
|
50
|
+
root.render(
|
|
51
|
+
createElement(MobileViewPicker, {
|
|
52
|
+
...all,
|
|
53
|
+
viewMode,
|
|
54
|
+
onViewModeChange: onChange,
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
const trigger = el.querySelector("button") as HTMLButtonElement;
|
|
59
|
+
await act(async () => {
|
|
60
|
+
trigger.dispatchEvent(
|
|
61
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true })
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
return { trigger, onChange };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
it("shows the current view and lists all views in the menu", async () => {
|
|
68
|
+
const { trigger } = await open("artifacts");
|
|
69
|
+
expect(trigger.textContent).toContain("Artifacts");
|
|
70
|
+
const items = [...document.querySelectorAll('[role="menuitem"]')];
|
|
71
|
+
expect(items.map((i) => i.textContent)).toEqual([
|
|
72
|
+
"Chat",
|
|
73
|
+
"Files",
|
|
74
|
+
"Git",
|
|
75
|
+
"Artifacts",
|
|
76
|
+
"Log",
|
|
77
|
+
"Workers3",
|
|
78
|
+
]);
|
|
79
|
+
expect(items[3].getAttribute("aria-current")).toBe("true");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("selecting an item calls the view-change handler", async () => {
|
|
83
|
+
const { onChange } = await open("terminal");
|
|
84
|
+
const log = [...document.querySelectorAll('[role="menuitem"]')].find((i) =>
|
|
85
|
+
i.textContent?.startsWith("Log")
|
|
86
|
+
) as HTMLElement;
|
|
87
|
+
await act(async () => {
|
|
88
|
+
log.click();
|
|
89
|
+
});
|
|
90
|
+
expect(onChange).toHaveBeenCalledWith("log");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Check,
|
|
5
|
+
ChevronDown,
|
|
6
|
+
FolderOpen,
|
|
7
|
+
GitBranch,
|
|
8
|
+
Package,
|
|
9
|
+
ScrollText,
|
|
10
|
+
Terminal as TerminalIcon,
|
|
11
|
+
Users,
|
|
12
|
+
type LucideIcon,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuTrigger,
|
|
19
|
+
} from "@/components/ui/dropdown-menu";
|
|
20
|
+
import type { ViewMode } from "@/contexts/PaneContext";
|
|
21
|
+
|
|
22
|
+
export interface MobileView {
|
|
23
|
+
mode: ViewMode;
|
|
24
|
+
label: string;
|
|
25
|
+
icon: LucideIcon;
|
|
26
|
+
count?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MobileViewAvailability {
|
|
30
|
+
isLocalSession: boolean;
|
|
31
|
+
hasLog?: boolean;
|
|
32
|
+
isConductor: boolean;
|
|
33
|
+
workerCount: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The one list of views on a phone; the picker renders it.
|
|
37
|
+
export function mobileViews({
|
|
38
|
+
isLocalSession,
|
|
39
|
+
hasLog,
|
|
40
|
+
isConductor,
|
|
41
|
+
workerCount,
|
|
42
|
+
}: MobileViewAvailability): MobileView[] {
|
|
43
|
+
const views: (MobileView | false)[] = [
|
|
44
|
+
{ mode: "terminal", label: "Chat", icon: TerminalIcon },
|
|
45
|
+
isLocalSession && { mode: "files", label: "Files", icon: FolderOpen },
|
|
46
|
+
isLocalSession && { mode: "git", label: "Git", icon: GitBranch },
|
|
47
|
+
{ mode: "artifacts", label: "Artifacts", icon: Package },
|
|
48
|
+
!!hasLog && { mode: "log", label: "Log", icon: ScrollText },
|
|
49
|
+
isConductor && {
|
|
50
|
+
mode: "workers",
|
|
51
|
+
label: "Workers",
|
|
52
|
+
icon: Users,
|
|
53
|
+
count: workerCount,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
return views.filter((v): v is MobileView => !!v);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function MobileViewPicker({
|
|
60
|
+
viewMode,
|
|
61
|
+
onViewModeChange,
|
|
62
|
+
...availability
|
|
63
|
+
}: MobileViewAvailability & {
|
|
64
|
+
viewMode: ViewMode;
|
|
65
|
+
onViewModeChange: (mode: ViewMode) => void;
|
|
66
|
+
}) {
|
|
67
|
+
const views = mobileViews(availability);
|
|
68
|
+
const current = views.find((v) => v.mode === viewMode) ?? views[0];
|
|
69
|
+
const CurrentIcon = current.icon;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<DropdownMenu modal={false}>
|
|
73
|
+
<DropdownMenuTrigger asChild>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
aria-label={`View: ${current.label}`}
|
|
77
|
+
onClick={(e) => e.stopPropagation()}
|
|
78
|
+
className="bg-accent/50 relative flex h-8 shrink-0 items-center gap-1 rounded-md px-2 text-xs after:absolute after:inset-x-0 after:-inset-y-1.5 after:content-['']"
|
|
79
|
+
>
|
|
80
|
+
<CurrentIcon className="h-4 w-4" />
|
|
81
|
+
<span className="max-[359px]:hidden">{current.label}</span>
|
|
82
|
+
<ChevronDown className="text-muted-foreground h-3 w-3" />
|
|
83
|
+
</button>
|
|
84
|
+
</DropdownMenuTrigger>
|
|
85
|
+
<DropdownMenuContent align="end" className="min-w-40">
|
|
86
|
+
{views.map(({ mode, label, icon: Icon, count }) => (
|
|
87
|
+
<DropdownMenuItem
|
|
88
|
+
key={mode}
|
|
89
|
+
aria-current={mode === current.mode}
|
|
90
|
+
onSelect={() => onViewModeChange(mode)}
|
|
91
|
+
className="h-11 gap-2"
|
|
92
|
+
>
|
|
93
|
+
<Icon className="h-4 w-4" />
|
|
94
|
+
<span className="flex-1">{label}</span>
|
|
95
|
+
{count !== undefined && (
|
|
96
|
+
<span className="bg-primary/20 text-primary rounded px-1 text-[10px]">
|
|
97
|
+
{count}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
{mode === current.mode && <Check className="h-4 w-4" />}
|
|
101
|
+
</DropdownMenuItem>
|
|
102
|
+
))}
|
|
103
|
+
</DropdownMenuContent>
|
|
104
|
+
</DropdownMenu>
|
|
105
|
+
);
|
|
106
|
+
}
|