@saadnvd1/agent-os 0.3.684 → 0.3.692

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.
Files changed (130) hide show
  1. package/app/api/dev-servers/detect/route.ts +1 -2
  2. package/app/api/discovered-models/route.ts +34 -0
  3. package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +2 -4
  4. package/app/page.tsx +1 -2
  5. package/components/Artifacts/ArtifactsPanel.tsx +1 -1
  6. package/components/Draft/DraftPanel.tsx +10 -2
  7. package/components/Draft/draftModels.test.ts +31 -0
  8. package/components/Draft/draftModels.ts +17 -0
  9. package/components/FileExplorer/FileTabs.tsx +1 -1
  10. package/components/Pane/DesktopTabBar.tsx +2 -2
  11. package/components/QuickSwitcher.tsx +24 -21
  12. package/components/SessionList/SessionRowMenu.tsx +0 -1
  13. package/components/Settings/ExperimentsSection.test.ts +7 -1
  14. package/components/Settings/NoApprovalsSection.test.ts +7 -1
  15. package/components/Terminal/TerminalToolbar.tsx +1 -1
  16. package/components/Terminal/VirtualKeyboard.tsx +1 -1
  17. package/components/WorkerCard.tsx +1 -7
  18. package/components/Workspaces/WorkspaceNameDialog.tsx +5 -3
  19. package/components/a/AButton.tsx +1 -6
  20. package/components/a/AIconButton.tsx +1 -5
  21. package/contexts/PaneContext.tsx +1 -6
  22. package/data/agents/index.ts +37 -0
  23. package/data/lumifyhub/index.ts +2 -4
  24. package/hooks/useDirectoryBrowser.ts +5 -4
  25. package/hooks/useDrawerAnimation.ts +11 -14
  26. package/hooks/useFileDrop.ts +2 -6
  27. package/hooks/useKeybarVisibility.ts +10 -29
  28. package/hooks/useSidebarPinned.ts +7 -23
  29. package/hooks/useSpeechRecognition.ts +19 -5
  30. package/hooks/useStoredFlag.ts +47 -0
  31. package/hooks/useViewport.ts +11 -17
  32. package/lib/approvals/no-approvals.ts +1 -7
  33. package/lib/artifacts/store.ts +1 -2
  34. package/lib/away/report.ts +1 -2
  35. package/lib/away/run.ts +1 -2
  36. package/lib/away/state.ts +1 -2
  37. package/lib/bookmarks/store.ts +1 -2
  38. package/lib/bus/delivery.ts +1 -2
  39. package/lib/bus/index.ts +1 -2
  40. package/lib/chat/compact-context.ts +1 -2
  41. package/lib/chat/discovered-models.ts +87 -0
  42. package/lib/chat/stop.ts +1 -2
  43. package/lib/chat/worker/host.ts +1 -1
  44. package/lib/code-health/reports.ts +1 -2
  45. package/lib/db/lumifyhub.ts +2 -4
  46. package/lib/db/schedules-move.ts +1 -2
  47. package/lib/db/session-parts/tasks.ts +1 -2
  48. package/lib/db/stacks.ts +3 -9
  49. package/lib/db/types.ts +1 -7
  50. package/lib/doctor/checks.ts +7 -9
  51. package/lib/done/archive.ts +1 -2
  52. package/lib/experiments/meter.ts +1 -7
  53. package/lib/health/env/classes.ts +0 -2
  54. package/lib/health/env/recordHit.ts +1 -2
  55. package/lib/health/spin.ts +1 -4
  56. package/lib/health/store.ts +1 -2
  57. package/lib/hosts/index.ts +1 -2
  58. package/lib/hosts/peer-sync.ts +2 -4
  59. package/lib/hosts/remote-api.ts +1 -2
  60. package/lib/lumifyhub/types.ts +2 -8
  61. package/lib/model-catalog.ts +9 -0
  62. package/lib/notify/index.ts +1 -2
  63. package/lib/notify/push/index.ts +2 -7
  64. package/lib/orchestrator/external-pr.ts +1 -2
  65. package/lib/orchestrator/gates.ts +1 -6
  66. package/lib/orchestrator/needs-you-items.ts +16 -20
  67. package/lib/orchestrator/pr-wake.ts +1 -2
  68. package/lib/orchestrator/rebase.ts +1 -2
  69. package/lib/orchestrator/review.ts +1 -2
  70. package/lib/orchestrator/usage.ts +1 -2
  71. package/lib/plans/claim.ts +1 -2
  72. package/lib/plans/features.ts +4 -11
  73. package/lib/plans/release.ts +2 -4
  74. package/lib/ports.ts +1 -2
  75. package/lib/program-status/records.ts +1 -1
  76. package/lib/project-config/database.ts +1 -2
  77. package/lib/projects.ts +1 -2
  78. package/lib/recipes/settle.ts +2 -4
  79. package/lib/recipes/store.ts +2 -4
  80. package/lib/schedules/store.ts +2 -4
  81. package/lib/search/conversations/backfill.ts +2 -4
  82. package/lib/security/auth.ts +1 -2
  83. package/lib/security/passkeys.ts +1 -2
  84. package/lib/security/presence.ts +1 -7
  85. package/lib/session-artifacts/types.ts +1 -8
  86. package/lib/session-audit/transcript.ts +1 -3
  87. package/lib/session-logs/index.ts +1 -2
  88. package/lib/sessions/launch.ts +13 -7
  89. package/lib/share/snapshot.ts +1 -7
  90. package/lib/share/store.ts +1 -2
  91. package/lib/sidebar/orchestrator-row.ts +1 -6
  92. package/lib/sidebar/row-menu.ts +1 -2
  93. package/lib/sidebar/shelves.ts +1 -6
  94. package/lib/simulator/index.ts +1 -2
  95. package/lib/simulator/lease.ts +1 -5
  96. package/lib/stacks/index.ts +1 -1
  97. package/lib/stacks/plan.ts +1 -1
  98. package/lib/stacks/restack.ts +1 -2
  99. package/lib/stacks/tick.ts +1 -2
  100. package/lib/status/socket.ts +1 -2
  101. package/lib/tasks/contract/authorize.ts +1 -2
  102. package/lib/tasks/contract/plan.ts +1 -2
  103. package/lib/tasks/contract/sync.ts +1 -2
  104. package/lib/tasks/holds.ts +1 -5
  105. package/lib/tasks/index.ts +1 -2
  106. package/lib/tasks/loop-types.ts +1 -6
  107. package/lib/tasks/move-guard.ts +1 -2
  108. package/lib/tasks/move.ts +1 -2
  109. package/lib/tasks/moved-away.ts +1 -2
  110. package/lib/tasks/pause-hold.ts +1 -2
  111. package/lib/tasks/pause.ts +3 -6
  112. package/lib/tasks/pr-activity.ts +1 -3
  113. package/lib/tasks/queue-rows.ts +2 -4
  114. package/lib/tasks/queue-types.ts +1 -5
  115. package/lib/tasks/queue.ts +1 -2
  116. package/lib/tasks/refresh-base.ts +1 -2
  117. package/lib/tasks/start.ts +2 -4
  118. package/lib/tasks/starter.ts +1 -2
  119. package/lib/tasks/state.ts +1 -6
  120. package/lib/theme-config.ts +1 -8
  121. package/lib/updates/banner.ts +1 -6
  122. package/lib/updates/run.ts +1 -2
  123. package/lib/usage/aggregate.ts +1 -1
  124. package/lib/usage/nightly.ts +1 -2
  125. package/lib/usage/rate-limits.ts +1 -2
  126. package/lib/usage/turns.ts +1 -2
  127. package/lib/usage/windows.ts +5 -7
  128. package/lib/workspaces.ts +1 -2
  129. package/lib/worktree-sweep/databases.ts +0 -1
  130. 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
