Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ All notable changes to **omp-web** (`@kahme247/ompweb`) are documented in this f

### Fixes & Improvements

- **Passive tool context shows its wrapper.** Context wrapped in a tag such as `<system-reminder source="graphify">` now names the tag and every attribute on the tool card's `↳ Context (system-reminder · source=graphify): …` line. Expanding the card shows the context as a small nested card with that header and the body below, like developer reminders. Before, the tag name and attribute keys were dropped, so a graphify nudge looked like unlabelled text.
- `ompweb hash-password` in a terminal finishes when you press Enter. The hidden prompt waited for a newline, but a terminal in raw mode sends Enter as a carriage return, so the prompt never ended.
- The Notifications tab now tells unread notifications from read ones: an unread notification has an accent dot before it and a bold title on a tinted row, and read notifications are dimmed. The tab also lists only the notifications that can also be pushed (task finished, input needed, error, model switch). Confirmations such as "Saved", copy results, update notices and agent `notify` calls still show as toasts but no longer fill the tab. A session's next notification of the same type is unread again instead of inheriting the earlier one's read state.
- Fit the agent's question panel on a phone. A long question, header or option preview no longer widens the panel past the screen and clips the answer text at the right edge, and previews wrap instead of scrolling sideways.
Expand Down
18 changes: 18 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3934,6 +3934,24 @@ html[data-touch-targets="accessible"] .right-panel-toolbar {
line-height: 1.4;
overflow-wrap: anywhere;
}
.tool-call-context-card {
border: 1px solid var(--border);
border-radius: var(--radius-control);
overflow: hidden;
background: var(--bg);
}
.tool-call-context-title {
padding: 4px 8px;
border-bottom: 1px solid var(--border);
background: var(--bg-panel);
color: var(--text-muted);
font-family: var(--font-mono);
font-weight: 650;
}
.tool-call-context-body {
padding: 5px 8px;
color: var(--text-muted);
}
.tool-call-output {
max-height: 260px;
overflow: auto;
Expand Down
19 changes: 13 additions & 6 deletions components/MessageView.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -574,17 +574,24 @@ test("passive tool context from a session file renders on the batch's last tool
const segment = view.container.querySelector(".tool-call-details .tool-call-context");
assert.equal(segment?.textContent, "↳ Context: Treat\tthis result\nas authoritative.");
assert.equal(view.container.querySelector(".activity-row-secondary[title]"), null, "no truncated row while expanded");
// A harness wrapper (omp's rule reminders) becomes a label instead of raw tag text.
// A harness wrapper shows as a label, titled like a developer message, never as raw tag text.
const wrapped = new Map(toolResults);
wrapped.set("call-2", { ...toolResults.get("call-2"), passiveContext: '<system-reminder reason="rule_violation" rule="ts-set-map">\nUse a Record.\n</system-reminder>' });
const labeled = renderToStaticMarkup(React.createElement(MessageView, { message: single, toolResults: wrapped }));
assert.match(labeled, /↳ <\/span>Context \(rule_violation · ts-set-map\): Use a Record\.<\/div>/);
assert.doesNotMatch(labeled, /system-reminder/);
// The length cap can cut the closing tag; the opening tag still never leaks.
assert.match(labeled, /↳ <\/span>Context \(system-reminder · reason=rule_violation · rule=ts-set-map\): Use a Record\.<\/div>/);
assert.doesNotMatch(labeled, /&lt;/);
// The length cap can cut the closing tag; the opening tag still never leaks, and empty values drop out.
wrapped.set("call-2", { ...toolResults.get("call-2"), passiveContext: '<system-reminder reason="" rule="ts-set-map">\nUse a Rec' });
const cut = renderToStaticMarkup(React.createElement(MessageView, { message: single, toolResults: wrapped }));
assert.match(cut, /↳ <\/span>Context \(ts-set-map\): Use a Rec<\/div>/);
assert.doesNotMatch(cut, /system-reminder/);
assert.match(cut, /↳ <\/span>Context \(system-reminder · rule=ts-set-map\): Use a Rec<\/div>/);
assert.doesNotMatch(cut, /&lt;|reason=/);
// Expanded, the wrapper becomes a nested card: its title as the header, the body below.
wrapped.set("call-2", { ...toolResults.get("call-2"), passiveContext: '<system-reminder source="graphify">\nMANDATORY: run `graphify query`.\n</system-reminder>' });
const card = render(React.createElement(MessageView, { message: single, toolResults: wrapped }));
fireEvent.click(card.getAllByRole("button", { expanded: false }).at(-1));
const nested = card.container.querySelector(".tool-call-details .tool-call-context-card");
assert.equal(nested?.querySelector(".tool-call-context-title")?.textContent, "↳ system-reminder · source=graphify");
assert.equal(nested?.querySelector(".tool-call-context-body")?.textContent, "MANDATORY: run `graphify query`.");
// The marked message is no row of its own; the unmarked one still is.
assert.equal(messages[4].customType, "passive-tool-context");
assert.equal(html(messages[4]), "");
Expand Down
38 changes: 26 additions & 12 deletions components/MessageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,8 @@ const ToolCallBlock = memo(function ToolCallBlock({
.map((b) => b.text)
.join("\n"))
: null;
const passiveContextText = result?.passiveContext ? contextLine(result.passiveContext, t) : null;
const passiveWrapper = result?.passiveContext ? parseLeadingWrapper(result.passiveContext) : null;
const passiveContextText = result?.passiveContext ? contextLine(result.passiveContext, passiveWrapper, t) : null;
const resultImages = result && Array.isArray(result.content)
? result.content.filter((b): b is ImageContent => b.type === "image")
: [];
Expand Down Expand Up @@ -1195,7 +1196,15 @@ const ToolCallBlock = memo(function ToolCallBlock({
) : null}
{passiveContextText && (
<div className="tool-call-context">
<span aria-hidden>↳ </span>{passiveContextText}
{passiveWrapper ? (
// Nested like a developer message: the wrapper as a header, its body below.
<div className="tool-call-context-card">
<div className="tool-call-context-title"><span aria-hidden>↳ </span>{wrapperTitle(passiveWrapper)}</div>
{passiveWrapper.body && <div className="tool-call-context-body">{passiveWrapper.body}</div>}
</div>
) : (
<><span aria-hidden>↳ </span>{passiveContextText}</>
)}
</div>
)}
</div>
Expand Down Expand Up @@ -1317,14 +1326,20 @@ function parseLeadingWrapper(text: string): { tag: string; attrs: [string, strin
};
}

/** "Context (rule_violation · ts-set-map · …): body" for a wrapped passive context, plain otherwise. */
function contextLine(text: string, t: (key: string, params?: Record<string, string>) => string): string {
const wrapper = parseLeadingWrapper(text);
const label = wrapper?.attrs.map(([, value]) => value).filter(Boolean).join(" · ");
const body = wrapper ? wrapper.body : text;
return label
? t("messageView.passiveContextLabeled", { label, text: body })
: t("messageView.passiveContext", { text: body });
/** `<system-reminder reason="…" rule="…">` → "system-reminder · reason=… · rule=…"; empty values are skipped. */
function wrapperTitle(wrapper: { tag: string; attrs: [string, string][] }): string {
return [wrapper.tag, ...wrapper.attrs.filter(([, value]) => value).map(([key, value]) => `${key}=${value}`)].join(" · ");
}

/** "Context (system-reminder · rule=ts-set-map · …): body" for a wrapped passive context, plain otherwise. */
function contextLine(
text: string,
wrapper: { tag: string; attrs: [string, string][]; body: string } | null,
t: (key: string, params?: Record<string, string>) => string,
): string {
return wrapper
? t("messageView.passiveContextLabeled", { label: wrapperTitle(wrapper), text: wrapper.body })
: t("messageView.passiveContext", { text });
}

function friendlyHiddenLabel(customType: string, t: (key: string) => string): string {
Expand Down Expand Up @@ -1559,12 +1574,11 @@ function CustomMessageView({ message, cwd, onOpenFile }: { message: CustomMessag
// them verbatim.
const isPlainText = message.customType === "async-result" || message.customType === "lsp-late-diagnostic";
const displayText = ircEnvelope ? ircEnvelope.body : isPlainText || isDeveloper ? stripHiddenWrappers(text) : text;
// `<system-reminder reason="…" rule="…">` → "system-reminder · reason=… · rule=…".
const wrapper = isDeveloper ? parseLeadingWrapper(text) : null;
const firstLine = displayText.split("\n").find((line) => line.trim())?.trim() ?? "";
const collapsedPreview = firstLine && firstLine !== displayText.trim() ? `${firstLine} …` : firstLine;
const title = wrapper
? [wrapper.tag, ...wrapper.attrs.map(([key, value]) => `${key}=${value}`)].join(" · ")
? wrapperTitle(wrapper)
: isIrc
? (ircEnvelope?.sender ?? formatCustomType(message.customType))
: message.customType === "advisor"
Expand Down
Loading