@saadnvd1/agent-os 0.3.700 → 0.3.723
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/app/api/agent-roles/route.test.ts +67 -0
- package/app/api/agent-roles/route.ts +61 -0
- package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
- package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
- package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
- package/app/api/analyst/[id]/review/route.ts +22 -0
- package/app/api/analyst/[id]/route.ts +44 -0
- package/app/api/analyst/route.ts +10 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/spawn/route.ts +34 -2
- package/app/api/claims/[id]/route.ts +20 -0
- package/app/api/claims/check/route.ts +23 -0
- package/app/api/claims/check.test.ts +113 -0
- package/app/api/claims/exchange/route.ts +42 -0
- package/app/api/claims/outside.test.ts +90 -0
- package/app/api/claims/route.test.ts +193 -0
- package/app/api/claims/route.ts +73 -0
- package/app/api/hosts/[id]/route.ts +9 -1
- package/app/api/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -0
- package/app/api/peer/unlink/route.ts +17 -0
- package/app/api/peers/[hostId]/audit/route.ts +14 -0
- package/app/api/peers/[hostId]/route.ts +20 -0
- package/app/api/peers/gate.test.ts +64 -1
- package/app/api/peers/pending/route.test.ts +37 -0
- package/app/api/peers/pending/route.ts +11 -4
- package/app/api/session-groups/[id]/restore/route.ts +11 -0
- package/app/api/session-groups/[id]/route.ts +25 -0
- package/app/api/session-groups/order/route.ts +15 -0
- package/app/api/session-groups/route.ts +21 -0
- package/app/api/sessions/[id]/analyst/route.ts +11 -0
- package/app/api/sessions/[id]/crew/route.ts +38 -0
- package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
- package/app/api/sessions/[id]/export/route.test.ts +125 -0
- package/app/api/sessions/[id]/export/route.ts +34 -0
- package/app/api/sessions/[id]/fork/route.ts +3 -0
- package/app/api/sessions/[id]/group/route.ts +18 -0
- package/app/api/sessions/[id]/model/route.ts +49 -0
- package/app/api/sessions/[id]/route.ts +14 -4
- package/app/api/sessions/[id]/summarize/route.ts +2 -0
- package/app/api/sessions/claims.test.ts +122 -0
- package/app/api/sessions/group/route.ts +17 -0
- package/app/api/sessions/route.ts +31 -14
- package/app/page.tsx +10 -0
- package/components/Chat/Messages.tsx +104 -10
- package/components/Claims/ClaimFiles.tsx +38 -0
- package/components/Claims/ClaimRow.tsx +82 -0
- package/components/Claims/ClaimsSheet.tsx +176 -0
- package/components/Claims/OverlapPill.tsx +31 -0
- package/components/Claims/StartClaimsNotice.tsx +70 -0
- package/components/Claims/claims-ui.test.ts +161 -0
- package/components/Claims/format.ts +57 -0
- package/components/Claims/index.ts +3 -0
- package/components/Draft/DraftChips.tsx +6 -1
- package/components/Draft/DraftPanel.tsx +90 -3
- package/components/Draft/ModelUnavailableDialog.tsx +78 -0
- package/components/Draft/RoleChips.tsx +110 -0
- package/components/Export/ExportActions.tsx +136 -0
- package/components/Export/ExportSheet.tsx +217 -0
- package/components/Export/RangePicker.tsx +144 -0
- package/components/Export/Seg.tsx +38 -0
- package/components/Export/TurnSelectBar.tsx +45 -0
- package/components/Hosts/AccountMachines.tsx +32 -18
- package/components/Hosts/HostRow.tsx +155 -14
- package/components/Hosts/IncomingLinkSheet.tsx +28 -10
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/Models/AgentRoleCard.tsx +114 -0
- package/components/Models/AgentRolesSection.tsx +132 -0
- package/components/Models/ModelBadges.tsx +48 -0
- package/components/Models/ModelsSection.tsx +4 -2
- package/components/Models/RoleModelSelect.tsx +87 -0
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/MobileTabBar.tsx +12 -0
- package/components/Pane/PaneMenu.tsx +10 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +7 -1
- package/components/SessionList/SelectionToolbar.tsx +52 -1
- package/components/SessionList/SessionRow.tsx +19 -7
- package/components/SessionList/SessionRowMenu.tsx +23 -0
- package/components/SessionList/SessionShelves.tsx +5 -0
- package/components/SessionList/SidebarBody.tsx +6 -0
- package/components/SessionList/SidebarContextMenu.test.ts +104 -0
- package/components/SessionList/SidebarContextMenu.tsx +59 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
- package/components/SessionList/index.tsx +18 -14
- package/components/SessionList/useSidebarData.ts +10 -0
- package/components/Sessions/RolePill.tsx +196 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/analyst/index.ts +3 -0
- package/data/analyst/keys.ts +6 -0
- package/data/analyst/mutations.ts +116 -0
- package/data/analyst/queries.ts +35 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/models/index.ts +98 -0
- package/data/peers/keys.ts +1 -0
- package/data/peers/queries.ts +31 -0
- package/data/push/topics.ts +9 -0
- package/data/session-groups/index.ts +156 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/pin.ts +2 -6
- package/data/sessions/queries.ts +36 -29
- package/data/sessions/shelf.ts +2 -6
- package/data/tasks/index.ts +2 -0
- package/hooks/useSessions.ts +0 -12
- package/lib/__fixtures__/peer-rpc.ts +65 -33
- package/lib/agents/brief.ts +1 -0
- package/lib/agents/spawn.ts +4 -0
- package/lib/analyst/__fixtures__/items.ts +57 -0
- package/lib/analyst/__fixtures__/mocks.ts +49 -0
- package/lib/analyst/brief.ts +17 -0
- package/lib/analyst/buildDigest.ts +204 -0
- package/lib/analyst/createLink.ts +98 -0
- package/lib/analyst/endLink.ts +31 -0
- package/lib/analyst/forward.ts +109 -0
- package/lib/analyst/index.ts +16 -0
- package/lib/analyst/linkView.ts +38 -0
- package/lib/analyst/onTurnEnd.ts +119 -0
- package/lib/analyst/parseReview.ts +41 -0
- package/lib/analyst/pushDigest.ts +143 -0
- package/lib/analyst/resume.ts +35 -0
- package/lib/analyst/route.ts +50 -0
- package/lib/analyst/signals.ts +121 -0
- package/lib/analyst/store.ts +303 -0
- package/lib/analyst/types.ts +140 -0
- package/lib/bus/condense.ts +5 -1
- package/lib/bus/index.ts +13 -0
- package/lib/bus/read-scope.ts +12 -0
- package/lib/bus/role-alias.ts +70 -0
- package/lib/bus/targets.ts +3 -0
- package/lib/chat/drivers/opencode-mapper.ts +55 -1
- package/lib/chat/drivers/pi-approvals.ts +7 -4
- package/lib/chat/drivers/pi-mapper.ts +38 -0
- package/lib/chat/drivers/pi-text-tools.ts +104 -0
- package/lib/chat/drivers/pi.ts +8 -3
- package/lib/chat/drivers/turn-items.ts +36 -11
- package/lib/chat/drivers/turn-outcome.ts +108 -0
- package/lib/chat/events.ts +8 -0
- package/lib/chat/registry.ts +13 -0
- package/lib/chat/store.ts +19 -0
- package/lib/chat/turns.ts +1 -0
- package/lib/chat/worker/extras.ts +4 -2
- package/lib/claims/collectLocal.ts +123 -0
- package/lib/claims/describe.ts +41 -0
- package/lib/claims/exchange.ts +91 -0
- package/lib/claims/goal.ts +45 -0
- package/lib/claims/index.ts +10 -0
- package/lib/claims/loop.ts +51 -0
- package/lib/claims/machine.ts +21 -0
- package/lib/claims/mine.ts +43 -0
- package/lib/claims/notifyOverlaps.ts +101 -0
- package/lib/claims/onMerged.ts +85 -0
- package/lib/claims/outside.ts +36 -0
- package/lib/claims/overlap.ts +87 -0
- package/lib/claims/overlapCache.ts +86 -0
- package/lib/claims/overlapText.ts +36 -0
- package/lib/claims/peers.ts +56 -0
- package/lib/claims/startCheck.ts +100 -0
- package/lib/claims/startWarning.ts +29 -0
- package/lib/claims/store.ts +366 -0
- package/lib/claims/types.ts +102 -0
- package/lib/claims/view.ts +80 -0
- package/lib/claims/yield.ts +24 -0
- package/lib/db/changes.ts +12 -0
- package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
- package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
- package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
- package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
- package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
- package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
- package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
- package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
- package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
- package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +3 -0
- package/lib/db/types.ts +19 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/done/archive.ts +2 -0
- package/lib/drafts.ts +4 -0
- package/lib/export/build.ts +470 -0
- package/lib/export/compact.ts +156 -0
- package/lib/export/estimate.ts +83 -0
- package/lib/export/fixtures.ts +45 -0
- package/lib/export/html.tsx +195 -0
- package/lib/export/json.ts +39 -0
- package/lib/export/load.ts +64 -0
- package/lib/export/markdown.ts +127 -0
- package/lib/export/request.ts +36 -0
- package/lib/export/service.ts +142 -0
- package/lib/export/summary.ts +86 -0
- package/lib/export/turns.ts +63 -0
- package/lib/export/view.ts +173 -0
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/models/agent-binding.ts +102 -0
- package/lib/models/agent-roles-view.ts +157 -0
- package/lib/models/agent-roles.ts +87 -0
- package/lib/models/availability-refs.ts +17 -0
- package/lib/models/availability.ts +91 -0
- package/lib/models/catalog.ts +156 -0
- package/lib/models/config.ts +27 -0
- package/lib/models/locality.ts +33 -0
- package/lib/models/providers.ts +11 -0
- package/lib/models/resolve.ts +50 -12
- package/lib/models/roles.ts +12 -0
- package/lib/needs-you.ts +1 -1
- package/lib/orchestration.ts +2 -0
- package/lib/orchestrator/act.ts +54 -4
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/claim-wait.ts +71 -0
- package/lib/orchestrator/conditions.ts +28 -1
- package/lib/orchestrator/describe.ts +6 -1
- package/lib/orchestrator/facts.ts +12 -2
- package/lib/orchestrator/read.ts +5 -1
- package/lib/orchestrator/serve.ts +37 -2
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +31 -1
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/orchestrator/untrusted.ts +5 -0
- package/lib/orchestrator/watcher.ts +19 -1
- package/lib/peer/account.ts +74 -2
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/client.ts +26 -1
- package/lib/peer/link-cli.ts +19 -4
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +14 -2
- package/lib/peer/name.ts +32 -0
- package/lib/peer/pairing.ts +34 -5
- package/lib/peer/pin.ts +10 -3
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-server.ts +53 -5
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +6 -2
- package/lib/security/demo.ts +7 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -0
- package/lib/session-groups/assignSessions.ts +81 -0
- package/lib/session-groups/cli.ts +61 -0
- package/lib/session-groups/createGroup.ts +28 -0
- package/lib/session-groups/index.ts +21 -0
- package/lib/session-groups/removeGroup.ts +36 -0
- package/lib/session-groups/reorderGroups.ts +31 -0
- package/lib/session-groups/resolveGroup.ts +19 -0
- package/lib/session-groups/route.ts +32 -0
- package/lib/session-groups/store.ts +85 -0
- package/lib/session-groups/types.ts +25 -0
- package/lib/session-groups/updateGroup.ts +35 -0
- package/lib/session-groups/workspace.ts +21 -0
- package/lib/sessions/crew.ts +72 -0
- package/lib/sessions/launch.ts +54 -11
- package/lib/settings-url.ts +1 -0
- package/lib/share/markdown.ts +2 -2
- package/lib/share/redact.ts +16 -10
- package/lib/share/render.tsx +1 -1
- package/lib/share/snapshot.ts +2 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +8 -1
- package/lib/sidebar/shelves.ts +90 -15
- package/lib/sidebar/visible-order.ts +5 -0
- package/lib/tasks/hold-checks.ts +3 -1
- package/lib/tasks/holds.ts +3 -0
- package/lib/tasks/index.ts +11 -0
- package/lib/tasks/queue-rows.ts +8 -1
- package/lib/tasks/queue-start.ts +1 -0
- package/lib/tasks/queue-types.ts +9 -1
- package/lib/tasks/queue.ts +15 -5
- package/lib/tasks/schedule-types.ts +1 -0
- package/lib/test-temp.ts +19 -3
- package/lib/workspaces.ts +11 -3
- package/lib/worktree-deps/caches.ts +3 -2
- package/lib/worktree-deps/clone.ts +3 -1
- package/lib/worktree-deps/paths.ts +26 -0
- package/lib/worktree-sweep/testTemp.ts +23 -106
- package/package.json +1 -1
- package/scripts/agent-os +1 -1
- package/scripts/autodeploy +2 -0
- package/scripts/autodeploy.test.ts +31 -1
- package/scripts/claim-hook.sh +31 -0
- package/scripts/claim-hook.test.ts +230 -0
- package/scripts/lib/cli-links.sh +36 -0
- package/scripts/lib/commands.sh +3 -0
- package/scripts/lib/machine.sh +27 -2
- package/scripts/lib/machine.test.ts +25 -3
- package/scripts/sync-cli-links +7 -0
- package/scripts/sync-cli-links.test.ts +69 -0
- package/server.ts +12 -1
- package/stores/claimsUi.ts +22 -0
- package/stores/exportUi.ts +87 -0
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { Check, Copy, Download, Loader2, Share2 } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { writeClipboard } from "@/hooks/useCopyToClipboard";
|
|
8
|
+
import { fetchExport, type ExportEstimate } from "@/data/exports";
|
|
9
|
+
import type { ExportRequest } from "@/lib/export/view";
|
|
10
|
+
|
|
11
|
+
type Busy = "copy" | "download" | "share" | null;
|
|
12
|
+
|
|
13
|
+
function save(blob: Blob, name: string) {
|
|
14
|
+
const url = URL.createObjectURL(blob);
|
|
15
|
+
const a = document.createElement("a");
|
|
16
|
+
a.href = url;
|
|
17
|
+
a.download = name;
|
|
18
|
+
a.click();
|
|
19
|
+
URL.revokeObjectURL(url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Copy for agent, Download and (where the browser can) Share. Never blocked
|
|
23
|
+
// by a sensitive-data warning, only by the size limit.
|
|
24
|
+
export function ExportActions({
|
|
25
|
+
sessionId,
|
|
26
|
+
title,
|
|
27
|
+
req,
|
|
28
|
+
est,
|
|
29
|
+
}: {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
title: string;
|
|
32
|
+
req: ExportRequest;
|
|
33
|
+
est: ExportEstimate | null;
|
|
34
|
+
}) {
|
|
35
|
+
const [busy, setBusy] = useState<Busy>(null);
|
|
36
|
+
const [copied, setCopied] = useState(false);
|
|
37
|
+
// Only rendered on the client (the sheet loads with ssr off).
|
|
38
|
+
const [canShare] = useState(() => {
|
|
39
|
+
try {
|
|
40
|
+
const probe = new File([""], "x.md", { type: "text/markdown" });
|
|
41
|
+
return !!navigator.canShare?.({ files: [probe] });
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const wire = JSON.parse(JSON.stringify(req)) as ExportRequest;
|
|
48
|
+
const file = () => fetchExport(sessionId, wire, title, est?.selected ?? null);
|
|
49
|
+
const disabled = !est || est.tooBig || busy !== null;
|
|
50
|
+
|
|
51
|
+
const run =
|
|
52
|
+
(kind: Exclude<Busy, null>, work: () => Promise<void>) => async () => {
|
|
53
|
+
setBusy(kind);
|
|
54
|
+
try {
|
|
55
|
+
await work();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if ((err as Error).name !== "AbortError")
|
|
58
|
+
toast.error((err as Error).message || "Export failed");
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(null);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const copy = run("copy", async () => {
|
|
65
|
+
const pending = file();
|
|
66
|
+
// Safari only allows a copy started inside the tap: hand it the promise.
|
|
67
|
+
if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
|
|
68
|
+
await navigator.clipboard.write([
|
|
69
|
+
new ClipboardItem({
|
|
70
|
+
"text/plain": pending.then(
|
|
71
|
+
(f) => new Blob([f.blob], { type: "text/plain" })
|
|
72
|
+
),
|
|
73
|
+
}),
|
|
74
|
+
]);
|
|
75
|
+
} else {
|
|
76
|
+
await writeClipboard(await (await pending).blob.text());
|
|
77
|
+
}
|
|
78
|
+
setCopied(true);
|
|
79
|
+
setTimeout(() => setCopied(false), 1500);
|
|
80
|
+
toast.success("Copied. Paste it into your AI chat.");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const download = run("download", async () => {
|
|
84
|
+
const f = await file();
|
|
85
|
+
save(f.blob, f.name);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const share = run("share", async () => {
|
|
89
|
+
const f = await file();
|
|
90
|
+
const shared = new File([f.blob], f.name, { type: f.blob.type });
|
|
91
|
+
if (!navigator.canShare?.({ files: [shared] })) return save(f.blob, f.name);
|
|
92
|
+
await navigator.share({ files: [shared], title });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const Spin = <Loader2 className="h-4 w-4 animate-spin" />;
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:justify-end">
|
|
98
|
+
<Button
|
|
99
|
+
className="min-h-11 sm:order-last"
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
onClick={copy}
|
|
102
|
+
>
|
|
103
|
+
{busy === "copy" ? (
|
|
104
|
+
Spin
|
|
105
|
+
) : copied ? (
|
|
106
|
+
<Check className="h-4 w-4" />
|
|
107
|
+
) : (
|
|
108
|
+
<Copy className="h-4 w-4" />
|
|
109
|
+
)}
|
|
110
|
+
{req.format === "md" ? "Copy for agent" : "Copy"}
|
|
111
|
+
</Button>
|
|
112
|
+
<div className="flex gap-2">
|
|
113
|
+
<Button
|
|
114
|
+
variant="outline"
|
|
115
|
+
className="min-h-11 flex-1 sm:flex-none"
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
onClick={download}
|
|
118
|
+
>
|
|
119
|
+
{busy === "download" ? Spin : <Download className="h-4 w-4" />}
|
|
120
|
+
Download
|
|
121
|
+
</Button>
|
|
122
|
+
{canShare && (
|
|
123
|
+
<Button
|
|
124
|
+
variant="outline"
|
|
125
|
+
className="min-h-11 flex-1 sm:flex-none"
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
onClick={share}
|
|
128
|
+
>
|
|
129
|
+
{busy === "share" ? Spin : <Share2 className="h-4 w-4" />}
|
|
130
|
+
Share…
|
|
131
|
+
</Button>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useSnapshot } from "valtio";
|
|
5
|
+
import { AlertTriangle, Info } from "lucide-react";
|
|
6
|
+
import { Switch } from "@/components/ui/switch";
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "@/components/ui/dialog";
|
|
13
|
+
import { useExportEstimate, type ExportEstimate } from "@/data/exports";
|
|
14
|
+
import { exportUi, exportUiActions } from "@/stores/exportUi";
|
|
15
|
+
import {
|
|
16
|
+
COMPACT_RULES,
|
|
17
|
+
EXPORT_FORMATS,
|
|
18
|
+
formatBytes,
|
|
19
|
+
redactionStatus,
|
|
20
|
+
tokenLabel,
|
|
21
|
+
type ExportFormat,
|
|
22
|
+
type ExportMode,
|
|
23
|
+
type ExportRequest,
|
|
24
|
+
} from "@/lib/export/view";
|
|
25
|
+
import { cn } from "@/lib/utils";
|
|
26
|
+
import { ExportActions } from "./ExportActions";
|
|
27
|
+
import { RangePicker } from "./RangePicker";
|
|
28
|
+
import { Seg } from "./Seg";
|
|
29
|
+
import { TurnSelectBar } from "./TurnSelectBar";
|
|
30
|
+
|
|
31
|
+
// Opened from a menu or a message: what to export (format, detail, turns,
|
|
32
|
+
// masking), what it comes to, and where it goes (copy, download, share).
|
|
33
|
+
export function ExportSheet() {
|
|
34
|
+
const ui = useSnapshot(exportUi);
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<Dialog
|
|
38
|
+
open={ui.open && !!ui.sessionId}
|
|
39
|
+
onOpenChange={(open) => !open && exportUiActions.close()}
|
|
40
|
+
>
|
|
41
|
+
{ui.sessionId && (
|
|
42
|
+
<SheetBody
|
|
43
|
+
key={ui.sessionId}
|
|
44
|
+
sessionId={ui.sessionId}
|
|
45
|
+
title={ui.title}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
</Dialog>
|
|
49
|
+
{ui.selecting && <TurnSelectBar />}
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function SheetBody({ sessionId, title }: { sessionId: string; title: string }) {
|
|
55
|
+
const { req, fromItemId } = useSnapshot(exportUi);
|
|
56
|
+
const [est, setEst] = useState<ExportEstimate | null>(null);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
const estimate = useExportEstimate(sessionId);
|
|
59
|
+
const seq = useRef(0);
|
|
60
|
+
const wire = JSON.stringify(req);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const mine = ++seq.current;
|
|
64
|
+
const timer = setTimeout(async () => {
|
|
65
|
+
try {
|
|
66
|
+
const e = await estimate.mutateAsync(JSON.parse(wire) as ExportRequest);
|
|
67
|
+
if (mine !== seq.current) return;
|
|
68
|
+
setEst(e);
|
|
69
|
+
setError(null);
|
|
70
|
+
exportUi.turns = e.turns;
|
|
71
|
+
const from = exportUi.fromItemId;
|
|
72
|
+
if (from) {
|
|
73
|
+
exportUi.fromItemId = null;
|
|
74
|
+
const turn = e.turns.find((t) => t.id === from);
|
|
75
|
+
if (turn)
|
|
76
|
+
exportUiActions.setRequest({
|
|
77
|
+
range: { from: turn.n, to: e.selected.of },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (mine === seq.current) setError((err as Error).message);
|
|
82
|
+
}
|
|
83
|
+
}, 250);
|
|
84
|
+
return () => clearTimeout(timer);
|
|
85
|
+
// The mutation object changes every render; the request is the input.
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
}, [wire, fromItemId]);
|
|
88
|
+
|
|
89
|
+
const name = est?.title ?? title;
|
|
90
|
+
const set = exportUiActions.setRequest;
|
|
91
|
+
const size = est?.estimate[req.mode];
|
|
92
|
+
const stale = estimate.isPending;
|
|
93
|
+
const token = (m: ExportMode) =>
|
|
94
|
+
est ? tokenLabel(est.estimate[m].tokens).replace("about ", "~") : " ";
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<DialogContent className="flex h-[100dvh] w-screen max-w-none flex-col gap-3 rounded-none p-4 sm:h-auto sm:max-h-[90vh] sm:max-w-[560px] sm:rounded-lg">
|
|
98
|
+
<div>
|
|
99
|
+
<DialogTitle>Export for AI</DialogTitle>
|
|
100
|
+
<DialogDescription className="truncate">{name}</DialogDescription>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto">
|
|
104
|
+
<Section label="Format">
|
|
105
|
+
<Seg<ExportFormat>
|
|
106
|
+
label="Format"
|
|
107
|
+
value={req.format}
|
|
108
|
+
options={EXPORT_FORMATS.map((f) => ({ id: f.id, label: f.label }))}
|
|
109
|
+
onChange={(format) => set({ format })}
|
|
110
|
+
/>
|
|
111
|
+
<p className="text-muted-foreground text-xs">
|
|
112
|
+
{EXPORT_FORMATS.find((f) => f.id === req.format)?.hint}
|
|
113
|
+
</p>
|
|
114
|
+
</Section>
|
|
115
|
+
|
|
116
|
+
<Section label="Detail">
|
|
117
|
+
<Seg<ExportMode>
|
|
118
|
+
label="Detail"
|
|
119
|
+
value={req.mode}
|
|
120
|
+
options={[
|
|
121
|
+
{ id: "compact", label: "Compact", sub: token("compact") },
|
|
122
|
+
{ id: "full", label: "Full", sub: token("full") },
|
|
123
|
+
]}
|
|
124
|
+
onChange={(mode) => set({ mode })}
|
|
125
|
+
/>
|
|
126
|
+
<details className="text-muted-foreground text-xs">
|
|
127
|
+
<summary className="flex min-h-8 cursor-pointer items-center">
|
|
128
|
+
What Compact drops
|
|
129
|
+
</summary>
|
|
130
|
+
<ul className="list-disc space-y-0.5 pl-5">
|
|
131
|
+
{COMPACT_RULES.map((r) => (
|
|
132
|
+
<li key={r}>{r}</li>
|
|
133
|
+
))}
|
|
134
|
+
</ul>
|
|
135
|
+
</details>
|
|
136
|
+
</Section>
|
|
137
|
+
|
|
138
|
+
<Section label="Range">
|
|
139
|
+
<RangePicker
|
|
140
|
+
range={req.range}
|
|
141
|
+
est={est}
|
|
142
|
+
onChange={(range) => set({ range })}
|
|
143
|
+
onSelectInChat={() => exportUiActions.startSelect()}
|
|
144
|
+
/>
|
|
145
|
+
</Section>
|
|
146
|
+
|
|
147
|
+
<Section label="Masking">
|
|
148
|
+
<label className="flex min-h-11 items-center justify-between gap-3 text-sm">
|
|
149
|
+
<span>Mask paths, machine names and emails</span>
|
|
150
|
+
<Switch
|
|
151
|
+
checked={req.redact === "all"}
|
|
152
|
+
onCheckedChange={(on) => set({ redact: on ? "all" : "secrets" })}
|
|
153
|
+
/>
|
|
154
|
+
</label>
|
|
155
|
+
<p className="text-muted-foreground text-xs">
|
|
156
|
+
Credentials are always masked.{" "}
|
|
157
|
+
{est ? redactionStatus(est.hits) + "." : ""}
|
|
158
|
+
</p>
|
|
159
|
+
</Section>
|
|
160
|
+
|
|
161
|
+
<div
|
|
162
|
+
className={cn(
|
|
163
|
+
"space-y-2 rounded-lg border p-3 text-sm",
|
|
164
|
+
stale && "opacity-60"
|
|
165
|
+
)}
|
|
166
|
+
aria-live="polite"
|
|
167
|
+
>
|
|
168
|
+
<p className="font-medium">
|
|
169
|
+
{size
|
|
170
|
+
? `${formatBytes(size.bytes)} · ${tokenLabel(size.tokens)}`
|
|
171
|
+
: (error ?? "Working out the size…")}
|
|
172
|
+
</p>
|
|
173
|
+
{est?.warnings.map((w) => (
|
|
174
|
+
<p
|
|
175
|
+
key={w.code}
|
|
176
|
+
className={cn(
|
|
177
|
+
"flex gap-2 text-xs",
|
|
178
|
+
w.level === "danger" && "text-destructive",
|
|
179
|
+
w.level === "warn" && "text-amber-600 dark:text-amber-400",
|
|
180
|
+
w.level === "info" && "text-muted-foreground"
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
{w.level === "info" ? (
|
|
184
|
+
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
185
|
+
) : (
|
|
186
|
+
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
187
|
+
)}
|
|
188
|
+
<span>{w.text}</span>
|
|
189
|
+
</p>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<ExportActions
|
|
195
|
+
sessionId={sessionId}
|
|
196
|
+
title={name}
|
|
197
|
+
req={req as ExportRequest}
|
|
198
|
+
est={est}
|
|
199
|
+
/>
|
|
200
|
+
</DialogContent>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function Section({
|
|
205
|
+
label,
|
|
206
|
+
children,
|
|
207
|
+
}: {
|
|
208
|
+
label: string;
|
|
209
|
+
children: React.ReactNode;
|
|
210
|
+
}) {
|
|
211
|
+
return (
|
|
212
|
+
<section className="space-y-2">
|
|
213
|
+
<h3 className="text-muted-foreground text-xs font-medium">{label}</h3>
|
|
214
|
+
{children}
|
|
215
|
+
</section>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { MousePointerClick } from "lucide-react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import type { ExportEstimate } from "@/data/exports";
|
|
7
|
+
import type { ExportRange } from "@/lib/export/view";
|
|
8
|
+
|
|
9
|
+
type Chip = "all" | "last1" | "last5" | "compaction" | "custom";
|
|
10
|
+
|
|
11
|
+
const CHIPS: { id: Chip; label: string }[] = [
|
|
12
|
+
{ id: "all", label: "Whole session" },
|
|
13
|
+
{ id: "last1", label: "Last turn" },
|
|
14
|
+
{ id: "last5", label: "Last 5" },
|
|
15
|
+
{ id: "compaction", label: "Since compaction" },
|
|
16
|
+
{ id: "custom", label: "Custom" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const chipOf = (r: ExportRange | undefined): Chip =>
|
|
20
|
+
!r
|
|
21
|
+
? "all"
|
|
22
|
+
: "sinceCompaction" in r
|
|
23
|
+
? "compaction"
|
|
24
|
+
: "last" in r
|
|
25
|
+
? r.last === 1
|
|
26
|
+
? "last1"
|
|
27
|
+
: r.last === 5
|
|
28
|
+
? "last5"
|
|
29
|
+
: "custom"
|
|
30
|
+
: "custom";
|
|
31
|
+
|
|
32
|
+
const when = (at: string) => at.slice(5, 16).replace("T", " ");
|
|
33
|
+
const clock = (at: string) => at.slice(11, 16);
|
|
34
|
+
|
|
35
|
+
const SELECT =
|
|
36
|
+
"bg-background border-input min-h-11 w-full min-w-0 rounded-md border px-2 text-sm";
|
|
37
|
+
|
|
38
|
+
export function RangePicker({
|
|
39
|
+
range,
|
|
40
|
+
est,
|
|
41
|
+
onChange,
|
|
42
|
+
onSelectInChat,
|
|
43
|
+
}: {
|
|
44
|
+
range: ExportRange | undefined;
|
|
45
|
+
est: ExportEstimate | null;
|
|
46
|
+
onChange: (r: ExportRange | undefined) => void;
|
|
47
|
+
onSelectInChat: () => void;
|
|
48
|
+
}) {
|
|
49
|
+
const chip = chipOf(range);
|
|
50
|
+
const turns = (est?.turns ?? []).filter((t) => t.n > 0);
|
|
51
|
+
const sel = est?.selected;
|
|
52
|
+
const pick = (id: Chip) => {
|
|
53
|
+
if (id === "all") onChange(undefined);
|
|
54
|
+
else if (id === "last1") onChange({ last: 1 });
|
|
55
|
+
else if (id === "last5") onChange({ last: 5 });
|
|
56
|
+
else if (id === "compaction") onChange({ sinceCompaction: true });
|
|
57
|
+
else
|
|
58
|
+
onChange({
|
|
59
|
+
from: sel?.from ?? 1,
|
|
60
|
+
to: sel?.to ?? Math.max(1, turns.length),
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const custom = range && "from" in range ? range : null;
|
|
64
|
+
const options = turns.map((t) => (
|
|
65
|
+
<option key={t.n} value={t.n}>
|
|
66
|
+
{`#${t.n} · ${when(t.at)} · ${t.excerpt.slice(0, 40)}`}
|
|
67
|
+
</option>
|
|
68
|
+
));
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
<div className="flex flex-wrap gap-1.5" role="group" aria-label="Range">
|
|
73
|
+
{CHIPS.map((c) => (
|
|
74
|
+
<button
|
|
75
|
+
key={c.id}
|
|
76
|
+
type="button"
|
|
77
|
+
aria-pressed={chip === c.id}
|
|
78
|
+
onClick={() => pick(c.id)}
|
|
79
|
+
className={cn(
|
|
80
|
+
"min-h-11 rounded-full border px-3.5 text-sm transition-colors",
|
|
81
|
+
chip === c.id
|
|
82
|
+
? "border-primary bg-primary/10 text-foreground"
|
|
83
|
+
: "text-muted-foreground hover:bg-foreground/[0.04]"
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
{c.label}
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{custom && (
|
|
92
|
+
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-2">
|
|
93
|
+
<label className="text-muted-foreground space-y-1 text-xs">
|
|
94
|
+
From
|
|
95
|
+
<select
|
|
96
|
+
className={SELECT}
|
|
97
|
+
value={custom.from}
|
|
98
|
+
onChange={(e) =>
|
|
99
|
+
onChange({
|
|
100
|
+
from: Number(e.target.value),
|
|
101
|
+
to: Math.max(custom.to, Number(e.target.value)),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
>
|
|
105
|
+
{options}
|
|
106
|
+
</select>
|
|
107
|
+
</label>
|
|
108
|
+
<label className="text-muted-foreground space-y-1 text-xs">
|
|
109
|
+
To
|
|
110
|
+
<select
|
|
111
|
+
className={SELECT}
|
|
112
|
+
value={custom.to}
|
|
113
|
+
onChange={(e) =>
|
|
114
|
+
onChange({
|
|
115
|
+
from: Math.min(custom.from, Number(e.target.value)),
|
|
116
|
+
to: Number(e.target.value),
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
{options}
|
|
121
|
+
</select>
|
|
122
|
+
</label>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
<div className="flex items-center justify-between gap-2">
|
|
127
|
+
<p className="text-muted-foreground min-w-0 text-xs">
|
|
128
|
+
{sel && sel.startedAt && sel.endedAt
|
|
129
|
+
? `Turns ${sel.from}–${sel.to} · ${sel.turns} ${sel.turns === 1 ? "turn" : "turns"} · ${clock(sel.startedAt)}–${clock(sel.endedAt)}`
|
|
130
|
+
: "Counting turns…"}
|
|
131
|
+
</p>
|
|
132
|
+
<Button
|
|
133
|
+
variant="outline"
|
|
134
|
+
size="sm"
|
|
135
|
+
className="min-h-11 shrink-0"
|
|
136
|
+
onClick={onSelectInChat}
|
|
137
|
+
>
|
|
138
|
+
<MousePointerClick className="h-4 w-4" />
|
|
139
|
+
Select in chat
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
// A row of exclusive choices, each at least 44px tall.
|
|
6
|
+
export function Seg<T extends string>({
|
|
7
|
+
label,
|
|
8
|
+
value,
|
|
9
|
+
options,
|
|
10
|
+
onChange,
|
|
11
|
+
}: {
|
|
12
|
+
label: string;
|
|
13
|
+
value: T;
|
|
14
|
+
options: { id: T; label: string; sub?: string }[];
|
|
15
|
+
onChange: (id: T) => void;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex gap-1.5" role="group" aria-label={label}>
|
|
19
|
+
{options.map((o) => (
|
|
20
|
+
<button
|
|
21
|
+
key={o.id}
|
|
22
|
+
type="button"
|
|
23
|
+
aria-pressed={value === o.id}
|
|
24
|
+
onClick={() => onChange(o.id)}
|
|
25
|
+
className={cn(
|
|
26
|
+
"flex min-h-11 min-w-0 flex-1 flex-col items-center justify-center rounded-lg border px-2 text-sm transition-colors",
|
|
27
|
+
value === o.id
|
|
28
|
+
? "border-primary bg-primary/10 text-foreground"
|
|
29
|
+
: "text-muted-foreground hover:bg-foreground/[0.04]"
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<span className="font-medium">{o.label}</span>
|
|
33
|
+
{o.sub && <span className="text-[11px] opacity-80">{o.sub}</span>}
|
|
34
|
+
</button>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSnapshot } from "valtio";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { exportUi, exportUiActions, pickedRange } from "@/stores/exportUi";
|
|
6
|
+
import { tokenLabel } from "@/lib/export/view";
|
|
7
|
+
|
|
8
|
+
// The bar under the chat while turns are being picked for an export.
|
|
9
|
+
export function TurnSelectBar() {
|
|
10
|
+
const { pick, turns } = useSnapshot(exportUi);
|
|
11
|
+
const range = pickedRange(pick);
|
|
12
|
+
const picked = range
|
|
13
|
+
? turns.filter((t) => t.n >= range.from && t.n <= range.to)
|
|
14
|
+
: [];
|
|
15
|
+
const tokens = picked.reduce((sum, t) => sum + t.tokens, 0);
|
|
16
|
+
const count = range ? range.to - range.from + 1 : 0;
|
|
17
|
+
const text = !range
|
|
18
|
+
? "Tap the first turn to export"
|
|
19
|
+
: pick.to === null
|
|
20
|
+
? `Turn ${range.from} · tap the last turn, or export just this one`
|
|
21
|
+
: `Turns ${range.from}–${range.to} · ${count} ${count === 1 ? "turn" : "turns"} · ${tokenLabel(tokens)}`;
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
role="region"
|
|
25
|
+
aria-label="Select turns to export"
|
|
26
|
+
className="bg-background fixed inset-x-0 bottom-0 z-40 flex flex-wrap items-center gap-2 border-t p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
|
|
27
|
+
>
|
|
28
|
+
<p className="min-w-[140px] flex-1 text-sm">{text}</p>
|
|
29
|
+
<Button
|
|
30
|
+
variant="outline"
|
|
31
|
+
className="min-h-11"
|
|
32
|
+
onClick={exportUiActions.cancelSelect}
|
|
33
|
+
>
|
|
34
|
+
Cancel
|
|
35
|
+
</Button>
|
|
36
|
+
<Button
|
|
37
|
+
className="min-h-11"
|
|
38
|
+
disabled={!range}
|
|
39
|
+
onClick={exportUiActions.finishSelect}
|
|
40
|
+
>
|
|
41
|
+
Export…
|
|
42
|
+
</Button>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { ExternalLink, Link2, Loader2, RefreshCw } from "lucide-react";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Notice } from "@/components/ui/notice";
|
|
6
7
|
import { LinkForm } from "@/components/Welcome/ConnectBlock";
|
|
7
8
|
import {
|
|
8
9
|
useAccountMachines,
|
|
9
10
|
useRefreshAccount,
|
|
10
11
|
useStartLink,
|
|
11
12
|
} from "@/data/peers";
|
|
12
|
-
import {
|
|
13
|
+
import { UPGRADE_URL } from "@/lib/peer/reasons";
|
|
13
14
|
import type { AccountMachine } from "@/lib/peer/machines";
|
|
14
15
|
import { cn } from "@/lib/utils";
|
|
15
16
|
import { WaitingLinks } from "./IncomingLinkSheet";
|
|
@@ -57,7 +58,12 @@ function MachineRow({
|
|
|
57
58
|
Linked
|
|
58
59
|
</span>
|
|
59
60
|
) : machine.linkState === "suspended" ? (
|
|
60
|
-
<span
|
|
61
|
+
<span
|
|
62
|
+
className="text-xs text-amber-700 dark:text-amber-400"
|
|
63
|
+
title={machine.suspendedReason ?? undefined}
|
|
64
|
+
>
|
|
65
|
+
Suspended
|
|
66
|
+
</span>
|
|
61
67
|
) : (
|
|
62
68
|
<Button
|
|
63
69
|
variant="outline"
|
|
@@ -122,23 +128,31 @@ export function AccountMachines() {
|
|
|
122
128
|
/>
|
|
123
129
|
</Button>
|
|
124
130
|
</div>
|
|
131
|
+
{data.uncheckedSince && (
|
|
132
|
+
<Notice tone="warning">
|
|
133
|
+
Couldn't check your account since{" "}
|
|
134
|
+
{new Date(data.uncheckedSince).toLocaleString([], {
|
|
135
|
+
dateStyle: "medium",
|
|
136
|
+
timeStyle: "short",
|
|
137
|
+
})}
|
|
138
|
+
. Machines already linked keep working; new links wait until it's
|
|
139
|
+
back.
|
|
140
|
+
</Notice>
|
|
141
|
+
)}
|
|
125
142
|
{data.entitlements && !entitled && (
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</a>
|
|
140
|
-
</Button>
|
|
141
|
-
</div>
|
|
143
|
+
<Notice
|
|
144
|
+
action={
|
|
145
|
+
<Button asChild variant="outline" className="min-h-11 sm:min-h-9">
|
|
146
|
+
<a href={UPGRADE_URL} target="_blank" rel="noopener noreferrer">
|
|
147
|
+
Upgrade
|
|
148
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
149
|
+
</a>
|
|
150
|
+
</Button>
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
Link your machines with Pro: tasks move between them over any network,
|
|
154
|
+
end-to-end encrypted.
|
|
155
|
+
</Notice>
|
|
142
156
|
)}
|
|
143
157
|
{entitled && <WaitingLinks />}
|
|
144
158
|
{data.error && (
|