Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
859154f
fix(web-ui): make the crons page's links actually links, and flatten …
ReganBell Jul 29, 2026
0c35efb
feat(deploy): 'Request access' button on the app not-shared page
ReganBell Jul 29, 2026
5f70c6b
perf(web-ui): one app with in-document panes, and a byte-budgeted tra…
16francej Jul 30, 2026
8967b66
fix(web-ui): restore the pane conversation's height chain lost with t…
16francej Jul 30, 2026
e8c451f
web-ui: set a scope's default model from its project page, and keep S…
16francej Jul 31, 2026
c561b51
fix(web-ui): ten UX papercuts from user feedback
16francej Jul 31, 2026
db76de0
fix(web-ui): poll for the new-chat title from turn start, not turn end
ReganBell Jul 31, 2026
c8bee5b
web-ui: allow attaching files while a turn is running
ReganBell Jul 31, 2026
e1601b4
web-ui: icon-only background chip with cog/binoculars + custom tooltip
ReganBell Jul 31, 2026
16e3036
Rename sidebar sessions on double-click
ReganBell Aug 1, 2026
bb39c9b
web-ui: hover archive quick-action on session rows
ReganBell Aug 1, 2026
5837b62
Never lose a steered message when its run has ended
ReganBell Jul 31, 2026
4494c78
Add friendly Slack headers and project links
ReganBell Jul 31, 2026
6f56ac1
fix(web-ui): a canvas pane's composer can be typed into again
16francej Jul 31, 2026
3638076
web-ui: keep already-shown assistant text visible when re-attaching t…
ReganBell Aug 1, 2026
0cc7f27
Give group DMs a pretty project path and fix the main lint break
ReganBell Aug 1, 2026
213600c
fix(web-ui): hold a steer typed before the run slot goes live instead…
16francej Aug 3, 2026
9c2be0d
Merge remote-tracking branch 'public/main' into up-webui
ReganBell Aug 4, 2026
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
15 changes: 14 additions & 1 deletion plugins/web-ui/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,16 @@ const routeRequest = async (req: IncomingMessage, res: ServerResponse) => {
return relay(res, r);
}

if (method === "GET" && /^\/api\/sessions\/[^/]+\/entries\/\d+$/.test(path)) {
const [, , , rawId, , seq] = path.split("/");
const id = decodeURIComponent(rawId!);
const r = await coreFetch(
"GET",
`/v1/sessions/${encodeURIComponent(id)}/entries/${seq}?viewer=${encodeURIComponent(user)}`,
);
return relay(res, r);
}

