From d57630e04703f13c290bd4d2e231d9d475893b37 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Mon, 9 Mar 2026 07:02:41 +0000 Subject: [PATCH 01/17] fix: resolve channel names via Slack API for all sessions channelInfoCache in http-routes.ts was never populated for non-DM channels, so most sessions showed no channel name. Now resolves channel info via Slack API (conversations.info) with caching, used by both main dashboard and personal dashboard. Co-Authored-By: Claude Opus 4.6 --- packages/libclaudebox/http-routes.ts | 35 ++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/libclaudebox/http-routes.ts b/packages/libclaudebox/http-routes.ts index ad84e3b..89dd4ff 100644 --- a/packages/libclaudebox/http-routes.ts +++ b/packages/libclaudebox/http-routes.ts @@ -128,6 +128,27 @@ function getSlackChannelInfo(channelId: string): SlackChannelInfo | null { return null; // unknown channel — no info } +/** Resolve channel info via Slack API and cache it. */ +async function resolveSlackChannelInfo(channelId: string): Promise { + if (channelInfoCache.has(channelId)) return channelInfoCache.get(channelId)!; + if (channelId.startsWith("D")) { const info = { name: "", isDm: true }; channelInfoCache.set(channelId, info); return info; } + if (!SLACK_BOT_TOKEN || !channelId) return null; + try { + const resp = await fetch(`https://slack.com/api/conversations.info?channel=${channelId}`, { + headers: { "Authorization": `Bearer ${SLACK_BOT_TOKEN}` }, + }); + const data = await resp.json() as any; + if (data.ok) { + const ch = data.channel || {}; + const isDm = !!ch.is_im || !!ch.is_mpim; + const info: SlackChannelInfo = { name: ch.name || "", isDm }; + channelInfoCache.set(channelId, info); + return info; + } + } catch {} + return null; +} + /** Strip "Slack thread context..." suffix from prompts for display */ function stripSlackContext(prompt: string): string { // Match any variant: "Slack thread context:", "Slack thread context (recent):", etc. @@ -171,10 +192,10 @@ async function buildDashboardData(store: WorktreeStore, profileFilter?: string): if (channelId) channelIds.add(channelId); } const channelInfoMap = new Map(); - for (const id of channelIds) { - const info = getSlackChannelInfo(id); + await Promise.all([...channelIds].map(async (id) => { + const info = await resolveSlackChannelInfo(id); if (info) channelInfoMap.set(id, info); - } + })); // Build flat workspace list const workspaces: WorkspaceCard[] = []; @@ -1087,12 +1108,12 @@ const routes: Route[] = [ for (const s of all) { if (s.slack_channel) channelIds.add(s.slack_channel); } - // Resolve channel names + // Resolve channel names (async via Slack API) const channelNameMap = new Map(); - for (const id of channelIds) { - const info = getSlackChannelInfo(id); + await Promise.all([...channelIds].map(async (id) => { + const info = await resolveSlackChannelInfo(id); if (info) channelNameMap.set(id, info); - } + })); // Group by worktree first (like buildDashboardData), then enrich const worktreeMap = new Map(); From 4dc1515b0ad6ba9db6b4402463bfc276a6a50214 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Mon, 9 Mar 2026 07:05:25 +0000 Subject: [PATCH 02/17] feat: show artifacts and last reply on dashboard cards Extract artifacts (PRs, issues, gists) and latest reply from activity.jsonl in buildDashboardData. Render as compact clickable chips and truncated reply preview on each workspace card. Co-Authored-By: Claude Opus 4.6 --- packages/libclaudebox/html/dashboard.ts | 23 +++++++++++++++++++++ packages/libclaudebox/html/shared.ts | 4 ++++ packages/libclaudebox/http-routes.ts | 27 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/packages/libclaudebox/html/dashboard.ts b/packages/libclaudebox/html/dashboard.ts index 7f6c98c..d534f36 100644 --- a/packages/libclaudebox/html/dashboard.ts +++ b/packages/libclaudebox/html/dashboard.ts @@ -142,6 +142,18 @@ a{color:#7aa2f7;text-decoration:none}a:hover{text-decoration:underline} .badge-tag{color:#bb9af7;border-color:#1a1a2a;background:#0d0a0d;cursor:pointer} .badge-tag:hover{border-color:#bb9af7} +/* Card artifacts */ +.card-artifacts{display:flex;gap:4px;margin-top:6px;flex-wrap:wrap} +.card-artifact{font-size:10px;padding:2px 8px;border-radius:10px;text-decoration:none;transition:all 0.15s} +.card-artifact:hover{opacity:0.85;text-decoration:none} +.card-artifact.a-pr{color:#9ece6a;border:1px solid #1a2a1a;background:#0a0d0a} +.card-artifact.a-issue{color:#f7768e;border:1px solid #2a1a1a;background:#0d0a0a} +.card-artifact.a-gist{color:#bb9af7;border:1px solid #1a1a2a;background:#0d0a0d} +.card-artifact.a-link{color:#7dcfff;border:1px solid #1a2a2a;background:#0a0d0d} + +/* Card reply */ +.card-reply{font-size:11px;color:#555;margin-top:6px;padding:6px 8px;background:#050505;border-left:2px solid #1a1a1a;border-radius:0 2px 2px 0;word-break:break-word;max-height:60px;overflow:hidden;line-height:1.4} + /* Kebab menu */ .kebab{color:#333;cursor:pointer;padding:2px 6px;font-size:16px;line-height:1;border-radius:2px;flex-shrink:0} .kebab:hover{color:#888;background:#111} @@ -347,6 +359,17 @@ function WorkspaceCard({ w, onRefresh, nested }) { \${!nested && w.origin === "github" ? html\`github\` : null} \${w.profile ? html\`\${w.profile}\` : null} + \${w.artifacts && w.artifacts.length > 0 ? html\` +
+ \${w.artifacts.map(a => html\` + e.stopPropagation()}>\${a.text} + \`)} +
+ \` : null} + \${w.lastReply ? html\` +
\${w.lastReply}
+ \` : null} \${menuOpen ? html\`