@saadnvd1/agent-os 0.3.698 → 0.3.700

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 (65) hide show
  1. package/app/api/bus/send/route.ts +11 -1
  2. package/app/api/bus/wait/route.ts +22 -0
  3. package/app/api/peer/hello/reveal/route.ts +6 -0
  4. package/app/api/peer/hello/route.ts +9 -0
  5. package/app/api/peer/hello/status/route.ts +6 -0
  6. package/app/api/peers/account/route.ts +9 -0
  7. package/app/api/peers/gate.test.ts +111 -0
  8. package/app/api/peers/link/[id]/confirm/route.ts +27 -0
  9. package/app/api/peers/link/[id]/route.ts +20 -0
  10. package/app/api/peers/link/route.ts +23 -0
  11. package/app/api/peers/pending/[id]/accept/route.ts +31 -0
  12. package/app/api/peers/pending/[id]/decline/route.ts +13 -0
  13. package/app/api/peers/pending/route.ts +7 -0
  14. package/app/api/sessions/route.ts +18 -1
  15. package/app/page.tsx +2 -0
  16. package/components/Hosts/AccountMachines.tsx +180 -0
  17. package/components/Hosts/HostRow.tsx +44 -35
  18. package/components/Hosts/HostsDialog.tsx +49 -37
  19. package/components/Hosts/IncomingLinkSheet.tsx +228 -0
  20. package/components/Hosts/LinkCodeSheet.tsx +186 -0
  21. package/components/Hosts/sheet.ts +3 -0
  22. package/components/SessionList/RowSubtitle.tsx +11 -1
  23. package/components/Welcome/ConnectBlock.tsx +1 -1
  24. package/data/peers/index.ts +2 -0
  25. package/data/peers/keys.ts +6 -0
  26. package/data/peers/queries.ts +141 -0
  27. package/lib/agents/brief.ts +3 -1
  28. package/lib/bus/format.ts +25 -4
  29. package/lib/bus/index.ts +113 -106
  30. package/lib/bus/resolve.ts +16 -2
  31. package/lib/bus/targets.ts +122 -0
  32. package/lib/bus/waits/kick.ts +12 -0
  33. package/lib/bus/waits/lines.ts +69 -0
  34. package/lib/bus/waits/register.ts +83 -0
  35. package/lib/bus/waits/store.ts +196 -0
  36. package/lib/bus/waits/tick.ts +192 -0
  37. package/lib/connect/control-api.ts +108 -2
  38. package/lib/connect/heartbeat.ts +11 -3
  39. package/lib/connect/serve.ts +2 -0
  40. package/lib/db/migrations/20261020T1600_bus_waits.ts +35 -0
  41. package/lib/db/types.ts +4 -0
  42. package/lib/done/archive.ts +2 -0
  43. package/lib/orchestrator/describe.ts +13 -0
  44. package/lib/orchestrator/facts.ts +17 -0
  45. package/lib/peer/account.ts +198 -0
  46. package/lib/peer/cli.ts +24 -0
  47. package/lib/peer/code.ts +3 -0
  48. package/lib/peer/hello-route.ts +32 -0
  49. package/lib/peer/hello.ts +346 -0
  50. package/lib/peer/link-cli.ts +233 -0
  51. package/lib/peer/machines.ts +72 -0
  52. package/lib/peer/notify.ts +32 -0
  53. package/lib/peer/pairing-out.ts +382 -0
  54. package/lib/peer/pairing.ts +403 -0
  55. package/lib/peer/pin.ts +55 -0
  56. package/lib/peer/rpc-intake.ts +37 -8
  57. package/lib/peer/rpc-server.ts +19 -1
  58. package/lib/schedules/when.ts +1 -1
  59. package/lib/security/auth.ts +10 -2
  60. package/lib/security/gate.ts +3 -0
  61. package/lib/sessions/end.ts +3 -0
  62. package/package.json +1 -1
  63. package/scripts/agent-os +2 -0
  64. package/scripts/lib/commands.sh +2 -0
  65. package/server.ts +3 -0
