@saadnvd1/agent-os 0.3.603 → 0.3.606
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/page.tsx +3 -8
- package/components/Bookmarks/ChatBookmarksButton.tsx +4 -0
- package/components/Bus/MessagesPanel.tsx +155 -0
- package/components/Bus/index.ts +1 -2
- package/components/Chat/ChatPanel.tsx +9 -1
- package/components/CommandPalette/useAppCommands.ts +12 -4
- package/components/Features/FeaturesSection.tsx +2 -2
- package/components/Inbox/InboxButton.tsx +39 -0
- package/components/Inbox/InboxDialog.tsx +139 -0
- package/components/Inbox/NeedsYouList.tsx +120 -0
- package/components/Inbox/index.ts +2 -0
- package/components/LumifyHub/index.ts +0 -1
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/PaneMenu.test.ts +118 -0
- package/components/Pane/PaneMenu.tsx +37 -0
- package/components/SessionList/SidebarHeader.tsx +7 -4
- package/components/Tasks/TaskRow.tsx +3 -3
- package/components/Tasks/TasksPanel.tsx +63 -0
- package/components/Tasks/index.ts +1 -1
- package/components/views/DesktopAppActions.tsx +10 -63
- package/components/views/DesktopView.tsx +1 -11
- package/hooks/useInbox.ts +69 -0
- package/lib/env-setup.ts +27 -12
- package/lib/inbox/counts.ts +140 -0
- package/lib/project-config/agentos.schema.json +14 -0
- package/lib/project-config/schema.ts +10 -0
- package/lib/worktree-deps/caches.ts +250 -0
- package/lib/worktree-deps/clone.ts +94 -9
- package/lib/worktree-deps/fastcopy.ts +89 -0
- package/lib/worktree-deps/index.ts +14 -5
- package/lib/worktree-deps/paths.ts +20 -0
- package/lib/worktree-deps/spare.ts +26 -8
- package/package.json +1 -1
- package/scripts/bench/worktree-setup.ts +101 -0
- package/scripts/check-bundle.mjs +25 -10
- package/scripts/check-bundle.test.ts +2 -2
- package/stores/inboxUi.ts +60 -0
- package/stores/needsYouFocus.ts +11 -3
- package/components/Bus/MessagesButton.tsx +0 -25
- package/components/Bus/MessagesDialog.tsx +0 -176
- package/components/LumifyHub/Docs/DocsButton.tsx +0 -31
- package/components/NotificationsMenu.tsx +0 -86
- package/components/Tasks/TasksButton.tsx +0 -28
- package/components/Tasks/TasksDialog.tsx +0 -75
- package/stores/busUi.ts +0 -12
- package/stores/tasksUi.ts +0 -14
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a Linux (or macOS) worktree can be given a copy of the main checkout's
|
|
3
|
+
* files without paying for every byte: an APFS clone, a reflink where the
|
|
4
|
+
* filesystem has them, or hardlinks from a store AgentOS owns. Null when
|
|
5
|
+
* none works, and the caller installs or copies plainly instead.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFile } from "child_process";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { promisify } from "util";
|
|
13
|
+
import type { CopyMode } from "./spare";
|
|
14
|
+
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
export type FastMode = Exclude<CopyMode, "copy">;
|
|
18
|
+
|
|
19
|
+
// Where hardlinked dependencies are kept: one real copy per lockfile, so a
|
|
20
|
+
// write through a link can only ever reach this, never the main checkout.
|
|
21
|
+
export const STORE_ROOT = path.join(os.homedir(), ".agent-os", "deps-store");
|
|
22
|
+
|
|
23
|
+
// Files tools rewrite in place. A hardlink would carry the write back into
|
|
24
|
+
// the store, so these are real copies in every worktree.
|
|
25
|
+
export const MUTABLE_IN_MODULES = [".package-lock.json", ".cache", ".vite"];
|
|
26
|
+
|
|
27
|
+
const probed = new Map<string, boolean>();
|
|
28
|
+
|
|
29
|
+
// Whether `dir`'s filesystem reflinks: tried on a scratch file made there.
|
|
30
|
+
export async function canReflink(dir: string): Promise<boolean> {
|
|
31
|
+
const dev = String((await fs.promises.stat(dir)).dev);
|
|
32
|
+
const known = probed.get(dev);
|
|
33
|
+
if (known !== undefined) return known;
|
|
34
|
+
const probe = path.join(dir, `.aos-reflink-probe-${process.pid}`);
|
|
35
|
+
let ok = false;
|
|
36
|
+
try {
|
|
37
|
+
await fs.promises.writeFile(probe, "x");
|
|
38
|
+
await execFileAsync("cp", ["--reflink=always", probe, `${probe}.copy`]);
|
|
39
|
+
ok = true;
|
|
40
|
+
} catch {
|
|
41
|
+
ok = false;
|
|
42
|
+
} finally {
|
|
43
|
+
await fs.promises.rm(probe, { force: true });
|
|
44
|
+
await fs.promises.rm(`${probe}.copy`, { force: true });
|
|
45
|
+
}
|
|
46
|
+
probed.set(dev, ok);
|
|
47
|
+
return ok;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const device = (p: string) =>
|
|
51
|
+
fs.promises.stat(p).then(
|
|
52
|
+
(s) => s.dev,
|
|
53
|
+
() => null
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export async function fastCopyMode(opts: {
|
|
57
|
+
sourcePath: string;
|
|
58
|
+
worktreePath: string;
|
|
59
|
+
storeRoot?: string;
|
|
60
|
+
platform?: NodeJS.Platform;
|
|
61
|
+
}): Promise<FastMode | null> {
|
|
62
|
+
const platform = opts.platform ?? process.platform;
|
|
63
|
+
if (platform === "darwin") return "clone";
|
|
64
|
+
if (platform !== "linux") return null;
|
|
65
|
+
const wt = await device(opts.worktreePath);
|
|
66
|
+
if (wt === null) return null;
|
|
67
|
+
if (
|
|
68
|
+
(await device(opts.sourcePath)) === wt &&
|
|
69
|
+
(await canReflink(opts.worktreePath))
|
|
70
|
+
)
|
|
71
|
+
return "reflink";
|
|
72
|
+
const store = opts.storeRoot ?? STORE_ROOT;
|
|
73
|
+
await fs.promises.mkdir(store, { recursive: true }).catch(() => undefined);
|
|
74
|
+
return (await device(store)) === wt ? "hardlink" : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Replace the entries of a hardlinked node_modules that tools rewrite in
|
|
78
|
+
// place with real copies.
|
|
79
|
+
export async function unlinkMutable(modules: string): Promise<void> {
|
|
80
|
+
for (const name of MUTABLE_IN_MODULES) {
|
|
81
|
+
const entry = path.join(modules, name);
|
|
82
|
+
if (!(await fs.promises.lstat(entry).catch(() => null))) continue;
|
|
83
|
+
const copy = `${entry}.aos-copy`;
|
|
84
|
+
await fs.promises.rm(copy, { recursive: true, force: true });
|
|
85
|
+
await fs.promises.cp(entry, copy, { recursive: true });
|
|
86
|
+
await fs.promises.rm(entry, { recursive: true, force: true });
|
|
87
|
+
await fs.promises.rename(copy, entry);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
import { exec } from "child_process";
|
|
7
7
|
import { promisify } from "util";
|
|
8
8
|
import type { SetupResult } from "../env-setup";
|
|
9
|
-
import {
|
|
9
|
+
import { NO_FAST_COPY, cloneDeps } from "./clone";
|
|
10
10
|
import { detectPackageManager, setupEnv } from "./install";
|
|
11
11
|
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
13
|
|
|
14
|
+
const COMMANDS = {
|
|
15
|
+
clone: "cp -Rc",
|
|
16
|
+
reflink: "cp -a --reflink=always",
|
|
17
|
+
hardlink: "cp -al (from the store)",
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* Run a setup command in the worktree directory
|
|
16
22
|
*/
|
|
@@ -57,15 +63,18 @@ export async function bringDependencies(
|
|
|
57
63
|
if (pm) result.packageManager = pm.name;
|
|
58
64
|
|
|
59
65
|
const clone = await deps.clone({ sourcePath, worktreePath, only });
|
|
60
|
-
//
|
|
61
|
-
if (!pm && !clone.ok && !clone.cloned.length && clone.reason ===
|
|
66
|
+
// Without a fast copy only node_modules has a fallback (the install below).
|
|
67
|
+
if (!pm && !clone.ok && !clone.cloned.length && clone.reason === NO_FAST_COPY)
|
|
62
68
|
return;
|
|
63
69
|
if (clone.ok || !pm) {
|
|
64
70
|
result.steps.push({
|
|
65
71
|
name: "Clone dependencies",
|
|
66
|
-
command: "cp
|
|
72
|
+
command: clone.mode ? COMMANDS[clone.mode] : "cp",
|
|
67
73
|
success: clone.ok,
|
|
68
|
-
output:
|
|
74
|
+
output: [
|
|
75
|
+
...clone.cloned.map((c) => `${c.rel} (${c.from})`),
|
|
76
|
+
...(clone.skipped ?? []).map((r) => `skipped ${r}`),
|
|
77
|
+
].join(", "),
|
|
69
78
|
error: clone.ok ? undefined : clone.reason,
|
|
70
79
|
});
|
|
71
80
|
if (!clone.ok) result.success = false;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path checks shared by what setup places into a worktree (agentos.json's
|
|
3
|
+
* `copy`, `clone` and `caches`): everything resolved through symlinks to a
|
|
4
|
+
* place inside its checkout, so a link can't carry a copy somewhere else.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
|
|
10
|
+
export const inside = (root: string, p: string) =>
|
|
11
|
+
p === root || p.startsWith(root + path.sep);
|
|
12
|
+
|
|
13
|
+
// The real path of `p`'s deepest part that exists.
|
|
14
|
+
export async function realExisting(p: string): Promise<string> {
|
|
15
|
+
for (let at = p; ; at = path.dirname(at)) {
|
|
16
|
+
const real = await fs.promises.realpath(at).catch(() => null);
|
|
17
|
+
if (real) return real;
|
|
18
|
+
if (path.dirname(at) === at) return at;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -49,18 +49,35 @@ export async function takeSpare(
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
// How a tree is copied: an APFS clone (macOS), a reflink (Linux on a
|
|
53
|
+
// copy-on-write filesystem), hardlinks (Linux, from AgentOS's own store), or
|
|
54
|
+
// a plain copy.
|
|
55
|
+
export type CopyMode = "clone" | "reflink" | "hardlink" | "copy";
|
|
56
|
+
|
|
57
|
+
export const copyArgs = (mode: CopyMode, from: string, to: string) =>
|
|
58
|
+
mode === "clone"
|
|
59
|
+
? [process.platform === "darwin" ? "-Rc" : "-R", from, to]
|
|
60
|
+
: mode === "reflink"
|
|
61
|
+
? ["-a", "--reflink=always", from, to]
|
|
62
|
+
: mode === "hardlink"
|
|
63
|
+
? ["-al", from, to]
|
|
64
|
+
: ["-a", from, to];
|
|
65
|
+
|
|
66
|
+
// What a copy with no mode given does: clones on macOS, a plain copy elsewhere.
|
|
67
|
+
const defaultMode = (): CopyMode =>
|
|
68
|
+
process.platform === "darwin" ? "clone" : "copy";
|
|
56
69
|
|
|
57
70
|
// A clone that stalls (a locked or dying volume) is killed rather than
|
|
58
71
|
// holding a task's start forever.
|
|
59
72
|
const COPY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
60
73
|
|
|
61
|
-
export function copyTree(
|
|
74
|
+
export function copyTree(
|
|
75
|
+
from: string,
|
|
76
|
+
to: string,
|
|
77
|
+
mode: CopyMode = defaultMode()
|
|
78
|
+
): Promise<boolean> {
|
|
62
79
|
return new Promise((resolve) => {
|
|
63
|
-
const child = spawn("cp",
|
|
80
|
+
const child = spawn("cp", copyArgs(mode, from, to), {
|
|
64
81
|
stdio: "ignore",
|
|
65
82
|
timeout: COPY_TIMEOUT_MS,
|
|
66
83
|
killSignal: "SIGKILL",
|
|
@@ -104,7 +121,8 @@ export async function replenishSpare(
|
|
|
104
121
|
root: string,
|
|
105
122
|
sourcePath: string,
|
|
106
123
|
rel: string,
|
|
107
|
-
lockHash: string
|
|
124
|
+
lockHash: string,
|
|
125
|
+
mode?: CopyMode
|
|
108
126
|
): Promise<"made" | "exists" | "failed"> {
|
|
109
127
|
const spare = sparePath(root, sourcePath, rel, lockHash);
|
|
110
128
|
if (building.has(spare) || fs.existsSync(spare)) return "exists";
|
|
@@ -113,7 +131,7 @@ export async function replenishSpare(
|
|
|
113
131
|
await fs.promises.mkdir(root, { recursive: true });
|
|
114
132
|
await pruneSpares(root, sourcePath, rel, spare);
|
|
115
133
|
const tmp = `${spare}.${process.pid}-${Date.now()}`;
|
|
116
|
-
let made = await copyTree(path.join(sourcePath, rel), tmp);
|
|
134
|
+
let made = await copyTree(path.join(sourcePath, rel), tmp, mode);
|
|
117
135
|
if (made) made = await takeSpare(tmp, spare);
|
|
118
136
|
await fs.promises.rm(tmp, { recursive: true, force: true });
|
|
119
137
|
return made ? "made" : "failed";
|
package/package.json
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Times a new worktree's setup and its first typecheck, test and build, and
|
|
3
|
+
* the disk it takes, against a checkout. Run it with the seeding on, then
|
|
4
|
+
* with AGENTOS_NO_BUILD_CACHES=1 (and, to see an install on Linux, --install).
|
|
5
|
+
*
|
|
6
|
+
* npx tsx scripts/bench/worktree-setup.ts <checkout> [--test <file>]
|
|
7
|
+
* [--build] [--install] [--cwd <subdir>] [--drop <seeded path>]
|
|
8
|
+
*
|
|
9
|
+
* It makes a throwaway git worktree of the checkout's HEAD in the temp
|
|
10
|
+
* directory and removes it at the end; it touches nothing else.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as os from "os";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { setupWorktree } from "../../lib/env-setup";
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const flag = (name: string) => args.includes(name);
|
|
21
|
+
const value = (name: string) => args[args.indexOf(name) + 1];
|
|
22
|
+
const source = path.resolve(args[0]);
|
|
23
|
+
const sub = flag("--cwd") ? value("--cwd") : ".";
|
|
24
|
+
|
|
25
|
+
const freeKb = (dir: string) =>
|
|
26
|
+
Number(
|
|
27
|
+
execFileSync("df", ["-Pk", dir], { encoding: "utf-8" })
|
|
28
|
+
.trim()
|
|
29
|
+
.split("\n")[1]
|
|
30
|
+
.split(/\s+/)[3]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
function timed(command: string, cwd: string) {
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const run = spawnSync(command, { cwd, shell: true, stdio: "ignore" });
|
|
36
|
+
return { seconds: (Date.now() - start) / 1000, ok: run.status === 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aos-bench-"));
|
|
41
|
+
const worktree = path.join(root, "wt");
|
|
42
|
+
execFileSync("git", ["worktree", "add", "-q", "--detach", worktree, "HEAD"], {
|
|
43
|
+
cwd: source,
|
|
44
|
+
});
|
|
45
|
+
const out: Record<string, unknown> = {
|
|
46
|
+
source: path.basename(source),
|
|
47
|
+
platform: process.platform,
|
|
48
|
+
seeding: process.env.AGENTOS_NO_BUILD_CACHES ? "off" : "on",
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
const before = freeKb(root);
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
let steps: string[] = [];
|
|
54
|
+
if (flag("--install")) {
|
|
55
|
+
const r = timed("npm ci --include=dev", worktree);
|
|
56
|
+
steps = [`npm ci ok=${r.ok}`];
|
|
57
|
+
} else {
|
|
58
|
+
const result = await setupWorktree({
|
|
59
|
+
worktreePath: worktree,
|
|
60
|
+
sourcePath: source,
|
|
61
|
+
});
|
|
62
|
+
steps = result.steps.map(
|
|
63
|
+
(s) => `${s.name}: ${s.success ? "ok" : "failed"}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
out.setupSeconds = (Date.now() - start) / 1000;
|
|
67
|
+
out.setupSteps = steps;
|
|
68
|
+
out.diskMbAfterSetup = Math.round((before - freeKb(root)) / 1024);
|
|
69
|
+
// Take a seeded cache back out to measure without it.
|
|
70
|
+
if (flag("--drop"))
|
|
71
|
+
fs.rmSync(path.join(worktree, value("--drop")), {
|
|
72
|
+
recursive: true,
|
|
73
|
+
force: true,
|
|
74
|
+
});
|
|
75
|
+
const dir = path.join(worktree, sub);
|
|
76
|
+
const pkg = path.join(dir, "package.json");
|
|
77
|
+
const typecheck = fs.existsSync(pkg)
|
|
78
|
+
? JSON.parse(fs.readFileSync(pkg, "utf-8")).scripts?.typecheck
|
|
79
|
+
: undefined;
|
|
80
|
+
if (typecheck) out.typecheck = timed("npm run typecheck", dir);
|
|
81
|
+
else if (fs.existsSync(path.join(dir, "tsconfig.json")))
|
|
82
|
+
out.typecheck = timed("npx tsc --noEmit", dir);
|
|
83
|
+
if (flag("--test"))
|
|
84
|
+
out.test = timed(`npx vitest run ${value("--test")}`, dir);
|
|
85
|
+
if (flag("--build")) {
|
|
86
|
+
out.build = timed("npx next build", dir);
|
|
87
|
+
out.diskMbAfterBuild = Math.round((before - freeKb(root)) / 1024);
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
execFileSync("git", ["worktree", "remove", "--force", worktree], {
|
|
91
|
+
cwd: source,
|
|
92
|
+
});
|
|
93
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
console.log(JSON.stringify(out));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch((error) => {
|
|
99
|
+
console.error(error);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
package/scripts/check-bundle.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// The app's first-load JavaScript,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
// The app's first-load JavaScript, watched against a budget. Going over it
|
|
2
|
+
// warns rather than fails: growth is sometimes worth it, and the point is to
|
|
3
|
+
// notice a big jump (a syntax highlighter's every grammar, a diagram renderer
|
|
4
|
+
// imported eagerly) before it ships to the phone. On CI the warning becomes an
|
|
5
|
+
// annotation on the PR. A missing or unreadable size still fails, so a broken
|
|
6
|
+
// check can't pass quietly. Run after `next build`.
|
|
7
|
+
import { appendFileSync, readFileSync } from "fs";
|
|
5
8
|
|
|
6
9
|
// Bytes, uncompressed. 1.04 MB when this was set (2026-10-07), down from 2.07.
|
|
7
10
|
const BUDGETS = { "/": 1_200_000 };
|
|
@@ -9,7 +12,20 @@ const BUDGETS = { "/": 1_200_000 };
|
|
|
9
12
|
const statsPath =
|
|
10
13
|
process.argv[2] ?? ".next/diagnostics/route-bundle-stats.json";
|
|
11
14
|
const stats = JSON.parse(readFileSync(statsPath, "utf-8"));
|
|
15
|
+
const kb = (n) => `${Math.round(n / 1024)} KB`;
|
|
12
16
|
let failed = false;
|
|
17
|
+
|
|
18
|
+
function warn(text) {
|
|
19
|
+
console.warn(`bundle: ${text}`);
|
|
20
|
+
if (process.env.GITHUB_ACTIONS)
|
|
21
|
+
console.log(`::warning title=Bundle size::${text}`);
|
|
22
|
+
if (process.env.GITHUB_STEP_SUMMARY)
|
|
23
|
+
appendFileSync(
|
|
24
|
+
process.env.GITHUB_STEP_SUMMARY,
|
|
25
|
+
`> [!WARNING]\n> ${text}\n`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
for (const [route, budget] of Object.entries(BUDGETS)) {
|
|
14
30
|
const entry = stats.find((r) => r.route === route);
|
|
15
31
|
if (!entry) {
|
|
@@ -18,18 +34,17 @@ for (const [route, budget] of Object.entries(BUDGETS)) {
|
|
|
18
34
|
continue;
|
|
19
35
|
}
|
|
20
36
|
const bytes = entry.firstLoadUncompressedJsBytes;
|
|
21
|
-
const kb = (n) => `${Math.round(n / 1024)} KB`;
|
|
22
37
|
if (typeof bytes !== "number" || !Number.isFinite(bytes)) {
|
|
23
38
|
// A Next.js upgrade that renames the field must not pass by default.
|
|
24
39
|
console.error(`bundle: no firstLoadUncompressedJsBytes for ${route}`);
|
|
25
40
|
failed = true;
|
|
26
41
|
} else if (bytes > budget) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
42
|
+
const over = Math.round(((bytes - budget) / budget) * 100);
|
|
43
|
+
warn(
|
|
44
|
+
`${route} loads ${kb(bytes)} of JS first, ${over}% over its ${kb(budget)} budget. ` +
|
|
45
|
+
"If that's not worth it, load the heavy part with next/dynamic or import(); " +
|
|
46
|
+
"`npx next experimental-analyze` shows what's in it."
|
|
31
47
|
);
|
|
32
|
-
failed = true;
|
|
33
48
|
} else {
|
|
34
49
|
console.log(`bundle: ${route} ${kb(bytes)} of ${kb(budget)}`);
|
|
35
50
|
}
|
|
@@ -21,9 +21,9 @@ describe("the first-load JS budget", () => {
|
|
|
21
21
|
expect(r.code).toBe(0);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it("
|
|
24
|
+
it("warns, without failing, on a build over budget", () => {
|
|
25
25
|
const r = run([{ route: "/", firstLoadUncompressedJsBytes: 2_000_000 }]);
|
|
26
|
-
expect([r.code, r.out]).toEqual([
|
|
26
|
+
expect([r.code, r.out]).toEqual([0, expect.stringContaining("% over its")]);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it("fails closed when the size is missing or not a number", () => {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { proxy } from "valtio";
|
|
2
|
+
import { INBOX_TABS, type InboxTab } from "@/lib/inbox/counts";
|
|
3
|
+
|
|
4
|
+
const TAB_KEY = "agentos:inbox-tab";
|
|
5
|
+
const SEEN_KEY = "agentos:inbox-messages-seen";
|
|
6
|
+
|
|
7
|
+
const read = (key: string) => {
|
|
8
|
+
try {
|
|
9
|
+
return typeof window === "undefined" ? null : localStorage.getItem(key);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const write = (key: string, value: string) => {
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem(key, value);
|
|
17
|
+
} catch {
|
|
18
|
+
// Private mode: the choice just isn't kept.
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const lastInboxTab = (): InboxTab | null => {
|
|
23
|
+
const saved = read(TAB_KEY);
|
|
24
|
+
return INBOX_TABS.find((t) => t === saved) ?? null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// The Inbox panel, shared by the bar, the sidebar menu and the palette.
|
|
28
|
+
// `request` is an explicit tab to open on; without one the panel picks.
|
|
29
|
+
export const inboxUi = proxy<{
|
|
30
|
+
open: boolean;
|
|
31
|
+
request: InboxTab | null;
|
|
32
|
+
// The newest message he has looked at; null until first seen.
|
|
33
|
+
seenMessageId: number | null;
|
|
34
|
+
}>({
|
|
35
|
+
open: false,
|
|
36
|
+
request: null,
|
|
37
|
+
seenMessageId: null,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const inboxUiActions = {
|
|
41
|
+
open: (tab?: InboxTab) => {
|
|
42
|
+
inboxUi.request = tab ?? null;
|
|
43
|
+
inboxUi.open = true;
|
|
44
|
+
},
|
|
45
|
+
setOpen: (open: boolean) => {
|
|
46
|
+
inboxUi.open = open;
|
|
47
|
+
},
|
|
48
|
+
rememberTab: (tab: InboxTab) => write(TAB_KEY, tab),
|
|
49
|
+
loadSeen: () => {
|
|
50
|
+
if (inboxUi.seenMessageId !== null) return;
|
|
51
|
+
const saved = read(SEEN_KEY);
|
|
52
|
+
if (saved !== null && Number.isFinite(Number(saved)))
|
|
53
|
+
inboxUi.seenMessageId = Number(saved);
|
|
54
|
+
},
|
|
55
|
+
markSeen: (id: number) => {
|
|
56
|
+
if (inboxUi.seenMessageId !== null && id <= inboxUi.seenMessageId) return;
|
|
57
|
+
inboxUi.seenMessageId = id;
|
|
58
|
+
write(SEEN_KEY, String(id));
|
|
59
|
+
},
|
|
60
|
+
};
|
package/stores/needsYouFocus.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { proxy } from "valtio";
|
|
2
2
|
|
|
3
|
-
// "Show this ask", raised by the Needs-you strip and
|
|
4
|
-
// above the chat, which opens itself and scrolls the card
|
|
3
|
+
// "Show this ask", raised by the Needs-you strip and the Inbox and handled
|
|
4
|
+
// by the asks list above the chat, which opens itself and scrolls the card
|
|
5
|
+
// into view. "Reply to this question" is handled by the orchestrator's chat.
|
|
5
6
|
export const needsYouFocus = proxy<{
|
|
6
7
|
ask: { id: number; at: number } | null;
|
|
7
|
-
|
|
8
|
+
reply: { workspaceId: string; id: number; text: string; at: number } | null;
|
|
9
|
+
}>({ ask: null, reply: null });
|
|
8
10
|
|
|
9
11
|
export const needsYouFocusActions = {
|
|
10
12
|
ask: (id: number) => {
|
|
11
13
|
needsYouFocus.ask = { id, at: Date.now() };
|
|
12
14
|
},
|
|
15
|
+
reply: (workspaceId: string, id: number, text: string) => {
|
|
16
|
+
needsYouFocus.reply = { workspaceId, id, text, at: Date.now() };
|
|
17
|
+
},
|
|
18
|
+
clearReply: () => {
|
|
19
|
+
needsYouFocus.reply = null;
|
|
20
|
+
},
|
|
13
21
|
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { MessagesSquare } from "lucide-react";
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
import { busUiActions } from "@/stores/busUi";
|
|
6
|
-
|
|
7
|
-
export function MessagesButton({
|
|
8
|
-
labelClassName,
|
|
9
|
-
}: {
|
|
10
|
-
labelClassName?: string;
|
|
11
|
-
}) {
|
|
12
|
-
return (
|
|
13
|
-
<Button
|
|
14
|
-
size="sm"
|
|
15
|
-
variant="ghost"
|
|
16
|
-
aria-label="Messages"
|
|
17
|
-
title="Messages"
|
|
18
|
-
className="h-7 px-2"
|
|
19
|
-
onClick={busUiActions.open}
|
|
20
|
-
>
|
|
21
|
-
<MessagesSquare className="h-4 w-4" />
|
|
22
|
-
<span className={labelClassName}>Messages</span>
|
|
23
|
-
</Button>
|
|
24
|
-
);
|
|
25
|
-
}
|