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
12 changes: 12 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ settings") are composition on top of this tenant and its settings rows,
not a separate object; the surface lives in `packages/chat-ui`'s
`workbench-settings`.

**In-room onboarding scene.** A named template's first-minute walkthrough
is one timeline card posted in the room's own voice, not a member's
message and not a side effect of hosting an agent. The card keeps a
stable header — the job, the promise, the ordered steps — and flips its
body in place through connect, pick-repos, and reviewing. Change-repos
returns the body to the picker without rewriting what the room already
recorded. An empty step list is omitted, not rendered as an empty rail.
The current step is named in words; colour is additive, never the only
signal. Consecutive agent-joined events collapse into one line so the
scene and the reviewers' own introductions are what a person reads
first.

**Streaming a reply.** An agent's live reply reaches the timeline through
one path, deltas to pixels:

Expand Down
24 changes: 19 additions & 5 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,16 @@ real, never a 404.
not home. The 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 `+` opens this
route. First-run after credential does not: `/` hops to Myra's one
DM (`openAgentDm` / find-or-reopen). There is no parallel Myra home
route and no Describe door.
empty channel with no host, then instantiate the picked Workbench
Definition — the agents, block workflows, and pending plugins it names
(Myra joins only when the definition names her; Code review's three
reviewers do not) — and run its ordered onboarding walkthrough as an
in-room card the room itself posts, never a side effect of hosting an
agent. The card reads live connection state and flips straight to the
repo pick, so there is one walkthrough, not a separate already-connected
dialog. The sidebar `+` opens this route. First-run after credential
does not: `/` hops to Myra's one DM (`openAgentDm` / find-or-reopen).
There is no parallel Myra home route and no Describe door.

**`/inbox` is gone as a page** (CL-6151). The path stays as a redirect
home so old links still resolve; it is not a live groups inbox.
Expand Down Expand Up @@ -283,6 +288,15 @@ personal-access-token paste, then the same card flips to pick
repositories. A GitHub App / hosted OAuth Connect as the welcome mat is
CL-6343 (out of scope), not the shipped card.

The room's own onboarding card renders as a scene, not a member's
message: no author row, the job as its title with the promise beneath,
and the walkthrough's steps listed with the current one marked in
words. Once repos are recorded the card shows the Reviewing state —
what it's reviewing now — with a change-repos link back to the picker,
never still offering Connect. Consecutive agent-joined rows collapse
into one line naming everyone, so a template room opens on the scene
and the reviewers' own introductions, never a join dump.

## State Pills

Status indicators (ok / warn / error / running) use semantic colors that
Expand Down
17 changes: 17 additions & 0 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ the current product path — do not document OAuth as the first Connect
step, and do not treat a PAT paste as a defect against an OAuth-first
welcome mat that has not shipped.

The room posts that card from `system@<workbenchId>`
(`POST /workbenches/:id/onboarding` in `packages/chat/src/routes.ts`).
`packages/chat-ui` renders a system-sender `connect-github` block as a
scene: no author row, no avatar, no "Member" label; the job title and
optional promise stay put while the body flips. Step labels come from
the block's `steps` array — an empty or omitted array does not draw a
step list. The current-step marker ("You're here") is applied only when
there are exactly three steps (connect / pick / review); `data-state` on
the step is colour, never the only signal. After the server has recorded
repos, the body is the reviewing state (repo names plus a `change repos`
control). Clicking `change repos` is client-local in
`connect-github-block-container` and does not mutate the recorded
selection. `onReviewingStarted` posts canned introductions from
`packages/code-review/src/introductions.ts` under each reviewer's own
address in roster order. Consecutive `workbench.agent-joined` rows
collapse to one line in `packages/chat-ui/src/timeline.tsx`.

