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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
site/public/media/*.mp4 filter=lfs diff=lfs merge=lfs -text
site/public/media/*.webm filter=lfs diff=lfs merge=lfs -text
site/public/media/*.png filter=lfs diff=lfs merge=lfs -text
7 changes: 5 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ jobs:
- uses: actions/setup-node@v6
with:
node-version: '22'
# web/ pins its own pnpm via packageManager; the repo-root package.json
# pins a different pnpm for Vercel, so point the action at web/ explicitly.
- uses: pnpm/action-setup@v6
with:
version: 10.34.5
package_json_file: web/package.json
run_install: false
- run: pnpm install --frozen-lockfile
working-directory: web
Expand All @@ -88,9 +90,10 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
# fmt runs `pnpm dlx` at the repo root; take the version from the root
# package.json's packageManager pin instead of double-specifying it.
- uses: pnpm/action-setup@v6
with:
version: 10.34.5
run_install: false
- uses: actions/setup-go@v7
with:
Expand Down
1 change: 1 addition & 0 deletions showcase/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Dependencies
node_modules/
.env.local

# All capture/render scratch (raw recordings, screenshots, transcodes).
# Published media lives under site/public/media/ (copied there by hand) and
Expand Down
4 changes: 3 additions & 1 deletion showcase/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ and `go test ./pkg/gen/<pkg>/` from the repo root.
3. `pnpm slacksim:build && pnpm exec vite preview --config vite.slacksim.config.ts --port 5178 --strictPort &`
4. Render:
`SIM_PORT=5178 node engine/capture/clip.mjs <name> out/clips/<name> dark 1600x900`
(and/or `still.mjs <name> <beat> …` for a caption-free still).
(and/or `still.mjs <name> <beat> …` for a caption-free still). For spoken
narration, put `ELEVENLABS_API_KEY=...` in ignored `.env.local` and run
`node --env-file=.env.local engine/capture/clip.mjs ...`.
5. Copy to `site/public/media/`, add a `site/content/_manifest.json` entry,
reference it from a guide (see [`site/AGENTS.md`](../site/AGENTS.md)).

Expand Down
15 changes: 12 additions & 3 deletions showcase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ showcase/
│ ├── capture/ Playwright drivers: shot.mjs (still), still.mjs (beat still),
│ │ clip.mjs (narrated webm+mp4+poster), webshot.mjs (real product login)
│ ├── mocks/ authored HTML mocks for surfaces we can't capture (artifact viewer)
│ └── tts/ pluggable TTS seam (stubbed; clips are caption-driven for now)
│ └── tts/ ElevenLabs/OpenAI narration, per-scene cache, and audio/video muxing
└── out/ gitignored render scratch (raw videos, throwaway shots)
```

Expand Down Expand Up @@ -72,17 +72,26 @@ The engine reads `SIM_PORT` (default 5178; set `SIM_PORT=5180` for consolesim).
# a still of a scenario played to beat N (caption-free)
SIM_PORT=5178 node engine/capture/still.mjs <scenario> <beatIndex> out/x.png dark 1600x900

# a narrated clip: plays every beat, records webm+mp4+poster
# a captioned clip: plays every beat, records webm+mp4+poster
SIM_PORT=5180 node engine/capture/clip.mjs <scenario> out/clips/<name> dark 1120x780

# with ElevenLabs narration (put ELEVENLABS_API_KEY=... in ignored .env.local)
SIM_PORT=5178 node --env-file=.env.local engine/capture/clip.mjs <scenario> out/clips/<name> dark 1280x720

# narrate the existing Agent Builder web UI recording without a new live session
node --env-file=.env.local engine/capture/narrate-builder.mjs out/clips/builder-first-agent

# a one-off still of any URL (5th arg is JS run against window.__showcase)
node engine/capture/shot.mjs "http://localhost:5178/?scenario=…" out/x.png 1600x900
```

Then copy the output into `site/public/media/`, add an entry to
`site/content/_manifest.json`, and reference it from a page with
`<Clip name="…" />` or `<Screenshot name="…" />` — see
[`site/AGENTS.md`](../site/AGENTS.md).
[`site/AGENTS.md`](../site/AGENTS.md). MP4, WebM, and PNG files in
`site/public/media/` use Git LFS. Run `git lfs pull` after checkout before
building or publishing the site so those media files contain playable assets
rather than LFS pointers.

## Generated content (run from the repo root)

Expand Down
93 changes: 93 additions & 0 deletions showcase/demos/slacksim/src/scenarios/hubspot-companies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { newScenario } from "../store/scenario";
import type { Story } from "../runtime/story";
import type { Block } from "../blockkit/types";
import contactShare from "../fixtures/blockkit/contact-share.json";
import contactShareResolved from "../fixtures/blockkit/contact-share-resolved.json";

// Scripted HubSpot example. Company names, people and scores are fabricated;
// the owner-approval card is captured from OAP's real Slack renderer.
const SALES = "C1";
const DM = "D1";
const REQUEST = "1756700000.000000";
const CARD = "1756700300.000000";
const OWNER = "U_OWNER";
const SAM = "U6";
const BOT = "U7";

export function hubspotCompanies(): Story {
const s = newScenario()
.workspace("Acme Robotics", { glyph: "A", accent: "#4a154b" })
.at("2026-08-31T09:00:00Z")
.user(OWNER, "jordan")
.user(SAM, "sam")
.bot(BOT, "hubspot-companies", { badge: "AGENT" })
.me(OWNER)
.channel(SALES, "sales", { starred: true })
.dm(DM, [BOT]);
return {
scenario: s.open(SALES).build(),
intro:
"HubSpot CRM: a scheduled company digest, with owner approval for private contacts.",
beats: [
{
id: "digest",
caption:
"Each week, the agent finds newly created companies with strong fit scores and tags their owners.",
hold: 1000,
run: (c) =>
c.postMessage(SALES, BOT, {
text: "*New companies this week*\n• *Circldot* — fit score 91, managed by <@U_OWNER>\n• *Northstar Labs* — fit score 85, managed by <@U_OWNER>\n2 companies met the reporting threshold.",
}),
},
{
id: "request",
caption:
"Sam asks for Circldot's contacts. Those records belong to Jordan, so the agent pauses.",
hold: 900,
run: (c) => {
c.postMessage(SALES, SAM, {
text: "<@U7> can you pull Circldot's contacts for the QBR?",
ts: REQUEST,
});
c.openThread(REQUEST);
c.postReply(SALES, REQUEST, BOT, {
text: "Circldot's contacts need approval from <@U_OWNER>, the account owner. I've asked Jordan now.",
});
c.setStatus(REQUEST, "waiting for Circldot's owner");
},
},
{
id: "owner-approval",
caption:
"The approval goes to Jordan in a DM, with the exact company and data being shared.",
hold: 1100,
run: (c) => {
c.postMessage(DM, BOT, { blocks: contactShare as Block[], ts: CARD });
c.switchChannel(DM);
},
},
{
id: "approve",
caption:
"Jordan approves. The card resolves in place before any contacts appear in the sales thread.",
hold: 900,
run: (c) =>
c.editMessage(DM, CARD, { blocks: contactShareResolved as Block[] }),
},
{
id: "contacts",
caption:
"Now the agent shares Circldot's contacts with Sam, and records whose approval allowed it.",
hold: 1100,
run: (c) => {
c.switchChannel(SALES);
c.openThread(REQUEST);
c.setStatus(REQUEST, undefined);
c.postReply(SALES, REQUEST, BOT, {
text: "Circldot contacts for <@U6>, approved by <@U_OWNER>:\n• Dana Okafor — VP Engineering\n• Priya Raman — Head of Data",
});
},
},
],
};
}
6 changes: 6 additions & 0 deletions showcase/demos/slacksim/src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { infoLeakageDemo } from "./info-leakage";
import { queuedReply } from "./queued-reply";
import { artifactRevise } from "./artifact-revise";
import { threadAdoption } from "./thread-adoption";
import { hubspotCompanies } from "./hubspot-companies";
import { reviewbotDemo } from "./reviewbot-demo";
import { pmAgentDemo } from "./pm-agent-demo";

// The registry of built-in slacksim stories. The capture engine and the dev
// server resolve a story by name (?scenario=<name>). A story is a scenario plus
Expand All @@ -32,6 +35,9 @@ export const STORIES: Record<string, () => Story> = {
"queued-reply": queuedReply,
"artifact-revise": artifactRevise,
"thread-adoption": threadAdoption,
"hubspot-companies": hubspotCompanies,
"reviewbot-demo": reviewbotDemo,
"pm-agent-demo": pmAgentDemo,
};

export const DEFAULT_SCENARIO = "reviewbot-pr";
Expand Down
53 changes: 53 additions & 0 deletions showcase/demos/slacksim/src/scenarios/pm-agent-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { newScenario } from "../store/scenario";
import type { Story } from "../runtime/story";

// Scripted product-manager example. The agent reads fabricated GitHub and
// Linear records, then reports alignment without writing to either service.
const PRODUCT = "C1";
const BOT = "U2";
const ROOT = "1756900000.000000";

export function pmAgentDemo(): Story {
const s = newScenario()
.workspace("Acme Robotics", { glyph: "A", accent: "#4a154b" })
.at("2026-09-02T15:00:00Z")
.user("U1", "jordan")
.bot(BOT, "pm-bot", { badge: "AGENT" })
.me("U1")
.channel(PRODUCT, "product", { starred: true });
s.message(PRODUCT, "U1", {
text: "<@U2> how do our open PRs and Linear issues align with our product goals this week?",
ts: ROOT,
});

return {
scenario: s.open(PRODUCT).openThread(ROOT).build(),
intro: "pm-bot: a read-only view of work in flight against product goals.",
beats: [
{
id: "read",
caption:
"pm-bot reads only the GitHub repos it was granted and the linked Linear issues.",
hold: 1000,
run: (c) => {
c.setStatus(ROOT, "reading GitHub and Linear");
c.postReply(PRODUCT, ROOT, BOT, {
text: "Reading `acme/web` and `acme/api`, then matching open PRs to Linear issues. I won't change either system.",
});
},
},
{
id: "summary",
caption:
"It groups work by goal, showing what is on track and what needs attention.",
hold: 1300,
run: (c) => {
c.setStatus(ROOT, undefined);
c.postReply(PRODUCT, ROOT, BOT, {
text: "*Weekly product alignment*\n✅ *Faster checkout:* `acme/web` PR #18 and LIN-241 address the payment retry flow.\n⚠️ *Self-serve onboarding:* LIN-255 is blocked; no open PR covers the first-run guide.\n➡️ Suggested next step: assign LIN-255 and link its implementation PR.\nNo changes were made in GitHub or Linear.",
});
},
},
],
};
}
78 changes: 78 additions & 0 deletions showcase/demos/slacksim/src/scenarios/reviewbot-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { newScenario } from "../store/scenario";
import type { Story } from "../runtime/story";

// Scripted example of examples/reviewbot. GitHub and Slack accounts are fake;
// the sequence follows the bundle's pull-request webhook → read-only review → Slack → Check Run flow.
const REVIEWS = "C1";
const BOT = "U2";
const ROOT = "1756800000.000000";

export function reviewbotDemo(): Story {
const s = newScenario()
.workspace("Acme Robotics", { glyph: "A", accent: "#4a154b" })
.at("2026-09-01T15:00:00Z")
.user("U1", "jordan")
.bot(BOT, "reviewbot", { badge: "AGENT" })
.me("U1")
.channel(REVIEWS, "auto-code-reviews", { starred: true });
s.message(REVIEWS, BOT, {
text: "GitHub pull request updated: `acme/widget` PR #42, head `8f27ca1`. Starting a review.",
ts: ROOT,
});

return {
scenario: s.open(REVIEWS).openThread(ROOT).build(),
intro: "reviewbot: a pull-request event starts a read-only review.",
beats: [
{
id: "read-diff",
caption:
"A verified pull-request event starts the session. reviewbot checks out the diff read-only and selects its review skill.",
hold: 1200,
run: (c) => {
c.setStatus(ROOT, "reading PR #42 diff");
c.postReply(REVIEWS, ROOT, BOT, {
text: "Reviewing 3 changed files with the staged `review-pr` skill. Repository access is read-only.",
});
},
},
{
id: "findings",
caption:
"The review finds a missing permission check and shows a report reference beside the Slack summary.",
hold: 1300,
run: (c) => {
c.postReply(REVIEWS, ROOT, BOT, {
text: "*PR #42 review — do not merge yet*\nHigh: `payments/handler.go` skips the account-owner check on retries. The new test covers the happy path but not a retry by a different user.",
attachments: [
{
color: "#e01e5a",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: ":page_facing_up: *pr42-review.html* — report reference",
},
},
],
},
],
});
},
},
{
id: "check-run",
caption:
"Only after delivery does reviewbot conclude the GitHub Check Run on the exact head commit.",
hold: 1200,
run: (c) => {
c.setStatus(ROOT, undefined);
c.postReply(REVIEWS, ROOT, BOT, {
text: "Check Run concluded: action required on `8f27ca1`. The review did not modify the repository or comment on the PR.",
});
},
},
],
};
}
Loading
Loading