Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0b1041d
web-ui: the transcript clips at its edges instead of dissolving into …
Sep 8, 2026
76f605a
web-ui: a long pinned prompt gets Show more instead of its own scrollbar
Sep 8, 2026
0a8b111
web-ui: the message hover actions get a real target to hit
Sep 8, 2026
e010bae
web-ui: dark mode defines the accent foreground it was falling back on
Sep 8, 2026
ee715f6
web-ui: the theme picker lines up with the other settings, and stops …
Sep 8, 2026
ec71114
web-ui: a list filter's caption stops bolding the dropdown it labels
Sep 8, 2026
71b9eb7
web-ui: dark mode dims behind a modal instead of washing out behind it
Sep 8, 2026
cf8b1c8
web-ui: the sidebar's icon-only row buttons say what they do
Sep 8, 2026
1703cd9
web-ui: count badges stop shifting as their digits change
Sep 8, 2026
6a51916
web-ui: an email draft folds its To and Subject away
Sep 8, 2026
c0ba6b1
web-ui: the folded draft fields get their spacing back
Sep 8, 2026
5253c94
web-ui: an inbox item stops squeezing itself into the viewport
Sep 8, 2026
2dd85e0
web-ui: the inbox item head stops floating in a row the assistant str…
Sep 8, 2026
230421d
web-ui: the inbox shows the same working wave the rest of the app does
Sep 8, 2026
db7693f
web-ui: the inbox assistant tracks the window between a floor and a c…
Sep 8, 2026
3df3fb3
web-ui: the inbox assistant ends at the pane's content box
Sep 8, 2026
47d38bf
web-ui: the inbox size observer is replaced, not stacked
Sep 8, 2026
a248bfe
web-ui: the inbox drops its subtitle
Sep 8, 2026
e6a3a41
web-ui: the inbox sync control uses the app's tooltip and the shared …
Sep 8, 2026
c3f7f0b
web-ui: the context and surface filters become one control
Sep 8, 2026
a27567f
web-ui: a nav entry always lands on its own index
Sep 8, 2026
cb1a6ae
web-ui: a filter menu opens below its button, not over the header abo…
Sep 8, 2026
54720e4
web-ui: the empty assistant offers three ways to start
Sep 8, 2026
98d3568
web-ui: the filter menus drop the header that repeated their button
Sep 8, 2026
0141fe8
Merge origin/main into evebouf/drop-transcript-fades
Sep 8, 2026
d54fbd7
web-ui: the inbox working line gets its wave back
Sep 9, 2026
07e6e9a
Merge origin/main: the conversation shows its latest message again
Sep 9, 2026
ed6125b
web-ui: one owner for the ask button, and links hover the app's tooltip
Sep 9, 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
7 changes: 7 additions & 0 deletions plugins/web-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 32 additions & 3 deletions plugins/web-ui/src/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ import { createForkOriginController, forkOriginView } from "./fork-origin";
import { base64ToBytes } from "./paste-text";
import { tip } from "./tooltip";
import { workSeconds, workedLabel } from "./work-duration";
import { markClampedPrompts } from "./prompt-clamp";
import { decorateTextCodeBlocks, normalizePlainTextFences } from "./text-code";

import { createTranscriptViewport } from "./transcript-viewport";
Expand All @@ -150,6 +151,7 @@ interface SettledRowKey {
speakerLabel: string | undefined;
edited: boolean;
deleted: boolean;
expanded: boolean;
tpl: TemplateResult | typeof nothing;
}
const settledRowCache = new WeakMap<object, SettledRowKey>();
Expand Down Expand Up @@ -219,6 +221,7 @@ export function createChatSurface(
inheritedLoaded: false,
pins: [] as SessionPin[],
pinsExpanded: false,
expandedPrompt: null as number | null,
labelSpeakers: false,
};

