Skip to content

Commit 7a89715

Browse files
authored
fix(app): suggest channel members after @ (#583)
* fix(app): suggest channel members after @, and keep unknown @words plain on the Dashboard * chore: confirm composer and dashboard notes * chore: confirm dashboard note
1 parent 19f3215 commit 7a89715

7 files changed

Lines changed: 44 additions & 18 deletions

‎.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ anchor: function Composer
44
created: 2026-10-15T02:50:44Z
55
norm: '1'
66
sig: 6e0e6c5c8afbd01a
7-
body_hash: 75fc838bf3451b29
8-
raw_hash: 1948feaaac06338f
9-
vouched: 2026-10-22T15:03:16Z
7+
body_hash: c1bc4d0ff6028d0a
8+
raw_hash: f2b2dae833097ad7
9+
vouched: 2026-10-22T15:13:29Z
1010
lines: 99-282
1111
---
1212

‎.lane/memory/apps/app/src/composer.tsx/01M4ZSEZBJJ3AMEBTR7SH36FW4-models-are-read-only-while-t.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ anchor: function Composer
44
created: 2026-10-15T12:44:15Z
55
norm: '1'
66
sig: 6e0e6c5c8afbd01a
7-
body_hash: 75fc838bf3451b29
8-
raw_hash: 1948feaaac06338f
9-
vouched: 2026-10-22T15:03:16Z
7+
body_hash: c1bc4d0ff6028d0a
8+
raw_hash: f2b2dae833097ad7
9+
vouched: 2026-10-22T15:13:29Z
1010
lines: 101-290
1111
---
1212

‎.lane/memory/apps/app/src/composer.tsx/01M4ZYQVGB6C87WBEV12WPQ8PF-a-withdrawn-message-returns.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ anchor: function Composer
44
created: 2026-10-15T14:16:28Z
55
norm: '1'
66
sig: 6e0e6c5c8afbd01a
7-
body_hash: 75fc838bf3451b29
8-
raw_hash: 1948feaaac06338f
9-
vouched: 2026-10-22T15:03:16Z
7+
body_hash: c1bc4d0ff6028d0a
8+
raw_hash: f2b2dae833097ad7
9+
vouched: 2026-10-22T15:13:25Z
1010
lines: 104-326
1111
---
1212

‎.lane/memory/apps/app/src/dashboard.tsx/01M5AYX7CX6GW2PYNTV3BS5F91-with-no-usable-model-on-this.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ anchor: function Dashboard
44
created: 2026-10-19T20:51:03Z
55
norm: '1'
66
sig: 4bfb45497157c0e0
7-
body_hash: d1696132fea7c765
8-
raw_hash: 907b4bdd8de26f76
9-
vouched: 2026-10-22T15:09:45Z
7+
body_hash: 97a54ddcbd872f30
8+
raw_hash: 758dbb3d2b2fb407
9+
vouched: 2026-10-22T15:14:58Z
1010
lines: 256-606
1111
---
1212

‎apps/app/src/composer.tsx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ReactNode, useEffect, useRef, useState, useSyncExternalStore } from "react";
1+
import { type ReactNode, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
22

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

7071
/** Below the composer: its attachment tray rises over the space above it and takes clicks there. */
@@ -98,6 +99,7 @@ export function Composer(
9899
failure,
99100
onProviders,
100101
accessory,
102+
members,
101103
}: Props,
102104
) {
103105
const composer = useRef<ChatComposerHandle>(null);
@@ -110,6 +112,7 @@ export function Composer(
110112
// The channel refuses a blocked teammate's ask, stop, and kill too.
111113
const blocked = shared === false && channel.owner !== user;
112114
const mode = blocked ? "chat" : stored;
115+
const mentions = useMemo(() => blocked ? members : [ACE_MENTION, ...members], [blocked, members]);
113116
const [models, setModels] = useState<ModelOption[]>();
114117
const [modelError, setModelError] = useState<string>();
115118
const status = useSyncExternalStore(host.subscribe, () => host.status);
@@ -319,7 +322,7 @@ export function Composer(
319322
modes={blocked ? BLOCKED_MODES : MODES}
320323
mode={mode}
321324
onModeChange={changeMode}
322-
mentions={blocked ? NO_MENTIONS : MENTIONS}
325+
mentions={mentions}
323326
models={choices}
324327
model={model}
325328
onModelChange={(value) => changePick({ model: value })}

‎apps/app/src/conversation.tsx‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
22

33
import { Button, Timeline, toast } from "@ace/ui";
4-
import type { Listing } from "@ace/host/protocol";
4+
import { channelProject, type Listing } from "@ace/host/protocol";
55

66
import { Checks } from "./checks";
77
import { Composer } from "./composer";
@@ -11,7 +11,8 @@ import { EditMessage } from "./edit-message";
1111
import { publish, useDetails } from "./details";
1212
import { host } from "./host";
1313
import { MessageQueue } from "./message-queue";
14-
import { OfflineHost } from "./onboarding";
14+
import { OfflineHost, useRoster } from "./onboarding";
15+
import { present } from "./presence";
1516
import { pendingQuestion, Question } from "./question";
1617
import { sources, toEvents } from "./timeline";
1718
import { useTranscript } from "./transcript";
@@ -158,6 +159,23 @@ export function Conversation(
158159
),
159160
[people],
160161
);
162+
const presence = useSyncExternalStore(host.subscribe, () => host.presence);
163+
const roster = useRoster();
164+
// Mentions name people by GitHub login. A name still holding @ is a Tailscale login with no
165+
// GitHub account, or an agent from another channel (`agent@<prefix>`), and can't be mentioned.
166+
const members = useMemo(() => {
167+
const self = people[user] || user;
168+
const names = new Set([
169+
...present(presence[channelProject(channel)], self, channel.id).map((value) => value.name),
170+
...transcript.items.flatMap((item) =>
171+
item.kind === "message" ? [people[item.author] || item.author] : []
172+
),
173+
...(roster || []).map((member) => member.github || member.login),
174+
]);
175+
return [...names]
176+
.filter((name) => name !== self && name !== "ace" && !name.includes("@"))
177+
.map((name) => ({ name, avatar: avatars[name] }));
178+
}, [people, user, presence, channel, transcript.items, roster, avatars]);
161179
const results = useMemo(
162180
() =>
163181
transcript.items.reduce(
@@ -304,6 +322,7 @@ export function Conversation(
304322
failure={failure}
305323
onProviders={onProviders}
306324
accessory={chat?.usage && <Usage value={chat.usage} context={chat.context} />}
325+
members={members}
307326
/>
308327
</div>
309328
</div>

‎apps/app/src/dashboard.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type ReactNode, useEffect, useRef, useState, useSyncExternalStore } from "react";
22

33
import {
4+
aceAvatar,
45
Button,
56
buttonVariants,
67
ChatComposer,
@@ -67,6 +68,8 @@ function adopt(project: AppProject): string {
6768
}
6869

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

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

0 commit comments

Comments
 (0)