Skip to content
Open
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
21 changes: 21 additions & 0 deletions src/core/dashboard-rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getTerminalAdvertisedPort } from './terminal-url.js';
import { getBotBrand } from '../bot-registry.js';
import { type Brand, chatAppLink } from '../im/lark/lark-hosts.js';
import { getSessionTokenUsage, type SessionTokenUsage } from './cost-calculator.js';
import { getIdentity } from '../im/lark/identity-cache.js';

export interface SessionRow {
sessionId: string;
Expand All @@ -25,6 +26,8 @@ export interface SessionRow {
closedAt?: number;
workingDir?: string;
chatId: string;
chatType?: 'group' | 'p2p';
chatDisplayName?: string;
rootMessageId: string;
threadId?: string;
/** Conversation unit ('thread' = topic-anchored, 'chat' = plain chat scope).
Expand Down Expand Up @@ -99,6 +102,20 @@ function sessionTokenUsage(s: Session, workingDir?: string): SessionTokenUsage |
});
}

function directChatDisplayName(s: Session, larkAppId?: string): string | undefined {
if (s.chatType !== 'p2p') return undefined;
const persisted = String(s.chatDisplayName ?? '').trim();
if (persisted) return persisted;
const appId = larkAppId ?? s.larkAppId;
if (!appId) return undefined;
for (const openId of [s.ownerOpenId, s.creatorOpenId, s.lastCallerOpenId]) {
if (!openId) continue;
const name = String(getIdentity(appId, openId)?.name ?? '').trim();
if (name) return name;
}
return undefined;
}

export function composeRowFromActive(ds: DaemonSession): SessionRow {
return {
sessionId: ds.session.sessionId,
Expand All @@ -114,6 +131,8 @@ export function composeRowFromActive(ds: DaemonSession): SessionRow {
lastMessageAt: sessionLastActivityAtMs(ds.session) || ds.lastMessageAt,
workingDir: ds.workingDir,
chatId: ds.chatId,
chatType: ds.chatType,
chatDisplayName: directChatDisplayName(ds.session, ds.larkAppId),
rootMessageId: ds.session.rootMessageId,
scope: ds.session.scope,
title: ds.session.title,
Expand Down Expand Up @@ -154,6 +173,8 @@ export function composeRowFromClosed(s: Session): SessionRow {
closedAt: s.closedAt ? Date.parse(s.closedAt) : undefined,
workingDir: s.workingDir,
chatId: s.chatId,
chatType: s.chatType,
chatDisplayName: directChatDisplayName(s, s.larkAppId),
rootMessageId: s.rootMessageId,
scope: s.scope,
title: s.title,
Expand Down
30 changes: 30 additions & 0 deletions src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,16 @@ function findChatReplyAlias(rootId: string, chatId: string, larkAppId: string):
const hit = diskSessions.find(s => s.status === 'active' && s.larkAppId === larkAppId && s.chatId === chatId && sessionHasReplyThreadAlias(s, rootId));
return hit ? { chatId: hit.chatId, sessionId: hit.sessionId } : null;
}

function setDirectChatDisplayNameFromSender(
session: Session,
chatType: 'group' | 'p2p' | undefined,
sender?: { type?: 'user' | 'bot'; name?: string },
): void {
if (chatType !== 'p2p' || sender?.type !== 'user') return;
const name = String(sender.name ?? '').trim();
if (name) session.chatDisplayName = name;
}
/**
* Per-run state for active workflow loops.
*
Expand Down Expand Up @@ -2404,9 +2414,13 @@ async function startInitialPassthroughSession(args: {

const botCfg = getBot(larkAppId).config;
refreshCliVersion(botCfg.cliId, botCfg.cliPathOverride);
const directChatSender = chatType === 'p2p'
? await resolveSender(larkAppId, senderOpenId, parsed.senderType)
: undefined;
const rootIdForStore = scope === 'thread' ? anchor : messageId;
const session = sessionStore.createSession(chatId, rootIdForStore, commandContent.substring(0, 50), chatType);
const now = Date.now();
setDirectChatDisplayNameFromSender(session, chatType, directChatSender);
session.larkAppId = larkAppId;
session.ownerOpenId = ownerOpenId;
session.creatorOpenId = creatorOpenId;
Expand Down Expand Up @@ -2675,6 +2689,13 @@ async function handleNewTopic(data: any, ctx: RoutingContext): Promise<void> {
const cmdRootIdForStore = scope === 'thread' ? anchor : messageId;
const session = sessionStore.createSession(chatId, cmdRootIdForStore, cmdContent.substring(0, 50), chatType);
const now = Date.now();
if (chatType === 'p2p') {
setDirectChatDisplayNameFromSender(
session,
chatType,
await resolveSender(larkAppId, senderOpenId, parsed.senderType),
);
}
session.larkAppId = larkAppId;
session.ownerOpenId = senderOpenId;
session.ownerUnionId = senderUnionId;
Expand Down Expand Up @@ -2761,6 +2782,7 @@ async function handleNewTopic(data: any, ctx: RoutingContext): Promise<void> {
const rootIdForStore = scope === 'thread' ? anchor : messageId;
const session = sessionStore.createSession(chatId, rootIdForStore, parsed.content.substring(0, 50), chatType);
const now = Date.now();
setDirectChatDisplayNameFromSender(session, chatType, newTopicSender);
session.larkAppId = larkAppId;
session.ownerOpenId = senderOpenId;
session.ownerUnionId = senderUnionId;
Expand Down Expand Up @@ -3365,6 +3387,13 @@ async function handleThreadReply(data: any, ctx: RoutingContext): Promise<void>
if (!existingDs && threadChatId && !SESSIONLESS_DAEMON_COMMANDS.has(cmd)) {
const session = sessionStore.createSession(threadChatId, anchor, cmdContent.substring(0, 50), ctxChatType);
const now = Date.now();
if (ctxChatType === 'p2p') {
setDirectChatDisplayNameFromSender(
session,
ctxChatType,
await getThreadSender(),
);
}
session.larkAppId = larkAppId;
session.ownerOpenId = threadSenderOpenId;
session.creatorOpenId = threadSenderOpenId; // stable creator (= dispatch orchestrator for /repo prime) — see Session.creatorOpenId
Expand Down Expand Up @@ -3594,6 +3623,7 @@ async function handleThreadReply(data: any, ctx: RoutingContext): Promise<void>
// injects it either into the immediate prompt or stashes it on
// pendingSender for the deferred spawn.
const autoCreateSender = await getThreadSender();
setDirectChatDisplayNameFromSender(session, autoCreateChatType, autoCreateSender);
const newDs: DaemonSession = {
session,
worker: null,
Expand Down
16 changes: 14 additions & 2 deletions src/dashboard/web/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,13 @@ const zh: DashboardMessages = {
'overview.noSchedules': '暂无定时任务。',
'sessions.title': '会话控制',
'sessions.subtitle': '定位飞书话题、打开 Web Terminal、关闭或恢复 CLI 会话。',
'sessions.search': '搜索工作目录 / 标题 / ID',
'sessions.search': '搜索群聊 / 工作目录 / 标题 / ID',
'sessions.anyStatus': '全部状态',
'sessions.adoptAny': 'adopt: 全部',
'sessions.adoptYes': 'adopt: 是',
'sessions.adoptNo': 'adopt: 否',
'sessions.chatAny': '所在聊天: 全部',
'sessions.showUnknownChats': '显示未知聊天',
'sessions.activeOnly': '仅活跃',
'sessions.closeSelected': '关闭选中',
'sessions.clearSelection': '取消选择',
Expand All @@ -146,6 +148,10 @@ const zh: DashboardMessages = {
'sessions.bot': 'bot',
'sessions.cli': 'CLI',
'sessions.chat': '群聊',
'sessions.location': '所在聊天',
'sessions.groupChat': '群聊',
'sessions.directChat': '单聊',
'sessions.chatUnknown': '未知聊天',
'sessions.openChat': '打开群聊',
'sessions.status': '状态',
'sessions.status.starting': '启动中',
Expand Down Expand Up @@ -1560,11 +1566,13 @@ const en: DashboardMessages = {
'overview.noSchedules': 'No schedules yet.',
'sessions.title': 'Session Control',
'sessions.subtitle': 'Locate Feishu topics, open Web Terminal, close or resume CLI sessions.',
'sessions.search': 'Search working dir / title / IDs',
'sessions.search': 'Search chat / working dir / title / IDs',
'sessions.anyStatus': 'Any status',
'sessions.adoptAny': 'adopt: any',
'sessions.adoptYes': 'adopt: yes',
'sessions.adoptNo': 'adopt: no',
'sessions.chatAny': 'Chat: all',
'sessions.showUnknownChats': 'Show unknown chats',
'sessions.activeOnly': 'Active only',
'sessions.closeSelected': 'Close selected',
'sessions.clearSelection': 'Clear',
Expand All @@ -1586,6 +1594,10 @@ const en: DashboardMessages = {
'sessions.bot': 'Bot',
'sessions.cli': 'CLI',
'sessions.chat': 'Chat',
'sessions.location': 'Chat',
'sessions.groupChat': 'Group',
'sessions.directChat': 'DM',
'sessions.chatUnknown': 'Unknown chat',
'sessions.openChat': 'Open chat',
'sessions.status': 'Status',
'sessions.status.starting': 'Starting',
Expand Down
18 changes: 18 additions & 0 deletions src/dashboard/web/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type SessionsViewMode = 'kanban' | 'board' | 'table';

export const THEME_STORAGE_KEY = 'botmux.dashboard.theme';
export const SESSIONS_VIEW_STORAGE_KEY = 'botmux.dashboard.sessions.view';
export const SESSIONS_SHOW_UNKNOWN_CHATS_STORAGE_KEY = 'botmux.dashboard.sessions.showUnknownChats';

export function normalizeThemeMode(value: unknown): ThemeMode | null {
return value === 'system' || value === 'light' || value === 'dark' ? value : null;
Expand All @@ -28,6 +29,15 @@ export function readStoredSessionsViewMode(storage: Storage | undefined): Sessio
return normalizeSessionsViewMode(storage?.getItem(SESSIONS_VIEW_STORAGE_KEY)) ?? 'board';
}

export function readStoredSessionsShowUnknownChats(storage: Storage | undefined): boolean {
try {
const raw = storage?.getItem(SESSIONS_SHOW_UNKNOWN_CHATS_STORAGE_KEY);
return raw == null ? true : raw === '1';
} catch {
return true;
}
}

// ── 看板列顺序(用户可拖拽/按钮自定义,从左到右)─────────────────────────────
export const SESSIONS_BOARD_ORDER_STORAGE_KEY = 'botmux.dashboard.sessions.boardOrder';
export const DEFAULT_BOARD_ORDER = ['needs-you', 'starting', 'working', 'idle'] as const;
Expand Down Expand Up @@ -67,6 +77,14 @@ export function writeStoredSessionsViewMode(storage: Storage | undefined, mode:
}
}

export function writeStoredSessionsShowUnknownChats(storage: Storage | undefined, show: boolean): void {
try {
storage?.setItem(SESSIONS_SHOW_UNKNOWN_CHATS_STORAGE_KEY, show ? '1' : '0');
} catch {
// localStorage 不可用时只在当前页生效
}
}

// ── 看板分组维度:工作流列 / 团队(筛选某团队的工作流)/ 机器人列 ─────────────
export type KanbanGroupBy = 'flow' | 'team' | 'bot';

Expand Down
12 changes: 12 additions & 0 deletions src/dashboard/web/sessions-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IDLE_CLEANUP_HOUR_OPTIONS } from '../session-cleanup.js';
import { mountReactPage, type PageDisposer } from './react-mount.js';
import { renderCliFilterGroup, SESSION_STATUS_OPTIONS, sessionStatusText, wireSessionsPage } from './sessions.js';
import { useT } from './react-hooks.js';
import { readStoredSessionsShowUnknownChats } from './preferences.js';

function SortHeader(props: { sort: string; label: string }) {
return <th data-sort={props.sort} data-label={props.label}>{props.label}</th>;
Expand Down Expand Up @@ -60,7 +61,17 @@ function SessionsPage() {
<option value="yes">{tr('sessions.adoptYes')}</option>
<option value="no">{tr('sessions.adoptNo')}</option>
</select>
<select name="chat" aria-label={tr('sessions.location')}>
<option value="">{tr('sessions.chatAny')}</option>
</select>
<span style={{ display: 'contents' }} dangerouslySetInnerHTML={{ __html: renderCliFilterGroup() }} />
<label className="filter-toggle">
<input
type="checkbox"
name="showUnknownChats"
defaultChecked={readStoredSessionsShowUnknownChats(typeof window === 'undefined' ? undefined : window.localStorage)}
/> <span>{tr('sessions.showUnknownChats')}</span>
</label>
<label className="filter-toggle">
<input type="checkbox" name="active" defaultChecked /> <span>{tr('sessions.activeOnly')}</span>
</label>
Expand Down Expand Up @@ -97,6 +108,7 @@ function SessionsPage() {
<SortHeader sort="botName" label={tr('sessions.bot')} />
<SortHeader sort="cliId" label={tr('sessions.cli')} />
<SortHeader sort="status" label={tr('sessions.status')} />
<SortHeader sort="chat" label={tr('sessions.location')} />
<SortHeader sort="tokenIn" label={tr('sessions.tokenIn')} />
<SortHeader sort="tokenOut" label={tr('sessions.tokenOut')} />
<SortHeader sort="title" label={tr('sessions.titleCol')} />
Expand Down
Loading