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
31 changes: 18 additions & 13 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ Top to bottom:

1. **Brand row** — logo mark and a "New workbench" button (`+`) that
opens `/new`.
2. **Bench list** — the "Workbenches" label, then rows of workbench
conversations, with search built into the list itself. Nothing
page-scoped ever renders in this body; it lists conversations, not
product sections.
2. **Bench list** — **Agents**, then **Channels**. Agent rows are
`kind: chat` DMs (one per agent). Channel rows are `kind: workbench`
rooms. Search is built into the list itself. Nothing page-scoped
ever renders in this body; it lists conversations, not product
sections.
3. **Footer rail** — Mission Control is pinned above the rail as its own
row. The first-run rail below it is Routines, Files, Skills, Agents, in
that order (CL-6765). Insights and Evals join that rail only when
Expand All @@ -35,13 +36,14 @@ Top to bottom:
direct one-click control to Settings, not a menu item — Settings
never cost two clicks to reach.

A workbench is an agent conversation, and the bench list IS the switcher —
its rows are the primary way to move between workbenches, with no separate
"switcher" control layered on top. The command palette's hidden "Switch
workbench" action is a second door onto the same list, reachable by search
rather than by scanning rows; it does not replace the sidebar as the
switching mechanism. Approvals render inside the conversation, never as a
standing band in the shell.
A workbench is a conversation tenant — a DM with one agent, or a
channel with many people and agents. The bench list IS the switcher —
its rows are the primary way to move between those conversations, with
no separate "switcher" control layered on top. The command palette's
hidden "Switch workbench" action is a second door onto the same list,
reachable by search rather than by scanning rows; it does not replace
the sidebar as the switching mechanism. Approvals render inside the
conversation, never as a standing band in the shell.

## Pages & Routing

Expand All @@ -59,8 +61,11 @@ behind at its old path — old links and bookmarks always land somewhere
real, never a 404.

**`/new` is the shipped create surface** (`NewWorkbenchPickerRoute`). The
primary act is a prompt: say what the workbench should do, or pick a
template shortcut underneath. The sidebar `+` and an empty-bench land on
primary act is a prompt: say what the channel should do, or pick a
named-template shortcut underneath. Blank `+` / prompt mint an empty
channel and invite nobody. Named templates mint that same empty
channel, then invite existing agents (including Myra as a participant,
never as the mint host). The sidebar `+` and an empty-bench land on
`/` both open this route — there is no Describe door.

**`/inbox` is gone as a page** (CL-6151). The path stays as a redirect
Expand Down
94 changes: 47 additions & 47 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ Platform, built on [Interchange](https://github.com/faremeter/interchange).

## The one concept

Everything in Workbench collapses to a single idea: **a workbench is an
agent conversation.** Opening a workbench means opening a conversation —
with one agent, or with a group of people and agents — and that
conversation is also its own tenant, so its membership and grants are its
own. There is no separate "project" or "space" object sitting above the
conversation; the conversation is the unit of work.

This is why the sidebar is one list of workbenches, not sections split by
kind — every workbench a person has, agent conversation or group
conversation alike, shows up the same way. See
Everything in Workbench collapses to a single idea: **a workbench is a
conversation tenant.** People and agents are both principals
(Interchange). Opening an agent opens the one 1:1 DM with that agent
(`kind: chat`) — two clicks never clone a second DM. Opening a channel
opens a multi-principal room (`kind: workbench`). There is no separate
"project" or "space" object sitting above the conversation; the
conversation is the unit of work.

This is why the sidebar is Agents then Channels, not one recency list
titled Workbenches. The same Interchange agent can sit in its DM and in
many channels; product reopens or invites, and does not clone the
definition or mint a sibling instance per room. See
[docs/GLOSSARY.md](docs/GLOSSARY.md) for the full term mapping and
[docs/CHAT.md](docs/CHAT.md) for how a conversation is built underneath.

Expand All @@ -32,19 +34,18 @@ team spaces); within a bench, they open, create, and work in workbenches.
Workbench is intentionally not a multi-pane IDE. The product surface is one
column at a time:

