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
9 changes: 6 additions & 3 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1513,11 +1513,14 @@ function defaultShape(id) {
value = Math.imul(value ^ value >>> 13, 3266489909);
return DEFAULT_SHAPES[((value ^ value >>> 16) >>> 0) % DEFAULT_SHAPES.length];
}
var colorOf = (bot) => isColor(bot?.color) ? bot.color : bot?.id ? defaultColor(String(bot.id)) : "black";
var remoteSeed = (handle) => `remote:${String(handle ?? "").trim().replace(/^@+/, "").toLowerCase()}`;
var lookSeed = (bot) => typeof bot?.remote?.handle === "string" && bot.remote.handle.trim() !== "" ? remoteSeed(bot.remote.handle) : bot?.id !== void 0 ? String(bot.id) : void 0;
var colorOf = (bot) => isColor(bot?.color) ? bot.color : lookSeed(bot) !== void 0 ? defaultColor(lookSeed(bot)) : "black";
function lookOf(bot) {
const avatar = bot?.avatar ?? {};
const seed = lookSeed(bot);
return {
shape: hasShape(avatar.shape) ? avatar.shape : bot?.id ? defaultShape(String(bot.id)) : FALLBACK_SHAPE,
shape: hasShape(avatar.shape) ? avatar.shape : seed !== void 0 ? defaultShape(seed) : FALLBACK_SHAPE,
image: avatar.image,
color: colorOf(bot)
};
Expand Down Expand Up @@ -4406,7 +4409,7 @@ function remoteSpeaker(source, roster) {
handle,
isChief: member?.isChief ?? chat?.remote?.isChief
});
const bot = chat ?? { id: `remote:${handle}`, name, color: member?.color, shape: member?.shape };
const bot = chat ?? { id: remoteSeed(handle), name, color: member?.color, shape: member?.shape };
return { bot, member, name, handle, chatId: chat ? currentPart(chat) : void 0 };
}
function RemoteMessage({ source, text, roster, actions, sessionId, msgKey, useUi }) {
Expand Down
7 changes: 4 additions & 3 deletions core/src/client/cells.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import { inkText } from './inks.js'
import { currentPart, linkMentions, mentionTarget, dayLabel, contentText, stripHeader, stripTrace, isHiddenTurn, isRelayContext, relaysBefore, relayPart, commRunOf, toolName, toolArgs, reminderPrompts } from './text.js'
import { SmileIcon, ReplyIcon, DotsIcon, CheckIcon, DownloadIcon, CopyIcon, MemoryIcon } from './icons.js'
import { colorOf, toolState, BotMark, RoomAvatar } from './mark.js'
import { colorOf, remoteSeed, toolState, BotMark, RoomAvatar } from './mark.js'
import { AnsweredCell, SettledQuestion, PendingQuestion } from './questions.js'
import { SecretToolCell } from './secret-card.js'
import { dateLocale, t } from './i18n.js'
Expand Down Expand Up @@ -199,8 +199,9 @@ export function remoteSpeaker(source, roster) {
handle,
isChief: member?.isChief ?? chat?.remote?.isChief,
})
// The sidebar's Bot record when there is one; else a stable look derived from the handle.
const bot = chat ?? { id: `remote:${handle}`, name, color: member?.color, shape: member?.shape }
// The sidebar's Bot record when there is one; else a synthetic one whose id seeds the
// same handle-derived look the record would get (characters.js remoteSeed).
const bot = chat ?? { id: remoteSeed(handle), name, color: member?.color, shape: member?.shape }
return { bot, member, name, handle, chatId: chat ? currentPart(chat) : undefined }
}

Expand Down
16 changes: 13 additions & 3 deletions core/src/client/characters.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/src/client/mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createElement as h, useEffect, useRef, useState } from 'react'
import { fnv1a, lookOf, shapeId, shapeOf, MASK_ID, nextMarkSerial, characterMarkup } from './characters.js'
import { t } from './i18n.js'

export { colorOf, lookOf } from './characters.js'
export { colorOf, lookOf, remoteSeed } from './characters.js'

