Files
openworker/surfaces/gui/src/components/SessionIntro.tsx
T
Rohit C PrasadandDevika 2b45018ffa OpenWorker: initial import
Imported from andrewyng/aisuite@1b4bbf303e
(contents of its platform/ directory, hoisted to the repo root).
Development history prior to this commit lives in that repository.

Co-authored-by: Devika <devikaverma11@gmail.com>
2026-07-21 11:09:41 -07:00

133 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { getConnectors, getSessionConnections } from "../api";
import type { Attachment } from "../types";
import { ConnectorIcon } from "../connectors/ConnectorIcon";
import { indexConnectors, visualFor, type ConnectorMap } from "../connectors/visuals";
import { useRoots } from "../useRoots";
import { AddFolderForm } from "./AddFolderForm";
// Empty-state for a fresh Cowork session (§27): a greeting, exactly three concrete template
// tasks, and the composer — nothing else. Each task carries its own setup: no icon tiles (the
// title is the row), connector dots on the sub-line (brand color = connected and enabled for
// this session, grayscale = not — §23's vocabulary), and sub-line copy that is always the task's
// OUTCOME, never connection state. Sources ready → "Start →" on hover, click prefills the
// composer. Not ready → "Configure " always visible (for a gated row the setup action IS the
// row's meaning), opening the §23 Session settings drawer — no second setup surface here.
const FOLDER_PROMPT = "Analyze the files in this folder and summarize what matters.";
const HUBSPOT_PROMPT =
"Create a report on my recent HubSpot leads: sources, stages, and who needs follow-up.";
const GH_SLACK_PROMPT =
"Set up a weekly progress report: summarize activity in my GitHub repos and post it to Slack every Friday morning.";
export function SessionIntro({
sessionId,
onOpenSessionSettings,
onPrefill,
}: {
sessionId: string;
// Opens the §23 Session settings drawer (sources section) — the gated rows' Configure target.
onOpenSessionSettings: () => void;
onPrefill: (text: string, attachments?: Attachment[]) => void;
}) {
const { roots, busy, error, addRoot } = useRoots(sessionId);
const [live, setLive] = useState<Set<string>>(new Set());
const [byName, setByName] = useState<ConnectorMap>({});
const [addingFolder, setAddingFolder] = useState(false);
useEffect(() => {
// Live = what this session can touch right now (connected AND not muted here) — the same
// truth the §23 glance renders, so the dots here can never disagree with the row above.
getSessionConnections(sessionId)
.then((c) => setLive(new Set(c.connected.filter((x) => x.enabled).map((x) => x.connector))))
.catch(() => {});
getConnectors()
.then((list) => setByName(indexConnectors(list)))
.catch(() => {});
}, [sessionId]);
const shared = roots.filter((r) => !r.primary);
const hubspotReady = live.has("hubspot");
const ghSlackReady = live.has("github") && live.has("slack");
const dot = (name: string, on: boolean) => (
<span className={"task-dot" + (on ? "" : " off")} key={name}>
<ConnectorIcon connector={visualFor(name, "connector", byName)} size={12} />
</span>
);
const pickFolder = () => {
// A shared folder already exists → straight to the prompt; otherwise share one first.
if (shared.length > 0) onPrefill(FOLDER_PROMPT);
else setAddingFolder((v) => !v);
};
return (
<div className="intro">
<h1 className="greeting">
<span className="mark"></span> What should we produce?
</h1>
<p className="intro-lede">
Pick a task to start I'll do the work and save the result. Or just type what you need
below.
</p>
<div className="intro-tasks">
<button className="task-card" data-testid="intro-task-folder" onClick={pickFolder}>
<span className="task-card-body">
<span className="task-card-title">Analyze the files in a directory</span>
<span className="task-card-sub">I'll read them and summarize what matters</span>
</span>
<span className="task-card-act">Pick a folder </span>
</button>
{addingFolder && (
<div className="intro-addfolder">
<AddFolderForm
startOpen
busy={busy}
onAdd={async (path, writable) => {
const ok = await addRoot(path, writable);
if (ok !== false) onPrefill(FOLDER_PROMPT);
return ok;
}}
onDismiss={() => setAddingFolder(false)}
/>
{error && <div className="roots-err">{error}</div>}
</div>
)}
<button
className={"task-card" + (hubspotReady ? "" : " gated")}
data-testid="intro-task-hubspot"
onClick={() => (hubspotReady ? onPrefill(HUBSPOT_PROMPT) : onOpenSessionSettings())}
>
<span className="task-card-body">
<span className="task-card-title">Create a report from my HubSpot leads</span>
<span className="task-card-sub">
{dot("hubspot", hubspotReady)}
Sources, stages, and who needs follow-up
</span>
</span>
<span className="task-card-act">{hubspotReady ? "Start →" : "Configure "}</span>
</button>
<button
className={"task-card" + (ghSlackReady ? "" : " gated")}
data-testid="intro-task-github-slack"
onClick={() => (ghSlackReady ? onPrefill(GH_SLACK_PROMPT) : onOpenSessionSettings())}
>
<span className="task-card-body">
<span className="task-card-title">Automate a weekly GitHub progress report to Slack</span>
<span className="task-card-sub">
{dot("github", live.has("github"))}
{dot("slack", live.has("slack"))}
Repo activity, summarized and posted every Friday
</span>
</span>
<span className="task-card-act">{ghSlackReady ? "Start →" : "Configure "}</span>
</button>
</div>
</div>
);
}