@korso/shepherd-ui 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,9 @@ image regenerate them.
32
32
  **only** here, never in the `.` export or `<Dashboard/>`.
33
33
  - **Hosted (Korso).** Korso imports `<Dashboard/>` + `createShepherdClient` from
34
34
  the `.` export and supplies its own auth layer (the BFF). No token gate.
35
+ Hosted shells that embed `<ShepherdRoot/>` may pass `onLogout`; Shepherd renders
36
+ the Sign out action at the bottom of Config, while the host callback performs
37
+ the actual session cleanup and navigation (cookies, OIDC, BFF state, etc.).
35
38
 
36
39
  ## Development
37
40
 
@@ -5,5 +5,10 @@ export interface ShepherdRootProps {
5
5
  * to the client's baseUrl, which is correct for self-host.
6
6
  */
7
7
  hubUrl?: string;
8
+ /**
9
+ * Hosted session logout hook. The embedding frontend/BFF owns cookies,
10
+ * session state, and OIDC cleanup; Shepherd only renders the Config action.
11
+ */
12
+ onLogout?: () => void;
8
13
  }
9
- export declare function ShepherdRoot({ hubUrl }: ShepherdRootProps): import("react").JSX.Element;
14
+ export declare function ShepherdRoot({ hubUrl, onLogout }: ShepherdRootProps): import("react").JSX.Element;
@@ -1,11 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import { Dashboard } from "./components/Dashboard.js";
4
4
  import { WorkspaceSwitcher } from "./config/WorkspaceSwitcher.js";
5
5
  import { ConfigPanel } from "./config/ConfigPanel.js";
6
+ import { SignOutAction } from "./config/SignOutAction.js";
6
7
  import { useShepherdClient } from "./context.js";
7
8
  import { describeError } from "./client.js";
8
- export function ShepherdRoot({ hubUrl }) {
9
+ export function ShepherdRoot({ hubUrl, onLogout }) {
9
10
  const client = useShepherdClient();
10
11
  const [load, setLoad] = useState({ status: "loading" });
11
12
  const [selectedId, setSelectedId] = useState(null);
@@ -48,6 +49,6 @@ export function ShepherdRoot({ hubUrl }) {
48
49
  // The Config tab's content. With a workspace selected it's the sidebar
49
50
  // <ConfigPanel>; with none it's a prompt pointing at the switcher, since
50
51
  // create/join now live there (the sidebar has nothing to configure).
51
- const configSection = (_jsxs("section", { className: "shepherd-config", "aria-labelledby": "config-heading", children: [_jsx("h2", { id: "config-heading", children: "Configuration" }), selected ? (_jsx(ConfigPanel, { workspace: selected, hubUrl: hubUrl, membersRefreshKey: membersRefreshKey, onMembersChanged: () => setMembersRefreshKey((k) => k + 1), onLeft: () => void fetchWorkspaces(), onDeleted: () => void fetchWorkspaces() })) : (_jsxs("p", { className: "config-none", children: ["You're not in a workspace yet. Use the ", _jsx("b", { children: "Get started \u25BE" }), " menu at the top to create one or join with an invite code."] }))] }));
52
+ const configSection = (_jsxs("section", { className: "shepherd-config", "aria-labelledby": "config-heading", children: [_jsx("h2", { id: "config-heading", children: "Configuration" }), selected ? (_jsx(ConfigPanel, { workspace: selected, hubUrl: hubUrl, membersRefreshKey: membersRefreshKey, onMembersChanged: () => setMembersRefreshKey((k) => k + 1), onLeft: () => void fetchWorkspaces(), onDeleted: () => void fetchWorkspaces(), onLogout: onLogout })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "config-none", children: ["You're not in a workspace yet. Use the ", _jsx("b", { children: "Get started \u25BE" }), " menu at the top to create one or join with an invite code."] }), onLogout && _jsx(SignOutAction, { onLogout: onLogout })] }))] }));
52
53
  return (_jsx("div", { className: "shepherd-root", children: _jsx(Dashboard, { workspaceId: selected?.id, config: configSection, switcher: switcher, hasWorkspace: hasWorkspace }) }));