// -------------------------------------------------------------------------
// Bot mark: the Bot's character (or its uploaded photo), looked up by lookOf.
Expand Down
26 changes: 24 additions & 2 deletions core/test/inbound-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { build } from 'esbuild'
// The inbound friend conversation's display: trace lines, the thinking row, the remote speaker.
const { outputFiles } = await build({
stdin: {
contents: "export { stripTrace, previewOf, nameMentions } from './src/client/text.js'\nexport { remoteSpeaker } from './src/client/cells.js'",
contents: "export { stripTrace, previewOf, nameMentions } from './src/client/text.js'\nexport { remoteSpeaker } from './src/client/cells.js'\nexport { lookOf } from './src/client/characters.js'",
resolveDir: fileURLToPath(new URL('..', import.meta.url)),
},
bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent',
Expand All @@ -25,7 +25,7 @@ runInNewContext(outputFiles[0].text, {
throw new Error(`Unexpected platform module ${name}`)
},
})
const { stripTrace, previewOf, nameMentions, remoteSpeaker } = module.exports
const { stripTrace, previewOf, nameMentions, remoteSpeaker, lookOf } = module.exports

test('a reply opening with a delegation trace shows without it', () => {
assert.equal(stripTrace('Chief → 助手\n\nHi Chief! Good to hear from you.'), 'Hi Chief! Good to hear from you.')
Expand Down Expand Up @@ -81,3 +81,25 @@ test("a friend Bot's handle in a preview reads as its display name (#71)", () =>
] }
assert.equal(nameMentions('@b.x.home 你在?', nested), '@请找 @a.x.home 的主人 你在?')
})

test("a friend Bot's generated look is seeded by its handle: with or without its chat record, one look (#84)", () => {
const source = { remote: { handle: '@bot-2a758c0f.danyang.home' }, senderName: 'X' }
const record = { id: 'session-y', name: 'X', remote: { handle: '@bot-2a758c0f.danyang.home' } }
const withRecord = remoteSpeaker(source, { bots: [record], remoteBots: [] })
const without = remoteSpeaker(source, { bots: [], remoteBots: [] })
assert.equal(withRecord.bot, record)
const kept = lookOf(withRecord.bot)
const fallback = lookOf(without.bot)
assert.equal(kept.shape, fallback.shape)
assert.equal(kept.color, fallback.color)
})