Optional `GITHUB_APP_CLIENT_ID` / `GITHUB_APP_CLIENT_SECRET` exist for
that future hosted path; leaving them unset is normal. See
`docs/connect-cards.md` and PRODUCT.md's Code review first minute.
Expand Down
22 changes: 17 additions & 5 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,23 @@ put there.
Code review is the product scene for the definition-driven path:
minting the Code review workbench opens an empty room with no host —
its Workbench Definition names three reviewers and no Myra. The room
itself posts the onboarding card: Connect GitHub with a personal access
token (the shipped path today). The same in-room card reads live
connection state and flips in place to pick repositories — already
connected GitHub is that card, not a `/new` dialog. A GitHub App /
hosted OAuth welcome mat is future work (CL-6343), not current product.
itself posts the onboarding card as a scene, not a member's message:
no author row, the job as its title, the promise beneath, and the
walkthrough's steps listed with the current one marked in words. A
walkthrough with no steps hides the step list rather than drawing an
empty one. Connect GitHub with a personal access token (the shipped
path today). The same in-room card reads live connection state and
flips in place to pick repositories — already connected GitHub is
that card, not a `/new` dialog — then Start reviewing. Once repos are
recorded the same card shows what it is reviewing, with a change-repos
link back to the picker — a reviewing card never still says Connect.
Once reviewing starts, each reviewer posts its own canned introduction
under its own address, in roster order — the first thing a person
reads is who is reviewing and what for, never a join dump. Consecutive
agent-joined rows collapse into one line naming everyone. A GitHub App
/ hosted OAuth welcome mat is future work (CL-6343), not current
product. Inviting teammates into the room is a later slice, not part
of this first minute.

Settle for a template-key-only wait posts from a system sender and
does not wake an agent. Generic `connections/pending` still wakes the
Expand Down
30 changes: 30 additions & 0 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ import {
isWorkbenchHostDefinitionName,
listConnectedProviders,
listDefaultInferencePreferences,
localPartOf,
parseParticipants,
postRoomMessage,
startWorkflowCommand,
sendWorkbenchMessage,
settleConnectedService,
Expand Down Expand Up @@ -2266,6 +2269,33 @@ export async function createHub(config: HubConfig) {
data: { updatedBy: principalId, settings: row.settings },
});
},
onReviewingStarted: async (
tenantId,
workbenchId,
_principalId,
introductions,
) => {
const row = await chatStore.getWorkbenchSettings(tenantId, workbenchId);
const participants = parseParticipants(
row?.settings["chat/participants"],
);
for (const introduction of introductions) {
const participant = participants.find(
(candidate) => candidate.handle === introduction.handle,
);
if (participant === undefined) continue;
await postRoomMessage(
{ roomMessages, publish: workbenchSubscribers.publish },
{
tenantId,
workbenchId,
sender: { name: null, address: participant.address },
runId: localPartOf(participant.address),
parts: [{ kind: "text", text: introduction.text }],
},
);
}
},
}),
);
// Template block workflows (CL-6405): the instantiate path's
Expand Down
1 change: 1 addition & 0 deletions bun.lock

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

10 changes: 7 additions & 3 deletions docs/connect-cards.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ the walkthrough. Connect opens a guided personal-access-token paste
(create a token with the `repo` scope, paste it, store encrypted).
After Connect succeeds — or when GitHub is already connected — the
same in-room card flips in place to pick repositories; there is no
`/new` already-connected dialog. Settling a credential a template room
is waiting on posts the connected notice from the system address and
never wakes an agent (`packages/chat/src/connect-pending.ts`).
`/new` already-connected dialog. Code review needs a repo pick before
reviewers are watching, then Start reviewing. Settling a credential a
template room is waiting on posts the connected notice from the system
address and never wakes an agent (`packages/chat/src/connect-pending.ts`)
— a reviewer roster has no host to answer. Once reviewing starts, each
reviewer posts its own canned introduction under its own address, in
roster order (`packages/code-review/src/introductions.ts`).

A GitHub App / hosted OAuth Connect as the welcome mat is CL-6343, out of
scope for the shipped card — do not document OAuth-first GitHub connect
Expand Down
93 changes: 90 additions & 3 deletions packages/chat-ui/src/blocks/connect-github-block-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,25 @@
// snapshot so a real disconnect still shows Connect.
import { useCallback, useEffect, useRef, useState } from "react";
import type { ConnectGithubBlockData } from "@corbits/chat/blocks";
import { reportError } from "@corbits/error-sink";

import { CHAT_STRINGS } from "../strings";
import type {
ConnectGithubActions,
ConnectGithubQuery,
} from "./connect-github-actions";
import type { OnboardingScene } from "./connect-github-block";
import { ConnectGithubBlockView } from "./connect-github-block";

/** Positions in the room's three-step walkthrough. Which one is current
* is read off the live connect state, never off the card's own data:
* disconnected means connect, connected with nothing recorded (or a
* person who pressed "change repos") means pick, and repos the server
* actually recorded means reviewing. */
const STEP_CONNECT = 0;
const STEP_PICK = 1;
const STEP_REVIEWING = 2;