if (method === "GET" && path.startsWith("/api/sessions/")) {
const id = decodeURIComponent(path.slice("/api/sessions/".length));
const qs = new URLSearchParams({ viewer: user });
Expand Down Expand Up @@ -1413,7 +1423,10 @@ const routeRequest = async (req: IncomingMessage, res: ServerResponse) => {
if (!threadRef.startsWith(`web:${user}:`)) {
const sessionId = typeof record.sessionId === "string" ? record.sessionId : "";
const visible = sessionId
? await coreFetch("GET", `/v1/sessions/${encodeURIComponent(sessionId)}?viewer=${encodeURIComponent(user)}`)
? await coreFetch(
"GET",
`/v1/sessions/${encodeURIComponent(sessionId)}?viewer=${encodeURIComponent(user)}&tailTurns=1`,
)
: null;
if (visible?.status !== 200) return json(res, 404, { error: "not_found" });
}
Expand Down
163 changes: 85 additions & 78 deletions plugins/web-ui/src/ambient-policy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { html, nothing, type TemplateResult } from "lit";
import { api } from "./core-bridge";
import { errMessage } from "../../chassis/src/errors";
import { fieldSelect } from "./ui";

export const BOT_MODES = ["ignore", "rollup", "action", "user"] as const;
export type BotMode = (typeof BOT_MODES)[number];
Expand Down Expand Up @@ -153,22 +154,28 @@ const BOT_MODE_LABELS: Record<BotMode, string> = {
user: "Treat like a person",
};

function ambientValue(enabled: boolean | null): string {
if (enabled === null) return "default";
return enabled ? "on" : "off";
}

function botRow(b: BotPolicyView, i: number): TemplateResult {
return html`
<div class="ambient-bot-row">
<span class="ambient-bot-name">${b.name}</span>
<select
class="ambient-bot-mode"
aria-label=${`Handling for ${b.name}`}
?disabled=${ambientPolicyState.saving}
@change=${(e: Event) => {
const mode = (e.currentTarget as HTMLSelectElement).value as BotMode;
${fieldSelect({
className: "ambient-bot-mode",
compact: true,
ariaLabel: `Handling for ${b.name}`,
disabled: ambientPolicyState.saving,
value: b.mode,
onChange: (value) => {
const mode = value as BotMode;
ambientPolicyState.bots = ambientPolicyState.bots.map((x, j) => (j === i ? { ...x, mode } : x));
markDirty();
}}
>
${BOT_MODES.map((m) => html`<option value=${m} ?selected=${m === b.mode}>${BOT_MODE_LABELS[m]}</option>`)}
</select>
},
options: BOT_MODES.map((m) => html`<option value=${m}>${BOT_MODE_LABELS[m]}</option>`),
})}
${
b.mode === "rollup"
? html`<label class="ambient-bot-hours"
Expand Down Expand Up @@ -226,78 +233,78 @@ export function ambientPolicySection(scopeId: string): TemplateResult | typeof n
<p class="context-panel-copy">Choose what this project should notice and act on.</p>
</div>
</div>
<label class="ambient-field" for="ambient-enabled">
<span class="ambient-field-label">Ambient behavior</span>
<span class="ambient-policy-hint"
>When off, the agent never acts on overheard messages here — it only responds to direct @mentions. Default: on
only when standing orders (or an action-mode bot) are set below — otherwise mention-only.</span
>
</label>
<select
id="ambient-enabled"
class="ambient-enabled-select"
data-focus-key="ambient-enabled"
?disabled=${ambientPolicyState.saving}
@change=${(e: Event) => {
const v = (e.currentTarget as HTMLSelectElement).value;
ambientPolicyState.ambientEnabled = v === "default" ? null : v === "on";
markDirty();
}}
>
<option value="default" ?selected=${ambientPolicyState.ambientEnabled === null}>
Default (on when standing orders are set)
</option>
<option value="on" ?selected=${ambientPolicyState.ambientEnabled === true}>On</option>
<option value="off" ?selected=${ambientPolicyState.ambientEnabled === false}>Off</option>
</select>
<label class="ambient-field" for="ambient-orders">
<span class="ambient-field-label">Standing orders</span>
<span class="ambient-policy-hint" id="ambient-orders-hint"
>Plain-language guidance for proactive work. Leave empty to respond only when addressed.</span
>
</label>
<textarea
id="ambient-orders"
data-focus-key="ambient-orders"
class="ambient-orders"
rows="4"
aria-describedby="ambient-orders-hint"
placeholder="For example: Flag anything that could delay the launch."
.value=${ambientPolicyState.orders}
?disabled=${ambientPolicyState.saving}
@input=${(e: InputEvent) => {
ambientPolicyState.orders = (e.currentTarget as HTMLTextAreaElement).value;
markDirty();
}}
></textarea>
<div class="ambient-field-heading">
<h3>Automated posters</h3>
<p class="ambient-policy-hint">Control how messages from bots and integrations wake the agent.</p>
<div class="ambient-group">
<label class="ambient-field-label" for="ambient-enabled">Ambient behavior</label>
${fieldSelect({
id: "ambient-enabled",
className: "ambient-enabled-select",
focusKey: "ambient-enabled",
describedBy: "ambient-enabled-hint",
disabled: ambientPolicyState.saving,
value: ambientValue(ambientPolicyState.ambientEnabled),
onChange: (v) => {
ambientPolicyState.ambientEnabled = v === "default" ? null : v === "on";
markDirty();
},
options: [
html`<option value="default">Default (on when standing orders are set)</option>`,
html`<option value="on">On</option>`,
html`<option value="off">Off</option>`,
],
})}
<p class="ambient-policy-hint" id="ambient-enabled-hint">
When off, the agent never acts on overheard messages here — it only responds to direct @mentions. Default: on
only when standing orders (or an action-mode bot) are set below — otherwise mention-only.
</p>
</div>
${ambientPolicyState.bots.length ? html`<div class="ambient-bot-list">${ambientPolicyState.bots.map((b, i) => botRow(b, i))}</div>` : html`<div class="empty compact">No bots added. All bot posts are treated as activity.</div>`}
<form
class="ambient-bot-add"
@submit=${(e: SubmitEvent) => {
e.preventDefault();
addBot();
}}
>
<input
data-focus-key="ambient-bot-name"
type="text"
maxlength="120"
aria-label="Bot name"
required
placeholder="Bot name"
.value=${ambientPolicyState.newBotName}
<div class="ambient-group">
<label class="ambient-field-label" for="ambient-orders">Standing orders</label>
<textarea
id="ambient-orders"
data-focus-key="ambient-orders"
class="ambient-orders"
rows="4"
aria-describedby="ambient-orders-hint"
placeholder="For example: Flag anything that could delay the launch."
.value=${ambientPolicyState.orders}
?disabled=${ambientPolicyState.saving}
@input=${(e: InputEvent) => {
ambientPolicyState.newBotName = (e.currentTarget as HTMLInputElement).value;
redraw();
ambientPolicyState.orders = (e.currentTarget as HTMLTextAreaElement).value;
markDirty();
}}
/>
<button class="btn" type="submit" ?disabled=${ambientPolicyState.saving}>Add bot</button>
</form>
></textarea>
<p class="ambient-policy-hint" id="ambient-orders-hint">
Plain-language guidance for proactive work. Leave empty to respond only when addressed.
</p>
</div>
<div class="ambient-group">
<h3 class="ambient-field-label">Automated posters</h3>
<p class="ambient-policy-hint">Control how messages from bots and integrations wake the agent.</p>
${ambientPolicyState.bots.length ? html`<div class="ambient-bot-list">${ambientPolicyState.bots.map((b, i) => botRow(b, i))}</div>` : html`<div class="empty compact">No bots added. All bot posts are treated as activity.</div>`}
<form
class="ambient-bot-add"
@submit=${(e: SubmitEvent) => {
e.preventDefault();
addBot();
}}
>
<input
data-focus-key="ambient-bot-name"
type="text"
maxlength="120"
aria-label="Bot name"
required
placeholder="Bot name"
.value=${ambientPolicyState.newBotName}
?disabled=${ambientPolicyState.saving}
@input=${(e: InputEvent) => {
ambientPolicyState.newBotName = (e.currentTarget as HTMLInputElement).value;
redraw();
}}
/>
<button class="btn" type="submit" ?disabled=${ambientPolicyState.saving}>Add bot</button>
</form>
</div>
<div class="ambient-policy-actions">
<button
class="btn primary"
Expand Down
Loading