test('handles equal modulo @ and case share one look; a stored look still wins', () => {
const at = lookOf({ id: 'a', remote: { handle: '@Bot.X.home' } })
const bare = lookOf({ id: 'b', remote: { handle: 'bot.x.home' } })
assert.equal(at.shape, bare.shape)
assert.equal(at.color, bare.color)
const owned = lookOf({ id: 'c', remote: { handle: '@bot.x.home' }, color: 'green', avatar: { shape: 'kitty' } })
assert.equal(owned.shape, 'kitty')
assert.equal(owned.color, 'green')
})
1 change: 1 addition & 0 deletions docs/subsystems/ds-bot-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Patched upstream `ds-bot` that ships as `core/`. Never edit `ds-bot/` by hand; c
7. `remoteTurn` (`src/host/remote-chat.js`) finds what the user typed on a friend's remote chat by skipping host-appended `role:'user'` context: forms `snapshot`, `instructions` (the AGENTS.md baseline on a chat's first turn) and `notice`, and the time-context kinds. Match by form, not by a whitelist of senders, or the first message of a new chat is dropped (#66).
8. A friend's remote Bot is stored and delivered by its handle (`@<bot>.<home>` — what the @ menu inserts and `sendUserMentions` matches). Where the user reads it — mention chips (`src/client/user-message.js`) and sidebar previews (`nameMentions` in `src/client/text.js`) — the client rewrites the handle to the display name taken from the local roster (`roster.bots[].remote`, `roster.remoteBots`), never from message text, so a remote-controlled name reaches the UI only through the sanitized roster records (#71; display names, #9).
9. `0002-flybot-api-host` `cleanRemote` sanitizes those peer-controlled `name` / `homeName` / `owner` labels (strip `@` and controls/newlines, 60-char cap) before they are stored. Details in [plugin-runtime](plugin-runtime.md) (#75).
10. A remote Bot's generated look is seeded by its handle (`remoteSeed` in `src/client/characters.js`), never by its session id: the chat record (`src/host/remote-chat.js`) carries no look, and every consumer funnels through `lookOf`/`colorOf`, so the record and `remoteSpeaker`'s no-record fallback (`src/client/cells.js`) render one stable look per handle — across surfaces, after a delete/re-add, and identically in the friend's home. A stored `color`/`avatar` on the record still wins. Carrying the friend side's real look over the wire needs Bot Social member-profile fields (#84).

## Tests

Expand Down
16 changes: 9 additions & 7 deletions patches/ds-bot/0003-flybot-api-client.patch
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ index 3651a710fe5fb33dfd2f056edbbb220a43bec732..65c268530255fa7465f4485cc11a9182
h('button', { type: 'button', className: 'bt-set-row bt-bs-row', onClick: duplicate },
h('span', { className: 'bt-set-copy' }, h('span', { className: 'bt-bs-label' }, t('Duplicate')), h('span', { className: 'bt-set-hint' }, t('A copy with the same job and model, and fresh memory.'))),
diff --git a/src/client/cells.js b/src/client/cells.js
index a99dadcaf392ae86a22e5ba8fbd73a29e0e9003a..32ef57c4c70cd12c1c795c4a140607c48f5f3875 100644
index a99dadcaf392ae86a22e5ba8fbd73a29e0e9003a..496f47999f8af691f1f8ad64258db09c3470477c 100644
--- a/src/client/cells.js
+++ b/src/client/cells.js
@@ -2,12 +2,16 @@ import { createElement as h, Fragment, useEffect, useLayoutEffect, useMemo, useR
Expand All @@ -95,7 +95,8 @@ index a99dadcaf392ae86a22e5ba8fbd73a29e0e9003a..32ef57c4c70cd12c1c795c4a140607c4
-import { linkMentions, mentionTarget, dayLabel, contentText, stripHeader, isHiddenTurn, isRelayContext, relaysBefore, relayPart, commRunOf, toolName, toolArgs, reminderPrompts } from './text.js'
+import { currentPart, linkMentions, mentionTarget, dayLabel, contentText, stripHeader, stripTrace, isHiddenTurn, isRelayContext, relaysBefore, relayPart, commRunOf, toolName, toolArgs, reminderPrompts } from './text.js'
import { SmileIcon, ReplyIcon, DotsIcon, CheckIcon, DownloadIcon, CopyIcon, MemoryIcon } from './icons.js'
import { colorOf, toolState, BotMark, RoomAvatar } from './mark.js'
-import { colorOf, toolState, BotMark, RoomAvatar } from './mark.js'
+import { colorOf, remoteSeed, toolState, BotMark, RoomAvatar } from './mark.js'
import { AnsweredCell, SettledQuestion, PendingQuestion } from './questions.js'
import { SecretToolCell } from './secret-card.js'
import { dateLocale, t } from './i18n.js'
Expand Down Expand Up @@ -127,7 +128,7 @@ index a99dadcaf392ae86a22e5ba8fbd73a29e0e9003a..32ef57c4c70cd12c1c795c4a140607c4
const running = data.status === 'running'
// A streaming reply stands in for the activity row.
useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts.length > 0 ? { text: true } : null)
@@ -180,6 +186,71 @@ function GroupMessage({ bot, text, room, roster, actions, msgKey, useUi }) {
@@ -180,6 +186,72 @@ function GroupMessage({ bot, text, room, roster, actions, msgKey, useUi }) {
h(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room?.id, msgKey, useUi, lead }))
}

Expand All @@ -144,8 +145,9 @@ index a99dadcaf392ae86a22e5ba8fbd73a29e0e9003a..32ef57c4c70cd12c1c795c4a140607c4
+ handle,
+ isChief: member?.isChief ?? chat?.remote?.isChief,
+ })
+ // The sidebar's Bot record when there is one; else a stable look derived from the handle.
+ const bot = chat ?? { id: `remote:${handle}`, name, color: member?.color, shape: member?.shape }
+ // The sidebar's Bot record when there is one; else a synthetic one whose id seeds the
+ // same handle-derived look the record would get (characters.js remoteSeed).
+ const bot = chat ?? { id: remoteSeed(handle), name, color: member?.color, shape: member?.shape }
+ return { bot, member, name, handle, chatId: chat ? currentPart(chat) : undefined }
+}
+
Expand Down Expand Up @@ -199,7 +201,7 @@ index a99dadcaf392ae86a22e5ba8fbd73a29e0e9003a..32ef57c4c70cd12c1c795c4a140607c4
// A centered event line that names a group chat and opens it.
function GroupEvent({ prefix, room, name, roster, actions }) {
return h('div', { className: 'bt-event' },
@@ -331,6 +402,11 @@ export function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnCl
@@ -331,6 +403,11 @@ export function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnCl
: body

function triggerBody() {
Expand All @@ -211,7 +213,7 @@ index a99dadcaf392ae86a22e5ba8fbd73a29e0e9003a..32ef57c4c70cd12c1c795c4a140607c4
if (source.kind === 'bot') {
// A part opens with its handoff package, so the earlier parts go right above it.
if (source.handoff === true) return roster.roomsById[sessionId] ? null : h(EarlierParts, { sessionId, roster, actions })
@@ -571,7 +647,10 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
@@ -571,7 +648,10 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
replies.map((reply, index) => {
const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? 'gray' }
if (reply.kind === 'user') {
Expand Down
Loading
Loading