@saadnvd1/agent-os 0.3.749 → 0.3.751

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,6 +63,16 @@ agent-os start
63
63
 
64
64
  Then open AgentOS, add a project from ⌘K, and press ⌥N. The [install guide](docs/guides/install.md) covers Windows (WSL2), the supported agents, first run and the `agent-os` commands. To use it from your phone, see the [phone guide](docs/guides/phone.md).
65
65
 
66
+ ## Connect
67
+
68
+ Connect (in development) lets you reach AgentOS from anywhere, without a tailnet or opening a port. Sign in at [runagentos.com](https://runagentos.com), get a code, and run:
69
+
70
+ ```bash
71
+ agent-os connect # links this machine with the code from runagentos.com
72
+ ```
73
+
74
+ The machine gets its own address at `https://m-<id>.runagentos.com`, served through a Cloudflare tunnel that AgentOS starts and stops with itself. Its key is made on the machine and never leaves it, and a device still has to be paired before it gets in. Connect also hosts the read-only links made with Share. How it works, and what it does and doesn't trust, is in [docs/reference/security.md](docs/reference/security.md#connect).
75
+
66
76
  ## Security
67
77
 
68
78
  AgentOS gives whoever uses it a terminal as you, so it decides who that is. This machine (localhost) is always trusted. Your tailnet is trusted by default. Everything else, including Wi-Fi and any reverse proxy, needs a paired device. `AGENTOS_AUTH=off` removes pairing and gives anyone who reaches the port a shell, so use it only behind a proxy that does its own login. The full model, Connect and demo mode are in [docs/reference/security.md](docs/reference/security.md).
@@ -1,21 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import type { ToolItem } from "@/lib/chat/group";
4
- import { toolOutcome } from "@/lib/chat/tool-outcome";
5
- import { cn } from "@/lib/utils";
6
- import { StatusIcon } from "./Tools";
4
+ import { failedSteps } from "@/lib/chat/step-summary";
7
5
 
8
- export const MAX_CHIPS = 24;
9
-
10
- const WORD = {
11
- running: "running",
12
- done: "ok",
13
- error: "failed",
14
- stopped: "not run",
15
- } as const;
16
-
17
- // One line per collapsed group: how each step ended, so the results aren't
18
- // lost behind the fold.
6
+ // Under a collapsed group that had failures: which steps failed. Nothing when
7
+ // every step passed.
19
8
  export function StepOutcomes({
20
9
  tools,
21
10
  onOpen,
@@ -23,44 +12,19 @@ export function StepOutcomes({
23
12
  tools: ToolItem[];
24
13
  onOpen: () => void;
25
14
  }) {
26
- const shown = tools.slice(0, MAX_CHIPS);
15
+ const { names, more } = failedSteps(tools);
16
+ if (names.length === 0) return null;
27
17
  return (
28
18
  <button
29
19
  type="button"
30
20
  onClick={onOpen}
31
21
  aria-label="Show steps"
32
- className="flex min-h-11 w-full flex-wrap items-center gap-x-3 gap-y-1 px-2 pb-1.5 text-left md:min-h-0"
22
+ className="text-destructive flex min-h-8 w-full min-w-0 items-center gap-x-3 px-2 pb-1 text-left text-xs md:min-h-0"
33
23
  >
34
- {shown.map((t) => {
35
- const code = toolOutcome(t);
36
- return (
37
- <span
38
- key={t.id}
39
- title={`${t.title}: ${WORD[t.status]}${code ? `, ${code}` : ""}`}
40
- aria-label={`${t.title}: ${WORD[t.status]}${code ? `, ${code}` : ""}`}
41
- className="flex min-w-0 items-center gap-1"
42
- >
43
- <StatusIcon status={t.status} />
44
- {code && (
45
- <span
46
- className={cn(
47
- "font-mono text-[11px]",
48
- t.status === "error"
49
- ? "text-destructive"
50
- : "text-muted-foreground"
51
- )}
52
- >
53
- {code}
54
- </span>
55
- )}
56
- </span>
57
- );
58
- })}
59
- {tools.length > shown.length && (
60
- <span className="text-muted-foreground/70 font-mono text-[11px]">
61
- +{tools.length - shown.length} more
62
- </span>
63
- )}
24
+ <span className="min-w-0 truncate">
25
+ {names.map((n) => `✕ ${n}`).join(" ")}
26
+ {more > 0 && ` +${more} more`}
27
+ </span>
64
28
  </button>
65
29
  );
66
30
  }
@@ -2,7 +2,6 @@ import { createElement } from "react";
2
2
  import { renderToStaticMarkup } from "react-dom/server";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import type { ToolItem } from "@/lib/chat/group";
5
- import { MAX_CHIPS } from "./StepOutcomes";
6
5
  import { ToolGroup } from "./Tools";
7
6
 
8
7
  const tool = (id: string, over: Partial<ToolItem> = {}): ToolItem => ({
@@ -20,7 +19,7 @@ const render = (tools: ToolItem[]) =>
20
19
  renderToStaticMarkup(createElement(ToolGroup, { tools }));
21
20
 
22
21
  describe("collapsed ToolGroup", () => {
23
- it("keeps each step's outcome in view", () => {
22
+ it("names the failed steps and counts the rest", () => {
24
23
  const html = render([
25
24
  tool("a", { output: "HTTP/2 200 \n" }),
26
25
  tool("b", { status: "error", output: "Exit code 7" }),
@@ -31,22 +30,20 @@ describe("collapsed ToolGroup", () => {
31
30
  expect(html).toContain("1 ok");
32
31
  expect(html).toContain("2 failed");
33
32
  expect(html).toContain("1 not run");
34
- expect(html).toContain("check a: ok, HTTP 200");
35
- expect(html).toContain("check b: failed, exit 7");
36
- expect(html).toContain("check c: failed, HTTP 404");
37
- expect(html).toContain("check d: not run");
33
+ expect(html).toContain("✕ check b");
34
+ expect(html).toContain("✕ check c");
35
+ expect(html).not.toContain("check a");
38
36
  });
39
37
 
40
- it("shows no strip while a step runs", () => {
41
- expect(render([tool("a", { status: "running" })])).not.toContain(
42
- "Show steps"
43
- );
38
+ it("has no outcome row when every step passed", () => {
39
+ const html = render([tool("a"), tool("b")]);
40
+ expect(html).toContain("2 steps · ran 2 commands");
41
+ expect(html).not.toContain("Show steps");
44
42
  });
45
43
 
46
- it("caps the strip", () => {
47
- const tools = Array.from({ length: MAX_CHIPS + 3 }, (_, i) =>
48
- tool(`x${i}`)
44
+ it("shows no outcome row while a step runs", () => {
45
+ expect(render([tool("a", { status: "running" })])).not.toContain(
46
+ "Show steps"
49
47
  );
50
- expect(render(tools)).toContain("+3 more");
51
48
  });
52
49
  });
@@ -9,6 +9,7 @@ import { needsBody } from "@/lib/chat/bodyCache";
9
9
  import { cn } from "@/lib/utils";
10
10
  import { formatElapsed } from "@/lib/chat/elapsed";
11
11
  import { stepCounts, toolOutcome } from "@/lib/chat/tool-outcome";
12
+ import { stepSummary } from "@/lib/chat/step-summary";
12
13
  import { StepOutcomes } from "./StepOutcomes";
13
14
  import { Highlighted } from "./Code";
14
15
  import { BackgroundButton, BackgroundState, useBackground } from "./background";
@@ -227,13 +228,12 @@ function ToolRow({ tool }: { tool: ToolItem }) {
227
228
  export function ToolGroup({ tools }: { tools: ToolItem[] }) {
228
229
  const running = tools.some((t) => t.status === "running");
229
230
  const { ok, failed, stopped } = stepCounts(tools);
230
- const edits = tools.filter((t) => t.diff || t.hasDiff).length;
231
231
  const moved = tools.filter((t) => t.backgroundTaskId).length;
232
232
  const [open, setOpen] = useRowState(`steps:${tools[0].id}`, false);
233
233
  const latest =
234
234
  tools.findLast((t) => t.status === "running") ?? tools[tools.length - 1];
235
235
  return (
236
- <div className="bg-foreground/[0.025] rounded-xl px-2 py-1">
236
+ <div className="bg-foreground/[0.025] rounded-xl px-2 py-0.5">
237
237
  <div className="flex items-center gap-1">
238
238
  <button
239
239
  type="button"
@@ -250,12 +250,7 @@ export function ToolGroup({ tools }: { tools: ToolItem[] }) {
250
250
  <Loader2 className="text-primary h-3.5 w-3.5 shrink-0 animate-spin" />
251
251
  ) : null}
252
252
  <span className="min-w-0 flex-1 truncate">
253
- {running
254
- ? latest.title
255
- : `${tools.length} step${tools.length === 1 ? "" : "s"}`}
256
- {!running &&
257
- edits > 0 &&
258
- ` · ${edits} edit${edits === 1 ? "" : "s"}`}
253
+ {running ? latest.title : stepSummary(tools)}
259
254
  {!running && (failed > 0 || stopped > 0) && ` · ${ok} ok`}
260
255
  </span>
261
256
  {running && <Elapsed tool={latest} />}
@@ -0,0 +1,54 @@
1
+ import type { ToolItem } from "./group";
2
+
3
+ export const MAX_FILES = 3;
4
+ export const MAX_FAILED = 3;
5
+
6
+ const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;
7
+
8
+ const baseName = (p: string) => p.split("/").filter(Boolean).pop() ?? p;
9
+
10
+ const editedPath = (t: ToolItem): string | undefined => {
11
+ if (t.diff?.path) return t.diff.path;
12
+ const i = (t.input ?? {}) as Record<string, unknown>;
13
+ const p = i.file_path ?? i.notebook_path;
14
+ return typeof p === "string" ? p : undefined;
15
+ };
16
+
17
+ // What a finished run of steps did, after its count: "5 steps · edited a.ts,
18
+ // lib/b.ts · ran 2 commands". Files edited more than once are named once.
19
+ export function stepSummary(tools: ToolItem[]): string {
20
+ const parts = [plural(tools.length, "step")];
21
+ const edited = tools.filter((t) => t.diff || t.hasDiff);
22
+ if (edited.length > 0) {
23
+ const files = [
24
+ ...new Set(
25
+ edited
26
+ .map(editedPath)
27
+ .filter((p): p is string => !!p)
28
+ .map(baseName)
29
+ ),
30
+ ];
31
+ if (files.length === 0) parts.push(plural(edited.length, "edit"));
32
+ else {
33
+ const more = files.length - MAX_FILES;
34
+ parts.push(
35
+ `edited ${files.slice(0, MAX_FILES).join(", ")}${more > 0 ? ` +${more}` : ""}`
36
+ );
37
+ }
38
+ }
39
+ const commands = tools.filter((t) => t.name === "Bash").length;
40
+ if (commands > 0) parts.push(`ran ${plural(commands, "command")}`);
41
+ return parts.join(" · ");
42
+ }
43
+
44
+ // The failed steps' names, for the line under a group that had failures.
45
+ export function failedSteps(tools: ToolItem[]): {
46
+ names: string[];
47
+ more: number;
48
+ } {
49
+ const failed = tools.filter((t) => t.status === "error");
50
+ return {
51
+ names: failed.slice(0, MAX_FAILED).map((t) => t.title),
52
+ more: Math.max(0, failed.length - MAX_FAILED),
53
+ };
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.749",
3
+ "version": "0.3.751",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"