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: 75fc838bf3451b29
raw_hash: 1948feaaac06338f
vouched: 2026-10-22T15:03:16Z
body_hash: c1bc4d0ff6028d0a
raw_hash: f2b2dae833097ad7
vouched: 2026-10-22T15:13:29Z
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: 75fc838bf3451b29
raw_hash: 1948feaaac06338f
vouched: 2026-10-22T15:03:16Z
body_hash: c1bc4d0ff6028d0a
raw_hash: f2b2dae833097ad7
vouched: 2026-10-22T15:13:29Z
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: 75fc838bf3451b29
raw_hash: 1948feaaac06338f
vouched: 2026-10-22T15:03:16Z
body_hash: c1bc4d0ff6028d0a
raw_hash: f2b2dae833097ad7
vouched: 2026-10-22T15:13:25Z
lines: 104-326
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Dashboard
created: 2026-10-19T20:51:03Z
norm: '1'
sig: 4bfb45497157c0e0
body_hash: d1696132fea7c765
raw_hash: 907b4bdd8de26f76
vouched: 2026-10-22T15:09:45Z
body_hash: 97a54ddcbd872f30
raw_hash: 758dbb3d2b2fb407
vouched: 2026-10-22T15:14:58Z
lines: 256-606
---

Expand Down
11 changes: 7 additions & 4 deletions apps/app/src/composer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ReactNode, useEffect, useRef, useState, useSyncExternalStore } from "react";
import { type ReactNode, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";

import {
aceAvatar,
Expand All @@ -24,8 +24,7 @@ const MODES = [
];
// A teammate the owner hasn't let invoke sees no @ace; the stored mode brings it back with access.
const BLOCKED_MODES = [{ id: "chat", name: "Chat", placeholder: "Message the channel" }];
const MENTIONS = [{ name: "ace", avatar: aceAvatar }];
const NO_MENTIONS: typeof MENTIONS = [];
const ACE_MENTION = { name: "ace", avatar: aceAvatar };
export const EFFORTS: Record<Effort, string> = {
off: "Off",
minimal: "Minimal",
Expand Down Expand Up @@ -65,6 +64,8 @@ type Props = {
failure?: string;
onProviders?: () => void;
accessory?: ReactNode;
/** People to suggest after @, by GitHub login. */
members: { name: string; avatar?: string }[];
};

/** Below the composer: its attachment tray rises over the space above it and takes clicks there. */
Expand Down Expand Up @@ -98,6 +99,7 @@ export function Composer(
failure,
onProviders,
accessory,
members,
}: Props,
) {
const composer = useRef<ChatComposerHandle>(null);
Expand All @@ -110,6 +112,7 @@ export function Composer(
// The channel refuses a blocked teammate's ask, stop, and kill too.
const blocked = shared === false && channel.owner !== user;
const mode = blocked ? "chat" : stored;
const mentions = useMemo(() => blocked ? members : [ACE_MENTION, ...members], [blocked, members]);
const [models, setModels] = useState<ModelOption[]>();
const [modelError, setModelError] = useState<string>();
const status = useSyncExternalStore(host.subscribe, () => host.status);
Expand Down Expand Up @@ -319,7 +322,7 @@ export function Composer(
modes={blocked ? BLOCKED_MODES : MODES}
mode={mode}
onModeChange={changeMode}
mentions={blocked ? NO_MENTIONS : MENTIONS}
mentions={mentions}
models={choices}
model={model}
onModelChange={(value) => changePick({ model: value })}
Expand Down
23 changes: 21 additions & 2 deletions apps/app/src/conversation.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";

import { Button, Timeline, toast } from "@ace/ui";
import type { Listing } from "@ace/host/protocol";
import { channelProject, type Listing } from "@ace/host/protocol";

import { Checks } from "./checks";
import { Composer } from "./composer";
Expand All @@ -11,7 +11,8 @@ import { EditMessage } from "./edit-message";
import { publish, useDetails } from "./details";
import { host } from "./host";
import { MessageQueue } from "./message-queue";
import { OfflineHost } from "./onboarding";
import { OfflineHost, useRoster } from "./onboarding";
import { present } from "./presence";
import { pendingQuestion, Question } from "./question";
import { sources, toEvents } from "./timeline";
import { useTranscript } from "./transcript";
Expand Down Expand Up @@ -158,6 +159,23 @@ export function Conversation(
),
[people],
);
const presence = useSyncExternalStore(host.subscribe, () => host.presence);
const roster = useRoster();
// Mentions name people by GitHub login. A name still holding @ is a Tailscale login with no
// GitHub account, or an agent from another channel (`agent@<prefix>`), and can't be mentioned.
const members = useMemo(() => {
const self = people[user] || user;
const names = new Set([
...present(presence[channelProject(channel)], self, channel.id).map((value) => value.name),
...transcript.items.flatMap((item) =>
item.kind === "message" ? [people[item.author] || item.author] : []
),
...(roster || []).map((member) => member.github || member.login),
]);
return [...names]
.filter((name) => name !== self && name !== "ace" && !name.includes("@"))
.map((name) => ({ name, avatar: avatars[name] }));
}, [people, user, presence, channel, transcript.items, roster, avatars]);
const results = useMemo(
() =>
transcript.items.reduce(
Expand Down Expand Up @@ -304,6 +322,7 @@ export function Conversation(
failure={failure}
onProviders={onProviders}
accessory={chat?.usage && <Usage value={chat.usage} context={chat.context} />}
members={members}
/>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions apps/app/src/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type ReactNode, useEffect, useRef, useState, useSyncExternalStore } from "react";

import {
aceAvatar,
Button,
buttonVariants,
ChatComposer,
Expand Down Expand Up @@ -67,6 +68,8 @@ function adopt(project: AppProject): string {
}

const MODES = [{ id: "ace", name: "Ace", placeholder: "Start a new channel" }];
// A new channel has no one else in it yet; a list also stops unknown @words becoming chips.
const MENTIONS = [{ name: "ace", avatar: aceAvatar }];

/** Where a new channel's agent works, offered while the cloud compute beta is on. */
const PLACES = [
Expand Down Expand Up @@ -449,6 +452,7 @@ export function Dashboard(
ref={composer}
scope={scope}
modes={MODES}
mentions={MENTIONS}
mode="ace"
agents={cloud ? PLACES : undefined}
agent={place}
Expand Down