@saadnvd1/agent-os 0.3.770 → 0.3.777
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/live-shares/[id]/route.ts +72 -0
- package/app/api/live-shares/route.test.ts +45 -0
- package/app/api/live-shares/route.ts +40 -0
- package/app/api/sessions/[id]/artifacts/[artifactId]/compare/route.ts +41 -0
- package/app/api/shares/preview/route.ts +6 -1
- package/app/api/shares/route.test.ts +51 -0
- package/app/page.tsx +6 -0
- package/components/Artifacts/ArtifactActions.tsx +27 -3
- package/components/Artifacts/ArtifactPreview.tsx +27 -12
- package/components/Artifacts/Compare/DiffView.tsx +116 -0
- package/components/Artifacts/Compare/SideBySide.tsx +60 -0
- package/components/Artifacts/Compare/VersionPicker.tsx +47 -0
- package/components/Artifacts/Compare/index.tsx +191 -0
- package/components/Chat/ChatPanel.tsx +6 -0
- package/components/Chat/EventRow.tsx +6 -0
- package/components/Chat/Messages.tsx +6 -0
- package/components/Chat/ShareFromHere.tsx +39 -0
- package/components/CommandPalette/CommandPalette.tsx +5 -4
- package/components/CommandPalette/CommandsBody.tsx +97 -131
- package/components/CommandPalette/PaletteRows.tsx +79 -0
- package/components/CommandPalette/PaletteTabs.tsx +51 -0
- package/components/CommandPalette/conversations/ConversationSearch.tsx +13 -11
- package/components/CommandPalette/useAppCommands.ts +14 -0
- package/components/CommandPalette/useConversationRows.ts +55 -0
- package/components/Live/GuestPromptCard.tsx +64 -0
- package/components/Live/InviteDialog.tsx +225 -0
- package/components/Live/LiveChip.tsx +109 -0
- package/components/Live/LivePanel.tsx +81 -0
- package/components/Live/LobbyCard.tsx +64 -0
- package/components/Pane/MobileTabBar.tsx +8 -0
- package/components/Pane/PaneMenu.tsx +10 -0
- package/components/SessionList/SessionRowMenu.tsx +6 -0
- package/components/SessionList/useSessionSwitching.ts +2 -2
- package/components/Share/MessageList.tsx +87 -0
- package/components/Share/RangePicker.tsx +59 -0
- package/components/Share/ShareDialog.tsx +113 -52
- package/components/Shares/SharedLinksPanel.tsx +51 -0
- package/data/live/index.ts +2 -0
- package/data/live/keys.ts +4 -0
- package/data/live/queries.ts +81 -0
- package/data/session-artifacts/keys.ts +2 -0
- package/data/session-artifacts/queries.ts +25 -0
- package/data/shares/queries.ts +11 -0
- package/hooks/usePaletteCommands.ts +4 -1
- package/lib/artifacts/actions.ts +11 -1
- package/lib/artifacts/compare/compareVersions.ts +66 -0
- package/lib/artifacts/compare/diff.ts +178 -0
- package/lib/artifacts/compare/extractText.ts +64 -0
- package/lib/artifacts/compare/index.ts +11 -0
- package/lib/artifacts/compare/types.ts +44 -0
- package/lib/artifacts/match.ts +64 -0
- package/lib/artifacts/store.ts +39 -10
- package/lib/artifacts/tools.ts +9 -4
- package/lib/artifacts/versions.ts +2 -1
- package/lib/chat/events.ts +4 -1
- package/lib/chat/registry.ts +10 -0
- package/lib/connect/control-api.ts +44 -0
- package/lib/db/migrations/20261029T1400_live_shares.ts +42 -0
- package/lib/live/clean.ts +54 -0
- package/lib/live/frames.ts +132 -0
- package/lib/live/inbox.ts +225 -0
- package/lib/live/publisher.ts +84 -0
- package/lib/live/ring.ts +61 -0
- package/lib/live/runtime.ts +51 -0
- package/lib/live/service.ts +318 -0
- package/lib/live/socket.ts +293 -0
- package/lib/live/store.ts +149 -0
- package/lib/live/types.ts +39 -0
- package/lib/palette/registry.ts +71 -17
- package/lib/palette/sections.ts +143 -0
- package/lib/palette/tabs.ts +21 -0
- package/lib/palette/usage.ts +81 -0
- package/lib/security/demo.ts +3 -0
- package/lib/share/connect.ts +24 -0
- package/lib/share/markdown.ts +3 -0
- package/lib/share/range.ts +189 -0
- package/lib/share/redact.ts +23 -1
- package/lib/share/render.tsx +3 -0
- package/lib/share/secret-patterns.ts +77 -0
- package/lib/share/service.ts +64 -19
- package/lib/share/snapshot.ts +39 -2
- package/lib/share/styles.ts +2 -0
- package/lib/share/view.ts +11 -0
- package/lib/sidebar/row-menu.ts +2 -0
- package/package.json +1 -1
- package/server.ts +2 -0
- package/stores/liveUi.ts +17 -0
- package/stores/palette.ts +13 -11
- package/stores/shareUi.ts +6 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { db } from "../db";
|
|
2
|
+
|
|
3
|
+
export interface LiveShareRow {
|
|
4
|
+
id: string;
|
|
5
|
+
session_id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
ends_with_session: number;
|
|
9
|
+
max_guests: number;
|
|
10
|
+
created_at: string;
|
|
11
|
+
expires_at: string | null;
|
|
12
|
+
ended_at: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type LogKind =
|
|
16
|
+
| "join_request"
|
|
17
|
+
| "admit"
|
|
18
|
+
| "deny"
|
|
19
|
+
| "kick"
|
|
20
|
+
| "suggestion"
|
|
21
|
+
| "sent"
|
|
22
|
+
| "dismissed";
|
|
23
|
+
|
|
24
|
+
export interface LogRow {
|
|
25
|
+
id: number;
|
|
26
|
+
share_id: string;
|
|
27
|
+
at: number;
|
|
28
|
+
kind: LogKind;
|
|
29
|
+
guest_id: string | null;
|
|
30
|
+
guest_name: string | null;
|
|
31
|
+
ref: string | null;
|
|
32
|
+
text: string | null;
|
|
33
|
+
sent_text: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function insertLiveShare(row: LiveShareRow): void {
|
|
37
|
+
db.prepare(
|
|
38
|
+
`INSERT INTO live_shares
|
|
39
|
+
(id, session_id, title, url, ends_with_session, max_guests, created_at, expires_at, ended_at)
|
|
40
|
+
VALUES (@id, @session_id, @title, @url, @ends_with_session, @max_guests, @created_at, @expires_at, @ended_at)`
|
|
41
|
+
).run(row);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getLiveShare(id: string): LiveShareRow | undefined {
|
|
45
|
+
return db.prepare(`SELECT * FROM live_shares WHERE id = ?`).get(id) as
|
|
46
|
+
LiveShareRow | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Shares this machine still holds: not ended, not past their time. */
|
|
50
|
+
export function activeLiveShares(now = Date.now()): LiveShareRow[] {
|
|
51
|
+
return (
|
|
52
|
+
db
|
|
53
|
+
.prepare(
|
|
54
|
+
`SELECT * FROM live_shares WHERE ended_at IS NULL ORDER BY created_at, rowid`
|
|
55
|
+
)
|
|
56
|
+
.all() as LiveShareRow[]
|
|
57
|
+
).filter((s) => !s.expires_at || Date.parse(s.expires_at) > now);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function activeLiveShareFor(
|
|
61
|
+
sessionId: string,
|
|
62
|
+
now = Date.now()
|
|
63
|
+
): LiveShareRow | undefined {
|
|
64
|
+
return activeLiveShares(now).find((s) => s.session_id === sessionId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function markLiveEnded(id: string): void {
|
|
68
|
+
db.prepare(
|
|
69
|
+
`UPDATE live_shares SET ended_at = COALESCE(ended_at, ?) WHERE id = ?`
|
|
70
|
+
).run(new Date().toISOString(), id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function logLive(
|
|
74
|
+
row: Omit<LogRow, "id" | "at"> & { at?: number }
|
|
75
|
+
): void {
|
|
76
|
+
db.prepare(
|
|
77
|
+
`INSERT INTO live_share_log (share_id, at, kind, guest_id, guest_name, ref, text, sent_text)
|
|
78
|
+
VALUES (@share_id, @at, @kind, @guest_id, @guest_name, @ref, @text, @sent_text)`
|
|
79
|
+
).run({ ...row, at: row.at ?? Date.now() });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function listLog(shareId: string): LogRow[] {
|
|
83
|
+
return db
|
|
84
|
+
.prepare(`SELECT * FROM live_share_log WHERE share_id = ? ORDER BY id`)
|
|
85
|
+
.all(shareId) as LogRow[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface OpenSuggestion {
|
|
89
|
+
id: string;
|
|
90
|
+
guestId: string;
|
|
91
|
+
guestName: string;
|
|
92
|
+
text: string;
|
|
93
|
+
at: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Suggestions with no sent or dismissed line yet, oldest first. */
|
|
97
|
+
export function openSuggestions(shareId: string): OpenSuggestion[] {
|
|
98
|
+
const rows = db
|
|
99
|
+
.prepare(
|
|
100
|
+
`SELECT s.ref, s.guest_id, s.guest_name, s.text, s.at FROM live_share_log s
|
|
101
|
+
WHERE s.share_id = ? AND s.kind = 'suggestion' AND NOT EXISTS (
|
|
102
|
+
SELECT 1 FROM live_share_log d
|
|
103
|
+
WHERE d.share_id = s.share_id AND d.ref = s.ref AND d.kind IN ('sent', 'dismissed'))
|
|
104
|
+
ORDER BY s.id`
|
|
105
|
+
)
|
|
106
|
+
.all(shareId) as {
|
|
107
|
+
ref: string;
|
|
108
|
+
guest_id: string;
|
|
109
|
+
guest_name: string;
|
|
110
|
+
text: string;
|
|
111
|
+
at: number;
|
|
112
|
+
}[];
|
|
113
|
+
return rows.map((r) => ({
|
|
114
|
+
id: r.ref,
|
|
115
|
+
guestId: r.guest_id,
|
|
116
|
+
guestName: r.guest_name,
|
|
117
|
+
text: r.text,
|
|
118
|
+
at: r.at,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Guests this machine admitted and has not removed since. */
|
|
123
|
+
export function admittedGuestIds(shareId: string): Set<string> {
|
|
124
|
+
const out = new Set<string>();
|
|
125
|
+
for (const l of listLog(shareId)) {
|
|
126
|
+
if (!l.guest_id) continue;
|
|
127
|
+
if (l.kind === "admit") out.add(l.guest_id);
|
|
128
|
+
else if (l.kind === "kick" || l.kind === "deny") out.delete(l.guest_id);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Whether a suggestion with this id was ever logged for the share. */
|
|
134
|
+
export function hasSuggestion(shareId: string, ref: string): boolean {
|
|
135
|
+
return !!db
|
|
136
|
+
.prepare(
|
|
137
|
+
`SELECT 1 FROM live_share_log WHERE share_id = ? AND kind = 'suggestion' AND ref = ? LIMIT 1`
|
|
138
|
+
)
|
|
139
|
+
.get(shareId, ref);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Whether a join request from this guest was already logged. */
|
|
143
|
+
export function hasJoinRequest(shareId: string, guestId: string): boolean {
|
|
144
|
+
return !!db
|
|
145
|
+
.prepare(
|
|
146
|
+
`SELECT 1 FROM live_share_log WHERE share_id = ? AND kind = 'join_request' AND guest_id = ? LIMIT 1`
|
|
147
|
+
)
|
|
148
|
+
.get(shareId, guestId);
|
|
149
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// What the host's screens show of a live share (served by /api/live-shares).
|
|
2
|
+
export interface LiveView {
|
|
3
|
+
id: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
title: string;
|
|
6
|
+
url: string;
|
|
7
|
+
expiresAt: string | null;
|
|
8
|
+
endsWithSession: boolean;
|
|
9
|
+
maxGuests: number;
|
|
10
|
+
connected: boolean;
|
|
11
|
+
notice: string | null;
|
|
12
|
+
guests: {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
status: "pending" | "admitted";
|
|
16
|
+
online: boolean;
|
|
17
|
+
/** Another guest here has the same name. */
|
|
18
|
+
duplicateName: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
suggestions: {
|
|
21
|
+
id: string;
|
|
22
|
+
guestId: string;
|
|
23
|
+
guestName: string;
|
|
24
|
+
text: string;
|
|
25
|
+
at: number;
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type LiveExpiry = "1h" | "session";
|
|
30
|
+
|
|
31
|
+
export const EXPIRY_LABEL: Record<LiveExpiry, string> = {
|
|
32
|
+
"1h": "1 hour",
|
|
33
|
+
session: "Until the session ends",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface LiveResponse {
|
|
37
|
+
linked: boolean;
|
|
38
|
+
shares: LiveView[];
|
|
39
|
+
}
|
package/lib/palette/registry.ts
CHANGED
|
@@ -3,6 +3,36 @@
|
|
|
3
3
|
* take them back the same way, so the palette itself knows no feature.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export type PaletteTab =
|
|
7
|
+
"all" | "sessions" | "commands" | "settings" | "conversations";
|
|
8
|
+
|
|
9
|
+
export const PALETTE_TABS: { id: PaletteTab; label: string }[] = [
|
|
10
|
+
{ id: "all", label: "All" },
|
|
11
|
+
{ id: "sessions", label: "Sessions" },
|
|
12
|
+
{ id: "commands", label: "Commands" },
|
|
13
|
+
{ id: "settings", label: "Settings" },
|
|
14
|
+
{ id: "conversations", label: "Conversations" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Which tab each heading's commands are found under; a heading not listed is
|
|
18
|
+
// a command. A command can name its own tab instead.
|
|
19
|
+
const GROUP_TAB: Record<string, PaletteTab> = {
|
|
20
|
+
Sessions: "sessions",
|
|
21
|
+
Settings: "settings",
|
|
22
|
+
Conversations: "conversations",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Headings whose commands act on the focused session.
|
|
26
|
+
const SESSION_GROUPS = new Set(["Chat", "Task"]);
|
|
27
|
+
|
|
28
|
+
export function tabOf(c: PaletteCommand): PaletteTab {
|
|
29
|
+
return c.tab ?? GROUP_TAB[c.group] ?? "commands";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function actsOnFocusedSession(c: PaletteCommand): boolean {
|
|
33
|
+
return SESSION_GROUPS.has(c.group);
|
|
34
|
+
}
|
|
35
|
+
|
|
6
36
|
export interface PaletteCommand {
|
|
7
37
|
id: string;
|
|
8
38
|
title: string;
|
|
@@ -14,6 +44,10 @@ export interface PaletteCommand {
|
|
|
14
44
|
hint?: string;
|
|
15
45
|
// A line under the title (what a conversation hit said).
|
|
16
46
|
detail?: string;
|
|
47
|
+
// Overrides the tab its group maps to.
|
|
48
|
+
tab?: PaletteTab;
|
|
49
|
+
// A session that is working or waiting on the user.
|
|
50
|
+
status?: "running" | "waiting";
|
|
17
51
|
// Running it leaves the palette open (it changes the palette itself).
|
|
18
52
|
keepOpen?: boolean;
|
|
19
53
|
icon?: unknown;
|
|
@@ -84,25 +118,45 @@ export function fuzzyScore(query: string, text: string): number | null {
|
|
|
84
118
|
return score;
|
|
85
119
|
}
|
|
86
120
|
|
|
87
|
-
|
|
88
|
-
|
|
121
|
+
export interface Scored {
|
|
122
|
+
c: PaletteCommand;
|
|
123
|
+
score: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The commands a query finds, best first, with their scores. `boost` (0 to 1,
|
|
127
|
+
// e.g. how much it's used) scales a match up by at most half: it can break a
|
|
128
|
+
// close call, never rescue a non-match or outrank a far better match. With no
|
|
129
|
+
// query the boost alone orders them, registry order breaking ties.
|
|
130
|
+
export function scoreCommands(
|
|
89
131
|
commands: PaletteCommand[],
|
|
90
|
-
query: string
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
query: string,
|
|
133
|
+
boost: (c: PaletteCommand) => number = () => 0
|
|
134
|
+
): Scored[] {
|
|
135
|
+
const empty = !query.trim();
|
|
93
136
|
return commands
|
|
94
137
|
.map((c, i) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
138
|
+
let base = 0;
|
|
139
|
+
if (!empty) {
|
|
140
|
+
const scores = [c.title, ...(c.keywords ?? []), c.group].map(
|
|
141
|
+
(text, j) => {
|
|
142
|
+
const s = fuzzyScore(query, text);
|
|
143
|
+
// A title match counts most.
|
|
144
|
+
return s === null ? null : j === 0 ? s * 1.5 : s;
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
base = Math.max(...scores.map((s) => s ?? -Infinity));
|
|
148
|
+
}
|
|
149
|
+
const b = boost(c);
|
|
150
|
+
return { c, i, base, score: empty ? b : base + Math.abs(base) * 0.5 * b };
|
|
104
151
|
})
|
|
105
|
-
.filter((m) => m.
|
|
106
|
-
.sort((a, b) => b.
|
|
107
|
-
.map((
|
|
152
|
+
.filter((m) => m.base > -Infinity)
|
|
153
|
+
.sort((a, b) => b.score - a.score || a.i - b.i)
|
|
154
|
+
.map(({ c, score }) => ({ c, score }));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function matchCommands(
|
|
158
|
+
commands: PaletteCommand[],
|
|
159
|
+
query: string
|
|
160
|
+
): PaletteCommand[] {
|
|
161
|
+
return scoreCommands(commands, query).map((m) => m.c);
|
|
108
162
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PALETTE_TABS,
|
|
3
|
+
actsOnFocusedSession,
|
|
4
|
+
scoreCommands,
|
|
5
|
+
tabOf,
|
|
6
|
+
type PaletteCommand,
|
|
7
|
+
type PaletteTab,
|
|
8
|
+
} from "./registry";
|
|
9
|
+
import { recentIds, usageBoost, type Usage } from "./usage";
|
|
10
|
+
|
|
11
|
+
export interface Section {
|
|
12
|
+
name: string;
|
|
13
|
+
items: PaletteCommand[];
|
|
14
|
+
// The tab that lists the whole section, when this is only part of it.
|
|
15
|
+
more?: { tab: PaletteTab; count: number };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const PER_TAB = 5;
|
|
19
|
+
export const RECENT = 5;
|
|
20
|
+
const NEEDS_YOU = 5;
|
|
21
|
+
const SUGGESTED = 6;
|
|
22
|
+
|
|
23
|
+
const label = (tab: PaletteTab) =>
|
|
24
|
+
PALETTE_TABS.find((t) => t.id === tab)?.label ?? tab;
|
|
25
|
+
|
|
26
|
+
export const boostFor = (usage: Usage, now: number) => (c: PaletteCommand) =>
|
|
27
|
+
usageBoost(usage[c.id], now);
|
|
28
|
+
|
|
29
|
+
// What a query finds in All: each tab's best few under its heading, the
|
|
30
|
+
// strongest group first, with how many more its tab holds. Conversations are
|
|
31
|
+
// the caller's, to put last.
|
|
32
|
+
export function allSections(
|
|
33
|
+
commands: PaletteCommand[],
|
|
34
|
+
query: string,
|
|
35
|
+
usage: Usage,
|
|
36
|
+
now: number
|
|
37
|
+
): Section[] {
|
|
38
|
+
const scored = scoreCommands(commands, query, boostFor(usage, now));
|
|
39
|
+
const byTab = new Map<PaletteTab, typeof scored>();
|
|
40
|
+
for (const m of scored) {
|
|
41
|
+
const tab = tabOf(m.c);
|
|
42
|
+
if (tab === "conversations") continue;
|
|
43
|
+
byTab.set(tab, [...(byTab.get(tab) ?? []), m]);
|
|
44
|
+
}
|
|
45
|
+
return [...byTab.entries()]
|
|
46
|
+
.sort((a, b) => b[1][0].score - a[1][0].score)
|
|
47
|
+
.map(([tab, ms]) => ({
|
|
48
|
+
name: label(tab),
|
|
49
|
+
items: ms.slice(0, PER_TAB).map((m) => m.c),
|
|
50
|
+
...(ms.length > PER_TAB
|
|
51
|
+
? { more: { tab, count: ms.length } }
|
|
52
|
+
: undefined),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// One tab's commands, for a query or (ordered by use) without one.
|
|
57
|
+
export function tabItems(
|
|
58
|
+
commands: PaletteCommand[],
|
|
59
|
+
tab: PaletteTab,
|
|
60
|
+
query: string,
|
|
61
|
+
usage: Usage,
|
|
62
|
+
now: number
|
|
63
|
+
): PaletteCommand[] {
|
|
64
|
+
return scoreCommands(
|
|
65
|
+
commands.filter((c) => tabOf(c) === tab),
|
|
66
|
+
query,
|
|
67
|
+
boostFor(usage, now)
|
|
68
|
+
).map((m) => m.c);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The short list All opens on: the last things run, sessions that need the
|
|
72
|
+
// user or are working, and what the focused session can do.
|
|
73
|
+
export function emptySections(
|
|
74
|
+
commands: PaletteCommand[],
|
|
75
|
+
usage: Usage
|
|
76
|
+
): Section[] {
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
const take = (items: PaletteCommand[], limit: number) => {
|
|
79
|
+
const out: PaletteCommand[] = [];
|
|
80
|
+
for (const c of items) {
|
|
81
|
+
if (out.length >= limit) break;
|
|
82
|
+
if (seen.has(c.id)) continue;
|
|
83
|
+
seen.add(c.id);
|
|
84
|
+
out.push(c);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
// A session that needs you is listed there, not also under Recent.
|
|
89
|
+
const rank = (c: PaletteCommand) => (c.status === "waiting" ? 0 : 1);
|
|
90
|
+
const busy = take(
|
|
91
|
+
commands.filter((c) => c.status).sort((a, b) => rank(a) - rank(b)),
|
|
92
|
+
NEEDS_YOU
|
|
93
|
+
);
|
|
94
|
+
const here = take(commands.filter(actsOnFocusedSession), 5);
|
|
95
|
+
const byId = new Map(commands.map((c) => [c.id, c]));
|
|
96
|
+
const recent = take(
|
|
97
|
+
recentIds(usage, commands.length)
|
|
98
|
+
.map((id) => byId.get(id))
|
|
99
|
+
.filter((c): c is PaletteCommand => !!c),
|
|
100
|
+
RECENT
|
|
101
|
+
);
|
|
102
|
+
const sections: Section[] = [
|
|
103
|
+
{ name: "Recent", items: recent },
|
|
104
|
+
{ name: "Needs you / running", items: busy },
|
|
105
|
+
{ name: "This session", items: here },
|
|
106
|
+
];
|
|
107
|
+
// A first run has no history: offer the commands that start things.
|
|
108
|
+
if (recent.length + busy.length + here.length < 4)
|
|
109
|
+
sections.push({
|
|
110
|
+
name: "Suggested",
|
|
111
|
+
items: take(
|
|
112
|
+
commands.filter((c) => tabOf(c) === "commands"),
|
|
113
|
+
SUGGESTED
|
|
114
|
+
),
|
|
115
|
+
});
|
|
116
|
+
return sections.filter((s) => s.items.length);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Each cut-short section ends with a row that opens its whole tab.
|
|
120
|
+
export function withSeeAll(
|
|
121
|
+
sections: Section[],
|
|
122
|
+
open: (tab: PaletteTab) => void,
|
|
123
|
+
icon?: unknown
|
|
124
|
+
): Section[] {
|
|
125
|
+
return sections.map((s) => {
|
|
126
|
+
const more = s.more;
|
|
127
|
+
if (!more) return s;
|
|
128
|
+
return {
|
|
129
|
+
...s,
|
|
130
|
+
items: [
|
|
131
|
+
...s.items,
|
|
132
|
+
{
|
|
133
|
+
id: `see-all:${more.tab}`,
|
|
134
|
+
title: `See all ${more.count} →`,
|
|
135
|
+
group: s.name,
|
|
136
|
+
icon,
|
|
137
|
+
keepOpen: true,
|
|
138
|
+
run: () => open(more.tab),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PALETTE_TABS, type PaletteTab } from "./registry";
|
|
2
|
+
|
|
3
|
+
const PREFIXES: Record<string, PaletteTab> = {
|
|
4
|
+
">": "commands",
|
|
5
|
+
"@": "sessions",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// A leading `>` or `@` on a fresh query picks a tab; null when there's none.
|
|
9
|
+
export function parsePrefix(
|
|
10
|
+
text: string
|
|
11
|
+
): { tab: PaletteTab; text: string } | null {
|
|
12
|
+
const tab = PREFIXES[text[0]];
|
|
13
|
+
return tab ? { tab, text: text.slice(1).trimStart() } : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The next tab (or the one before), wrapping round.
|
|
17
|
+
export function cycleTab(tab: PaletteTab, by: 1 | -1): PaletteTab {
|
|
18
|
+
const n = PALETTE_TABS.length;
|
|
19
|
+
const i = PALETTE_TABS.findIndex((t) => t.id === tab);
|
|
20
|
+
return PALETTE_TABS[(i + by + n) % n].id;
|
|
21
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface UsageEntry {
|
|
2
|
+
count: number;
|
|
3
|
+
last: number;
|
|
4
|
+
}
|
|
5
|
+
export type Usage = Record<string, UsageEntry>;
|
|
6
|
+
|
|
7
|
+
export const USAGE_KEY = "agentOS-palette-usage";
|
|
8
|
+
export const MAX_USAGE = 200;
|
|
9
|
+
const DAY = 86_400_000;
|
|
10
|
+
const HALF_LIFE_DAYS = 14;
|
|
11
|
+
// Uses at which the count part of the boost tops out.
|
|
12
|
+
const SATURATES_AT = 20;
|
|
13
|
+
|
|
14
|
+
// How strongly past use favours an item, 0 to 1: more uses count more (with
|
|
15
|
+
// diminishing returns) and the whole fades by half every two weeks.
|
|
16
|
+
export function usageBoost(entry: UsageEntry | undefined, now: number): number {
|
|
17
|
+
if (!entry) return 0;
|
|
18
|
+
const age = Math.max(0, now - entry.last) / DAY;
|
|
19
|
+
const weight = Math.min(
|
|
20
|
+
1,
|
|
21
|
+
Math.log2(1 + entry.count) / Math.log2(1 + SATURATES_AT)
|
|
22
|
+
);
|
|
23
|
+
return weight * Math.pow(0.5, age / HALF_LIFE_DAYS);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The usage with one more run of `id`, keeping the MAX_USAGE items that
|
|
27
|
+
// matter most right now.
|
|
28
|
+
export function recordUse(usage: Usage, id: string, now: number): Usage {
|
|
29
|
+
const next: Usage = {
|
|
30
|
+
...usage,
|
|
31
|
+
[id]: { count: (usage[id]?.count ?? 0) + 1, last: now },
|
|
32
|
+
};
|
|
33
|
+
const ids = Object.keys(next);
|
|
34
|
+
if (ids.length <= MAX_USAGE) return next;
|
|
35
|
+
const keep = ids
|
|
36
|
+
.sort((a, b) => usageBoost(next[b], now) - usageBoost(next[a], now))
|
|
37
|
+
.slice(0, MAX_USAGE);
|
|
38
|
+
return Object.fromEntries(keep.map((k) => [k, next[k]]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The ids last run most recently, newest first.
|
|
42
|
+
export function recentIds(usage: Usage, limit: number): string[] {
|
|
43
|
+
return Object.entries(usage)
|
|
44
|
+
.sort((a, b) => b[1].last - a[1].last)
|
|
45
|
+
.slice(0, limit)
|
|
46
|
+
.map(([id]) => id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function valid(e: unknown): e is UsageEntry {
|
|
50
|
+
const u = e as UsageEntry | null;
|
|
51
|
+
return (
|
|
52
|
+
!!u && Number.isFinite(u.count) && Number.isFinite(u.last) && u.count > 0
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function loadUsage(): Usage {
|
|
57
|
+
try {
|
|
58
|
+
const raw: unknown = JSON.parse(localStorage.getItem(USAGE_KEY) ?? "{}");
|
|
59
|
+
if (!raw || typeof raw !== "object") return {};
|
|
60
|
+
const usage: Usage = {};
|
|
61
|
+
for (const [id, e] of Object.entries(raw)) if (valid(e)) usage[id] = e;
|
|
62
|
+
return usage;
|
|
63
|
+
} catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function saveUsage(usage: Usage): void {
|
|
69
|
+
try {
|
|
70
|
+
localStorage.setItem(USAGE_KEY, JSON.stringify(usage));
|
|
71
|
+
} catch {
|
|
72
|
+
// Private mode or a full disk: ranking just forgets.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Records a run in storage and returns the new usage.
|
|
77
|
+
export function noteUse(id: string, now = Date.now()): Usage {
|
|
78
|
+
const next = recordUse(loadUsage(), id, now);
|
|
79
|
+
saveUsage(next);
|
|
80
|
+
return next;
|
|
81
|
+
}
|
package/lib/security/demo.ts
CHANGED
|
@@ -64,6 +64,7 @@ export const STATIC_SIBLINGS = [
|
|
|
64
64
|
"browse",
|
|
65
65
|
"check",
|
|
66
66
|
"clone",
|
|
67
|
+
"compare",
|
|
67
68
|
"connect",
|
|
68
69
|
"detect",
|
|
69
70
|
"done-idle",
|
|
@@ -84,6 +85,7 @@ export const STATIC_SIBLINGS = [
|
|
|
84
85
|
"pdf",
|
|
85
86
|
"preview",
|
|
86
87
|
"queue",
|
|
88
|
+
"resolve",
|
|
87
89
|
"status",
|
|
88
90
|
];
|
|
89
91
|
// No leading "-": an id that reaches a command line can't read as an option.
|
|
@@ -101,6 +103,7 @@ const READS = [
|
|
|
101
103
|
"sessions/[id]/pr",
|
|
102
104
|
"sessions/[id]/artifacts",
|
|
103
105
|
"sessions/[id]/artifacts/[id]",
|
|
106
|
+
"sessions/[id]/artifacts/[id]/compare",
|
|
104
107
|
"sessions/[id]/setup",
|
|
105
108
|
"sessions/[id]/preview",
|
|
106
109
|
"projects",
|
package/lib/share/connect.ts
CHANGED
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
clean,
|
|
7
7
|
listShares,
|
|
8
8
|
replaceShare,
|
|
9
|
+
createLiveShare,
|
|
9
10
|
revokeShare,
|
|
10
11
|
uploadShare,
|
|
12
|
+
type LiveExpiry,
|
|
11
13
|
type RemoteShare,
|
|
12
14
|
type ShareExpiry,
|
|
13
15
|
} from "@/lib/connect/control-api";
|
|
@@ -40,6 +42,16 @@ function explain(err: unknown): never {
|
|
|
40
42
|
`Connect refused it as too big or over your quota: ${clean(err.message, 120)}`,
|
|
41
43
|
413
|
|
42
44
|
);
|
|
45
|
+
if (err.status === 402)
|
|
46
|
+
throw new ShareError(
|
|
47
|
+
clean(
|
|
48
|
+
typeof err.body.error === "string"
|
|
49
|
+
? err.body.error
|
|
50
|
+
: "Your plan's live share limit is reached.",
|
|
51
|
+
160
|
|
52
|
+
),
|
|
53
|
+
402
|
|
54
|
+
);
|
|
43
55
|
if (err.status === 429)
|
|
44
56
|
throw new ShareError(
|
|
45
57
|
"Too many shares just now. Try again in a few minutes.",
|
|
@@ -95,3 +107,15 @@ export async function revokeRemote(id: string): Promise<void> {
|
|
|
95
107
|
explain(err);
|
|
96
108
|
}
|
|
97
109
|
}
|
|
110
|
+
|
|
111
|
+
export async function createLiveRemote(share: {
|
|
112
|
+
title: string;
|
|
113
|
+
expiresIn: LiveExpiry;
|
|
114
|
+
}) {
|
|
115
|
+
const { config, machineKey } = files();
|
|
116
|
+
try {
|
|
117
|
+
return await createLiveShare(config, machineKey, share);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
explain(err);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/lib/share/markdown.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { toMarkdown } from "mdast-util-to-markdown";
|
|
|
12
12
|
import type { Nodes as HastNodes } from "hast";
|
|
13
13
|
import type { Nodes, Parents, RootContent } from "mdast";
|
|
14
14
|
import type { Snapshot, SnapshotItem } from "./snapshot";
|
|
15
|
+
import { EARLIER_NOT_SHARED, LATER_NOT_SHARED } from "./range";
|
|
15
16
|
|
|
16
17
|
const DROP_TAGS = new Set([
|
|
17
18
|
"script",
|
|
@@ -119,7 +120,9 @@ export function chatToMarkdown(
|
|
|
119
120
|
): string {
|
|
120
121
|
const parts = [
|
|
121
122
|
agentHeader(snap.title, opts.sharedAt),
|
|
123
|
+
...(snap.cutBefore ? [`*${EARLIER_NOT_SHARED}.*`] : []),
|
|
122
124
|
...snap.items.map(itemToMarkdown),
|
|
125
|
+
...(snap.cutAfter ? [`*${LATER_NOT_SHARED}.*`] : []),
|
|
123
126
|
];
|
|
124
127
|
if (snap.toolsHidden) parts.push("*Tool calls and thinking were left out.*");
|
|
125
128
|
return parts.join("\n\n") + "\n";
|