@saadnvd1/agent-os 0.3.684 → 0.3.691
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/app/api/dev-servers/detect/route.ts +1 -2
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +2 -4
- package/app/page.tsx +1 -2
- package/components/Artifacts/ArtifactsPanel.tsx +1 -1
- package/components/FileExplorer/FileTabs.tsx +1 -1
- package/components/Pane/DesktopTabBar.tsx +2 -2
- package/components/QuickSwitcher.tsx +24 -21
- package/components/SessionList/SessionRowMenu.tsx +0 -1
- package/components/Settings/ExperimentsSection.test.ts +7 -1
- package/components/Settings/NoApprovalsSection.test.ts +7 -1
- package/components/Terminal/TerminalToolbar.tsx +1 -1
- package/components/Terminal/VirtualKeyboard.tsx +1 -1
- package/components/WorkerCard.tsx +1 -7
- package/components/Workspaces/WorkspaceNameDialog.tsx +5 -3
- package/components/a/AButton.tsx +1 -6
- package/components/a/AIconButton.tsx +1 -5
- package/contexts/PaneContext.tsx +1 -6
- package/data/lumifyhub/index.ts +2 -4
- package/hooks/useDirectoryBrowser.ts +5 -4
- package/hooks/useDrawerAnimation.ts +11 -14
- package/hooks/useFileDrop.ts +2 -6
- package/hooks/useKeybarVisibility.ts +10 -29
- package/hooks/useSidebarPinned.ts +7 -23
- package/hooks/useSpeechRecognition.ts +19 -5
- package/hooks/useStoredFlag.ts +47 -0
- package/hooks/useViewport.ts +11 -17
- package/lib/approvals/no-approvals.ts +1 -7
- package/lib/artifacts/store.ts +1 -2
- package/lib/away/report.ts +1 -2
- package/lib/away/run.ts +1 -2
- package/lib/away/state.ts +1 -2
- package/lib/bookmarks/store.ts +1 -2
- package/lib/bus/delivery.ts +1 -2
- package/lib/bus/index.ts +1 -2
- package/lib/chat/compact-context.ts +1 -2
- package/lib/chat/stop.ts +1 -2
- package/lib/chat/worker/host.ts +1 -1
- package/lib/code-health/reports.ts +1 -2
- package/lib/db/lumifyhub.ts +2 -4
- package/lib/db/schedules-move.ts +1 -2
- package/lib/db/session-parts/tasks.ts +1 -2
- package/lib/db/stacks.ts +3 -9
- package/lib/db/types.ts +1 -7
- package/lib/doctor/checks.ts +7 -9
- package/lib/done/archive.ts +1 -2
- package/lib/experiments/meter.ts +1 -7
- package/lib/health/env/classes.ts +0 -2
- package/lib/health/env/recordHit.ts +1 -2
- package/lib/health/spin.ts +1 -4
- package/lib/health/store.ts +1 -2
- package/lib/hosts/index.ts +1 -2
- package/lib/hosts/peer-sync.ts +2 -4
- package/lib/hosts/remote-api.ts +1 -2
- package/lib/lumifyhub/types.ts +2 -8
- package/lib/notify/index.ts +1 -2
- package/lib/notify/push/index.ts +2 -7
- package/lib/orchestrator/external-pr.ts +1 -2
- package/lib/orchestrator/gates.ts +1 -6
- package/lib/orchestrator/needs-you-items.ts +16 -20
- package/lib/orchestrator/pr-wake.ts +1 -2
- package/lib/orchestrator/rebase.ts +1 -2
- package/lib/orchestrator/review.ts +1 -2
- package/lib/orchestrator/usage.ts +1 -2
- package/lib/plans/claim.ts +1 -2
- package/lib/plans/features.ts +4 -11
- package/lib/plans/release.ts +2 -4
- package/lib/ports.ts +1 -2
- package/lib/program-status/records.ts +1 -1
- package/lib/project-config/database.ts +1 -2
- package/lib/projects.ts +1 -2
- package/lib/recipes/settle.ts +2 -4
- package/lib/recipes/store.ts +2 -4
- package/lib/schedules/store.ts +2 -4
- package/lib/search/conversations/backfill.ts +2 -4
- package/lib/security/auth.ts +1 -2
- package/lib/security/passkeys.ts +1 -2
- package/lib/security/presence.ts +1 -7
- package/lib/session-artifacts/types.ts +1 -8
- package/lib/session-audit/transcript.ts +1 -3
- package/lib/session-logs/index.ts +1 -2
- package/lib/sessions/launch.ts +1 -2
- package/lib/share/snapshot.ts +1 -7
- package/lib/share/store.ts +1 -2
- package/lib/sidebar/orchestrator-row.ts +1 -6
- package/lib/sidebar/row-menu.ts +1 -2
- package/lib/sidebar/shelves.ts +1 -6
- package/lib/simulator/index.ts +1 -2
- package/lib/simulator/lease.ts +1 -5
- package/lib/stacks/index.ts +1 -1
- package/lib/stacks/plan.ts +1 -1
- package/lib/stacks/restack.ts +1 -2
- package/lib/stacks/tick.ts +1 -2
- package/lib/status/socket.ts +1 -2
- package/lib/tasks/contract/authorize.ts +1 -2
- package/lib/tasks/contract/plan.ts +1 -2
- package/lib/tasks/contract/sync.ts +1 -2
- package/lib/tasks/holds.ts +1 -5
- package/lib/tasks/index.ts +1 -2
- package/lib/tasks/loop-types.ts +1 -6
- package/lib/tasks/move-guard.ts +1 -2
- package/lib/tasks/move.ts +1 -2
- package/lib/tasks/moved-away.ts +1 -2
- package/lib/tasks/pause-hold.ts +1 -2
- package/lib/tasks/pause.ts +3 -6
- package/lib/tasks/pr-activity.ts +1 -3
- package/lib/tasks/queue-rows.ts +2 -4
- package/lib/tasks/queue-types.ts +1 -5
- package/lib/tasks/queue.ts +1 -2
- package/lib/tasks/refresh-base.ts +1 -2
- package/lib/tasks/start.ts +2 -4
- package/lib/tasks/starter.ts +1 -2
- package/lib/tasks/state.ts +1 -6
- package/lib/theme-config.ts +1 -8
- package/lib/updates/banner.ts +1 -6
- package/lib/updates/run.ts +1 -2
- package/lib/usage/aggregate.ts +1 -1
- package/lib/usage/nightly.ts +1 -2
- package/lib/usage/rate-limits.ts +1 -2
- package/lib/usage/turns.ts +1 -2
- package/lib/usage/windows.ts +5 -7
- package/lib/workspaces.ts +1 -2
- package/lib/worktree-sweep/databases.ts +0 -1
- package/package.json +3 -3
|
@@ -16,8 +16,7 @@ export async function GET(request: NextRequest) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const project = queries.getProject(db).get(projectId) as
|
|
19
|
-
|
|
|
20
|
-
| undefined;
|
|
19
|
+
Project | undefined;
|
|
21
20
|
if (!project) {
|
|
22
21
|
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
|
23
22
|
}
|
|
@@ -12,8 +12,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|
|
12
12
|
const { dsId } = await params;
|
|
13
13
|
|
|
14
14
|
const existing = queries.getProjectDevServer(db).get(dsId) as
|
|
15
|
-
|
|
|
16
|
-
| undefined;
|
|
15
|
+
ProjectDevServer | undefined;
|
|
17
16
|
if (!existing) {
|
|
18
17
|
return NextResponse.json(
|
|
19
18
|
{ error: "Dev server config not found" },
|
|
@@ -49,8 +48,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|
|
49
48
|
const { dsId } = await params;
|
|
50
49
|
|
|
51
50
|
const existing = queries.getProjectDevServer(db).get(dsId) as
|
|
52
|
-
|
|
|
53
|
-
| undefined;
|
|
51
|
+
ProjectDevServer | undefined;
|
|
54
52
|
if (!existing) {
|
|
55
53
|
return NextResponse.json(
|
|
56
54
|
{ error: "Dev server config not found" },
|
package/app/page.tsx
CHANGED
|
@@ -237,8 +237,7 @@ function HomeContent() {
|
|
|
237
237
|
|
|
238
238
|
// Get terminal for a pane, with fallback to first available
|
|
239
239
|
const getTerminalWithFallback = useCallback(():
|
|
240
|
-
|
|
241
|
-
| undefined => {
|
|
240
|
+
{ terminal: TerminalHandle; paneId: string; tabId: string } | undefined => {
|
|
242
241
|
debugLog(
|
|
243
242
|
`getTerminalWithFallback called, total refs: ${terminalRefs.current.size}, focusedPaneId: ${focusedPaneId}`
|
|
244
243
|
);
|
|
@@ -141,7 +141,7 @@ export function ArtifactsPanel({
|
|
|
141
141
|
</button>
|
|
142
142
|
)}
|
|
143
143
|
</div>
|
|
144
|
-
<div className="
|
|
144
|
+
<div className="mt-2.5 flex scrollbar-none gap-1.5 overflow-x-auto">
|
|
145
145
|
{(Object.keys(FILTER_LABELS) as ArtifactFilter[])
|
|
146
146
|
.filter((f) => f === "all" || tally[f])
|
|
147
147
|
.map((f) => (
|
|
@@ -41,7 +41,7 @@ export function FileTabs({
|
|
|
41
41
|
return (
|
|
42
42
|
<div
|
|
43
43
|
ref={scrollRef}
|
|
44
|
-
className="bg-muted/30 scrollbar-none
|
|
44
|
+
className="bg-muted/30 flex scrollbar-none items-center gap-0.5 overflow-x-auto px-1"
|
|
45
45
|
>
|
|
46
46
|
{files.map((file) => {
|
|
47
47
|
const isActive = file.path === activeFilePath;
|
|
@@ -102,7 +102,7 @@ export function DesktopTabBar({
|
|
|
102
102
|
return (
|
|
103
103
|
<div
|
|
104
104
|
className={cn(
|
|
105
|
-
"
|
|
105
|
+
"@container/bar flex scrollbar-none items-center gap-1 overflow-x-auto px-1.5 py-1.5 transition-colors",
|
|
106
106
|
"shadow-[inset_0_-1px_0_hsl(var(--foreground)/0.06)]",
|
|
107
107
|
isFocused ? "bg-foreground/[0.025]" : "bg-transparent"
|
|
108
108
|
)}
|
|
@@ -110,7 +110,7 @@ export function DesktopTabBar({
|
|
|
110
110
|
{leading}
|
|
111
111
|
|
|
112
112
|
{/* Tabs */}
|
|
113
|
-
<div className="
|
|
113
|
+
<div className="flex min-w-28 flex-1 scrollbar-none items-center gap-0.5 overflow-x-auto">
|
|
114
114
|
{tabs.map((tab) => (
|
|
115
115
|
<div
|
|
116
116
|
key={tab.id}
|
|
@@ -37,14 +37,33 @@ export function QuickSwitcher({
|
|
|
37
37
|
currentSessionId,
|
|
38
38
|
activeSessionWorkingDir,
|
|
39
39
|
}: QuickSwitcherProps) {
|
|
40
|
-
const [
|
|
41
|
-
const [query,
|
|
40
|
+
const [chosenMode, setMode] = useState<"sessions" | "code">("sessions");
|
|
41
|
+
const [query, setQueryState] = useState("");
|
|
42
42
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
43
43
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
44
44
|
|
|
45
45
|
// Check if ripgrep is available
|
|
46
46
|
const { data: ripgrepAvailable } = useRipgrepAvailable();
|
|
47
47
|
|
|
48
|
+
// Code search needs ripgrep
|
|
49
|
+
const mode = ripgrepAvailable === false ? "sessions" : chosenMode;
|
|
50
|
+
|
|
51
|
+
const setQuery = (next: string) => {
|
|
52
|
+
setQueryState(next);
|
|
53
|
+
setSelectedIndex(0);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Reset state when dialog opens
|
|
57
|
+
const [wasOpen, setWasOpen] = useState(open);
|
|
58
|
+
if (open !== wasOpen) {
|
|
59
|
+
setWasOpen(open);
|
|
60
|
+
if (open) {
|
|
61
|
+
setMode("sessions");
|
|
62
|
+
setQueryState("");
|
|
63
|
+
setSelectedIndex(0);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
// Filter sessions based on search query
|
|
49
68
|
const filteredSessions = sessions.filter((session) => {
|
|
50
69
|
if (!query) return true;
|
|
@@ -56,28 +75,12 @@ export function QuickSwitcher({
|
|
|
56
75
|
);
|
|
57
76
|
});
|
|
58
77
|
|
|
59
|
-
// Reset state when dialog opens
|
|
60
78
|
useEffect(() => {
|
|
61
|
-
if (open)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
setSelectedIndex(0);
|
|
65
|
-
setTimeout(() => inputRef.current?.focus(), 50);
|
|
66
|
-
}
|
|
79
|
+
if (!open) return;
|
|
80
|
+
const t = setTimeout(() => inputRef.current?.focus(), 50);
|
|
81
|
+
return () => clearTimeout(t);
|
|
67
82
|
}, [open]);
|
|
68
83
|
|
|
69
|
-
// Force sessions mode if ripgrep is not available
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
if (ripgrepAvailable === false && mode === "code") {
|
|
72
|
-
setMode("sessions");
|
|
73
|
-
}
|
|
74
|
-
}, [ripgrepAvailable, mode]);
|
|
75
|
-
|
|
76
|
-
// Reset selected index when filtered results change
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
setSelectedIndex(0);
|
|
79
|
-
}, [query]);
|
|
80
|
-
|
|
81
84
|
// Handle keyboard navigation
|
|
82
85
|
const handleKeyDown = useCallback(
|
|
83
86
|
(e: React.KeyboardEvent) => {
|
|
@@ -32,7 +32,6 @@ import { selectionActions } from "@/stores/sessionSelection";
|
|
|
32
32
|
import { schedulesUiActions } from "@/stores/schedulesUi";
|
|
33
33
|
import { recipesUiActions } from "@/stores/recipesUi";
|
|
34
34
|
import { rowGroup } from "@/lib/recipes/chip";
|
|
35
|
-
import { finishActions } from "@/lib/sidebar/row-actions";
|
|
36
35
|
import {
|
|
37
36
|
useArchiveAction,
|
|
38
37
|
useDoneAction,
|
|
@@ -3,7 +3,13 @@ import { act, createElement } from "react";
|
|
|
3
3
|
import { createRoot, type Root } from "react-dom/client";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
|
-
for (const key of [
|
|
6
|
+
for (const key of [
|
|
7
|
+
"Event",
|
|
8
|
+
"CustomEvent",
|
|
9
|
+
"FocusEvent",
|
|
10
|
+
"HTMLButtonElement",
|
|
11
|
+
"HTMLFormElement",
|
|
12
|
+
])
|
|
7
13
|
Object.defineProperty(globalThis, key, {
|
|
8
14
|
value: (window as unknown as Record<string, unknown>)[key],
|
|
9
15
|
configurable: true,
|
|
@@ -4,7 +4,13 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
6
|
// Radix dispatches its own events; they must be jsdom's, not Node's.
|
|
7
|
-
for (const key of [
|
|
7
|
+
for (const key of [
|
|
8
|
+
"Event",
|
|
9
|
+
"CustomEvent",
|
|
10
|
+
"FocusEvent",
|
|
11
|
+
"HTMLButtonElement",
|
|
12
|
+
"HTMLFormElement",
|
|
13
|
+
])
|
|
8
14
|
Object.defineProperty(globalThis, key, {
|
|
9
15
|
value: (window as unknown as Record<string, unknown>)[key],
|
|
10
16
|
configurable: true,
|
|
@@ -380,7 +380,7 @@ export function TerminalToolbar({
|
|
|
380
380
|
onInsert={sendText}
|
|
381
381
|
/>
|
|
382
382
|
<div
|
|
383
|
-
className="bg-background/95 border-border scrollbar-none
|
|
383
|
+
className="bg-background/95 border-border flex scrollbar-none items-center gap-1 overflow-x-auto border-t px-2 py-1.5 backdrop-blur"
|
|
384
384
|
onTouchEnd={(e) => e.stopPropagation()}
|
|
385
385
|
>
|
|
386
386
|
{/* Mic button */}
|
|
@@ -295,7 +295,7 @@ function TerminalShortcutsBar({
|
|
|
295
295
|
onClose={() => setShowPasteModal(false)}
|
|
296
296
|
onPaste={handleModalPaste}
|
|
297
297
|
/>
|
|
298
|
-
<div className="scrollbar-none
|
|
298
|
+
<div className="flex scrollbar-none items-center gap-1.5 overflow-x-auto px-2 py-1.5">
|
|
299
299
|
{/* Paste button */}
|
|
300
300
|
<FastButton
|
|
301
301
|
onPress={handlePaste}
|
|
@@ -19,13 +19,7 @@ import {
|
|
|
19
19
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
|
20
20
|
|
|
21
21
|
export type WorkerStatus =
|
|
22
|
-
| "
|
|
23
|
-
| "running"
|
|
24
|
-
| "waiting"
|
|
25
|
-
| "idle"
|
|
26
|
-
| "completed"
|
|
27
|
-
| "failed"
|
|
28
|
-
| "dead";
|
|
22
|
+
"pending" | "running" | "waiting" | "idle" | "completed" | "failed" | "dead";
|
|
29
23
|
|
|
30
24
|
export interface WorkerInfo {
|
|
31
25
|
id: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Dialog,
|
|
6
6
|
DialogContent,
|
|
@@ -28,9 +28,11 @@ export function WorkspaceNameDialog({
|
|
|
28
28
|
onClose,
|
|
29
29
|
}: WorkspaceNameDialogProps) {
|
|
30
30
|
const [name, setName] = useState(initialName);
|
|
31
|
-
|
|
31
|
+
const [synced, setSynced] = useState({ open, initialName });
|
|
32
|
+
if (synced.open !== open || synced.initialName !== initialName) {
|
|
33
|
+
setSynced({ open, initialName });
|
|
32
34
|
if (open) setName(initialName);
|
|
33
|
-
}
|
|
35
|
+
}
|
|
34
36
|
|
|
35
37
|
return (
|
|
36
38
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
package/components/a/AButton.tsx
CHANGED
|
@@ -33,12 +33,7 @@ import { cn } from "@/lib/utils";
|
|
|
33
33
|
|
|
34
34
|
export type AButtonSize = "sm" | "md" | "lg";
|
|
35
35
|
export type AButtonVariant =
|
|
36
|
-
| "
|
|
37
|
-
| "ghost"
|
|
38
|
-
| "outline"
|
|
39
|
-
| "secondary"
|
|
40
|
-
| "destructive"
|
|
41
|
-
| "link";
|
|
36
|
+
"default" | "ghost" | "outline" | "secondary" | "destructive" | "link";
|
|
42
37
|
|
|
43
38
|
export interface AButtonProps extends Omit<
|
|
44
39
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
@@ -33,11 +33,7 @@ import { cn } from "@/lib/utils";
|
|
|
33
33
|
|
|
34
34
|
export type AIconButtonSize = "sm" | "md" | "lg";
|
|
35
35
|
export type AIconButtonHighlight =
|
|
36
|
-
| "
|
|
37
|
-
| "green"
|
|
38
|
-
| "red"
|
|
39
|
-
| "orange"
|
|
40
|
-
| "purple";
|
|
36
|
+
"blue" | "green" | "red" | "orange" | "purple";
|
|
41
37
|
|
|
42
38
|
export interface AIconButtonProps extends Omit<
|
|
43
39
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
package/contexts/PaneContext.tsx
CHANGED
|
@@ -27,12 +27,7 @@ import {
|
|
|
27
27
|
import { useViewport } from "@/hooks/useViewport";
|
|
28
28
|
|
|
29
29
|
export type ViewMode =
|
|
30
|
-
| "
|
|
31
|
-
| "files"
|
|
32
|
-
| "git"
|
|
33
|
-
| "workers"
|
|
34
|
-
| "log"
|
|
35
|
-
| "artifacts";
|
|
30
|
+
"terminal" | "files" | "git" | "workers" | "log" | "artifacts";
|
|
36
31
|
|
|
37
32
|
export interface PaneViewState {
|
|
38
33
|
viewMode: ViewMode;
|
package/data/lumifyhub/index.ts
CHANGED
|
@@ -86,8 +86,7 @@ export function useLhWorkspaces(enabled: boolean) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export type WorkspaceLinkTarget =
|
|
89
|
-
| {
|
|
90
|
-
| { create: true; name?: string };
|
|
89
|
+
{ lhWorkspaceId: string } | { create: true; name?: string };
|
|
91
90
|
|
|
92
91
|
export function useLinkWorkspace() {
|
|
93
92
|
return useInvalidating(
|
|
@@ -120,8 +119,7 @@ export function useWorkspaceBoards(workspaceId: string | null) {
|
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
export type BoardLinkTarget =
|
|
123
|
-
| {
|
|
124
|
-
| { create: true; title?: string };
|
|
122
|
+
{ boardId: string } | { create: true; title?: string };
|
|
125
123
|
|
|
126
124
|
export function useLinkBoard() {
|
|
127
125
|
return useInvalidating(
|
|
@@ -32,13 +32,14 @@ export function useDirectoryBrowser(options: UseDirectoryBrowserOptions = {}) {
|
|
|
32
32
|
const currentPath = data?.resolvedPath || requestedPath;
|
|
33
33
|
|
|
34
34
|
// Filter and sort files from query data
|
|
35
|
+
const dataFiles = data?.files;
|
|
35
36
|
const files = useMemo(() => {
|
|
36
|
-
if (!
|
|
37
|
+
if (!dataFiles) return [];
|
|
37
38
|
const items = filterRef.current
|
|
38
|
-
?
|
|
39
|
-
:
|
|
39
|
+
? dataFiles.filter(filterRef.current)
|
|
40
|
+
: dataFiles;
|
|
40
41
|
return sortFiles(items);
|
|
41
|
-
}, [
|
|
42
|
+
}, [dataFiles]);
|
|
42
43
|
|
|
43
44
|
const filteredFiles = useMemo(
|
|
44
45
|
() =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Hook for smooth drawer enter animations.
|
|
@@ -6,22 +6,19 @@ import { useState, useEffect, useRef } from "react";
|
|
|
6
6
|
*/
|
|
7
7
|
export function useDrawerAnimation(open: boolean) {
|
|
8
8
|
const [isAnimatingIn, setIsAnimatingIn] = useState(false);
|
|
9
|
-
const hasAnimated = useRef(false);
|
|
10
9
|
|
|
11
10
|
useEffect(() => {
|
|
12
|
-
if (open
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!open) {
|
|
21
|
-
hasAnimated.current = false;
|
|
11
|
+
if (!open) return;
|
|
12
|
+
let inner = 0;
|
|
13
|
+
const outer = requestAnimationFrame(() => {
|
|
14
|
+
inner = requestAnimationFrame(() => setIsAnimatingIn(true));
|
|
15
|
+
});
|
|
16
|
+
return () => {
|
|
17
|
+
cancelAnimationFrame(outer);
|
|
18
|
+
cancelAnimationFrame(inner);
|
|
22
19
|
setIsAnimatingIn(false);
|
|
23
|
-
}
|
|
20
|
+
};
|
|
24
21
|
}, [open]);
|
|
25
22
|
|
|
26
|
-
return isAnimatingIn;
|
|
23
|
+
return open && isAnimatingIn;
|
|
27
24
|
}
|
package/hooks/useFileDrop.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback,
|
|
3
|
+
import { useState, useCallback, type RefObject } from "react";
|
|
4
4
|
|
|
5
5
|
interface UseFileDropOptions {
|
|
6
6
|
/** Disable drop handling (e.g., while uploading) */
|
|
@@ -29,11 +29,7 @@ export function useFileDrop(
|
|
|
29
29
|
const [isDragging, setIsDragging] = useState(false);
|
|
30
30
|
|
|
31
31
|
// Reset drag state when disabled
|
|
32
|
-
|
|
33
|
-
if (options?.disabled) {
|
|
34
|
-
setIsDragging(false);
|
|
35
|
-
}
|
|
36
|
-
}, [options?.disabled]);
|
|
32
|
+
if (options?.disabled && isDragging) setIsDragging(false);
|
|
37
33
|
|
|
38
34
|
const handleDragOver = useCallback(
|
|
39
35
|
(e: React.DragEvent) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useStoredFlag } from "./useStoredFlag";
|
|
4
5
|
|
|
5
6
|
const STORAGE_KEY = "agentOS-keybar-visible";
|
|
6
7
|
|
|
@@ -9,34 +10,14 @@ const STORAGE_KEY = "agentOS-keybar-visible";
|
|
|
9
10
|
* Default: hidden on mobile to maximize terminal space.
|
|
10
11
|
*/
|
|
11
12
|
export function useKeybarVisibility() {
|
|
12
|
-
const [isVisible,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
}, []);
|
|
22
|
-
|
|
23
|
-
const toggle = useCallback(() => {
|
|
24
|
-
setIsVisible((prev) => {
|
|
25
|
-
const next = !prev;
|
|
26
|
-
localStorage.setItem(STORAGE_KEY, String(next));
|
|
27
|
-
return next;
|
|
28
|
-
});
|
|
29
|
-
}, []);
|
|
30
|
-
|
|
31
|
-
const show = useCallback(() => {
|
|
32
|
-
setIsVisible(true);
|
|
33
|
-
localStorage.setItem(STORAGE_KEY, "true");
|
|
34
|
-
}, []);
|
|
35
|
-
|
|
36
|
-
const hide = useCallback(() => {
|
|
37
|
-
setIsVisible(false);
|
|
38
|
-
localStorage.setItem(STORAGE_KEY, "false");
|
|
39
|
-
}, []);
|
|
13
|
+
const [isVisible, setVisible] = useStoredFlag(STORAGE_KEY, false);
|
|
14
|
+
|
|
15
|
+
const toggle = useCallback(
|
|
16
|
+
() => setVisible(!isVisible),
|
|
17
|
+
[isVisible, setVisible]
|
|
18
|
+
);
|
|
19
|
+
const show = useCallback(() => setVisible(true), [setVisible]);
|
|
20
|
+
const hide = useCallback(() => setVisible(false), [setVisible]);
|
|
40
21
|
|
|
41
22
|
return { isVisible, toggle, show, hide };
|
|
42
23
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useStoredFlag } from "./useStoredFlag";
|
|
4
5
|
|
|
5
6
|
const STORAGE_KEY = "agentOS-sidebar-pinned";
|
|
6
7
|
|
|
@@ -11,29 +12,12 @@ const STORAGE_KEY = "agentOS-sidebar-pinned";
|
|
|
11
12
|
* reveals on hover near the left edge.
|
|
12
13
|
*/
|
|
13
14
|
export function useSidebarPinned() {
|
|
14
|
-
const [isPinned,
|
|
15
|
+
const [isPinned, setPinned] = useStoredFlag(STORAGE_KEY, true);
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (stored === "false") {
|
|
21
|
-
setIsPinned(false);
|
|
22
|
-
}
|
|
23
|
-
}, []);
|
|
24
|
-
|
|
25
|
-
const setPinned = useCallback((pinned: boolean) => {
|
|
26
|
-
setIsPinned(pinned);
|
|
27
|
-
localStorage.setItem(STORAGE_KEY, String(pinned));
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
30
|
-
const togglePin = useCallback(() => {
|
|
31
|
-
setIsPinned((prev) => {
|
|
32
|
-
const next = !prev;
|
|
33
|
-
localStorage.setItem(STORAGE_KEY, String(next));
|
|
34
|
-
return next;
|
|
35
|
-
});
|
|
36
|
-
}, []);
|
|
17
|
+
const togglePin = useCallback(
|
|
18
|
+
() => setPinned(!isPinned),
|
|
19
|
+
[isPinned, setPinned]
|
|
20
|
+
);
|
|
37
21
|
|
|
38
22
|
return { isPinned, togglePin, setPinned };
|
|
39
23
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useState,
|
|
5
|
+
useRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useSyncExternalStore,
|
|
9
|
+
} from "react";
|
|
4
10
|
|
|
5
11
|
// Web Speech API types
|
|
6
12
|
interface SpeechRecognitionEvent extends Event {
|
|
@@ -46,19 +52,27 @@ declare global {
|
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
const noopSubscribe = () => () => {};
|
|
56
|
+
|
|
57
|
+
function getSpeechRecognitionAPI() {
|
|
58
|
+
return window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
export function useSpeechRecognition(
|
|
50
62
|
onTranscript: (text: string, isFinal: boolean) => void
|
|
51
63
|
) {
|
|
52
64
|
const [isListening, setIsListening] = useState(false);
|
|
53
|
-
const
|
|
65
|
+
const isSupported = useSyncExternalStore(
|
|
66
|
+
noopSubscribe,
|
|
67
|
+
() => !!getSpeechRecognitionAPI(),
|
|
68
|
+
() => false
|
|
69
|
+
);
|
|
54
70
|
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
|
55
71
|
const lastTranscriptRef = useRef("");
|
|
56
72
|
|
|
57
73
|
useEffect(() => {
|
|
58
74
|
// Check for browser support
|
|
59
|
-
const SpeechRecognitionAPI =
|
|
60
|
-
window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
61
|
-
setIsSupported(!!SpeechRecognitionAPI);
|
|
75
|
+
const SpeechRecognitionAPI = getSpeechRecognitionAPI();
|
|
62
76
|
|
|
63
77
|
if (SpeechRecognitionAPI) {
|
|
64
78
|
const recognition = new SpeechRecognitionAPI();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
4
|
+
|
|
5
|
+
const listeners = new Set<() => void>();
|
|
6
|
+
|
|
7
|
+
function subscribe(fn: () => void) {
|
|
8
|
+
listeners.add(fn);
|
|
9
|
+
window.addEventListener("storage", fn);
|
|
10
|
+
return () => {
|
|
11
|
+
listeners.delete(fn);
|
|
12
|
+
window.removeEventListener("storage", fn);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function read(key: string): string | null {
|
|
17
|
+
try {
|
|
18
|
+
return localStorage.getItem(key);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A boolean kept in localStorage, shared by every caller on the page. The
|
|
25
|
+
// server and the hydrating render see `fallback`; the stored value follows.
|
|
26
|
+
export function useStoredFlag(
|
|
27
|
+
key: string,
|
|
28
|
+
fallback: boolean
|
|
29
|
+
): [boolean, (next: boolean) => void] {
|
|
30
|
+
const value = useSyncExternalStore(
|
|
31
|
+
subscribe,
|
|
32
|
+
() => read(key),
|
|
33
|
+
() => null
|
|
34
|
+
);
|
|
35
|
+
const set = useCallback(
|
|
36
|
+
(next: boolean) => {
|
|
37
|
+
try {
|
|
38
|
+
localStorage.setItem(key, String(next));
|
|
39
|
+
} catch {
|
|
40
|
+
// Storage unavailable: the choice lasts until reload.
|
|
41
|
+
}
|
|
42
|
+
listeners.forEach((fn) => fn());
|
|
43
|
+
},
|
|
44
|
+
[key]
|
|
45
|
+
);
|
|
46
|
+
return [value === null ? fallback : value === "true", set];
|
|
47
|
+
}
|
package/hooks/useViewport.ts
CHANGED
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import { useMediaQuery } from "./useMediaQuery";
|
|
5
|
+
|
|
6
|
+
const noop = () => () => {};
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Mobile-first viewport detection hook
|
|
7
10
|
* Breakpoint: 768px (md in Tailwind)
|
|
8
11
|
*/
|
|
9
12
|
export function useViewport() {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Initial check
|
|
19
|
-
checkViewport();
|
|
20
|
-
setIsHydrated(true);
|
|
21
|
-
|
|
22
|
-
// Listen for resize
|
|
23
|
-
window.addEventListener("resize", checkViewport);
|
|
24
|
-
return () => window.removeEventListener("resize", checkViewport);
|
|
25
|
-
}, []);
|
|
13
|
+
const isMobile = useMediaQuery("(max-width: 767.98px)");
|
|
14
|
+
// False on the server and while hydrating, true after.
|
|
15
|
+
const isHydrated = useSyncExternalStore(
|
|
16
|
+
noop,
|
|
17
|
+
() => true,
|
|
18
|
+
() => false
|
|
19
|
+
);
|
|
26
20
|
|
|
27
21
|
return {
|
|
28
22
|
isMobile,
|
|
@@ -14,13 +14,7 @@ import { markAllStatusDirty } from "../status/dirty";
|
|
|
14
14
|
export const NO_APPROVALS_KEY = "approvals.none";
|
|
15
15
|
|
|
16
16
|
export type AuditSource =
|
|
17
|
-
| "
|
|
18
|
-
| "brake"
|
|
19
|
-
| "plan"
|
|
20
|
-
| "chat"
|
|
21
|
-
| "terminal"
|
|
22
|
-
| "merge-approval"
|
|
23
|
-
| "mode";
|
|
17
|
+
"ask" | "brake" | "plan" | "chat" | "terminal" | "merge-approval" | "mode";
|
|
24
18
|
|
|
25
19
|
export interface AuditRow {
|
|
26
20
|
id: number;
|
package/lib/artifacts/store.ts
CHANGED
|
@@ -54,8 +54,7 @@ export function createArtifact(
|
|
|
54
54
|
export function getArtifact(id: string): Artifact | undefined {
|
|
55
55
|
if (!ID.test(id)) return undefined;
|
|
56
56
|
return db.prepare(`SELECT * FROM artifacts WHERE id = ?`).get(id) as
|
|
57
|
-
|
|
|
58
|
-
| undefined;
|
|
57
|
+
Artifact | undefined;
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
export function listArtifacts(sessionId: string): Artifact[] {
|
package/lib/away/report.ts
CHANGED
|
@@ -150,8 +150,7 @@ function merged(
|
|
|
150
150
|
);
|
|
151
151
|
return rows.map((r) => {
|
|
152
152
|
const done = action.get(away.id, `deploy:${r.id}`) as
|
|
153
|
-
|
|
154
|
-
| undefined;
|
|
153
|
+
{ kind: string; detail: string | null } | undefined;
|
|
155
154
|
const deploy: DeployState =
|
|
156
155
|
done?.kind === "deploy_ok"
|
|
157
156
|
? "deployed"
|