@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.
Files changed (46) hide show
  1. package/app/page.tsx +3 -8
  2. package/components/Bookmarks/ChatBookmarksButton.tsx +4 -0
  3. package/components/Bus/MessagesPanel.tsx +155 -0
  4. package/components/Bus/index.ts +1 -2
  5. package/components/Chat/ChatPanel.tsx +9 -1
  6. package/components/CommandPalette/useAppCommands.ts +12 -4
  7. package/components/Features/FeaturesSection.tsx +2 -2
  8. package/components/Inbox/InboxButton.tsx +39 -0
  9. package/components/Inbox/InboxDialog.tsx +139 -0
  10. package/components/Inbox/NeedsYouList.tsx +120 -0
  11. package/components/Inbox/index.ts +2 -0
  12. package/components/LumifyHub/index.ts +0 -1
  13. package/components/Pane/DesktopTabBar.tsx +2 -0
  14. package/components/Pane/PaneMenu.test.ts +118 -0
  15. package/components/Pane/PaneMenu.tsx +37 -0
  16. package/components/SessionList/SidebarHeader.tsx +7 -4
  17. package/components/Tasks/TaskRow.tsx +3 -3
  18. package/components/Tasks/TasksPanel.tsx +63 -0
  19. package/components/Tasks/index.ts +1 -1
  20. package/components/views/DesktopAppActions.tsx +10 -63
  21. package/components/views/DesktopView.tsx +1 -11
  22. package/hooks/useInbox.ts +69 -0
  23. package/lib/env-setup.ts +27 -12
  24. package/lib/inbox/counts.ts +140 -0
  25. package/lib/project-config/agentos.schema.json +14 -0
  26. package/lib/project-config/schema.ts +10 -0
  27. package/lib/worktree-deps/caches.ts +250 -0
  28. package/lib/worktree-deps/clone.ts +94 -9
  29. package/lib/worktree-deps/fastcopy.ts +89 -0
  30. package/lib/worktree-deps/index.ts +14 -5
  31. package/lib/worktree-deps/paths.ts +20 -0
  32. package/lib/worktree-deps/spare.ts +26 -8
  33. package/package.json +1 -1
  34. package/scripts/bench/worktree-setup.ts +101 -0
  35. package/scripts/check-bundle.mjs +25 -10
  36. package/scripts/check-bundle.test.ts +2 -2
  37. package/stores/inboxUi.ts +60 -0
  38. package/stores/needsYouFocus.ts +11 -3
  39. package/components/Bus/MessagesButton.tsx +0 -25
  40. package/components/Bus/MessagesDialog.tsx +0 -176
  41. package/components/LumifyHub/Docs/DocsButton.tsx +0 -31
  42. package/components/NotificationsMenu.tsx +0 -86
  43. package/components/Tasks/TasksButton.tsx +0 -28
  44. package/components/Tasks/TasksDialog.tsx +0 -75
  45. package/stores/busUi.ts +0 -12
  46. 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 { NOT_MAC, cloneDeps } from "./clone";
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
- // Off macOS only node_modules has a fallback (the install below).
61
- if (!pm && !clone.ok && !clone.cloned.length && clone.reason === NOT_MAC)
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 -Rc",
72
+ command: clone.mode ? COMMANDS[clone.mode] : "cp",
67
73
  success: clone.ok,
68
- output: clone.cloned.map((c) => `${c.rel} (${c.from})`).join(", "),
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
- // macOS clones (APFS, no extra disk); elsewhere this is a plain copy, which
53
- // production never asks for: Linux worktrees install instead.
54
- export const cloneArgs = (from: string, to: string) =>
55
- process.platform === "darwin" ? ["-Rc", from, to] : ["-R", from, to];
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(from: string, to: string): Promise<boolean> {
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", cloneArgs(from, to), {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.603",
3
+ "version": "0.3.606",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
@@ -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
+ });
@@ -1,7 +1,10 @@
1
- // The app's first-load JavaScript, held under a budget: a heavy library
2
- // imported eagerly (a syntax highlighter's every grammar, a diagram renderer)
3
- // fails here instead of shipping to the phone. Run after `next build`.
4
- import { readFileSync } from "fs";
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
- console.error(
28
- `bundle: ${route} loads ${kb(bytes)} of JS first, over its ${kb(budget)} budget.\n` +
29
- " Load the heavy part with next/dynamic or import(); see what's in it with\n" +
30
- " `npx next experimental-analyze`."
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("fails a build over budget", () => {
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([1, expect.stringContaining("over its")]);
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
+ };
@@ -1,13 +1,21 @@
1
1
  import { proxy } from "valtio";
2
2
 
3
- // "Show this ask", raised by the Needs-you strip and handled by the asks list
4
- // above the chat, which opens itself and scrolls the card into view.
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
- }>({ ask: null });
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
- }