- **A sidebar of workbenches** lists every conversation in the selected
bench, flat, most-recently-active first. There is no separate "channels"
vs. "chats" grouping the sidebar exposes to a person — every row is a
workbench.
- **"+ New Workbench" always creates.** It opens `/new` — the shipped
- **A sidebar of Agents then Channels** lists conversations in the
selected bench. Agent rows are DMs (`kind: chat`); channel rows are
rooms (`kind: workbench`). There is no one recency list titled
Workbenches.
- **"New workbench" always creates.** It opens `/new` — the shipped
prompt-primary picker — never a picker of existing things to join.
Starting a new workbench is the one way in, whether the result is a
one-on-one conversation with an agent or a group conversation
with people and agents together.
- **Agents are templates.** Starting a new agent conversation means picking
an agent definition (a named, reusable capability) as the starting point
— the same definition can be launched into any number of separate
conversations, each with its own history and its own tenant.
Plus mints an empty channel. Nobody is auto-hosted.
- **Agents are principals, not templates.** Opening Sales opens Sales —
the one 1:1 tenant with that agent. The same agent can sit in its DM
and in many channels. Product reopens or invites; it does not clone
the definition or mint a sibling instance per room. Myra is an agent
row, not a special home slot.
- The active workbench occupies the main column; a contextual panel beside
it carries account-wide surfaces (approvals, recent activity) that stay
visible regardless of which workbench is open.
Expand All @@ -57,17 +58,19 @@ lands on the create surface rather than an empty shell:
1. **Login.**
2. **Credential** — connect a model provider (one-click OAuth for
supported providers, or a pasted API key); see `packages/onboarding`.
3. **Create** — `/` hops an empty bench to `/new`, the prompt-primary
picker (`apps/web/src/pages/new-workbench-picker.tsx`). A prompt box is
the primary act: typing a goal and submitting creates a blank
workbench and sends that text as the first message. Prefab template
rows underneath are one-click shortcuts, not a kind-then-Create
second step. There is no Describe door and no
3. **Create** — `/` hops an empty bench (zero workbenches) to `/new`, the
prompt-primary picker (`apps/web/src/pages/new-workbench-picker.tsx`).
A prompt box is the primary act: typing a goal and submitting mints an
empty channel and sends that text as the first message; blank plus
invites nobody. Named-template rows underneath mint that same empty
channel, then invite existing principals (including Myra as a
participant, never as mint `definitionId`) — one-click shortcuts, not
a kind-then-Create second step. There is no Describe door and no
`describe-first-workbench.tsx`.

A bench that already has one or more workbenches skips create and lands
in an existing conversation instead (see
`apps/web/src/pages/home-page.tsx`).
on `workbenches[0]` (see `apps/web/src/pages/home-page.tsx`). Myra is an
agent row, not a home slot.

The shell's first-run destinations stay small on purpose (CL-6765):
Mission Control is pinned above the footer rail; the rail itself is
Expand Down Expand Up @@ -154,23 +157,24 @@ as an explicit absence, never a fabricated zero. See `packages/insights`.

User-facing surfaces (UI, docs, support) use exactly these nouns:

- **Workbench** — a single conversation, one-on-one with an agent or a
group of people and agents. Never "channel" or "bench" in user-facing
copy.
- **Space** — the user-facing name for a group conversation surface (a
workbench with more than one counterpart).
- **Chat** — the user-facing name for a one-on-one conversation (with an
agent or with a person).
- **Workbench** — the product name, and the mint verb ("New workbench").
A workbench is a conversation tenant: a DM or a channel.
- **Agent** — a coworker identity in the sidebar's Agents list. Opening
the row reopens that agent's one DM. Never "template."
- **DM** — the one 1:1 conversation with an agent. Never cloned by a
second open.
- **Channel** — a multi-party room. Plus mints an empty one; nobody is
auto-hosted. Named templates invite existing agents into that room.
- **Bench** — the shared team scope a person signs into and switches
between; shown in the bench switcher, never called a "workspace" or
"org" in copy.