Expand Down Expand Up @@ -398,6 +401,7 @@ export function createChatSurface(
chatState.earlierCount = 0;
chatState.loadingEarlier = false;
chatState.pins = [];
chatState.expandedPrompt = null;
chatState.host = document.createElement("div");
chatState.host.className = "custom-chat";

Expand Down Expand Up @@ -929,6 +933,7 @@ export function createChatSurface(
if (!sameSession) {
chatState.inheritedExpanded = false;
chatState.pins = [];
chatState.expandedPrompt = null;
}
syncLocation();

Expand Down Expand Up @@ -1029,6 +1034,7 @@ export function createChatSurface(
);
requestAnimationFrame(() => {
decorateTextCodeBlocks(host);
markClampedPrompts(host);
if (host.isConnected) transcriptViewport.sync(host.querySelector<HTMLElement>(".chat-scroll"));
});
};
Expand Down Expand Up @@ -1069,6 +1075,12 @@ export function createChatSurface(
else readonlyRedraw?.();
}

function togglePromptExpanded(index: number): void {
chatState.expandedPrompt = chatState.expandedPrompt === index ? null : index;
if (chatState.agent) drawActiveChat(chatState.agent);
else readonlyRedraw?.();
}

function linkifiedText(text: string): TemplateResult {
return html`${splitLinks(text).map((seg) =>
seg.kind === "link"
Expand Down Expand Up @@ -1286,7 +1298,10 @@ export function createChatSurface(
chatState.host,
);
decorateStreamingTail();
requestAnimationFrame(() => decorateTextCodeBlocks(chatState.host));
requestAnimationFrame(() => {
decorateTextCodeBlocks(chatState.host);
markClampedPrompts(chatState.host);
});
ctx.composer.resizeComposer();
scrollTranscript(opts.forceScroll);
postCurrentPaneState();
Expand Down Expand Up @@ -1422,6 +1437,7 @@ export function createChatSurface(
const speakerLabel = speakerLabelFor(message);
const edited = Boolean((message as { edited?: boolean }).edited);
const deleted = Boolean((message as { deleted?: boolean }).deleted);
const expanded = chatState.expandedPrompt === index;
const hit = settledRowCache.get(message as object);
if (
hit &&
Expand All @@ -1437,7 +1453,8 @@ export function createChatSurface(
hit.forkable === forkable &&
hit.speakerLabel === speakerLabel &&
hit.edited === edited &&
hit.deleted === deleted
hit.deleted === deleted &&
hit.expanded === expanded
) {
return hit.tpl;
}
Expand All @@ -1456,6 +1473,7 @@ export function createChatSurface(
speakerLabel,
edited,
deleted,
expanded,
tpl,
});
return tpl;
Expand All @@ -1476,10 +1494,21 @@ export function createChatSurface(
<article class="message-row user-row ${steered ? "steered-row" : ""}" data-index=${index}>
${steered ? html`<div class="steer-label">↪ steered the running task</div>` : nothing}
${speaker ? html`<div class="speaker-label">${speaker}</div>` : nothing}
<div class="message-bubble user-bubble ${deleted ? "deleted-bubble" : ""}">
<div
class="message-bubble user-bubble ${deleted ? "deleted-bubble" : ""}"
data-expanded=${chatState.expandedPrompt === index ? "true" : "false"}
>
${isReadOnlySlackView() ? slackWireBubble(messageText(message)) : markdown(messageText(message))}
${attachments.length ? html`<div class="message-files">${attachments.map(userAttachmentBadge)}</div>` : nothing}
${edited || deleted ? html`<span class="revision-badge">(${deleted ? "deleted" : "edited"})</span>` : nothing}
<button
class="prompt-toggle"
type="button"
aria-expanded=${chatState.expandedPrompt === index ? "true" : "false"}
@click=${() => togglePromptExpanded(index)}
>
${chatState.expandedPrompt === index ? "Show less" : "Show more"}
</button>
</div>
${
sendFailure
Expand Down
51 changes: 16 additions & 35 deletions plugins/web-ui/src/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { html, nothing, render, type TemplateResult } from "lit";
import {
ArrowLeft,
Boxes,
Check,
ChevronDown,
Folder,
FolderPlus,
Hash,
Expand All @@ -27,7 +25,7 @@ import {
} from "./core-bridge";
import { UI_BASE } from "./deep-link";
import { errMessage } from "../../chassis/src/errors";
import { actionSnippet, closeFormMenus, fieldSelect, formatBytes, icon, initials, relTime, toggleFormMenu } from "./ui";
import { actionSnippet, fieldSelect, formatBytes, icon, initials, menuSelect, relTime } from "./ui";
import { appState, replacePanePreservingFocus, switchView, syncUrlFromState } from "./shell";
import { startNewChat } from "./sessions";
import { groupDmTitle, openSession, refreshSessions, sessionsState, slackLogo, surfaceOf } from "./sessions";
Expand Down Expand Up @@ -274,38 +272,21 @@ export function scopeChip(scopeId: string | null, fallbackName?: string | null):
}

export function scopeFilterControl(current: string | null, onSelect: (scopeId: string | null) => void): TemplateResult {
const label = current ? metaForScope(current).title : "All contexts";
const option = (scopeId: string | null, text: string, glyph: IconNode) => {
const active = (current ?? null) === scopeId;
return html`
<button
class="menu-option ${active ? "active" : ""}"
type="button"
role="menuitemradio"
aria-checked=${active ? "true" : "false"}
@click=${(e: Event) => {
e.stopPropagation();
closeFormMenus();
onSelect(scopeId);
}}
>
<span class="menu-option-label scope-option-label">${icon(glyph, 14)}<span>${text}</span></span>
${active ? icon(Check, 15) : nothing}
</button>
`;
};
return html`
<div class="menu-control form-menu-control scope-filter">
<button class="menu-button" type="button" aria-haspopup="menu" aria-expanded="false" @click=${toggleFormMenu}>
<span class="menu-label">Filter by: ${label}</span>${icon(ChevronDown, 14)}
</button>
<div class="menu-popover" role="menu" hidden>
<div class="menu-title">Filter by context</div>
${option(null, "All contexts", Boxes)}
${contextsState.list.map((c) => option(c.scopeId, contextMeta(c).title, contextMeta(c).glyph))}
</div>
</div>
`;
return menuSelect({
value: current,
prefix: "Filter by: ",
ariaLabel: "Filter by context",
className: "scope-filter",
onSelect,
options: [
{ value: null, label: "All contexts", glyph: Boxes },
...contextsState.list.map((c) => ({
value: c.scopeId,
label: contextMeta(c).title,
glyph: contextMeta(c).glyph,
})),
],
});
}

function sessionsIn(scopeId: string): CoreSession[] {
Expand Down
10 changes: 5 additions & 5 deletions plugins/web-ui/src/crons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ function cronPageRow(c: CronView, mine: boolean): TemplateResult {
function cronRowActions(c: CronView): TemplateResult {
let stateAction = html`
<button
class="icon-btn subtle cron-action-btn"
class="icon-btn subtle compact"
type="button"
${tip("Enable")}
aria-label="Enable cron"
Expand All @@ -374,7 +374,7 @@ function cronRowActions(c: CronView): TemplateResult {
if (c.archived) {
stateAction = html`
<button
class="icon-btn subtle cron-action-btn"
class="icon-btn subtle compact"
type="button"
${tip("Unarchive")}
aria-label="Unarchive cron"
Expand All @@ -386,7 +386,7 @@ function cronRowActions(c: CronView): TemplateResult {
} else if (c.enabled) {
stateAction = html`
<button
class="icon-btn subtle cron-action-btn"
class="icon-btn subtle compact"
type="button"
${tip("Disable")}
aria-label="Disable cron"
Expand All @@ -399,7 +399,7 @@ function cronRowActions(c: CronView): TemplateResult {
return html`
<div class="cron-row-actions" aria-label="Cron actions">
<button
class="icon-btn subtle cron-action-btn"
class="icon-btn subtle compact"
type="button"
${tip("Edit")}
aria-label="Edit cron"
Expand All @@ -416,7 +416,7 @@ function cronRowActions(c: CronView): TemplateResult {
? nothing
: html`
<button
class="icon-btn subtle cron-action-btn"
class="icon-btn subtle compact"
type="button"
${tip("Archive")}
aria-label="Archive cron"
Expand Down
Loading