@@ -11,6 +11,7 @@ import {
11
11
  import { Button } from "@/components/ui/button";
12
12
  import { Input } from "@/components/ui/input";
13
13
  import { useHostsQuery, useCreateHost } from "@/data/hosts";
14
+ import { AccountMachines } from "./AccountMachines";
14
15
  import { HostRow } from "./HostRow";
15
16
 
16
17
  interface HostsDialogProps {
@@ -39,17 +40,19 @@ export function HostsDialog({ open, onClose }: HostsDialogProps) {
39
40
 
40
41
  return (
41
42
  <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
42
- <DialogContent className="max-h-[90vh] max-w-lg overflow-y-auto">
43
+ <DialogContent className="max-h-[90vh] max-w-lg overflow-y-auto [&>*]:min-w-0">
43
44
  <DialogHeader>
44
45
  <DialogTitle>Machines</DialogTitle>
45
46
  <DialogDescription>
46
- Sessions run in tmux on any machine you can reach with ssh keys.
47
- Projects pick a machine; tmux sessions already running there show up
48
- under the project whose folder they are in.
47
+ Link your other machines to start tasks on them and move tasks
48
+ between them. Through AgentOS Connect it works over any network.
49
49
  </DialogDescription>
50
50
  </DialogHeader>
51
51
 
52
+ <AccountMachines />
53
+
52
54
  <div className="space-y-2">
55
+ <p className="text-sm font-medium">Machines here</p>
53
56
  {isPending && (
54
57
  <div className="bg-muted/40 h-14 animate-pulse rounded-lg" />
55
58
  )}
@@ -58,39 +61,48 @@ export function HostsDialog({ open, onClose }: HostsDialogProps) {
58
61
  ))}
59
62
  </div>
60
63
 
61
- <form onSubmit={handleAdd} className="space-y-2 pt-2">
62
- <p className="text-sm font-medium">Add a machine</p>
63
- <div className="flex flex-col gap-2 sm:flex-row">
64
- <Input
65
- placeholder="Name (e.g. devbox)"
66
- value={name}
67
- onChange={(e) => setName(e.target.value)}
68
- className="h-11 sm:h-9"
69
- />
70
- <Input
71
- placeholder="ssh target (user@host or alias)"
72
- value={sshTarget}
73
- onChange={(e) => setSshTarget(e.target.value)}
74
- autoCapitalize="none"
75
- autoCorrect="off"
76
- spellCheck={false}
77
- className="h-11 sm:h-9"
78
- />
79
- </div>
80
- {createHost.error && (
81
- <p className="text-sm text-red-400">{createHost.error.message}</p>
82
- )}
83
- <div className="flex justify-end">
84
- <Button
85
- type="submit"
86
- disabled={
87
- !name.trim() || !sshTarget.trim() || createHost.isPending
88
- }
89
- >
90
- Add machine
91
- </Button>
92
- </div>
93
- </form>
64
+ <details className="group">
65
+ <summary className="text-muted-foreground flex min-h-11 cursor-pointer items-center text-sm select-none sm:min-h-9">
66
+ Other ways to link (Tailscale or ssh)
67
+ </summary>
68
+ <p className="text-muted-foreground pb-2 text-xs">
69
+ Sessions run in tmux on any machine you can reach with ssh keys.
70
+ Projects pick a machine; tmux sessions already running there show up
71
+ under the project whose folder they are in.
72
+ </p>
73
+ <form onSubmit={handleAdd} className="space-y-2">
74
+ <div className="flex flex-col gap-2 sm:flex-row">
75
+ <Input
76
+ placeholder="Name (e.g. devbox)"
77
+ value={name}
78
+ onChange={(e) => setName(e.target.value)}
79
+ className="h-11 sm:h-9"
80
+ />
81
+ <Input
82
+ placeholder="ssh target (user@host or alias)"
83
+ value={sshTarget}
84
+ onChange={(e) => setSshTarget(e.target.value)}
85
+ autoCapitalize="none"
86
+ autoCorrect="off"
87
+ spellCheck={false}
88
+ className="h-11 sm:h-9"
89
+ />
90
+ </div>
91
+ {createHost.error && (
92
+ <p className="text-sm text-red-400">{createHost.error.message}</p>
93
+ )}
94
+ <div className="flex justify-end">
95
+ <Button
96
+ type="submit"
97
+ disabled={
98
+ !name.trim() || !sshTarget.trim() || createHost.isPending
99
+ }
100
+ >
101
+ Add machine
102
+ </Button>
103
+ </div>
104
+ </form>
105
+ </details>
94
106
  </DialogContent>
95
107
  </Dialog>
96
108
  );
@@ -0,0 +1,228 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { CheckCircle2, ShieldAlert, X } from "lucide-react";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "@/components/ui/dialog";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Input } from "@/components/ui/input";
14
+ import { useAnswerLink, usePendingLinks } from "@/data/peers";
15
+ import { showCode } from "@/lib/peer/code";
16
+ import type { PendingView } from "@/lib/peer/pairing";
17
+ import { relativeTime } from "@/lib/schedules/when";
18
+ import { SHEET } from "./sheet";
19
+
20
+ /** Its Connect id and when it was last online, next to the account's name. */
21
+ function Origin({ request }: { request: PendingView }) {
22
+ const seen = request.lastSeenAt ? Date.parse(request.lastSeenAt) : NaN;
23
+ return (
24
+ <span className="text-muted-foreground font-mono text-xs">
25
+ {request.machineId.slice(0, 8)}
26
+ {Number.isFinite(seen) && ` · seen ${relativeTime(seen)}`}
27
+ </span>
28
+ );
29
+ }
30
+
31
+ /** On the machine being asked: type the code the other one shows. */
32
+ export function IncomingLinkSheet({
33
+ request,
34
+ onClose,
35
+ }: {
36
+ request: PendingView | null;
37
+ onClose: () => void;
38
+ }) {
39
+ const [code, setCode] = useState("");
40
+ const { accept, decline } = useAnswerLink();
41
+ const digits = code.replace(/\D/g, "");
42
+ const busy = accept.isPending || decline.isPending;
43
+ const close = () => {
44
+ setCode("");
45
+ accept.reset();
46
+ onClose();
47
+ };
48
+ const left = (accept.error as { attemptsLeft?: number } | null)?.attemptsLeft;
49
+ const done = accept.data;
50
+
51
+ if (done)
52
+ return (
53
+ <Dialog open={!!request} onOpenChange={(o) => !o && close()}>
54
+ <DialogContent className={SHEET}>
55
+ <DialogHeader>
56
+ <DialogTitle>Finish on {done.name}</DialogTitle>
57
+ <DialogDescription>
58
+ {done.name} is linked here. Type this code on {done.name} so it
59
+ links back to this machine.
60
+ </DialogDescription>
61
+ </DialogHeader>
62
+ <p
63
+ className="text-center font-mono text-4xl font-semibold tracking-[0.2em] tabular-nums sm:text-5xl"
64
+ aria-label={`Code ${done.confirmCode.split("").join(" ")}`}
65
+ data-testid="confirm-code"
66
+ >
67
+ {showCode(done.confirmCode)}
68
+ </p>
69
+ <p className="flex items-center justify-center gap-2 text-sm">
70
+ <CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
71
+ Code matched
72
+ </p>
73
+ <div className="flex justify-end">
74
+ <Button className="min-h-11 sm:min-h-9" onClick={close}>
75
+ Done
76
+ </Button>
77
+ </div>
78
+ </DialogContent>
79
+ </Dialog>
80
+ );
81
+
82
+ return (
83
+ <Dialog open={!!request} onOpenChange={(o) => !o && close()}>
84
+ <DialogContent className={SHEET}>
85
+ <DialogHeader>
86
+ <DialogTitle>Link {request?.name}?</DialogTitle>
87
+ <DialogDescription>
88
+ {request?.name} asked to link with this machine. Type the code it
89
+ shows to allow it.
90
+ </DialogDescription>
91
+ {request && <Origin request={request} />}
92
+ </DialogHeader>
93
+ <div className="flex gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
94
+ <ShieldAlert className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
95
+ <p>
96
+ Linked machines can start agents and run code on this machine. Only
97
+ link one you own.
98
+ </p>
99
+ </div>
100
+ <form
101
+ className="flex flex-col gap-2"
102
+ onSubmit={(e) => {
103
+ e.preventDefault();
104
+ if (request && digits.length === 6)
105
+ accept.mutate({ id: request.id, code: digits });
106
+ }}
107
+ >
108
+ <Input
109
+ value={code}
110
+ onChange={(e) => setCode(e.target.value.slice(0, 7))}
111
+ inputMode="numeric"
112
+ autoComplete="one-time-code"
113
+ placeholder="6-digit code"
114
+ aria-label="Code shown on the other machine"
115
+ className="h-12 text-center font-mono text-2xl tracking-[0.3em]"
116
+ autoFocus
117
+ />
118
+ {accept.error && (
119
+ <p className="text-destructive text-sm">
120
+ {accept.error.message}
121
+ {left ? ` ${left} ${left === 1 ? "try" : "tries"} left.` : ""}
122
+ </p>
123
+ )}
124
+ <div className="flex justify-end gap-2">
125
+ <Button
126
+ type="button"
127
+ variant="ghost"
128
+ className="min-h-11 sm:min-h-9"
129
+ disabled={busy}
130
+ onClick={() =>
131
+ request && decline.mutate(request.id, { onSuccess: close })
132
+ }
133
+ >
134
+ Decline
135
+ </Button>
136
+ <Button
137
+ type="submit"
138
+ className="min-h-11 sm:min-h-9"
139
+ disabled={busy || digits.length !== 6 || left === 0}
140
+ >
141
+ Link
142
+ </Button>
143
+ </div>
144
+ </form>
145
+ </DialogContent>
146
+ </Dialog>
147
+ );
148
+ }
149
+
150
+ /** A banner on every page while a machine is waiting for its code here. */
151
+ export function IncomingLinks() {
152
+ const { data: requests = [] } = usePendingLinks();
153
+ // Held while open, so a request voided by a wrong code still says why.
154
+ const [open, setOpen] = useState<PendingView | null>(null);
155
+ // Hidden until a request not seen yet arrives; it covers the header on phones.
156
+ const [later, setLater] = useState<string | null>(null);
157
+ const first = requests[0];
158
+ const hidden = later !== null && requests.every((r) => later.includes(r.id));
159
+ return (
160
+ <>
161
+ {first && !open && !hidden && (
162
+ <div
163
+ role="status"
164
+ className="popover-surface fixed top-[max(0.75rem,env(safe-area-inset-top))] left-1/2 z-50 flex w-[min(28rem,calc(100%-1.5rem))] -translate-x-1/2 items-center gap-3 rounded-xl px-3 py-2 shadow-lg"
165
+ >
166
+ <ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
167
+ <div className="flex min-w-0 flex-1 flex-col">
168
+ <p className="truncate text-sm">
169
+ <span className="font-medium">{first.name}</span> wants to link
170
+ {requests.length > 1 ? ` (+${requests.length - 1} more)` : ""}
171
+ </p>
172
+ <Origin request={first} />
173
+ </div>
174
+ <Button
175
+ size="sm"
176
+ className="min-h-11 sm:min-h-8"
177
+ onClick={() => setOpen(first)}
178
+ >
179
+ Review
180
+ </Button>
181
+ <Button
182
+ size="icon"
183
+ variant="ghost"
184
+ aria-label="Later"
185
+ className="h-11 w-11 shrink-0 sm:h-8 sm:w-8"
186
+ onClick={() => setLater(requests.map((r) => r.id).join(" "))}
187
+ >
188
+ <X className="h-4 w-4" />
189
+ </Button>
190
+ </div>
191
+ )}
192
+ <IncomingLinkSheet request={open} onClose={() => setOpen(null)} />
193
+ </>
194
+ );
195
+ }
196
+
197
+ /** In Machines: the requests waiting here, for after the banner was put off. */
198
+ export function WaitingLinks() {
199
+ const { data: requests = [] } = usePendingLinks();
200
+ const [open, setOpen] = useState<PendingView | null>(null);
201
+ if (requests.length === 0 && !open) return null;
202
+ return (
203
+ <>
204
+ {requests.map((r) => (
205
+ <div
206
+ key={r.id}
207
+ className="flex items-center gap-3 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2"
208
+ >
209
+ <ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
210
+ <div className="flex min-w-0 flex-1 flex-col">
211
+ <p className="truncate text-sm">
212
+ <span className="font-medium">{r.name}</span> wants to link
213
+ </p>
214
+ <Origin request={r} />
215
+ </div>
216
+ <Button
217
+ size="sm"
218
+ className="min-h-11 sm:min-h-8"
219
+ onClick={() => setOpen(r)}
220
+ >
221
+ Type code
222
+ </Button>
223
+ </div>
224
+ ))}
225
+ <IncomingLinkSheet request={open} onClose={() => setOpen(null)} />
226
+ </>
227
+ );
228
+ }
@@ -0,0 +1,186 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { CheckCircle2, Loader2, XCircle } from "lucide-react";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "@/components/ui/dialog";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Input } from "@/components/ui/input";
14
+ import { useCancelLink, useConfirmLink, useOutgoingLink } from "@/data/peers";
15
+ import { showCode } from "@/lib/peer/code";
16
+ import { SHEET } from "./sheet";
17
+
18
+ const ENDED: Record<string, string> = {
19
+ declined: "It was declined there, or the code was typed wrong 3 times.",
20
+ expired: "The request expired before the code was typed.",
21
+ };
22
+
23
+ /** Once the other machine took our code: type the one it shows now. */
24
+ function ConfirmForm({
25
+ id,
26
+ name,
27
+ onCancel,
28
+ }: {
29
+ id: string;
30
+ name: string;
31
+ onCancel: () => void;
32
+ }) {
33
+ const [code, setCode] = useState("");
34
+ const confirm = useConfirmLink();
35
+ const digits = code.replace(/\D/g, "");
36
+ const left = (confirm.error as { attemptsLeft?: number } | null)
37
+ ?.attemptsLeft;
38
+ return (
39
+ <form
40
+ className="flex flex-col gap-2"
41
+ onSubmit={(e) => {
42
+ e.preventDefault();
43
+ if (digits.length === 6) confirm.mutate({ id, code: digits });
44
+ }}
45
+ >
46
+ <Input
47
+ value={code}
48
+ onChange={(e) => setCode(e.target.value.slice(0, 7))}
49
+ inputMode="numeric"
50
+ autoComplete="one-time-code"
51
+ placeholder="6-digit code"
52
+ aria-label={`Code shown on ${name}`}
53
+ className="h-12 text-center font-mono text-2xl tracking-[0.3em]"
54
+ autoFocus
55
+ />
56
+ {confirm.error && (
57
+ <p className="text-destructive text-sm">
58
+ {confirm.error.message}
59
+ {left ? ` ${left} ${left === 1 ? "try" : "tries"} left.` : ""}
60
+ </p>
61
+ )}
62
+ <div className="flex justify-end gap-2">
63
+ <Button
64
+ type="button"
65
+ variant="ghost"
66
+ className="min-h-11 sm:min-h-9"
67
+ onClick={onCancel}
68
+ >
69
+ Cancel
70
+ </Button>
71
+ <Button
72
+ type="submit"
73
+ className="min-h-11 sm:min-h-9"
74
+ disabled={confirm.isPending || digits.length !== 6 || left === 0}
75
+ >
76
+ Finish
77
+ </Button>
78
+ </div>
79
+ </form>
80
+ );
81
+ }
82
+
83
+ /** On the machine that asks: the code to type on the other one. */
84
+ export function LinkCodeSheet({
85
+ linkId,
86
+ onClose,
87
+ onRetry,
88
+ }: {
89
+ linkId: string | null;
90
+ onClose: () => void;
91
+ onRetry: () => void;
92
+ }) {
93
+ const { data: link, error } = useOutgoingLink(linkId);
94
+ const cancel = useCancelLink();
95
+ const close = () => {
96
+ if ((link?.state === "pending" || link?.state === "confirm") && linkId)
97
+ cancel.mutate(linkId);
98
+ onClose();
99
+ };
100
+ const name = link?.name ?? "the other machine";
101
+
102
+ return (
103
+ <Dialog open={!!linkId} onOpenChange={(o) => !o && close()}>
104
+ <DialogContent className={SHEET}>
105
+ <DialogHeader>
106
+ <DialogTitle>Link {name}</DialogTitle>
107
+ <DialogDescription>
108
+ {link?.state === "confirm" ? (
109
+ <>
110
+ Last step: {name} shows a code of its own. Typing it here proves
111
+ it&apos;s really {name}.
112
+ </>
113
+ ) : link?.state === "accepted" ? (
114
+ <>Both machines list each other as linked now.</>
115
+ ) : (
116
+ <>
117
+ Open AgentOS on {name} and type this code there. Its banner, the
118
+ Machines screen, or{" "}
119
+ <code className="font-mono">agent-os link accept</code> in its
120
+ terminal all take it.
121
+ </>
122
+ )}
123
+ </DialogDescription>
124
+ </DialogHeader>
125
+ {link?.state === "confirm" && (
126
+ <ConfirmForm id={link.id} name={name} onCancel={close} />
127
+ )}
128
+ {link && link.state !== "confirm" && link.state !== "accepted" && (
129
+ <p
130
+ className="text-center font-mono text-4xl font-semibold tracking-[0.2em] tabular-nums sm:text-5xl"
131
+ aria-label={`Code ${link.code.split("").join(" ")}`}
132
+ data-testid="link-code"
133
+ >
134
+ {showCode(link.code)}
135
+ </p>
136
+ )}
137
+ {link?.state !== "confirm" && (
138
+ <>
139
+ <div className="flex min-h-11 items-center justify-center gap-2 text-sm">
140
+ {error ? (
141
+ <span className="text-destructive">{error.message}</span>
142
+ ) : !link || link.state === "pending" ? (
143
+ <>
144
+ <Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
145
+ <span className="text-muted-foreground">
146
+ Waiting for the code on {name}…
147
+ </span>
148
+ </>
149
+ ) : link.state === "accepted" ? (
150
+ <>
151
+ <CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
152
+ <span>Linked. Tasks can run on {name} now.</span>
153
+ </>
154
+ ) : (
155
+ <>
156
+ <XCircle className="text-destructive h-4 w-4" />
157
+ <span className="text-muted-foreground">
158
+ {ENDED[link.state] ?? link.error}
159
+ </span>
160
+ </>
161
+ )}
162
+ </div>
163
+ <div className="flex justify-end gap-2">
164
+ {link && !["pending", "accepted"].includes(link.state) && (
165
+ <Button
166
+ variant="outline"
167
+ className="min-h-11 sm:min-h-9"
168
+ onClick={onRetry}
169
+ >
170
+ Try again
171
+ </Button>
172
+ )}
173
+ <Button
174
+ variant={link?.state === "accepted" ? "default" : "ghost"}
175
+ className="min-h-11 sm:min-h-9"
176
+ onClick={close}
177
+ >
178
+ {link?.state === "pending" ? "Cancel" : "Done"}
179
+ </Button>
180
+ </div>
181
+ </>
182
+ )}
183
+ </DialogContent>
184
+ </Dialog>
185
+ );
186
+ }
@@ -0,0 +1,3 @@
1
+ /** A dialog that sits at the bottom of a phone's screen, centred above it. */
2
+ export const SHEET =
3
+ "max-sm:top-auto max-sm:bottom-0 max-sm:max-w-full max-sm:translate-y-0 max-sm:rounded-b-none max-sm:pb-[max(1.5rem,env(safe-area-inset-bottom))]";
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Repeat, Layers, Pause } from "lucide-react";
3
+ import { Repeat, Layers, Pause, Hourglass } from "lucide-react";
4
4
  import { HostBadge } from "@/components/Hosts";
