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-15T12:44:15Z
norm: '1'
sig: 6e0e6c5c8afbd01a
body_hash: 7a4597684360db67
raw_hash: 52e4e5fdab485c4d
vouched: 2026-10-22T17:52:41Z
body_hash: 69f69de77ff20470
raw_hash: 555034163125d546
vouched: 2026-10-22T20:30:45Z
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: 7a4597684360db67
raw_hash: 52e4e5fdab485c4d
vouched: 2026-10-22T17:52:41Z
body_hash: 69f69de77ff20470
raw_hash: 555034163125d546
vouched: 2026-10-22T20:30:45Z
lines: 104-326
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5JMY120BQ7Q19708TEHREZF
anchor: function Composer
created: 2026-10-22T20:30:39Z
norm: '1'
sig: 6e0e6c5c8afbd01a
body_hash: 69f69de77ff20470
raw_hash: 555034163125d546
lines: 97-433
---

the chat holds the model and effort pick (select op); the composer must not keep its own browser copy, or details and the CLI disagree with it
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function useTranscript
created: 2026-10-15T02:55:20Z
norm: '1'
sig: 3961cde46070e655
body_hash: 6eead60056a515f0
raw_hash: e83ea96acc31564a
vouched: 2026-10-22T15:28:03Z
body_hash: 715b52c056e02eac
raw_hash: 0c5f8df4fea08fca
vouched: 2026-10-22T20:30:45Z
lines: 174-275
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function useTranscript
created: 2026-10-15T04:57:01Z
norm: '1'
sig: 3961cde46070e655
body_hash: 6eead60056a515f0
raw_hash: e83ea96acc31564a
vouched: 2026-10-22T15:28:03Z
body_hash: 715b52c056e02eac
raw_hash: 0c5f8df4fea08fca
vouched: 2026-10-22T20:30:45Z
lines: 197-314
---

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: 7770844bad8c4705
raw_hash: 5a2b69597fdf7f65
vouched: 2026-10-22T15:24:37Z
body_hash: eb91c91d476265b2
raw_hash: ec1c6c19cf199017
vouched: 2026-10-22T20:30:45Z
lines: 253-482
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function main
created: 2026-10-22T14:31:16Z
norm: '1'
sig: 7d5d3f63a3bddde4
body_hash: 7770844bad8c4705
raw_hash: 5a2b69597fdf7f65
vouched: 2026-10-22T15:24:37Z
body_hash: eb91c91d476265b2
raw_hash: ec1c6c19cf199017
vouched: 2026-10-22T20:30:45Z
lines: 332-614
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-15T02:58:00Z
norm: '1'
sig: 1a43097a4e1ad24c
body_hash: 3d68c7dfc3bf4c24
raw_hash: eac2665348c286fe
vouched: 2026-10-22T14:47:34Z
body_hash: 5e79b1f5e63ad3c5
raw_hash: 1f3f06a6f1a6594b
vouched: 2026-10-22T20:30:45Z
lines: 1-194
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-14T17:33:22Z
norm: '1'
sig: c905677fa4be33c1
body_hash: 97ca0ac50b13c167
raw_hash: 4b1f599e8b95d697
vouched: 2026-10-22T15:29:33Z
body_hash: 4bdc53b3fed0ba8d
raw_hash: 7726f4f136727a5a
vouched: 2026-10-22T20:30:45Z
lines: 114-792
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-15T00:30:42Z
norm: '1'
sig: c905677fa4be33c1
body_hash: 97ca0ac50b13c167
raw_hash: 4b1f599e8b95d697
vouched: 2026-10-22T15:29:33Z
body_hash: 4bdc53b3fed0ba8d
raw_hash: 7726f4f136727a5a
vouched: 2026-10-22T20:30:46Z
lines: 114-792
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-15T02:34:37Z
norm: '1'
sig: c905677fa4be33c1
body_hash: 97ca0ac50b13c167
raw_hash: 4b1f599e8b95d697
vouched: 2026-10-22T15:29:33Z
body_hash: 4bdc53b3fed0ba8d
raw_hash: 7726f4f136727a5a
vouched: 2026-10-22T20:30:46Z
lines: 114-794
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-15T10:17:55Z
norm: '1'
sig: c905677fa4be33c1
body_hash: 97ca0ac50b13c167
raw_hash: 4b1f599e8b95d697
vouched: 2026-10-22T15:29:33Z
body_hash: 4bdc53b3fed0ba8d
raw_hash: 7726f4f136727a5a
vouched: 2026-10-22T20:30:46Z
lines: 121-817
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-16T22:11:35Z
norm: '1'
sig: c905677fa4be33c1
body_hash: 97ca0ac50b13c167
raw_hash: 4b1f599e8b95d697
vouched: 2026-10-22T15:29:33Z
body_hash: 4bdc53b3fed0ba8d
raw_hash: 7726f4f136727a5a
vouched: 2026-10-22T20:30:46Z
lines: 127-873
---