53
54
  }
@@ -48,7 +48,7 @@ export function Chat({ announcements, selectedRepo, nowMs }) {
48
48
  chatRef.current.scrollTop = chatRef.current.scrollHeight;
49
49
  }
50
50
  }, [messages.length]);
51
- return (_jsx("div", { id: "chat", className: "chat", ref: chatRef, children: messages.length === 0 ? (_jsx("div", { className: "empty", children: "No announcements yet \u2014 agents will post here as they coordinate." })) : (messages.map((a, i) => {
51
+ return (_jsx("div", { id: "chat", className: "chat", ref: chatRef, children: messages.length === 0 ? (_jsx("div", { className: "empty", children: "No announcements yet. Agents will post here as they coordinate." })) : (messages.map((a, i) => {
52
52
  const targeted = a.targetAgentName !== null || a.toAdmin;
53
53
  const className = "msg" +
54
54
  (targeted ? " msg--targeted" : "") +
@@ -142,5 +142,5 @@ export function Composer({ agents, selectedRepo, workspaceId, onSent }) {
142
142
  accept(name);
143
143
  }, children: [_jsx("div", { className: "ma", style: { background: colorForName(name) }, children: initialsFor(name) }), _jsx("span", { children: name })] }, name))) }), _jsxs("form", { id: "chat-form", className: "chat-form", onSubmit: (e) => {
144
144
  void onSubmit(e);
145
- }, children: [_jsx("input", { id: "chat-input", className: "chat-input", type: "text", autoComplete: "off", "aria-label": "Message the team", placeholder: "Message the team\u2026 use @name to direct it", role: "combobox", "aria-expanded": popOpen, "aria-controls": "mention-pop", "aria-autocomplete": "list", "aria-activedescendant": popOpen ? `mention-opt-${active}` : undefined, ref: inputRef, value: value, onChange: (e) => onInput(e.target.value), onKeyDown: onKeyDown }), _jsx("button", { id: "chat-send", className: "chat-send", type: "submit", disabled: sending, children: "Send" })] }), failed ? (_jsx("div", { className: "chat__note", role: "status", children: "send failed \u2014 retry" })) : null] }));
145
+ }, children: [_jsx("input", { id: "chat-input", className: "chat-input", type: "text", autoComplete: "off", "aria-label": "Message the team", placeholder: "Message the team\u2026 use @name to direct it", role: "combobox", "aria-expanded": popOpen, "aria-controls": "mention-pop", "aria-autocomplete": "list", "aria-activedescendant": popOpen ? `mention-opt-${active}` : undefined, ref: inputRef, value: value, onChange: (e) => onInput(e.target.value), onKeyDown: onKeyDown }), _jsx("button", { id: "chat-send", className: "chat-send", type: "submit", disabled: sending, children: "Send" })] }), failed ? (_jsx("div", { className: "chat__note", role: "status", children: "send failed, retry" })) : null] }));
146
146
  }
@@ -28,6 +28,11 @@ export interface DashboardProps {
28
28
  * has its implicit team workspace).
29
29
  */
30
30
  hasWorkspace?: boolean;
31
+ /**
32
+ * Optional header logout seam. The dashboard only renders the control and
33
+ * invokes this callback; authentication side effects belong to the caller.
34
+ */
35
+ onLogout?: () => void;
31
36
  }
32
37
  /**
33
38
  * The Shepherd wallboard shell. Composes {@link useLandscapePolling} (which reads
@@ -51,4 +56,4 @@ export interface DashboardProps {
51
56
  *
52
57
  * @returns The dashboard element.
53
58
  */