Internally these map onto platform primitives (tenant, DM) — see
[docs/GLOSSARY.md](docs/GLOSSARY.md) for the authoritative table. Code and
API paths generally keep the platform's own names; "workbench" is the one
exception (CL-6260), since its package (`@corbits/chat`) is ours, not the
platform's — only user-facing surfaces use the rest of the product
vocabulary above.
Internally these map onto platform primitives (principal, tenant, kind:
chat vs kind: workbench) — see [docs/GLOSSARY.md](docs/GLOSSARY.md) for
the authoritative table. Code and API paths generally keep the
platform's own names; "workbench" is the one exception (CL-6260), since
its package (`@corbits/chat`) is ours, not the platform's — only
user-facing surfaces use the rest of the product vocabulary above.

## Open questions

Expand All @@ -179,10 +183,6 @@ vocabulary above.
succeeded) stays **target** until CL-6737 and CL-6738 land — see
IMPLEMENTATION.md open questions; do not document those guarantees as
shipped.
- Whether "Space" and "Chat" as user-facing labels are fully rolled out
across the UI or still landing incrementally is not settled in the
docs reviewed for this pass — treat the sidebar and conversation-header
copy as the source of truth over this document if they disagree.
- The precise boundary of what Insights surfaces to a non-admin bench
member (all tenant activity vs. only their own) is not spelled out in
`packages/insights`'s own docs as of this writing.
7 changes: 7 additions & 0 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -3105,6 +3105,13 @@ tr.insights-row-clickable:hover {
color: var(--muted-foreground);
}

.shell-panel-list-empty {
margin: 0 0 0.35rem;
padding: 0.15rem 0.4rem 0.35rem;
font-size: 0.78rem;
color: var(--muted-foreground);
}