Expand Down
10 changes: 8 additions & 2 deletions apps/app/src/channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ function where(channel: Listing): DetailsFact[] {

function facts(channel: Listing, info?: ChannelInfo): DetailsFact[] {
const root = info?.chats.find((value) => value.id === chat);
const model = root?.model || channel.model;
const chosen = root?.model || channel.model;
// A chat without a model runs what Automatic picks, which a default model change moves.
const model = chosen || info?.automatic;
// "off" also stands for an always-thinking provider's default, so it isn't shown as a level.
const effort = root?.model && root.effort !== "off" ? root.effort : undefined;
const usage = info?.chats.reduce(
Expand All @@ -136,7 +138,11 @@ function facts(channel: Listing, info?: ChannelInfo): DetailsFact[] {
...(model
? [{
label: "Model",
value: effort ? `${model.modelId} · ${EFFORTS[effort]}` : model.modelId,
value: effort
? `${model.modelId} · ${EFFORTS[effort]}`
: chosen
? model.modelId
: `${model.modelId} (Automatic)`,
title: `${model.provider}/${model.modelId}${effort ? `, ${EFFORTS[effort]} effort` : ""}`,
}]
: []),
Expand Down
76 changes: 48 additions & 28 deletions apps/app/src/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export const EFFORTS: Record<Effort, string> = {
};
const ACE = /(^|\s)@ace\b/i;
const LEADING = /^\s*@ace\b/i;
const UNPICKED: { model?: string; effort?: Effort } = {};

const key = (model: ModelRef) => `${model.provider}/${model.modelId}`;
// Errors a provider gives for the model or its key, which another try with them would repeat.
Expand All @@ -61,6 +60,8 @@ type Props = {
user: string;
current?: ModelRef;
currentEffort?: Effort;
/** What the channel picks for a chat on Automatic. */
automatic?: ModelRef;
busy: boolean;
/** What a pending send waits on, as its submit control's label. */
waiting: string;
Expand Down Expand Up @@ -100,6 +101,7 @@ export function Composer(
user,
current,
currentEffort,
automatic,
busy,
waiting,
shared,
Expand All @@ -118,7 +120,7 @@ export function Composer(
const typed = useRef(0);
const [pending, setPending] = useState(false);
const [attached, setAttached] = useState<Attachment[]>([]);
// Kept beside the channel's draft so a deliberate mode and an unsent model pick survive relaunch.
// Kept beside the channel's draft so a deliberate mode survives relaunch.
const scope = `/channels/${channel.id}`;
const [stored, changeMode] = useLocalStorage(`ace:mode:${scope}`, "ace");
// The channel refuses a blocked teammate's ask, stop, and kill too.
Expand Down Expand Up @@ -204,14 +206,14 @@ export function Composer(
active = false;
};
}, [channel.id, mode, status, settingsVersion, offline, ready]);
const [{ model: picked, effort: pickedEffort }, changePick] = useLocalStorage(
`ace:pick:${scope}/${id}`,
UNPICKED,
);
const model = picked || (current ? key(current) : "auto");
// The chat holds the pick, so details, the CLI and other clients name what its next run uses.
// A pick shows until the chat's info reports a change, which follows the pick's own answer.
const chosen = `${current ? key(current) : "auto"} ${currentEffort || "off"}`;
const [picking, setPicking] = useState<{ model: string; effort?: Effort; from: string }>();
if (picking && picking.from !== chosen) setPicking(undefined);
const model = picking?.model || (current ? key(current) : "auto");
const levels = models?.find((value) => key(value) === model)?.efforts;
const requestedEffort = pickedEffort
|| (current && key(current) === model ? currentEffort : undefined) || "off";
const requestedEffort = picking?.effort || currentEffort || "off";
const effort = requestedEffort === "off" || levels?.includes(requestedEffort)
? requestedEffort
: "off";
Expand All @@ -230,7 +232,11 @@ export function Composer(
choices.unshift({ id: model, name: model.split("/").slice(1).join("/"), vendor: "" });
}
// Automatic stays offered so a chat whose model lost its key can hand the choice back.
choices.unshift({ id: "auto", name: "Automatic", vendor: "" });
choices.unshift({
id: "auto",
name: automatic ? `Automatic (${automatic.modelId})` : "Automatic",
vendor: "",
});
// Formatting tools crowd a phone's composer; they stay one tap away.
const phone = useMedia("(width < 40rem)");

Expand All @@ -246,6 +252,33 @@ export function Composer(
setAttached(kept);
}

async function pick(next: string, nextEffort?: Effort) {
const from = chosen;
setPicking({ model: next, effort: nextEffort, from });
const [provider, ...rest] = next.split("/");
try {
const result = await host.channel<{ model: ModelRef; effort: Effort }>(channel.id, {
op: "select",
chat: id,
author: user,
...(next === "auto"
? { automatic: true }
: { model: { provider: provider!, modelId: rest.join("/") } }),
...(nextEffort ? { effort: nextEffort } : {}),
});
setPicking({ model: key(result.model), effort: result.effort, from });
} catch (error) {
setPicking(undefined);
const { message } = error as Error;
toast.error("Could not change the model", {
description: message,
...(onProviders && /provider key/i.test(message)
? { action: { label: "Providers", onClick: onProviders } }
: {}),
});
}
}

async function send(text: string, mode: string, attached: Attachment[]) {
if (sending.current) return;
// Posted, a blocked teammate's "@ace" would read as an ask nobody answers.
Expand All @@ -269,18 +302,14 @@ export function Composer(
try {
const encoded = await attachments(attached);
if (invoke) {
const [provider, ...rest] = model.split("/");
// The chat holds the pick; asking without a model lets the channel replace one whose
// provider lost its key.
await host.channel(channel.id, {
op: "ask",
...(levels ? { effort } : {}),
chat: id,
author: user,
text: body,
...encoded,
// An unpicked model lets the channel replace a chat model whose provider lost its key.
...(picked && picked !== "auto"
? { model: { provider: provider!, modelId: rest.join("/") } }
: {}),
});
} else {
await host.channel(channel.id, {
Expand Down Expand Up @@ -315,23 +344,14 @@ export function Composer(
sending.current = true;
setPending(true);
try {
const [provider, ...rest] = model.split("/");
await host.channel(channel.id, {
op: "ask",
chat: id,
author: user,
text: RETRY,
...(reset
? { automatic: true }
: {
...(levels ? { effort } : {}),
...(picked && picked !== "auto"
? { model: { provider: provider!, modelId: rest.join("/") } }
: {}),
}),
...(reset ? { automatic: true } : {}),
});
if (reset) {
changePick(UNPICKED);
toast("Switched this chat to Automatic", {
description: `${model} failed, so Retry let Ace choose the model.`,
});
Expand All @@ -358,10 +378,10 @@ export function Composer(
mentions={mentions}
models={choices}
model={model}
onModelChange={(value) => changePick({ model: value })}
onModelChange={(value) => void pick(value)}
efforts={efforts}
effort={effort}
onEffortChange={(value) => changePick({ model: picked, effort: value as Effort })}
onEffortChange={(value) => void pick(model, value as Effort)}
attachments={attached}
onAttachmentsChange={attach}
busy={busy}
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export function Conversation(
user={user}
current={model}
currentEffort={chat?.effort}
automatic={transcript.info?.automatic}
busy={transcript.busy}
waiting={waiting}
shared={transcript.shared}
Expand Down
Loading