- | Project
20
- | undefined;
19
+ Project | undefined;
21
20
  if (!project) {
22
21
  return NextResponse.json({ error: "Project not found" }, { status: 404 });
23
22
  }
@@ -0,0 +1,34 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { discoveredModels } from "@/lib/chat/discovered-models";
4
+ import { getProject } from "@/lib/projects";
5
+
6
+ const query = z.object({
7
+ agent: z.string().min(1),
8
+ projectId: z.string().optional(),
9
+ refresh: z.string().optional(),
10
+ });
11
+
12
+ // GET /api/discovered-models?agent=&projectId=&refresh=1 - the models an agent
13
+ // reports for a project's folder, for a draft that has no session yet. Empty
14
+ // for an agent with a fixed list.
15
+ export async function GET(request: NextRequest) {
16
+ const parsed = query.safeParse(
17
+ Object.fromEntries(request.nextUrl.searchParams)
18
+ );
19
+ if (!parsed.success)
20
+ return NextResponse.json({ error: "agent is required" }, { status: 400 });
21
+ const { agent, projectId, refresh } = parsed.data;
22
+ const folder = projectId ? getProject(projectId)?.working_directory : null;
23
+ try {
24
+ return NextResponse.json({
25
+ models: await discoveredModels(agent, folder, refresh !== undefined),
26
+ });
27
+ } catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return NextResponse.json(
30
+ { error: `Couldn't list the models: ${message}` },
31
+ { status: 502 }
32
+ );
33
+ }
34
+ }
@@ -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
- | ProjectDevServer
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
- | ProjectDevServer
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
- | { terminal: TerminalHandle; paneId: string; tabId: string }
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="scrollbar-none mt-2.5 flex gap-1.5 overflow-x-auto">
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) => (
@@ -8,7 +8,8 @@ import { Button } from "@/components/ui/button";
8
8
  import type { ChatFile, ChatImage } from "@/lib/chat/events";
9
9
  import type { Project } from "@/lib/db";
10
10
  import { draftComposerKey, type Draft } from "@/lib/drafts";
11
- import { getModelOptions } from "@/lib/model-catalog";
11
+ import { discoversModels } from "@/lib/model-catalog";
12
+ import { useDiscoveredModels } from "@/data/agents";
12
13
  import { useHostsQuery } from "@/data/hosts";
13
14
  import { useGitCheck } from "@/data/git/queries";
14
15
  import { useLaunchSession } from "@/data/sessions";
@@ -18,6 +19,7 @@ import { setPendingPrompt } from "@/stores/initialPrompt";
18
19
  import { draftsActions, draftsStore, newDraft } from "@/stores/drafts";
19
20
  import { DraftChips } from "./DraftChips";
20
21
  import { DraftHeading } from "./DraftHeading";
22
+ import { draftModels } from "./draftModels";
21
23
 
22
24
  // A new session before its first send: choices on top, a composer below.
23
25
  // Sending makes the session (and its worktree) and turns this tab into it.
@@ -41,6 +43,12 @@ export function DraftPanel({
41
43
  const { data: git } = useGitCheck(
42
44
  project && draft?.hostId === "local" ? project.working_directory : ""
43
45
  );
46
+ const discovers = !!draft && discoversModels(draft.agentType);
47
+ const found = useDiscoveredModels(
48
+ draft?.agentType ?? "",
49
+ draft?.projectId ?? null,
50
+ discovers
51
+ );
44
52
  const launch = useLaunchSession();
45
53
  const createTask = useCreateTask();
46
54
  const [prefill, setPrefill] = useState<{ text: string; at: number }>();
@@ -148,7 +156,7 @@ export function DraftPanel({
148
156
  }
149
157
  onSend={(text, images, files) => void send(text, images, files)}
150
158
  onStop={() => {}}
151
- models={getModelOptions(draft.agentType)}
159
+ models={draftModels(draft.agentType, found)}
152
160
  model={draft.model}
153
161
  onSetModel={(model) => draftsActions.update(draft.id, { model })}
154
162
  access={draft.openPr ? undefined : draft.access}
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { draftModels } from "./draftModels";
3
+
4
+ const qwen = { value: "ollama/qwen3", label: "Qwen 3" };
5
+
6
+ describe("draftModels", () => {
7
+ it("lists the default and then what OpenCode reports", () => {
8
+ const list = draftModels("opencode", {
9
+ data: [qwen],
10
+ isPending: false,
11
+ isError: false,
12
+ });
13
+ expect(list.map((m) => m.value)).toEqual(["default", "ollama/qwen3"]);
14
+ });
15
+
16
+ it("says so beside the default while loading and on failure", () => {
17
+ const loading = draftModels("opencode", {
18
+ isPending: true,
19
+ isError: false,
20
+ });
21
+ const failed = draftModels("opencode", { isPending: false, isError: true });
22
+ expect(loading).toHaveLength(1);
23
+ expect(loading[0].label).toContain("loading");
24
+ expect(failed[0].label).toContain("couldn't");
25
+ });
26
+
27
+ it("keeps a fixed list as it is", () => {
28
+ const list = draftModels("claude", { isPending: true, isError: false });
29
+ expect(list.map((m) => m.value)).toContain("opus");
30
+ });
31
+ });
@@ -0,0 +1,17 @@
1
+ import type { ChatModel } from "@/lib/chat/events";
2
+ import type { Draft } from "@/lib/drafts";
3
+ import { discoversModels, getModelOptions } from "@/lib/model-catalog";
4
+
5
+ // A fixed list, or the agent's default followed by what it reports; while it
6
+ // loads or if it fails, the default alone says so.
7
+ export function draftModels(
8
+ agentType: Draft["agentType"],
9
+ found: { data?: ChatModel[]; isPending: boolean; isError: boolean }
10
+ ): ChatModel[] {
11
+ const options = getModelOptions(agentType);
12
+ if (!discoversModels(agentType)) return options;
13
+ if (found.data) return [...options, ...found.data];
14
+ const [first, ...rest] = options;
15
+ const note = found.isError ? "couldn't list models" : "loading models…";
16
+ return [{ ...first, label: `${first.label} (${note})` }, ...rest];
17
+ }
@@ -41,7 +41,7 @@ export function FileTabs({
41
41
  return (
42
42
  <div
43
43
  ref={scrollRef}
44
- className="bg-muted/30 scrollbar-none flex items-center gap-0.5 overflow-x-auto px-1"
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
- "scrollbar-none @container/bar flex items-center gap-1 overflow-x-auto px-1.5 py-1.5 transition-colors",
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="scrollbar-none flex min-w-28 flex-1 items-center gap-0.5 overflow-x-auto">
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 [mode, setMode] = useState<"sessions" | "code">("sessions");
41
- const [query, setQuery] = useState("");
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
- setMode("sessions");
63
- setQuery("");
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 ["Event", "CustomEvent", "FocusEvent", "HTMLButtonElement"])
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 ["Event", "CustomEvent", "FocusEvent", "HTMLButtonElement"])
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 flex items-center gap-1 overflow-x-auto border-t px-2 py-1.5 backdrop-blur"
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 flex items-center gap-1.5 overflow-x-auto px-2 py-1.5">
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
- | "pending"
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 { useEffect, useState } from "react";
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
- useEffect(() => {
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
- }, [open, initialName]);
35
+ }
34
36
 
35
37
  return (
36
38
  <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
@@ -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
- | "default"
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
- | "blue"
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>,
@@ -27,12 +27,7 @@ import {
27
27
  import { useViewport } from "@/hooks/useViewport";
28
28
 
29
29
  export type ViewMode =
30
- | "terminal"
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;
@@ -1,5 +1,6 @@
1
1
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import type { AgentProbe } from "@/lib/agents/probe";
3
+ import type { ChatModel } from "@/lib/chat/events";
3
4
 
4
5
  export const agentKeys = {
5
6
  all: ["agents"] as const,
@@ -35,3 +36,39 @@ export function useRecheckAgents() {
35
36
  onSuccess: (agents) => client.setQueryData(agentKeys.status(), agents),
36
37
  });
37
38
  }
39
+
40
+ export const discoveredModelKeys = {
41
+ list: (agent: string, projectId: string | null) =>
42
+ [...agentKeys.all, "models", agent, projectId] as const,
43
+ };
44
+
45
+ // What an agent without a fixed list reports it can run, for a draft's
46
+ // picker. Off for agents with a fixed list.
47
+ export function useDiscoveredModels(
48
+ agent: string,
49
+ projectId: string | null,
50
+ enabled: boolean
51
+ ) {
52
+ const client = useQueryClient();
53
+ const key = discoveredModelKeys.list(agent, projectId);
54
+ const fetchModels = async (refresh: boolean) => {
55
+ const params = new URLSearchParams({ agent });
56
+ if (projectId) params.set("projectId", projectId);
57
+ if (refresh) params.set("refresh", "1");
58
+ const res = await fetch(`/api/discovered-models?${params}`);
59
+ const data = await res.json().catch(() => ({}));
60
+ if (!res.ok) throw new Error(data.error || "Couldn't list the models");
61
+ return data.models as ChatModel[];
62
+ };
63
+ const query = useQuery({
64
+ queryKey: key,
65
+ queryFn: () => fetchModels(false),
66
+ enabled,
67
+ staleTime: 5 * 60_000,
68
+ retry: false,
69
+ });
70
+ return {
71
+ ...query,
72
+ refresh: async () => client.setQueryData(key, await fetchModels(true)),
73
+ };
74
+ }
@@ -86,8 +86,7 @@ export function useLhWorkspaces(enabled: boolean) {
86
86
  }
87
87
 
88
88
  export type WorkspaceLinkTarget =
89
- | { lhWorkspaceId: string }
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
- | { boardId: string }
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 (!data?.files) return [];
37
+ if (!dataFiles) return [];
37
38
  const items = filterRef.current
38
- ? data.files.filter(filterRef.current)
39
- : data.files;
39
+ ? dataFiles.filter(filterRef.current)
40
+ : dataFiles;
40
41
  return sortFiles(items);
41
- }, [data?.files]);
42
+ }, [dataFiles]);
42
43
 
43
44
  const filteredFiles = useMemo(
44
45
  () =>
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef } from "react";
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 && !hasAnimated.current) {
13
- hasAnimated.current = true;
14
- requestAnimationFrame(() => {
15
- requestAnimationFrame(() => {
16
- setIsAnimatingIn(true);
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
  }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useCallback, useEffect, type RefObject } from "react";
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
- useEffect(() => {
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 { useState, useCallback, useEffect } from "react";
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, setIsVisible] = useState(false);
13
-
14
- // Load persisted state on mount
15
- useEffect(() => {
16
- if (typeof window === "undefined") return;
17
- const stored = localStorage.getItem(STORAGE_KEY);
18
- if (stored === "true") {
19
- setIsVisible(true);
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
  }