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
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Composer
created: 2026-10-15T02:50:44Z
norm: '1'
sig: 6e0e6c5c8afbd01a
body_hash: 5f7b73ecdc92491d
raw_hash: 92a0017bb10f2de6
vouched: 2026-10-19T20:57:54Z
body_hash: f265dfc748a0c278
raw_hash: dd0efcf54d33e479
vouched: 2026-10-22T14:39:32Z
lines: 99-282
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Composer
created: 2026-10-15T12:44:15Z
norm: '1'
sig: 6e0e6c5c8afbd01a
body_hash: 5f7b73ecdc92491d
raw_hash: 92a0017bb10f2de6
vouched: 2026-10-19T20:57:54Z
body_hash: f265dfc748a0c278
raw_hash: dd0efcf54d33e479
vouched: 2026-10-22T14:39:32Z
lines: 101-290
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Composer
created: 2026-10-15T14:16:28Z
norm: '1'
sig: 6e0e6c5c8afbd01a
body_hash: 5f7b73ecdc92491d
raw_hash: 92a0017bb10f2de6
vouched: 2026-10-19T20:57:54Z
body_hash: f265dfc748a0c278
raw_hash: dd0efcf54d33e479
vouched: 2026-10-22T14:39:32Z
lines: 104-326
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function main
created: 2026-10-15T04:47:23Z
norm: '1'
sig: 7d5d3f63a3bddde4
body_hash: fd9cef47cdc61076
raw_hash: e9c988923aa657a6
vouched: 2026-10-22T14:31:33Z
body_hash: 42c616dba5adb167
raw_hash: 1a7c366637365036
vouched: 2026-10-22T14:45:22Z
lines: 253-482
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function main
created: 2026-10-22T14:31:16Z
norm: '1'
sig: 7d5d3f63a3bddde4
body_hash: fd9cef47cdc61076
raw_hash: e9c988923aa657a6
body_hash: 42c616dba5adb167
raw_hash: 1a7c366637365036
vouched: 2026-10-22T14:45:22Z
lines: 332-614
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function useComposer
created: 2026-10-16T22:41:13Z
norm: '1'
sig: 381929b0cbaf2245
body_hash: 8f1d85407de87351
raw_hash: 6588ffae72ff82db
body_hash: 4dcfff341604590f
raw_hash: 03d5450461fb3892
vouched: 2026-10-22T14:39:32Z
lines: 597-1226
---

Expand Down
7 changes: 5 additions & 2 deletions apps/app/src/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,12 @@ export function Composer(
const sent = new Set(attached);
setAttached((current) => current.filter((value) => !sent.has(value)));
} catch (error) {
const { message } = error as Error;
// Only a missing key is fixed in Providers; busy, access and size errors are not.
const keys = invoke && onProviders && /provider key/i.test(message);
toast.error(invoke ? "Could not invoke agent" : "Could not send message", {
description: (error as Error).message,
...(invoke && onProviders ? { action: { label: "Providers", onClick: onProviders } } : {}),
description: message,
...(keys ? { action: { label: "Providers", onClick: onProviders } } : {}),
});
} finally {
sending.current = false;
Expand Down
7 changes: 6 additions & 1 deletion apps/app/src/conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,12 @@ export function Conversation(
);
act({ op: "react", emoji, remove }, uid);
}}
onCopy={(uid) => void navigator.clipboard.writeText(byKey.get(uid)!.text)}
onCopy={(uid) =>
navigator.clipboard.writeText(byKey.get(uid)!.text).then(
() => toast("Copied message"),
(error) =>
toast.error("Could not copy the message", { description: error.message }),
)}
onDelete={setDeleting}
editable={editable}
editing={edited?.key}
Expand Down
9 changes: 7 additions & 2 deletions apps/host/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ function clip(text: string, length: number): string {
return line.length > length ? `${line.slice(0, length - 1)}…` : line;
}

/** Whether `(run ended)` already printed, so `ask` doesn't print a second ending. */
let ended = false;

function render(event: Event, info: ChannelInfo) {
const tag = event.chat === 1 && info.chats.length === 1 ? "" : dim(`[${event.chat}] `);
switch (event.kind) {
Expand All @@ -151,7 +154,9 @@ function render(event: Event, info: ChannelInfo) {
case "result":
return console.log(dim(`${tag} ${event.error ? "✗" : "←"} ${clip(event.text, 100)}`));
case "run":
return event.state === "end" && console.log(dim(`${tag} (run ended)`));
if (event.state !== "end") return;
ended = true;
return console.log(dim(`${tag} (run ended)`));
}
}

Expand Down Expand Up @@ -536,7 +541,7 @@ async function main() {
process.exit(130);
});
const status = await session.request<string>({ op: "wait", submission });
if (status === "unanswered") console.log(dim(" (stopped before answering)"));
if (status === "unanswered" && !ended) console.log(dim(" (stopped before answering)"));
return;
}
case "watch":
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/components/composer/chat-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ function useComposer({
return true;
}
}
return history.handleKey(v, e, m => updateMode(m || ms[0]!.id));
return history.handleKey(v, e, m => updateMode(m || ms[0]!.id), modeRef.current);
}