/* CL-6224: per-workbench Timeline — one wall-clock spine (chat messages,
thread forks, routine runs, tasks, approvals), day dividers, and a right
rail of per-day KPI counts. Matches the Insights stage's own visual
Expand Down
66 changes: 63 additions & 3 deletions apps/web/src/command-palette-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ function context(overrides: {
const ctx = {
path: overrides.path,
navigate: (to: string) => navigated.push(to),
tenantId: overrides.tenantId ?? "tenant-1",
tenantId:
overrides.tenantId !== undefined ? overrides.tenantId : "tenant-1",
cycleTheme: () => {
themeCycled = true;
},
Expand Down Expand Up @@ -73,8 +74,8 @@ describe("ACTION_COMMANDS", () => {
test("new-workbench and new-agent speak consumer language, not mint", () => {
const workbench = ACTION_COMMANDS.find((c) => c.id === "new-workbench");
const agent = ACTION_COMMANDS.find((c) => c.id === "new-agent");
expect(workbench?.subtitle).toBe("Start a new workbench with Myra");
expect(agent?.subtitle).toBe("Start a new workbench with Myra");
expect(workbench?.subtitle).toBe("Start a new workbench");
expect(agent?.subtitle).toBe("Start a new workbench");
expect(workbench?.subtitle.toLowerCase()).not.toContain("mint");
expect(agent?.subtitle.toLowerCase()).not.toContain("mint");
});
Expand Down Expand Up @@ -156,6 +157,65 @@ describe("runActionCommand", () => {
await runActionCommand("talk-to-myra", ctx);
expect(navigated).toEqual([]);
});

test("talk-to-myra opens Myra's DM via kind=chat + definitionId, not a title-match mint", async () => {
const calls: { readonly path: string; readonly init?: RequestInit }[] = [];
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
const path =
typeof input === "string" ? input : new URL(String(input)).pathname;
calls.push(init === undefined ? { path } : { path, init });
if (path.includes("/workflows/definitions")) {
return Promise.resolve(
new Response(
JSON.stringify({
data: [
{
id: "wfd_assistant",
tenantId: "tenant-1",
name: "assistant",
currentVersion: "1",
status: "deployed",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
],
nextCursor: null,
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
}
if (path.endsWith("/chat/workbenches")) {
return Promise.resolve(
new Response(
JSON.stringify({
id: "chan_myra_dm",
title: "Myra",
kind: "chat",
pinned: false,
participants: [],
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
}
throw new Error(`unexpected fetch: ${init?.method ?? "GET"} ${path}`);
}) as typeof fetch;

const { ctx, navigated } = context({ path: "/" });
await runActionCommand("talk-to-myra", ctx);

const createCall = calls.find((call) =>
call.path.endsWith("/chat/workbenches"),
);
expect(createCall?.init?.method).toBe("POST");
expect(JSON.parse(String(createCall?.init?.body))).toEqual({
kind: "chat",
definitionId: "wfd_assistant",
reuseExisting: true,
});
expect(navigated).toEqual(["/w/chan_myra_dm"]);
});
});

// Backs the `>` command palette's "New routine" and "Make this a routine"
Expand Down
16 changes: 9 additions & 7 deletions apps/web/src/command-palette-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
// never hops to `/routines` first the way this used to.

import { createPendingDialogRequest } from "@corbits/shell-layout";
import { WORKBENCH_PATH_PREFIX, workbenchPath } from "./workbench-path";
import { WORKBENCH_PATH_PREFIX } from "./workbench-path";
import { NEW_WORKBENCH_PATH } from "./routes";
import { ensureMyraWorkbench } from "./myra-workbench";
import { listAgentDefinitions } from "./agents-api";
import { openAgentDmChat } from "./agent-dm-launch";
import { findMyraDefinition } from "./myra-workbench";
import { requestLibraryUpload } from "./library-upload";
import type { RoutinePanelSubject } from "./shell/canvas-availability";

Expand Down Expand Up @@ -96,12 +98,12 @@ export const ACTION_COMMANDS: readonly ActionCommand[] = [
{
id: "new-workbench",
title: "New workbench",
subtitle: "Start a new workbench with Myra",
subtitle: "Start a new workbench",
},
{
id: "new-agent",
title: "New workbench",
subtitle: "Start a new workbench with Myra",
subtitle: "Start a new workbench",
},
{
id: "new-routine",
Expand Down Expand Up @@ -194,9 +196,9 @@ export async function runActionCommand(
}
case "talk-to-myra": {
if (ctx.tenantId === null) return;
const result = await ensureMyraWorkbench(ctx.tenantId);
if (result.kind === "ready")
ctx.navigate(workbenchPath(result.workbenchId));
const myra = findMyraDefinition(await listAgentDefinitions(ctx.tenantId));
if (myra === undefined) return;
await openAgentDmChat(ctx.tenantId, myra.id, ctx.navigate);
return;
}
case "go-workbenches": {
Expand Down
9 changes: 5 additions & 4 deletions apps/web/src/command-palette-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -451,9 +451,10 @@ export function CommandPaletteProvider({
[artifactsQuery],
);

// Order matches the mock's buildCmdkEntries: Commands, Workbenches,
// Pages, then the unscoped catalogs (Runs, Routines, Skills, Library),
// with People & agents last among the palette's groups.
// Order matches the mock's buildCmdkEntries: Commands, Agents &
// Channels, Pages, then the unscoped catalogs (Runs, Routines,
// Skills, Library), with People & agents last among the palette's
// groups.
const sources = useMemo<readonly PaletteSource[]>(
() => [
{
Expand All @@ -464,7 +465,7 @@ export function CommandPaletteProvider({
},
{
id: "workbenches",
heading: "Workbenches",
heading: "Agents & Channels",
kind: "workbenches",
items: workbenchItems,
},
Expand Down
Loading
Loading