From 56e6a7b32d94ccea8172908683bf027dea6334c0 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 11:13:13 -0400 Subject: [PATCH 1/3] fix(app): suggest channel members after @, and keep unknown @words plain on the Dashboard --- apps/app/src/composer.tsx | 11 +++++++---- apps/app/src/conversation.tsx | 23 +++++++++++++++++++++-- apps/app/src/dashboard.tsx | 4 ++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/apps/app/src/composer.tsx b/apps/app/src/composer.tsx index a7908256..560b7b9c 100644 --- a/apps/app/src/composer.tsx +++ b/apps/app/src/composer.tsx @@ -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, @@ -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 = { off: "Off", minimal: "Minimal", @@ -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. */ @@ -98,6 +99,7 @@ export function Composer( failure, onProviders, accessory, + members, }: Props, ) { const composer = useRef(null); @@ -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(); const [modelError, setModelError] = useState(); const status = useSyncExternalStore(host.subscribe, () => host.status); @@ -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 })} diff --git a/apps/app/src/conversation.tsx b/apps/app/src/conversation.tsx index afd257a6..313218d1 100644 --- a/apps/app/src/conversation.tsx +++ b/apps/app/src/conversation.tsx @@ -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"; @@ -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"; @@ -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@`), 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( @@ -304,6 +322,7 @@ export function Conversation( failure={failure} onProviders={onProviders} accessory={chat?.usage && } + members={members} /> diff --git a/apps/app/src/dashboard.tsx b/apps/app/src/dashboard.tsx index e72a05ca..2ffb2045 100644 --- a/apps/app/src/dashboard.tsx +++ b/apps/app/src/dashboard.tsx @@ -1,6 +1,7 @@ import { type ReactNode, useEffect, useRef, useState, useSyncExternalStore } from "react"; import { + aceAvatar, Button, buttonVariants, ChatComposer, @@ -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 = [ @@ -449,6 +452,7 @@ export function Dashboard( ref={composer} scope={scope} modes={MODES} + mentions={MENTIONS} mode="ace" agents={cloud ? PLACES : undefined} agent={place} From d02eb01152743ff337de49ea6f241d3936736827 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 11:14:18 -0400 Subject: [PATCH 2/3] chore: confirm composer and dashboard notes --- ...M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md | 6 +++--- ...M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md | 6 +++--- ...1M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md b/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md index 549bcfb1..5c7f93f7 100644 --- a/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md +++ b/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md @@ -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 --- diff --git a/.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md b/.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md index 6e37e455..9f362852 100644 --- a/.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md +++ b/.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md @@ -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 --- diff --git a/.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md b/.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md index b93dd58e..5cc309a2 100644 --- a/.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md +++ b/.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md @@ -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 --- From 43412d28a41dc942647136fe43e2add7c1a718e8 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 11:14:58 -0400 Subject: [PATCH 3/3] chore: confirm dashboard note --- ...M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md b/.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md index 95018c9b..95cba7ae 100644 --- a/.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md +++ b/.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md @@ -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 ---