Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions components/AnsiText.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ test("widget text cannot inject HTML (escape_html)", () => {
assert.doesNotMatch(html, /<script>/);
});

test("OSC 8 hyperlinks open in a new tab", () => {
const html = toHtml("Preview: \x1b]8;;https://example.com/\x1b\\example.com\x1b]8;;\x1b\\");
assert.equal(
html,
'<span>Preview: <a target="_blank" rel="noopener noreferrer" href="https://example.com/">example.com</a></span>',
);
});

test("link-like text stays escaped", () => {
const html = toHtml('<a href="https://example.com/">x</a>');
assert.doesNotMatch(html, /<a /);
assert.doesNotMatch(html, /target=/);
});

test("newlines inside text are preserved", () => {
assert.equal(toHtml("line one\nline two"), "<span>line one\nline two</span>");
});
Expand Down
9 changes: 8 additions & 1 deletion components/AnsiText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ import { AnsiUp } from "ansi_up";
* reset codes) so any extension's widget output renders faithfully.
* `ansi_up` escapes HTML entities by default, so widget text cannot inject
* markup.
*
* OSC 8 hyperlinks open in a new tab, like the links in chat Markdown, so a
* click never navigates away from the session (#1374). `ansi_up` only emits
* http(s) links, and with HTML escaped, `<a href="` can only come from it.
*/

export function AnsiText({ text }: { text: string }) {
const html = useMemo(() => new AnsiUp().ansi_to_html(text), [text]);
const html = useMemo(
() => new AnsiUp().ansi_to_html(text).replaceAll('<a href="', '<a target="_blank" rel="noopener noreferrer" href="'),
[text],
);
return <span dangerouslySetInnerHTML={{ __html: html }} />;
}
9 changes: 8 additions & 1 deletion demo/components/AnsiText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ import { AnsiUp } from "ansi_up";
* reset codes) so any extension's widget output renders faithfully.
* `ansi_up` escapes HTML entities by default, so widget text cannot inject
* markup.
*
* OSC 8 hyperlinks open in a new tab, like the links in chat Markdown, so a
* click never navigates away from the session (#1374). `ansi_up` only emits
* http(s) links, and with HTML escaped, `<a href="` can only come from it.
*/

export function AnsiText({ text }: { text: string }) {
const html = useMemo(() => new AnsiUp().ansi_to_html(text), [text]);
const html = useMemo(
() => new AnsiUp().ansi_to_html(text).replaceAll('<a href="', '<a target="_blank" rel="noopener noreferrer" href="'),
[text],
);
return <span dangerouslySetInnerHTML={{ __html: html }} />;
}
1 change: 1 addition & 0 deletions docs/agents/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ New sessions and the files tab ("the files tab" here is the files section below
## Extension status bar and command buttons (`components/ExtensionStatusBar.tsx`, `ExtensionWidgets.tsx`)
- `ctx.ui.setStatus(key, text)` lands in the wrapper's `extensionStatuses` map, reaches the page as an `extension_ui_request` (`setStatus`), and is restored from `GET /api/agent/[id]` state after a reload. The shelf under the composer shows one cell per status in key order (like Pi's footer), separated by vertical rules. The widget triggers share one `extension-status-row` with the status line so the row scrolls sideways as a whole; the expanded widget panel stays above it, outside the scroll.
- A key `command:/name args` makes its cell a button that sends that slash command through `handleSend` (disabled while the session is busy). The command lives in the key because `setStatus` has no room for it, and Pi's own footer prints only the text, so the same status is plain text there. Only `/…` commands qualify: anything else would reach the model, and `!` would run a shell command.
- OSC 8 hyperlinks in statuses, widgets and custom panels (`components/AnsiText.tsx`) open in a new tab, like chat Markdown's links: a preview link must not navigate the page away from its session, and a phone has no modifier-click (#1374).
- A status with nothing visible (empty after stripping ANSI) gets no cell. Touch screens get 44px cells (widget triggers included) only when the bar has a command button.
- Statuses exist only while the session's wrapper is alive, so a fresh chat or a session nobody has prompted yet shows no cells (and no buttons) until its first message.

Expand Down
Loading