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
1 change: 1 addition & 0 deletions apps/web/src/shell/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export function AppShell({
canvasOpen={canvasState.open}
onToggleCanvas={() => setCanvasState(toggleCanvasColumn)}
canvasAllowed={canvasAllowed}
onOpenInCanvas={handleOpenInCanvas}
/>
</div>
</>
Expand Down
48 changes: 48 additions & 0 deletions apps/web/src/shell/channel-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";

import type { Channel } from "@corbits/chat-ui";

import { resolveChannelTitle } from "./channel-context";
import type { BenchActivityQuery } from "./bench-activity";

const channel = (partial: {
id: string;
title: string;
kind?: string;
}): Channel =>
({
id: partial.id,
title: partial.title,
kind: partial.kind ?? "channel",
pinned: false,
participants: [],
}) as Channel;

const ready: BenchActivityQuery = {
kind: "ready",
channels: [channel({ id: "ch_1", title: "Myra" })],
chats: [channel({ id: "ch_2", title: " ", kind: "chat" })],
routines: [],
};

describe("resolveChannelTitle", () => {
test("returns null without a channel id", () => {
expect(resolveChannelTitle(ready, null)).toBeNull();
});

test("returns null while activity is loading", () => {
expect(resolveChannelTitle({ kind: "loading" }, "ch_1")).toBeNull();
});

test("returns the channel title when found", () => {
expect(resolveChannelTitle(ready, "ch_1")).toBe("Myra");
});

test("falls back to Untitled channel for blank titles", () => {
expect(resolveChannelTitle(ready, "ch_2")).toBe("Untitled channel");
});

test("returns null when the channel is not in the list", () => {
expect(resolveChannelTitle(ready, "ch_missing")).toBeNull();
});
});
31 changes: 31 additions & 0 deletions apps/web/src/shell/channel-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Resolve the active channel's display title for the contextual page band.
// Reuses the same channel/chat lists the Channels band already loads so the
// page title does not invent a second fetch.

import type { Channel } from "@corbits/chat-ui";

import type { BenchActivityQuery } from "./bench-activity";

/** Prefer channel title; fall back to untitled label; null when still loading. */
export function resolveChannelTitle(
activity: BenchActivityQuery,
channelId: string | null,
): string | null {
if (channelId === null) return null;
if (activity.kind !== "ready") return null;
const match = findChannel(activity.channels, activity.chats, channelId);
if (match === undefined) return null;
const title = match.title?.trim();
return title && title.length > 0 ? title : "Untitled channel";
}

function findChannel(
channels: readonly Channel[],
chats: readonly Channel[],
channelId: string,
): Channel | undefined {
return (
channels.find((c) => c.id === channelId) ??
chats.find((c) => c.id === channelId)
);
}
2 changes: 1 addition & 1 deletion apps/web/src/shell/contextual-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function ContextualPanel({
{pageSpecific !== null ? (
<section
className="panel-band panel-band-page-specific"
aria-label={`${pageBand.title} details`}
aria-label="Page details"
>
<h3 className="panel-band-heading">{pageBand.title}</h3>
{pageSpecific}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/shell/panel-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export type PanelAction = {
};

export type PageBand = {
readonly title: string;
/** String or live component (e.g. resolved channel name). */
readonly title: ReactNode;
readonly subtitle?: string;
readonly settingsPath?: string;
readonly actions?: readonly PanelAction[];
Expand Down
50 changes: 37 additions & 13 deletions apps/web/src/shell/panel-contributions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { KeyboardEvent } from "react";
import { useBench } from "../bench-context";
import { channelIdFromPath, channelPath, isChannelPath } from "../channel-path";
import { useBenchActivity } from "./bench-activity";
import { resolveChannelTitle } from "./channel-context";
import {
registerPanelContribution,
type PanelRenderContext,
Expand Down Expand Up @@ -228,6 +229,21 @@ function ChannelDetails({ channel }: { readonly channel: Channel }) {
);
}

/** Live page-band title for an open channel — falls back while loading. */
function ChannelPageTitle({
channelId,
fallback,
}: {
readonly channelId: string | null;
readonly fallback: string;
}) {
const { selectedTenantId } = useBench();
const activity = useBenchActivity(selectedTenantId);
const title = resolveChannelTitle(activity, channelId);
if (title !== null) return title;
return fallback;
}

function ChannelsBand({
path,
onOpenInCanvas,
Expand Down Expand Up @@ -491,20 +507,28 @@ export function ensurePanelContributions(): void {
registerPanelContribution({
id: "channels",
match: (path) => isChannelPath(path),
pageBand: (ctx) => ({
title: "Channels",
subtitle: "Open a conversation in the canvas",
actions: [
{
id: "new-channel",
label: "New channel",
onSelect: () => {
window.dispatchEvent(new CustomEvent("workbench:chat:new-channel"));
if (!isChannelPath(ctx.path)) ctx.onNavigate(channelPath(null));
pageBand: (ctx) => {
const channelId = channelIdFromPath(ctx.path);
return {
title: <ChannelPageTitle channelId={channelId} fallback="Channels" />,
subtitle:
channelId === null
? "Open a conversation in the canvas"
: "Channel open in the canvas",
actions: [
{
id: "new-channel",
label: "New channel",
onSelect: () => {
window.dispatchEvent(
new CustomEvent("workbench:chat:new-channel"),
);
if (!isChannelPath(ctx.path)) ctx.onNavigate(channelPath(null));
},
},
},
],
}),
],
};
},
pageSpecific: (ctx) => (
<ChannelsBand path={ctx.path} onOpenInCanvas={ctx.onOpenInCanvas} />
),
Expand Down
Loading