5
5
  import { rowGroup } from "@/lib/recipes/chip";
6
6
  import type { Session } from "@/lib/db";
@@ -77,6 +77,16 @@ export function RowSubtitle({
77
77
  </span>
78
78
  </span>
79
79
  )}
80
+ {(session.waiting_on || session.waited_by) && (
81
+ <span className="text-muted-foreground flex min-w-0 items-center gap-1 text-xs">
82
+ <Hourglass aria-hidden className="h-3 w-3 shrink-0" />
83
+ <span className="truncate">
84
+ {session.waiting_on
85
+ ? `waiting on ${session.waiting_on}`
86
+ : `${session.waited_by} waiting on it`}
87
+ </span>
88
+ </span>
89
+ )}
80
90
  {group && (
81
91
  <span
82
92
  className="text-muted-foreground/70 flex min-w-0 shrink-0 items-center gap-1 truncate text-xs @[14rem]:shrink"
@@ -29,7 +29,7 @@ function Command() {
29
29
  );
30
30
  }
31
31
 
32
- function LinkForm() {
32
+ export function LinkForm() {
33
33
  const [code, setCode] = useState("");
34
34
  const link = useLinkConnect();
35
35
  const submit = () => {
@@ -0,0 +1,2 @@
1
+ export * from "./keys";
2
+ export * from "./queries";
@@ -0,0 +1,6 @@
1
+ export const peerKeys = {
2
+ all: ["peers"] as const,
3
+ account: () => [...peerKeys.all, "account"] as const,
4
+ pending: () => [...peerKeys.all, "pending"] as const,
5
+ link: (id: string) => [...peerKeys.all, "link", id] as const,
6
+ };