@saadnvd1/agent-os 0.3.580 → 0.3.581

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.
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import type { MouseEvent } from "react";
4
3
  import {
5
4
  Bot,
6
5
  CalendarClock,
@@ -15,6 +14,7 @@ import { eventParts, originOf } from "@/lib/chat/origin";
15
14
  import { hrefWithSession } from "@/lib/session-url";
16
15
  import { cn } from "@/lib/utils";
17
16
  import { UserMessage } from "./Messages";
17
+ import { openSessionLink as openSession } from "./session-link";
18
18
  import { useRowState } from "./rowState";
19
19
 
20
20
  type UserItem = Extract<ChatItem, { kind: "user" }>;
@@ -34,14 +34,6 @@ const SHORT = 32;
34
34
  export const clock = (at: number) =>
35
35
  new Date(at).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
36
36
 
37
- // Opens the session through its address, the way a shared link does.
38
- function openSession(e: MouseEvent<HTMLAnchorElement>, id: string) {
39
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
40
- e.preventDefault();
41
- window.history.pushState(null, "", hrefWithSession(location.href, id));
42
- window.dispatchEvent(new PopStateEvent("popstate"));
43
- }
44
-
45
37
  function Source({ origin }: { origin: ChatOrigin }) {
46
38
  const name = (
47
39
  <span className="text-foreground/80 block truncate font-medium">
@@ -12,27 +12,49 @@ import type { ChatItem } from "@/lib/chat/events";
12
12
  import { cn } from "@/lib/utils";
13
13
  import { leadingCommand } from "@/lib/chat/commands";
14
14
  import { compactedLine } from "@/lib/chat/stale";
15
- import { splitInlineCode, splitUserText } from "@/lib/chat/user-text";
15
+ import { splitProse, splitUserText } from "@/lib/chat/user-text";
16
+ import { inAppSessionId } from "@/lib/session-url";
16
17
  import { CodeBlock } from "./Code";
17
18
  import { CopyButton } from "./CopyButton";
18
19
  import { ImageThumb } from "./ImageThumb";
19
20
  import { Markdown } from "./Markdown";
21
+ import { openSessionLink } from "./session-link";
20
22
  import { UndoButton } from "./Undo";
21
23
  import { useRowState } from "./rowState";
22
24
 
23
25
  type Of<K extends ChatItem["kind"]> = Extract<ChatItem, { kind: K }>;
24
26
 
27
+ function UserLink({ href }: { href: string }) {
28
+ const id =
29
+ typeof window === "undefined"
30
+ ? null
31
+ : inAppSessionId(href, window.location.origin);
32
+ return (
33
+ <a
34
+ href={href}
35
+ target={id ? undefined : "_blank"}
36
+ rel={id ? undefined : "noopener noreferrer"}
37
+ onClick={id ? (e) => openSessionLink(e, id) : undefined}
38
+ className="font-medium [overflow-wrap:anywhere] underline underline-offset-2"
39
+ >
40
+ {href}
41
+ </a>
42
+ );
43
+ }
44
+
25
45
  function Prose({ text }: { text: string }) {
26
46
  return (
27
47
  <>
28
- {splitInlineCode(text).map((p, i) =>
29
- p.code ? (
48
+ {splitProse(text).map((p, i) =>
49
+ p.kind === "code" ? (
30
50
  <code
31
51
  key={i}
32
52
  className="bg-foreground/[0.06] rounded px-1 py-0.5 font-mono text-[0.9em]"
33
53
  >
34
54
  {p.text}
35
55
  </code>
56
+ ) : p.kind === "link" ? (
57
+ <UserLink key={i} href={p.text} />
36
58
  ) : (
37
59
  <span key={i}>{p.text}</span>
38
60
  )
@@ -0,0 +1,10 @@
1
+ import type { MouseEvent } from "react";
2
+ import { hrefWithSession } from "@/lib/session-url";
3
+
4
+ // Opens the session through its address, the way a shared link does.
5
+ export function openSessionLink(e: MouseEvent<HTMLAnchorElement>, id: string) {
6
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
7
+ e.preventDefault();
8
+ window.history.pushState(null, "", hrefWithSession(location.href, id));
9
+ window.dispatchEvent(new PopStateEvent("popstate"));
10
+ }
@@ -62,3 +62,55 @@ export function splitInlineCode(
62
62
  if (last < text.length) out.push({ code: false, text: text.slice(last) });
63
63
  return out;
64
64
  }
65
+
66
+ export type ProsePart =
67
+ | { kind: "text"; text: string }
68
+ | { kind: "code"; text: string }
69
+ | { kind: "link"; text: string };
70
+
71
+ const URL_RE = /https?:\/\/[^\s<>"'`]+/gi;
72
+ const TRAILING = /[.,;:!?]$/;
73
+
74
+ // Punctuation that ends a sentence, or a closing paren with no opener in the
75
+ // URL, belongs to the text around it.
76
+ function trimUrl(url: string): string {
77
+ for (;;) {
78
+ if (TRAILING.test(url)) url = url.slice(0, -1);
79
+ else if (url.endsWith(")") && count(url, ")") > count(url, "("))
80
+ url = url.slice(0, -1);
81
+ else return url;
82
+ }
83
+ }
84
+
85
+ function count(s: string, ch: string): number {
86
+ return s.split(ch).length - 1;
87
+ }
88
+
89
+ // http(s) URLs in plain text, split out so they can be links.
90
+ export function splitLinks(text: string): { link: boolean; text: string }[] {
91
+ const out: { link: boolean; text: string }[] = [];
92
+ let last = 0;
93
+ for (const m of text.matchAll(URL_RE)) {
94
+ const url = trimUrl(m[0]);
95
+ if (!/^https?:\/\/[^/?#]+/i.test(url)) continue;
96
+ if (m.index > last)
97
+ out.push({ link: false, text: text.slice(last, m.index) });
98
+ out.push({ link: true, text: url });
99
+ last = m.index + url.length;
100
+ }
101
+ if (last < text.length) out.push({ link: false, text: text.slice(last) });
102
+ return out;
103
+ }
104
+
105
+ // Prose split into code spans, links and the text between. Links are only
106
+ // looked for outside code spans.
107
+ export function splitProse(text: string): ProsePart[] {
108
+ return splitInlineCode(text).flatMap((p): ProsePart[] =>
109
+ p.code
110
+ ? [{ kind: "code", text: p.text }]
111
+ : splitLinks(p.text).map((l) => ({
112
+ kind: l.link ? "link" : "text",
113
+ text: l.text,
114
+ }))
115
+ );
116
+ }
@@ -26,6 +26,18 @@ export function hrefWithSession(href: string, id: string | null): string {
26
26
  return url.pathname + url.search + url.hash;
27
27
  }
28
28
 
29
+ /** The session a link to this same instance opens, or null for any other link. */
30
+ export function inAppSessionId(href: string, origin: string): string | null {
31
+ let url: URL;
32
+ try {
33
+ url = new URL(href);
34
+ } catch {
35
+ return null;
36
+ }
37
+ if (url.origin !== origin || url.pathname !== "/") return null;
38
+ return sessionIdFromHref(href);
39
+ }
40
+
29
41
  export function sessionLink(origin: string, id: string): string {
30
42
  return `${origin}/?${SESSION_PARAM}=${encodeURIComponent(id)}`;
31
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.580",
3
+ "version": "0.3.581",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"