54
- export declare function Dashboard({ workspaceId, config, switcher, hasWorkspace, }?: DashboardProps): ReactElement;
59
+ export declare function Dashboard({ workspaceId, config, switcher, hasWorkspace, onLogout, }?: DashboardProps): ReactElement;
@@ -128,7 +128,7 @@ function computeCounts(snapshot) {
128
128
  *
129
129
  * @returns The dashboard element.
130
130
  */
131
- export function Dashboard({ workspaceId, config, switcher, hasWorkspace, } = {}) {
131
+ export function Dashboard({ workspaceId, config, switcher, hasWorkspace, onLogout, } = {}) {
132
132
  const hasConfig = config != null;
133
133
  // Only the hosted shell with an explicit empty account suppresses the board;
134
134
  // self-host (hasWorkspace undefined) always has its implicit team workspace.
@@ -275,7 +275,7 @@ export function Dashboard({ workspaceId, config, switcher, hasWorkspace, } = {})
275
275
  // makes sense for a live board — hide it on the Config tab and when there is
276
276
  // no workspace to poll. The brand and the tab strip stay in every state.
277
277
  const showBoardChrome = !noWorkspace && activeTab !== "config";
278
- return (_jsxs("div", { id: "board", children: [_jsxs("header", { children: [_jsx("h1", { className: "brand", style: { margin: 0, font: "inherit" }, children: "Shepherd" }), switcher, showBoardChrome && (_jsxs(_Fragment, { children: [_jsx(RepoSelect, { repos: repos, counts: counts, selected: effectiveRepo, onSelect: onSelectRepo }), _jsxs("span", { className: "vitals", children: [_jsx("b", { id: "vitals-online", children: online }), " online · ", _jsx("b", { id: "vitals-active", children: active }), " active"] })] })), _jsx("span", { className: "grow" }), showBoardChrome && (_jsxs(_Fragment, { children: [_jsx("span", { id: "status", className: "status" + (statusView.kind ? ` status--${statusView.kind}` : ""), children: statusView.text }), _jsx("span", { id: "freshness", className: "freshness", children: freshness })] })), _jsx("nav", { className: "tabs", role: "tablist", "aria-label": "Shepherd views", children: tabs.map(({ id, label }, index) => (_jsx("button", { ref: (el) => {
278
+ return (_jsxs("div", { id: "board", children: [_jsxs("header", { children: [_jsx("h1", { className: "brand", style: { margin: 0, font: "inherit" }, children: "Shepherd" }), switcher, showBoardChrome && (_jsxs(_Fragment, { children: [_jsx(RepoSelect, { repos: repos, counts: counts, selected: effectiveRepo, onSelect: onSelectRepo }), _jsxs("span", { className: "vitals", children: [_jsx("b", { id: "vitals-online", children: online }), " online · ", _jsx("b", { id: "vitals-active", children: active }), " active"] })] })), _jsx("span", { className: "grow" }), showBoardChrome && (_jsxs(_Fragment, { children: [_jsx("span", { id: "status", className: "status" + (statusView.kind ? ` status--${statusView.kind}` : ""), children: statusView.text }), _jsx("span", { id: "freshness", className: "freshness", children: freshness })] })), onLogout && (_jsx("button", { type: "button", className: "header-signout", onClick: onLogout, children: "Sign out" })), _jsx("nav", { className: "tabs", role: "tablist", "aria-label": "Shepherd views", children: tabs.map(({ id, label }, index) => (_jsx("button", { ref: (el) => {
279
279
  tabRefs.current[index] = el;
280
280
  }, className: "tab" + (activeTab === id ? " tab--active" : ""), "data-tab": id, type: "button", role: "tab", id: `tab-${id}`, "aria-controls": `panel-${id}`, "aria-selected": activeTab === id, tabIndex: activeTab === id ? 0 : -1, onClick: () => onTab(id), onKeyDown: (e) => onTabKeyDown(e, index), children: label }, id))) })] }), _jsx("section", { id: "panel-tasks", role: "tabpanel", "aria-labelledby": "tab-tasks", hidden: activeTab !== "tasks", children: noWorkspace ? (_jsx(EmptyState, { onGetStarted: () => onTab("config") })) : (_jsxs(_Fragment, { children: [_jsx(Crew, { agents: agents, tasks: tasks, selectedRepo: effectiveRepo }), _jsxs("div", { className: "board", children: [_jsxs("div", { className: "col", children: [_jsxs("div", { className: "colhead", children: [_jsx("h2", { children: "Active" }), _jsx("span", { className: "n", id: "active-count", children: activeCount })] }), _jsx(ActiveList, { tasks: tasks, nowMs: nowMs, selectedRepo: effectiveRepo })] }), _jsx("div", { className: "board__rule" }), _jsxs("div", { className: "col", children: [_jsxs("div", { className: "colhead", children: [_jsx("h2", { children: "Done" }), _jsx("span", { className: "n", id: "done-count", children: doneCount })] }), _jsx(DoneList, { tasks: tasks, nowMs: nowMs, selectedRepo: effectiveRepo, doneShown: doneShown, onLoadMore: onLoadMore })] })] })] })) }), _jsx("section", { id: "panel-chat", role: "tabpanel", "aria-labelledby": "tab-chat", hidden: activeTab !== "chat", children: noWorkspace ? (_jsx(EmptyState, { onGetStarted: () => onTab("config") })) : (_jsxs("div", { className: "chat-wrap", children: [_jsx(Chat, { announcements: announcements, selectedRepo: effectiveRepo, nowMs: nowMs }), _jsx(Composer, { agents: agents, selectedRepo: effectiveRepo, workspaceId: workspaceId, onSent: refresh })] })) }), hasConfig && (_jsx("section", { id: "panel-config", role: "tabpanel", "aria-labelledby": "tab-config", hidden: activeTab !== "config", children: config })), _jsx(FeedbackWidget, { workspaceId: workspaceId })] }));
281
281
  }
@@ -36,7 +36,7 @@ export function DoneList({ tasks, nowMs, selectedRepo, doneShown, onLoadMore, })
36
36
  lastDay = day;
37
37
  const dropped = t.status === "dropped";
38
38
  const meta = dropped
39
- ? `went offline ${formatRelative(t.endedAt ?? t.createdAt, nowMs)} no done signal`
39
+ ? `went offline ${formatRelative(t.endedAt ?? t.createdAt, nowMs)}, no done signal`
40
40
  : `finished ${formatRelative(t.endedAt ?? t.createdAt, nowMs)}${formatActiveDuration(t.createdAt, t.endedAt)
41
41
  ? " · " + formatActiveDuration(t.createdAt, t.endedAt)
42
42
  : ""}`;
@@ -12,5 +12,10 @@ export interface ConfigPanelProps {
12
12
  onLeft?: () => void;
13
13
  /** Called after a successful delete, so the shell re-lists its workspaces. */
14
14
  onDeleted?: () => void;
15
+ /**
16
+ * Hosted session logout hook. Shepherd renders the control while the host owns
17
+ * the authentication side effect.
18
+ */
19
+ onLogout?: () => void;
15
20
  }
16
- export declare function ConfigPanel({ workspace, hubUrl, membersRefreshKey, onMembersChanged, onLeft, onDeleted, }: ConfigPanelProps): import("react").JSX.Element;
21
+ export declare function ConfigPanel({ workspace, hubUrl, membersRefreshKey, onMembersChanged, onLeft, onDeleted, onLogout, }: ConfigPanelProps): import("react").JSX.Element;
@@ -4,13 +4,14 @@ import { GeneralSettings } from "./GeneralSettings.js";
4
4
  import { Members } from "./Members.js";
5
5
  import { Invites } from "./Invites.js";
6
6
  import { ConnectAgent } from "./ConnectAgent.js";
7
+ import { SignOutAction } from "./SignOutAction.js";
7
8
  const SECTIONS = [
8
9
  { id: "general", label: "General" },
9
10
  { id: "members", label: "Members" },
10
11
  { id: "agent", label: "Agent" },
11
12
  ];
12
- export function ConfigPanel({ workspace, hubUrl, membersRefreshKey, onMembersChanged, onLeft, onDeleted, }) {
13
+ export function ConfigPanel({ workspace, hubUrl, membersRefreshKey, onMembersChanged, onLeft, onDeleted, onLogout, }) {
13
14
  const [section, setSection] = useState("general");
14
15
  const isAdmin = workspace.role === "admin";
15
- return (_jsxs("div", { className: "config-layout", children: [_jsx("nav", { className: "config-nav", "aria-label": "Configuration sections", children: SECTIONS.map(({ id, label }) => (_jsx("button", { type: "button", className: "config-nav__item" + (section === id ? " config-nav__item--on" : ""), "aria-current": section === id ? "page" : undefined, onClick: () => setSection(id), children: label }, id))) }), _jsxs("div", { className: "config-panel", children: [section === "general" && (_jsx(GeneralSettings, { workspace: workspace, onLeft: onLeft, onDeleted: onDeleted })), section === "members" && (_jsxs(_Fragment, { children: [_jsx(Members, { workspaceId: workspace.id, refreshKey: membersRefreshKey, canRemove: isAdmin }), isAdmin && (_jsx(Invites, { workspaceId: workspace.id, onMembersChanged: onMembersChanged }))] })), section === "agent" && _jsx(ConnectAgent, { hubUrl: hubUrl })] })] }));
16
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "config-layout", children: [_jsx("nav", { className: "config-nav", "aria-label": "Configuration sections", children: SECTIONS.map(({ id, label }) => (_jsx("button", { type: "button", className: "config-nav__item" + (section === id ? " config-nav__item--on" : ""), "aria-current": section === id ? "page" : undefined, onClick: () => setSection(id), children: label }, id))) }), _jsxs("div", { className: "config-panel", children: [section === "general" && (_jsx(GeneralSettings, { workspace: workspace, onLeft: onLeft, onDeleted: onDeleted })), section === "members" && (_jsxs(_Fragment, { children: [_jsx(Members, { workspaceId: workspace.id, refreshKey: membersRefreshKey, canRemove: isAdmin }), isAdmin && (_jsx(Invites, { workspaceId: workspace.id, onMembersChanged: onMembersChanged }))] })), section === "agent" && _jsx(ConnectAgent, { hubUrl: hubUrl })] })] }), onLogout && _jsx(SignOutAction, { onLogout: onLogout })] }));
16
17
  }
@@ -24,5 +24,5 @@ export function ConfirmDeleteWorkspace({ workspaceName, busy, error, onConfirm,
24
24
  onCancel();
25
25
  }, children: _jsxs("div", { className: "shepherd-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": headingId, "aria-describedby": descId,
26
26
  // Clicks inside the dialog must not bubble to the backdrop's cancel.
27
- onClick: (e) => e.stopPropagation(), children: [_jsx("h3", { id: headingId, children: "Delete workspace" }), _jsxs("p", { id: descId, className: "shepherd-modal__body", children: ["This permanently deletes ", _jsx("b", { children: workspaceName }), " and all of its data \u2014 agents, sessions, tasks, announcements, change history, tokens, invites, and members. This cannot be undone."] }), _jsxs("label", { htmlFor: `${headingId}-input`, className: "shepherd-modal__label", children: ["Type ", _jsx("b", { children: workspaceName }), " to confirm"] }), _jsx("input", { id: `${headingId}-input`, ref: inputRef, type: "text", value: typed, autoComplete: "off", disabled: busy, onChange: (e) => setTyped(e.target.value) }), error && _jsx("p", { role: "alert", children: error }), _jsxs("div", { className: "shepherd-modal__actions", children: [_jsx("button", { type: "button", onClick: onCancel, disabled: busy, children: "Cancel" }), _jsx("button", { type: "button", className: "danger", onClick: onConfirm, disabled: !confirmed || busy, children: busy ? "Deleting…" : "Delete workspace" })] })] }) }));
27
+ onClick: (e) => e.stopPropagation(), children: [_jsx("h3", { id: headingId, children: "Delete workspace" }), _jsxs("p", { id: descId, className: "shepherd-modal__body", children: ["This permanently deletes ", _jsx("b", { children: workspaceName }), " and all of its data: agents, sessions, tasks, announcements, change history, tokens, invites, and members. This cannot be undone."] }), _jsxs("label", { htmlFor: `${headingId}-input`, className: "shepherd-modal__label", children: ["Type ", _jsx("b", { children: workspaceName }), " to confirm"] }), _jsx("input", { id: `${headingId}-input`, ref: inputRef, type: "text", value: typed, autoComplete: "off", disabled: busy, onChange: (e) => setTyped(e.target.value) }), error && _jsx("p", { role: "alert", children: error }), _jsxs("div", { className: "shepherd-modal__actions", children: [_jsx("button", { type: "button", onClick: onCancel, disabled: busy, children: "Cancel" }), _jsx("button", { type: "button", className: "danger", onClick: onConfirm, disabled: !confirmed || busy, children: busy ? "Deleting…" : "Delete workspace" })] })] }) }));
28
28
  }
@@ -206,12 +206,12 @@ export function ConnectAgent({ hubUrl }) {
206
206
  setCopied(true);
207
207
  setTimeout(() => setCopied(false), 2000);
208
208
  }
209
- return (_jsxs("section", { className: "shepherd-connect-agent", "aria-labelledby": headingId, children: [_jsxs("div", { className: "card-head", children: [_jsx("h3", { id: headingId, children: "Connect your agent" }), _jsx("p", { className: "card-sub", children: "Generate a token \u2014 it works across all your workspaces \u2014 then paste the command into your coding tool." }), _jsx("p", { className: "card-sub", children: "The first time the agent changes files in a repo, Shepherd asks which workspace to coordinate that repo with \u2014 once per repo, right in your tool." })] }), _jsxs("div", { className: "card-body", children: [_jsxs("div", { className: "field", children: [_jsx("label", { htmlFor: "connect-tool", children: "Tool" }), _jsx("select", { id: "connect-tool", value: tool, onChange: (e) => setTool(e.target.value), children: TOOLS.map((t) => (_jsx("option", { value: t.id, children: t.label }, t.id))) })] }), _jsxs("div", { className: "field generate", children: [_jsx("label", { htmlFor: "token-name", children: "Token name (optional)" }), _jsxs("div", { className: "field__row", children: [_jsx("input", { id: "token-name", type: "text", value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
209
+ return (_jsxs("section", { className: "shepherd-connect-agent", "aria-labelledby": headingId, children: [_jsxs("div", { className: "card-head", children: [_jsx("h3", { id: headingId, children: "Connect your agent" }), _jsx("p", { className: "card-sub", children: "Generate a token; it works across all your workspaces, so paste the command into your coding tool." }), _jsx("p", { className: "card-sub", children: "The first time the agent changes files in a repo, Shepherd asks which workspace to coordinate that repo with, once per repo, right in your tool." })] }), _jsxs("div", { className: "card-body", children: [_jsxs("div", { className: "field", children: [_jsx("label", { htmlFor: "connect-tool", children: "Tool" }), _jsx("select", { id: "connect-tool", value: tool, onChange: (e) => setTool(e.target.value), children: TOOLS.map((t) => (_jsx("option", { value: t.id, children: t.label }, t.id))) })] }), _jsxs("div", { className: "field generate", children: [_jsx("label", { htmlFor: "token-name", children: "Token name (optional)" }), _jsxs("div", { className: "field__row", children: [_jsx("input", { id: "token-name", type: "text", value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
210
210
  if (e.key === "Enter" && !busy) {
211
211
  e.preventDefault();
212
212
  void generate();
213
213
  }
214
- }, placeholder: "e.g. laptop" }), _jsx("button", { type: "button", onClick: () => void generate(), disabled: busy, children: "Generate token" })] })] }), error && _jsx("p", { role: "alert", children: error }), status && _jsx("p", { role: "status", children: status }), rawToken && (_jsx("p", { className: "token-once", role: "status", children: "Copy this token now \u2014 it won't be shown again." })), _jsxs("div", { className: "install-command", children: [_jsx("pre", { "data-testid": "install-command", children: command }), _jsx("button", { type: "button", className: "install-command__copy", "aria-label": copied ? "Copied" : "Copy command", title: copied ? "Copied" : "Copy", onClick: () => void copyCommand(), children: copied ? (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) })) : (_jsxs("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }), _jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })] })) })] }), hook && (_jsxs("div", { className: "hook-setup", children: [_jsxs("p", { className: "card-sub", children: ["Message delivery sets itself up: the first time the agent runs, Shepherd adds its inbox hook to ", _jsx("code", { children: hook.target }), " ", "automatically \u2014 it delivers teammate announcements and reminds the agent to link each repo before its first write. Happens at most once; set ", _jsx("code", { children: "SHEPHERD_NO_AUTO_HOOKS=1" }), " to opt out."] }), hook.snippet && (_jsxs("details", { className: "hook-reference", children: [_jsx("summary", { children: "What gets added (manual equivalent)" }), _jsx("div", { className: "install-command", children: _jsx("pre", { "data-testid": "hook-snippet", children: hook.snippet }) })] }))] })), _jsx("h4", { children: "Existing tokens" }), loading ? (_jsx("p", { role: "status", children: "Loading\u2026" })) : tokens.length === 0 ? (_jsx("p", { children: "No tokens yet." })) : (_jsx("ul", { children: tokens.map((t) => {
214
+ }, placeholder: "e.g. laptop" }), _jsx("button", { type: "button", onClick: () => void generate(), disabled: busy, children: "Generate token" })] })] }), error && _jsx("p", { role: "alert", children: error }), status && _jsx("p", { role: "status", children: status }), rawToken && (_jsx("p", { className: "token-once", role: "status", children: "Copy this token now; it won't be shown again." })), _jsxs("div", { className: "install-command", children: [_jsx("pre", { "data-testid": "install-command", children: command }), _jsx("button", { type: "button", className: "install-command__copy", "aria-label": copied ? "Copied" : "Copy command", title: copied ? "Copied" : "Copy", onClick: () => void copyCommand(), children: copied ? (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) })) : (_jsxs("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }), _jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })] })) })] }), hook && (_jsxs("div", { className: "hook-setup", children: [_jsxs("p", { className: "card-sub", children: ["Message delivery sets itself up: the first time the agent runs, Shepherd adds its inbox hook to ", _jsx("code", { children: hook.target }), " ", "automatically. It delivers teammate announcements and reminds the agent to link each repo before its first write. This happens at most once; set", " ", _jsx("code", { children: "SHEPHERD_NO_AUTO_HOOKS=1" }), " to opt out."] }), hook.snippet && (_jsxs("details", { className: "hook-reference", children: [_jsx("summary", { children: "What gets added (manual equivalent)" }), _jsx("div", { className: "install-command", children: _jsx("pre", { "data-testid": "hook-snippet", children: hook.snippet }) })] }))] })), _jsx("h4", { children: "Existing tokens" }), loading ? (_jsx("p", { role: "status", children: "Loading\u2026" })) : tokens.length === 0 ? (_jsx("p", { children: "No tokens yet." })) : (_jsx("ul", { children: tokens.map((t) => {
215
215
  const name = t.name ?? t.id;
216
216
  return (_jsxs("li", { children: [_jsx("span", { children: name }), _jsx("span", { className: "token-meta", children: tokenMeta(t, Date.now()) }), t.revokedAt ? (_jsx("span", { className: "revoked", children: "revoked" })) : (_jsx("button", { type: "button", "aria-label": `Revoke token ${name}`, onClick: () => void revoke(t.id), disabled: revokingId === t.id, children: "Revoke" }))] }, t.id));
217
217
  }) }))] })] }));
@@ -0,0 +1,7 @@
1
+ /** Props for the hosted session sign out presentation control. */
2
+ export interface SignOutActionProps {
3
+ /** Called immediately when the operator chooses to end this dashboard session. */
4
+ onLogout: () => void;
5
+ }
6
+ /** Renders the session-level sign out control supplied by the host shell. */
7
+ export declare function SignOutAction({ onLogout }: SignOutActionProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ /** Renders the session-level sign out control supplied by the host shell. */
4
+ export function SignOutAction({ onLogout }) {
5
+ const headingId = useId();
6
+ return (_jsxs("section", { className: "config-signout", "aria-labelledby": headingId, children: [_jsx("div", { className: "card-head", children: _jsx("h3", { id: headingId, children: "Session" }) }), _jsxs("div", { className: "card-body", children: [_jsx("p", { className: "helper", children: "Sign out of this Shepherd dashboard session." }), _jsx("button", { type: "button", onClick: onLogout, children: "Sign out" })] })] }));
7
+ }