@saadnvd1/agent-os 0.3.768 → 0.3.775
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/sessions/[id]/artifacts/[artifactId]/compare/route.ts +41 -0
- package/app/api/sessions/resolve/route.test.ts +81 -0
- package/app/api/sessions/resolve/route.ts +14 -0
- package/app/api/shares/preview/route.ts +6 -1
- package/app/api/shares/route.test.ts +51 -0
- package/app/page.tsx +1 -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 +3 -9
- package/components/Chat/EventRow.tsx +4 -4
- package/components/Chat/Messages.tsx +12 -6
- package/components/Chat/SessionAnchor.tsx +22 -0
- package/components/Chat/ShareFromHere.tsx +39 -0
- package/components/Chat/session-link.ts +21 -4
- 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/Health/EnvFailures/SessionList.tsx +2 -2
- package/components/Pane/PaneMenu.tsx +1 -1
- package/components/SessionList/QueuedRow.tsx +4 -4
- package/components/SessionList/SessionRowMenu.tsx +1 -1
- 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 +3 -5
- 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/useCopyToClipboard.ts +5 -2
- package/hooks/usePaletteCommands.ts +4 -1
- package/hooks/useSessionHref.ts +20 -0
- package/hooks/useSessionUrl.ts +40 -11
- 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/away/deploy.ts +39 -2
- 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/reports/evidence/build.ts +4 -3
- package/lib/security/demo.ts +3 -0
- package/lib/session-audit/render.ts +2 -1
- package/lib/session-logs/gather.ts +2 -2
- package/lib/session-slug.ts +29 -0
- package/lib/session-url.ts +162 -40
- package/lib/sessions/resolve.ts +55 -0
- package/lib/settings-url.ts +1 -1
- package/lib/share/markdown.ts +3 -0
- package/lib/share/range.ts +189 -0
- package/lib/share/render.tsx +3 -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/welcome-url.ts +1 -1
- package/next.config.ts +4 -0
- package/package.json +1 -1
- package/stores/chatJump.ts +5 -1
- package/stores/palette.ts +13 -11
- package/stores/shareUi.ts +6 -1
package/lib/away/deploy.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
11
12
|
import type { ProjectConfig } from "../project-config";
|
|
12
13
|
import { gitFetch, isAncestor } from "../git-exec";
|
|
13
14
|
import { expandHome } from "../tasks/session";
|
|
@@ -19,14 +20,22 @@ export type DeployConfig = NonNullable<ProjectConfig["deploy"]>;
|
|
|
19
20
|
export type DeployVerdict =
|
|
20
21
|
| { state: "ok"; detail: string }
|
|
21
22
|
| { state: "failed"; detail: string }
|
|
22
|
-
| { state: "pending" }
|
|
23
|
+
| { state: "pending"; detail?: string }
|
|
23
24
|
| { state: "unchecked"; detail: string };
|
|
24
25
|
|
|
25
26
|
export const DEPLOY_WITHIN_MIN = 30;
|
|
26
27
|
// After the deploy lands, the server gets this long to answer healthy.
|
|
27
28
|
export const HEALTH_GRACE_MS = 3 * 60 * 1000;
|
|
28
29
|
|
|
30
|
+
// scripts/autodeploy holds a new head AGENTOS_MIN_GAP seconds after the last
|
|
31
|
+
// restart (`restarted`, next to `deployed`), so a merge isn't late until the
|
|
32
|
+
// gap is over plus this margin.
|
|
33
|
+
export const GAP_MARGIN_MS = 10 * 60 * 1000;
|
|
34
|
+
const DEFAULT_GAP_S = 3600;
|
|
35
|
+
|
|
29
36
|
export interface DeployDeps {
|
|
37
|
+
// When the deploy last restarted the server, in ms; null when unknown.
|
|
38
|
+
restartedAt: (file: string) => number | null;
|
|
30
39
|
// The commit a state file names, or null when there's no such file.
|
|
31
40
|
readSha: (file: string) => { sha: string; at: number } | null;
|
|
32
41
|
// Whether `sha` is in the history of `tip` (fetching it if need be);
|
|
@@ -89,7 +98,22 @@ async function health(
|
|
|
89
98
|
}
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
function restartedAt(file: string): number | null {
|
|
102
|
+
try {
|
|
103
|
+
const s = Number(fs.readFileSync(expandHome(file), "utf-8").trim());
|
|
104
|
+
return Number.isFinite(s) && s > 0 ? s * 1000 : null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const gapMs = () => {
|
|
111
|
+
const s = Number(process.env.AGENTOS_MIN_GAP ?? DEFAULT_GAP_S);
|
|
112
|
+
return (Number.isFinite(s) && s >= 0 ? s : DEFAULT_GAP_S) * 1000;
|
|
113
|
+
};
|
|
114
|
+
|
|
92
115
|
export const DEFAULT_DEPS: DeployDeps = {
|
|
116
|
+
restartedAt,
|
|
93
117
|
readSha,
|
|
94
118
|
contains,
|
|
95
119
|
health,
|
|
@@ -159,8 +183,21 @@ async function judge(
|
|
|
159
183
|
state: "failed",
|
|
160
184
|
detail: `the deploy of ${failed.sha.slice(0, 7)} (which has ${short}) failed; see the deploy's log`,
|
|
161
185
|
};
|
|
186
|
+
// The state files sit together; `restarted` is beside `deployed`.
|
|
187
|
+
const restarted = deps.restartedAt(
|
|
188
|
+
path.join(path.dirname(config.deployed), "restarted")
|
|
189
|
+
);
|
|
190
|
+
const gapEnd = restarted === null ? 0 : restarted + gapMs();
|
|
191
|
+
if (gapEnd > input.now)
|
|
192
|
+
return {
|
|
193
|
+
state: "pending",
|
|
194
|
+
detail: `deploy waiting: restart gap, ~${Math.ceil((gapEnd - input.now) / 60000)} min left`,
|
|
195
|
+
};
|
|
162
196
|
const within = (config.within_minutes ?? DEPLOY_WITHIN_MIN) * 60 * 1000;
|
|
163
|
-
if (
|
|
197
|
+
if (
|
|
198
|
+
input.now - input.mergedAt >= within &&
|
|
199
|
+
input.now >= gapEnd + GAP_MARGIN_MS
|
|
200
|
+
)
|
|
164
201
|
return {
|
|
165
202
|
state: "failed",
|
|
166
203
|
detail: `${short} not deployed ${Math.round(within / 60000)} minutes after the merge`,
|
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
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { TaskState } from "../../tasks/state";
|
|
|
6
6
|
import type { DigestSignals } from "../../analyst/types";
|
|
7
7
|
import { latestChecks } from "../../analyst/signals";
|
|
8
8
|
import type { EvidenceRef } from "../types";
|
|
9
|
+
import { sessionPath } from "../../session-url";
|
|
9
10
|
|
|
10
11
|
const EXCERPT = 1500;
|
|
11
12
|
const cap = (s: string, n = EXCERPT) =>
|
|
@@ -82,7 +83,7 @@ export function testEvidence(
|
|
|
82
83
|
status: c.passed ? "pass" : "fail",
|
|
83
84
|
at,
|
|
84
85
|
sha: head ?? undefined,
|
|
85
|
-
href:
|
|
86
|
+
href: sessionPath({ id: sessionId }),
|
|
86
87
|
}));
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -101,7 +102,7 @@ export function diffEvidence(
|
|
|
101
102
|
label: `${m[3]} (+${m[1] === "-" ? 0 : m[1]} −${m[2] === "-" ? 0 : m[2]})`,
|
|
102
103
|
status: "info",
|
|
103
104
|
at,
|
|
104
|
-
href:
|
|
105
|
+
href: sessionPath({ id: sessionId }),
|
|
105
106
|
});
|
|
106
107
|
if (out.length >= 40) break;
|
|
107
108
|
}
|
|
@@ -174,7 +175,7 @@ export function logEvidence(log: {
|
|
|
174
175
|
status: "pass",
|
|
175
176
|
at: log.createdAt,
|
|
176
177
|
excerpt: log.mergeCommit ? `Merge commit ${log.mergeCommit}` : undefined,
|
|
177
|
-
href:
|
|
178
|
+
href: sessionPath({ id: log.sessionId }),
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
181
|
|
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",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { PatternStat, Summary } from "./aggregate";
|
|
8
8
|
import type { AuditFindings } from "./store";
|
|
9
|
+
import { sessionPath } from "../session-url";
|
|
9
10
|
|
|
10
11
|
export const fmt = (n: number) =>
|
|
11
12
|
n >= 1e9
|
|
@@ -245,7 +246,7 @@ export function renderSummary(
|
|
|
245
246
|
"biggest output (est.)",
|
|
246
247
|
],
|
|
247
248
|
s.sessions.slice(0, 30).map((x) => [
|
|
248
|
-
`[${x.id.slice(0, 8)}](
|
|
249
|
+
`[${x.id.slice(0, 8)}](${sessionPath(x)})`,
|
|
249
250
|
x.kind,
|
|
250
251
|
x.name.slice(0, 40),
|
|
251
252
|
x.turns,
|
|
@@ -18,7 +18,7 @@ import { getWorkspace } from "../workspaces";
|
|
|
18
18
|
import { amendmentsOf } from "../orchestrator/amendments";
|
|
19
19
|
import { PLAN_FILE } from "../models/plan-step";
|
|
20
20
|
import { estimateCost, rowModel } from "../usage/cost";
|
|
21
|
-
import {
|
|
21
|
+
import { sessionPath } from "../session-url";
|
|
22
22
|
import type { LogData, LogEvent, LogUsageRow } from "./build";
|
|
23
23
|
|
|
24
24
|
const expandHome = (p: string) => p.replace(/^~/, os.homedir());
|
|
@@ -296,6 +296,6 @@ export function gatherLogData(s: Session, opts: GatherOpts = {}): LogData {
|
|
|
296
296
|
timeline,
|
|
297
297
|
usage: usageRows(s.id),
|
|
298
298
|
outcome: finalMessage(s.id),
|
|
299
|
-
sessionPath:
|
|
299
|
+
sessionPath: sessionPath(s),
|
|
300
300
|
};
|
|
301
301
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The readable part of a session's address (`/s/<slug>-<id8>`). Cosmetic:
|
|
2
|
+
// only the short id resolves, so the slug is rebuilt from the current name
|
|
3
|
+
// and a rename never breaks a link. No imports: the browser uses it too.
|
|
4
|
+
|
|
5
|
+
export const SLUG_MAX = 48;
|
|
6
|
+
export const SHORT_ID_LEN = 8;
|
|
7
|
+
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
|
|
10
|
+
export const isUuid = (id: string): boolean => UUID.test(id);
|
|
11
|
+
|
|
12
|
+
/** Lowercase ASCII words joined by `-`; `session` when nothing is left. */
|
|
13
|
+
export function sessionSlug(name: string): string {
|
|
14
|
+
const words = (name ?? "")
|
|
15
|
+
.normalize("NFKD")
|
|
16
|
+
.replace(/\p{M}/gu, "")
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
19
|
+
.replace(/^-+|-+$/g, "");
|
|
20
|
+
if (!words) return "session";
|
|
21
|
+
if (words.length <= SLUG_MAX) return words;
|
|
22
|
+
const cut = words.slice(0, SLUG_MAX);
|
|
23
|
+
const atWord = cut.lastIndexOf("-");
|
|
24
|
+
return (atWord > 0 ? cut.slice(0, atWord) : cut).replace(/-+$/, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The first group of a uuid, lowercase. */
|
|
28
|
+
export const shortId = (id: string): string =>
|
|
29
|
+
id.slice(0, SHORT_ID_LEN).toLowerCase();
|