function update(v: EditorView) {
Expand Down Expand Up @@ -1717,7 +1717,8 @@ function ComposerButtons({ state }: { state: ComposerState }) {
let { terminal, sendable, send, tip, stop, onStop, exit, edit, cancel, accessory } = state;
return (
<Composer.End>
{accessory}
{/* Above the actions' hover halo, which reaches over it. */}
{accessory && <span className="relative z-10 flex">{accessory}</span>}
<TooltipProvider>
{terminal
? (
Expand Down
9 changes: 8 additions & 1 deletion packages/ui/src/components/timeline/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ function AgentWorkingContextView(
...workingTextFont,
}}
>
{turn.recap && <span className="text-foreground/80">{turn.recap}</span>}
{turn.recap && <span className="text-foreground/80">{codeSpans(turn.recap)}</span>}
<span
className={cn("flex min-w-0 items-center gap-1.5", turn.recap && "text-xs")}
style={{ blockSize: WORKING_SUMMARY_HEIGHT }}
Expand Down Expand Up @@ -489,6 +489,13 @@ function AgentWorkingContextView(
);
}

/** Recaps are plain text, but models still wrap commands and paths in backticks. */
function codeSpans(text: string) {
return text.split(/`([^`]+)`/).map((part, i) =>
i % 2 ? <code key={i} className="font-mono text-[0.92em]">{part}</code> : part
);
}

function find(items: TimelineItem[], uid: string) {
for (let i = 0; i < items.length; i++) {
let item = items[i]!;
Expand Down
50 changes: 42 additions & 8 deletions packages/ui/src/hooks/use-history.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef } from "react";
import { TextSelection } from "prosemirror-state";
import type { Node } from "prosemirror-model";
import type { EditorView } from "prosemirror-view";

const MAX = 10;
Expand All @@ -25,13 +26,37 @@ function clear(view: EditorView) {
view.dispatch(tr);
}

/** Ace mode's empty composer still holds its `@ace` chip, so mentions alone count as empty. */
function blank(doc: Node) {
let text = "";
doc.descendants((node) => {
if (node.type.name === "mention") return false;
if (node.isText) text += node.text;
else if (node.isLeaf && node.isInline) text += "x";
});
return !text.trim();
}

/**
* Navigate sent-message history with ArrowUp/ArrowDown.
* Only starts when input is empty. Call `reset` on onChange to exit history mode.
*/
function useHistory(scope?: string) {
let k = `ace:history:${scope || location.pathname}`;
let index = useRef(-1);
// A recall dispatches a change like typing does; this keeps that change from ending browsing.
let recalling = useRef(false);
// The mode before browsing, restored when ↓ returns to an empty composer.
let origin = useRef<string>(undefined);

function recall(write: () => void) {
recalling.current = true;
try {
write();
} finally {
recalling.current = false;
}
}

function load(): Entry[] {
try {
Expand Down Expand Up @@ -69,18 +94,19 @@ function useHistory(scope?: string) {

/**
* ProseMirror key handler for history navigation. `onMode` receives the mode
* a recalled entry was sent in (undefined when returning to an empty box) so
* a recalled entry was sent in (`mode`, the one before browsing, when returning to an empty box) so
* the composer can restore mode state alongside the document.
*/
function handleKey(
view: EditorView,
e: KeyboardEvent,
onMode?: (mode?: string) => void,
mode?: string,
): boolean {
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return false;

let browsing = index.current !== -1;
if (!browsing && view.state.doc.textContent) return false;
if (!browsing && !blank(view.state.doc)) return false;

let items = load();
if (!items.length) return false;
Expand All @@ -89,9 +115,12 @@ function useHistory(scope?: string) {
let next = browsing ? index.current - 1 : items.length - 1;
if (next < 0) return browsing;
e.preventDefault();
if (!browsing) origin.current = mode;
index.current = next;
apply(view, items[next]!.doc);
onMode?.(items[next]!.mode);
recall(() => {
apply(view, items[next]!.doc);
onMode?.(items[next]!.mode);
});
return true;
}

Expand All @@ -101,13 +130,17 @@ function useHistory(scope?: string) {
let next = index.current + 1;
if (next >= items.length) {
index.current = -1;
clear(view);
onMode?.(undefined);
recall(() => {
clear(view);
onMode?.(origin.current);
});
return true;
}
index.current = next;
apply(view, items[next]!.doc);
onMode?.(items[next]!.mode);
recall(() => {
apply(view, items[next]!.doc);
onMode?.(items[next]!.mode);
});
return true;
}

Expand All @@ -116,6 +149,7 @@ function useHistory(scope?: string) {

/** Reset browsing index. Call on onChange to exit history mode. */
function reset() {
if (recalling.current) return;
index.current = -1;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/ui/emoji-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,13 @@ function EmojiPicker({ onSelect, theme }: Props) {
s.textContent = shadowCSS();
host.shadowRoot.appendChild(s);
}
// emoji-mart's autoFocus runs before the popover can take focus, so focus search a frame later.
let frame = requestAnimationFrame(() => host?.shadowRoot?.querySelector("input")?.focus());
picker.current = instance;
applyVars();

return () => {
cancelAnimationFrame(frame);
picker.current = null;
el.replaceChildren();
};
Expand Down