type ConnectedGithubQuery = Extract<ConnectGithubQuery, { kind: "connected" }>;

/** Survives container remounts so a post-connect loading flash never
Expand Down Expand Up @@ -60,6 +72,7 @@ function displayQueryOf(
}

export function ConnectGithubBlockContainer({
data,
messageId,
actions,
}: {
Expand All @@ -73,7 +86,16 @@ export function ConnectGithubBlockContainer({
const [selectedRepoIds, setSelectedRepoIds] = useState<readonly string[]>(
() => lastConnectedOf(messageId)?.selectedRepoIds ?? [],
);
/** A person who pressed "change repos" on the done state gets the
* picker back without the server's recorded selection changing — only
* pressing "Start reviewing" again writes anything. */
const [repickRequested, setRepickRequested] = useState(false);
const [startReviewingError, setStartReviewingError] = useState<
string | undefined
>(undefined);
const mountedRef = useRef(true);
const hadPickerRef = useRef(false);
const hadConnectRef = useRef(false);

const applyQuery = useCallback(
(result: ConnectGithubQuery) => {
Expand Down Expand Up @@ -123,17 +145,62 @@ export function ConnectGithubBlockContainer({
);

const displayQuery = displayQueryOf(messageId, query);
const recordedRepoIds =
displayQuery.kind === "connected" ? displayQuery.selectedRepoIds : [];
const currentStepIndex =
displayQuery.kind !== "connected"
? STEP_CONNECT
: recordedRepoIds.length === 0 || repickRequested
? STEP_PICK
: STEP_REVIEWING;
if (currentStepIndex === STEP_CONNECT) hadConnectRef.current = true;
if (currentStepIndex === STEP_PICK) hadPickerRef.current = true;
const scene: OnboardingScene = {
title: data.requiredForTemplate,
currentStepIndex,
...(data.promise !== undefined ? { promise: data.promise } : {}),
...(data.steps !== undefined ? { steps: data.steps } : {}),
};

if (displayQuery.kind === "error") {
return (
<ConnectGithubBlockView
scene={scene}
kind="error"
message={displayQuery.message}
onConnect={() => actions?.requestConnect()}
onSubmitAccessToken={submitAccessTokenAndRefresh}
/>
);
}

if (actions === undefined || displayQuery.kind !== "connected") {
return (
<ConnectGithubBlockView
scene={scene}
kind="disconnected"
onConnect={() => actions?.requestConnect()}
onSubmitAccessToken={submitAccessTokenAndRefresh}
/>
);
}

if (currentStepIndex === STEP_REVIEWING) {
return (
<ConnectGithubBlockView
scene={scene}
kind="reviewing"
repoNames={displayQuery.repos
.filter((repo) => recordedRepoIds.includes(repo.id))
.map((repo) => repo.name)}
onChangeRepos={() => setRepickRequested(true)}
{...(hadPickerRef.current ? { autoFocus: true } : {})}
/>
);
}

const connectedActions = actions;

function toggleRepo(repoId: string) {
setSelectedRepoIds((current) =>
current.includes(repoId)
Expand All @@ -142,8 +209,24 @@ export function ConnectGithubBlockContainer({
);
}

async function startReviewing(repoIds: readonly string[]) {
try {
setStartReviewingError(undefined);
await connectedActions.startReviewing(repoIds);
if (mountedRef.current) setRepickRequested(false);
} catch (cause) {
reportError(cause, { operation: "connect-github.startReviewing" });
if (mountedRef.current) {
setStartReviewingError(
CHAT_STRINGS.blockConnectGithubStartReviewingError,
);
}
}
}

return (
<ConnectGithubBlockView
scene={scene}
kind="connected"
orgName={displayQuery.orgName}
repos={displayQuery.repos}
Expand All @@ -152,13 +235,17 @@ export function ConnectGithubBlockContainer({
onSelectAll={() =>
setSelectedRepoIds(displayQuery.repos.map((repo) => repo.id))
}
onChangeConnection={actions.requestConnect}
onChangeConnection={connectedActions.requestConnect}
onStartReviewing={(repoIds) => {
void actions.startReviewing(repoIds);
void startReviewing(repoIds);
}}
onSkip={() => {
void actions.skip();
void connectedActions.skip();
}}
{...(startReviewingError !== undefined
? { error: startReviewingError }
: {})}
{...(hadConnectRef.current ? { autoFocus: true } : {})}
/>
);
}
Loading
Loading