fix(terminal): lead the welcome view with the operator (#978)

The default kernel view opens the operator full width first, with the dashboard and advisor one tab or window away.
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-10-07 22:12:29 -07:00
1 parent 9e08c89db6
commit 8f80086275
12 files changed
+95 -60

No files matched your search

+3 -2
View File
@@ -67,8 +67,9 @@ rig daemon start # if stopped
rig terminal open saved:kernel --window
```
`--window` opens a new terminal tab or window itself, with **TUI | advisor |
operator**. An installing agent can run it from its shell on the daemon's
`--window` opens a new terminal tab or window itself, with the **operator** filling
the first page. The dashboard and advisor each have their own tab or tmux window.
An installing agent can run it from its shell on the daemon's
desktop; the person copies nothing. It uses Herdr when installed, otherwise
the same layout in plain tmux, and preserves existing conversations. `rig tui`
is the team dashboard. Installation is finished when you are talking to the
+14 -10
View File
@@ -186,7 +186,8 @@ when it starts the daemon.
## Open the kernel conversations
This is the **welcome screen** or **OpenRig view**: TUI | advisor | operator, in a new terminal window or tab.
This is the **welcome screen** or **OpenRig view**: the operator in one full-width pane, in a new terminal window or tab.
The dashboard and advisor each have a separate tab or tmux window.
**Agents:** the `rigs` skill says when to open it, what to check, what to do when it can't open, and how to hand the
person's goal to the operator; [Install and sign in](#install-and-sign-in) says how to load it.
@@ -244,15 +245,18 @@ inventory problem.
### Confirm the view
The default `saved:kernel` composition is **TUI | advisor | operator**, in three
columns, for Claude-only, Codex-only and mixed kernels. The queue worker remains
reachable through the TUI. The composition uses the installed kernel's current
The default `saved:kernel` composition opens **the operator** alone on the first
page, for Claude-only, Codex-only and mixed kernels. With all three roles ready,
the dashboard is on the second page and the advisor on the third: select their
Herdr tab to switch. In plain tmux, press **Ctrl-b, then w** and choose `view-2` for the dashboard or `view-3`
for the advisor; `view-1` returns to the operator. The queue worker remains
reachable through the dashboard. The composition uses the installed kernel's current
bindings; no YAML edit, seat launch or daemon restart is needed. A custom saved
view named `kernel` still takes precedence.
Inspect `opened`, `absent`, `degraded`, `window` and any notes. Confirm the new
surface visibly shows the intended conversations and TUI, and that the original
terminal remains intact. The person sees Herdr only in a terminal they can see.
surface visibly shows the operator, with the dashboard and advisor reachable on
their own pages, and that the original terminal remains intact. The person sees Herdr only in a terminal they can see.
Creating its workspace or switching the shared TUI to `:terminals` does not open
that terminal. A created window or successful CLI response alone is not visual
confirmation. A partial view remains partial. If the shared TUI tile shows a
@@ -276,8 +280,8 @@ different points in the same journey:
| macOS asks for Automation access, or the window action is denied | The command asks the desktop terminal app to open a new surface. Let the person answer the system prompt. Read the error and inspect any new window before retrying an uncertain action; do not silently switch apps and open a duplicate. |
| No local display, or the configured daemon is remote | `--window` needs the daemon's desktop. An SSH shell or remote daemon address alone does not give the agent that desktop. Explain the limit and use the [manual](#talk-to-the-operator-in-any-terminal) or [SSH handoff](#headless-or-ssh-handoff) with the actual operator binding. |
| Herdr shows its introduction or agent-integration panel | These are Herdr's first-run panels. Follow the displayed controls: Return continues the intro and Esc closes the integration panel. Opening this view does not require installing Herdr's agent integrations or changing your Claude/Codex settings. |
| Three columns are cramped in an 80×24 window | The desktop app's window size can be smaller than the composed layout. Enlarge the newly opened window so the dashboard and conversations are readable. Changing or restarting the team is unnecessary. |
| The command returned, but visibility is unconfirmed | With authorized desktop tools, inspect the new window and a screenshot of its contents. A window listing alone proves only that a window exists. If the agent has only shell access, report what the command confirmed and ask the person to confirm the visible TUI, advisor and operator. Do not report visual success without seeing it. |
| A saved layout is cramped in an 80×24 window | The default kernel view gives each role a full-width page. A custom saved `kernel` view still controls its own layout; adjust that saved view if needed. Changing or restarting the team is unnecessary. |
| The command returned, but visibility is unconfirmed | With authorized desktop tools, inspect the new window and a screenshot of its contents. A window listing alone proves only that a window exists. If the agent has only shell access, report what the command confirmed and ask the person to confirm the visible operator and that the dashboard and advisor tabs or windows are available. Do not report visual success without seeing it. |
After a failure, inspect any newly opened surface before retrying. A tmux failure
may name a newly created viewing session for inspection. Do not replace a failed
@@ -321,8 +325,8 @@ is separate from the first-install desktop action above.
### Plain terminal: a new viewing session
`rig terminal open saved:kernel --provider tmux --window --json` creates and opens
the three-column view itself. It reuses the daemon's composition and preserves
existing sessions. The result names the viewing session. No pane names or shell
the operator page first, with separate dashboard and advisor windows. It reuses
the daemon's composition and preserves existing sessions. The result names the viewing session. No pane names or shell
commands need to be assembled by the person.
### Headless or SSH handoff
+1 -1
View File
@@ -233,7 +233,7 @@ export async function openTerminalWindow(client: DaemonClient, view: string, req
const viewMarker = `openrig:${composed.id}`;
const planMarker = `${viewMarker}#${planId.slice(0, 16)}`;
const marker = (label: string) => label.includes("#") ? label.slice(0, label.lastIndexOf("#")) : "";
const existing = tabs.find(tab => marker(tab.label) === planMarker);
const existing = tabs.find(tab => marker(tab.label) === planMarker && (composed.pages.length <= 1 || tab.label.endsWith("/1")));
const stale = tabs.filter(tab => {
const value = marker(tab.label);
return value !== planMarker && (value === viewMarker ||
@@ -91,6 +91,22 @@ describe("reopening a Herdr view in a desktop window", () => {
expect(process.exitCode).toBeUndefined();
});
it("reopens the first page even when the dashboard or advisor is listed first", async () => {
const tabs = [
{ workspace_id: "kernel", tab_id: "advisor", label: label("kernel", "l1/3") },
{ workspace_id: "kernel", tab_id: "dashboard", label: label("kernel", "l1/2") },
{ workspace_id: "kernel", tab_id: "operator", label: label("kernel", "l1/1") },
];
const f = fixture(structuredClone(tabs));
f.composed.opened = [f.composed.opened[2]!, f.composed.opened[0]!, f.composed.opened[1]!];
f.composed.pages = f.composed.opened.map(pane => [pane]);
await f.run();
expect(f.focus).toHaveBeenCalledExactlyOnceWith("operator");
expect(f.tabs).toEqual(tabs);
expect(f.post).not.toHaveBeenCalled();
expect(JSON.parse(logs[0]!)).toMatchObject({ ok: true, reusedWorkspace: { tabId: "operator" } });
});
it.each(["kernel-other", "kernel#other", "rig:kernel"])("does not reuse the different view %s", async other => {
const f = fixture([{ workspace_id: "same-human-name", tab_id: "unrelated", label: label(other) }]);
await f.run();
+13 -9
View File
@@ -75,16 +75,16 @@ it.each([
}
}, 40_000);
it("opens three real viewer panes while original sessions keep the same pane and process", async () => {
it.each([false, true])("opens real viewer panes and preserves source processes (one role per window=%s)", async paged => {
// Short names also fit macOS Unix sockets under CI's existing TMPDIR.
const source = `ws-${randomUUID().slice(0, 12)}`;
const viewer = `wv-${randomUUID().slice(0, 12)}`;
sockets.push(source, viewer);
const names = ["tui", "advisor", "operator"];
const names = paged ? ["operator", "tui", "advisor"] : ["tui", "advisor", "operator"];
for (const name of names) tmux(source, ["new-session", "-d", "-s", name, "sleep 60"]);
const original = tmux(source, ["list-panes", "-a", "-F", "#{session_name} #{pane_id} #{pane_pid}"]);
const panes = names.map(seat => ({ seat, label: seat, paneCommand: `tmux -L ${source} attach-session -t '=${seat}'` }));
const client = { baseUrl: "http://localhost:7433", get: async () => ({ status: 200, data: { planId: "fixture", status: {}, composed: { opened: panes, pages: [panes], columns: 3, absent: [], degraded: [] } } }) } as unknown as DaemonClient;
const client = { baseUrl: "http://localhost:7433", get: async () => ({ status: 200, data: { planId: "fixture", status: {}, composed: { opened: panes, pages: paged ? panes.map(pane => [pane]) : [panes], columns: paged ? 1 : 3, absent: [], degraded: [] } } }) } as unknown as DaemonClient;
const desktop = vi.fn(async () => "window");
const deps: WindowDeps = {
herdrConfig: () => { throw new Error("tmux must not prepare Herdr config"); },
@@ -98,12 +98,16 @@ it("opens three real viewer panes while original sessions keep the same pane and
launch: vi.fn(async () => {}), sleep: async () => {}, id: () => "owned-view",
};
const result = await openTerminalWindow(client, "saved:kernel", "tmux", deps);
expect(result).toMatchObject({ ok: true, opened: names });
expect(result).toMatchObject({ ok: true, opened: names, pages: paged ? 3 : 1 });
expect(desktop).toHaveBeenCalledTimes(1);
const actual = tmux(viewer, ["list-panes", "-t", "openrig-view-owned-view:view-1", "-F", "#{pane_title}|#{pane_top}|#{pane_left}|#{pane_dead}"]).trim().split("\n").map(line => line.split("|"));
expect(actual.map(row => row[0])).toEqual(names);
expect(actual.map(row => row[1])).toEqual(["0", "0", "0"]);
expect(actual.map(row => Number(row[2]))).toEqual([...actual.map(row => Number(row[2]))].sort((a, b) => a - b));
expect(actual.every(row => row[3] === "0")).toBe(true);
expect(tmux(viewer, ["list-windows", "-t", "=openrig-view-owned-view", "-F", "#{window_name}|#{window_active}"]).trim().split("\n"))
.toEqual(paged ? ["view-1|1", "view-2|0", "view-3|0"] : ["view-1|1"]);
for (const [index, expected] of (paged ? names.map(name => [name]) : [names]).entries()) {
const actual = tmux(viewer, ["list-panes", "-t", `openrig-view-owned-view:view-${index + 1}`, "-F", "#{pane_title}|#{pane_top}|#{pane_left}|#{pane_dead}"]).trim().split("\n").map(line => line.split("|"));
expect(actual.map(row => row[0])).toEqual(expected);
expect(actual.map(row => row[1])).toEqual(expected.map(() => "0"));
expect(actual.map(row => Number(row[2]))).toEqual([...actual.map(row => Number(row[2]))].sort((a, b) => a - b));
expect(actual.every(row => row[3] === "0")).toBe(true);
}
expect(tmux(source, ["list-panes", "-a", "-F", "#{session_name} #{pane_id} #{pane_pid}"])).toBe(original);
});
@@ -54,7 +54,8 @@ For “show me my agents”, “show me the terminals”, “see my agents” or
`rig terminal open saved:kernel --window --json` on the daemon's desktop. The request is already permission to
open it; otherwise ask **“Open the OpenRig view now?”** first. The command opens a new terminal tab/window using
installed herdr, otherwise plain tmux; it preserves your terminal and existing conversations. The default view
needs no YAML edit. Show TUI | advisor | operator in that order for Claude-only, Codex-only and mixed kernels.
needs no YAML edit. Show the operator in one full-width pane for Claude-only, Codex-only and mixed kernels.
The dashboard and advisor each remain available in their own tab or tmux window.
Keep the queue worker off the first view and accessible through the TUI. Reuse existing conversations and accounts.
Inspect the result and visible content using authorized desktop tools, or report that visibility is unconfirmed.
Herdr is visible only inside a terminal the person can see. Creating a workspace or switching the shared TUI
+3 -2
View File
@@ -38,8 +38,9 @@ guidance in `AGENTS.md` or `CLAUDE.md` plus the team's skills and plugins. Say s
Keep it.
- **Open the welcome screen:** for “show me my agents”, “show me the terminals”, “see my agents” or “welcome screen”, run
`rig terminal open saved:kernel --window` on the daemon's desktop. Otherwise ask “Open the OpenRig view now?”
first. It opens a new terminal tab/window itself: TUI | advisor | operator, using herdr when installed or plain
tmux otherwise. Preserve the current terminal. Check the result and visible content, or report what cannot be
first. It opens a new terminal tab/window itself with one full-width operator pane, using herdr when installed
or plain tmux otherwise. The dashboard and advisor each have their own tab or tmux window; select one to switch.
Preserve the current terminal. Check the result and visible content, or report what cannot be
verified. On a Mac the first open can raise two macOS prompts: “… is an app downloaded from the Internet”
(Open) and “… wants access to control …” (Allow). Tell the person to expect them and accept both; if the open
timed out while a prompt was up, check the desktop and run it once more. If the view is already open, point them
@@ -13,8 +13,8 @@ these plain commands let the health check finish without that extra prompt.
For “show me my agents”, “show me the terminals”, “see my agents” or “welcome screen”, run
`rig terminal open saved:kernel --window` on the daemon's desktop. It opens the
TUI | advisor | operator view itself in a new terminal tab/window. Inspect its
result and visible content, or state what you cannot confirm. On a desktop, do
operator in one full-width pane in a new terminal tab/window. The dashboard and advisor
each have their own tab or tmux window. Inspect its result and visible content, or state what you cannot confirm. On a desktop, do
not end by showing a table or suggesting a command for the person to type. Herdr is visible
only inside a terminal the person can see; changing the shared TUI to `:terminals`
does not open one. Over headless SSH, explain that limitation, ask the person to
@@ -17,7 +17,8 @@ machine and have things happen.
approval escalate.
- **operator.human** is the shared mission-control terminal. A fresh kernel
starts `rig tui` there. For “show me my agents”, “show me the terminals” or “welcome screen”, run
`rig terminal open saved:kernel --window` to open all three columns. Only if
`rig terminal open saved:kernel --window` to open the operator first, with the dashboard and advisor
in their own tabs or tmux windows. Only if
the window cannot open, `rig tui --shared` is the dashboard-only fallback;
detaching preserves navigation. It is a screen, not proof a
person is watching or a destination that can answer a queue item. Use the
@@ -141,7 +141,7 @@ function savedMemberToInput(m: SavedViewMember): ViewMemberInput {
};
}
type ResolvedView = { id: string; members: ViewMemberInput[]; kernelLayout?: string; columns?: number } | { code: string; error: string };
type ResolvedView = { id: string; members: ViewMemberInput[]; kernelLayout?: string; columns?: number; panesPerPage?: number } | { code: string; error: string };
type ComposedTerminalView = ComposedView & { kernelLayout?: string };
export class TerminalService {
@@ -204,7 +204,7 @@ export class TerminalService {
if (!view) return { code: "view_required", error: "a view argument is required" };
const resolved = await this.resolveView(view);
if ("code" in resolved) return resolved;
const composed = composeView(resolved.id, await this.refineLiveness(resolved.members), { resolveHost: (id) => this.deps.resolveHost(id), panesPerPage, localTmux: await this.resolveLocalTmux() });
const composed = composeView(resolved.id, await this.refineLiveness(resolved.members), { resolveHost: (id) => this.deps.resolveHost(id), panesPerPage: resolved.panesPerPage ?? panesPerPage, localTmux: await this.resolveLocalTmux() });
return resolved.kernelLayout ? { ...composed, kernelLayout: resolved.kernelLayout, columns: resolved.columns } : composed;
}
@@ -341,7 +341,7 @@ export class TerminalService {
}
/** An in-memory default; a user's saved kernel view always takes precedence. */
private async defaultKernelView(): Promise<{ id: string; members: ViewMemberInput[]; kernelLayout: string; columns: number } | null> {
private async defaultKernelView(): Promise<{ id: string; members: ViewMemberInput[]; kernelLayout: string; columns: number; panesPerPage: number } | null> {
const rows = await this.deps.listRigSeats("kernel");
if (rows == null) return null;
const advisor = rows.find(row => row.logicalId === "advisor.lead");
@@ -349,7 +349,8 @@ export class TerminalService {
const runtimesKnown = !!advisor?.runtime && !!operator?.runtime;
const singleRuntime = runtimesKnown && advisor!.runtime === operator!.runtime;
const kernelLayout = runtimesKnown ? (singleRuntime ? "single-runtime" : "dual-runtime") : "runtime layout unverified; TUI, advisor and operator";
const roles = ["operator.human", "advisor.lead", "operator.agent"];
// One full-width conversation per page; the operator is the first populated page when ready.
const roles = ["operator.agent", "operator.human", "advisor.lead"];
const members = roles.map(logicalId => {
const row = rows.find(row => row.logicalId === logicalId);
const bound = row && deriveViewMembers([row])[0];
@@ -357,7 +358,7 @@ export class TerminalService {
// Its logical ID names the absence; it is never used as a tmux target.
return bound ?? { seat: logicalId, label: logicalId, tmuxSession: null, host: null, readOnly: false, alive: false };
});
return { id: "kernel", members, kernelLayout, columns: members.length };
return { id: "kernel", members, kernelLayout, columns: 1, panesPerPage: 1 };
}
/** Refine local members' liveness with a real has-session probe (a dead seat → absent, honest-partial). */
+30 -25
View File
@@ -335,9 +335,9 @@ describe("default saved kernel conversations", () => {
operatorReady = true;
const preview = await service.previewView({ view: "saved:kernel" });
if (!("composed" in preview)) throw new Error("expected preview");
expect((await service.openView({ view: "saved:kernel", expectedPlan: preview.planId })).opened).toEqual(["tui-bound", "advisor-bound", "operator-bound"]);
expect((await service.openView({ view: "saved:kernel", expectedPlan: preview.planId })).opened).toEqual(["operator-bound", "tui-bound", "advisor-bound"]);
const newLabel = planHerdrLayout(herdr.lastView!, "fixed").pages[0]!.tabLabel;
expect(newLabel).toBe(`openrig:kernel#${preview.planId.slice(0, 16)}#fixed`);
expect(newLabel).toBe(`openrig:kernel#${preview.planId.slice(0, 16)}#fixed/1`);
expect(newLabel).not.toBe(oldLabel);
});
@@ -349,7 +349,7 @@ describe("default saved kernel conversations", () => {
});
const result = await new TerminalService(deps).openView({ view: "saved:kernel" });
expect(result.ok).toBe(!unavailable);
expect(result.opened).toEqual(unavailable ? [] : ["tui-bound", "advisor-bound", "operator-bound"]);
expect(result.opened).toEqual(unavailable ? [] : ["operator-bound", "tui-bound", "advisor-bound"]);
expect(result.notes?.join("\n")).toContain("Default kernel view:");
expect(result.notes?.join("\n")).not.toMatch(/lead pane|Can you see the team/);
if (unavailable) expect(result.notes).toContain("operator.agent: env -u TMUX tmux attach -t 'operator-bound'");
@@ -374,28 +374,30 @@ describe("default saved kernel conversations", () => {
expect(result.notes!.join("\n")).not.toContain("queue-bound");
});
it.each([["claude-code", "codex"], ["claude-code", "claude-code"], ["codex", "codex"]])("kernel geometry keeps three columns in preview, fingerprint and open for %s/%s", async (advisorRuntime, operatorRuntime) => {
it.each([["claude-code", "codex"], ["claude-code", "claude-code"], ["codex", "codex"]])("opens the operator first with dashboard and advisor on their own pages for %s/%s", async (advisorRuntime, operatorRuntime) => {
const { deps, herdr } = makeKernel(kernel(operatorRuntime, advisorRuntime));
const service = new TerminalService(deps);
const preview = await service.previewView({ view: "saved:kernel" });
if (!("composed" in preview)) throw new Error("expected preview");
expect(preview.composed.columns).toBe(3);
expect(preview.grids).toHaveLength(1);
expect(preview.grids[0]).toMatchObject({ columns: 3, rows: 1, blanks: 0 });
expect(preview.grids[0]!.root).toMatchObject({
type: "split", direction: "right", ratio: 1 / 3,
first: { type: "pane", label: "operator.human" },
second: { type: "split", direction: "right", ratio: 1 / 2,
first: { type: "pane", label: "advisor.lead" },
second: { type: "pane", label: "operator.agent" } },
});
expect(preview.composed.columns).toBe(1);
expect(preview.composed.pages.map(page => page.map(pane => pane.seat))).toEqual([
["operator-bound"], ["tui-bound"], ["advisor-bound"],
]);
expect(preview.grids.map(({ columns, rows, blanks }) => ({ columns, rows, blanks }))).toEqual(
Array(3).fill({ columns: 1, rows: 1, blanks: 0 }),
);
expect(preview.grids.map(grid => grid.root)).toMatchObject([
{ type: "pane", label: "operator.agent" },
{ type: "pane", label: "operator.human" },
{ type: "pane", label: "advisor.lead" },
]);
expect(preview.planId).toBe(createHash("sha256").update(JSON.stringify({
provider: "herdr", composed: preview.composed, grids: preview.grids,
})).digest("hex"));
const result = await service.openView({ view: "saved:kernel", expectedPlan: preview.planId });
expect(result.ok).toBe(true);
expect(herdr.lastView).toEqual({ ...preview.composed, planId: preview.planId });
expect(planHerdrLayout(herdr.lastView!, "fixed").pages[0]!.root).toEqual(preview.grids[0]!.root);
expect(planHerdrLayout(herdr.lastView!, "fixed").pages.map(page => page.root)).toEqual(preview.grids.map(grid => grid.root));
});
it.each(["saved:kernel", "saved:custom", "rig:kernel"])("kernel geometry preserves auto-grid for %s outside the default", async view => {
@@ -407,7 +409,7 @@ describe("default saved kernel conversations", () => {
expect(preview.composed.columns).toBeUndefined();
expect(preview.grids[0]).toMatchObject({ columns: 2, rows: 2, blanks: 1 });
expect((await service.openView({ view, expectedPlan: preview.planId })).ok).toBe(true);
expect(planHerdrLayout(herdr.lastView!, "fixed").pages[0]!.root).toEqual(preview.grids[0]!.root);
expect(planHerdrLayout(herdr.lastView!, "fixed").pages.map(page => page.root)).toEqual(preview.grids.map(grid => grid.root));
});
it("kernel geometry caps partial views to available panes while retaining named absences", async () => {
@@ -417,12 +419,15 @@ describe("default saved kernel conversations", () => {
const service = new TerminalService(deps);
const preview = await service.previewView({ view: "saved:kernel" });
if (!("composed" in preview)) throw new Error("expected preview");
expect(preview.composed.columns).toBe(3);
expect(preview.grids[0]).toMatchObject({ columns: 2, rows: 1, blanks: 0 });
expect(preview.composed.columns).toBe(1);
expect(preview.composed.pages.map(page => page.map(pane => pane.seat))).toEqual([["tui-bound"], ["advisor-bound"]]);
expect(preview.grids.map(({ columns, rows, blanks }) => ({ columns, rows, blanks }))).toEqual(
Array(2).fill({ columns: 1, rows: 1, blanks: 0 }),
);
const result = await service.openView({ view: "saved:kernel", expectedPlan: preview.planId });
expect(result.opened).toEqual(["tui-bound", "advisor-bound"]);
expect(result.absent.map(member => member.seat)).toEqual(["operator.agent"]);
expect(planHerdrLayout(herdr.lastView!, "fixed").pages[0]!.root).toEqual(preview.grids[0]!.root);
expect(planHerdrLayout(herdr.lastView!, "fixed").pages.map(page => page.root)).toEqual(preview.grids.map(grid => grid.root));
});
it("kernel geometry keeps auto-grid on each page without treating map indexes as columns", async () => {
@@ -448,15 +453,15 @@ describe("default saved kernel conversations", () => {
const saved: SavedView[] = [];
const { deps, herdr } = makeKernel(kernel(operatorRuntime, advisorRuntime), saved);
const service = new TerminalService(deps);
const expected = ["tui-bound", "advisor-bound", "operator-bound"];
const expected = ["operator-bound", "tui-bound", "advisor-bound"];
expect((await service.listViews()).saved[0]?.members.map(member => member.seat)).toEqual(expected);
expect(herdr.lastView).toBeNull();
const preview = await service.previewView({ view: "saved:kernel" });
expect("composed" in preview && preview.composed.opened.map(member => member.seat)).toEqual(expected);
expect(herdr.lastView).toBeNull();
expect((await service.openView({ view: "saved:kernel" })).opened).toEqual(expected);
expect(herdr.lastView?.opened[0]?.paneCommand).toContain("'actual-tui'");
expect(herdr.lastView?.opened[1]?.runtime).toBe(advisorRuntime);
expect(herdr.lastView?.opened[1]?.paneCommand).toContain("'actual-tui'");
expect(herdr.lastView?.opened[2]?.runtime).toBe(advisorRuntime);
expect(saved).toEqual([]);
});
@@ -509,7 +514,7 @@ describe("default saved kernel conversations", () => {
const result = await new TerminalService(deps).openView({ view: "saved:kernel" });
expect(result.ok).toBe(true);
expect(result.opened).toEqual(["tui-bound"]);
expect(result.absent.map(member => member.seat)).toEqual(["advisor.lead", "operator.agent"]);
expect(result.absent.map(member => member.seat)).toEqual(["operator.agent", "advisor.lead"]);
expect(result.notes?.join(" ")).toContain("runtime layout unverified");
});
@@ -517,7 +522,7 @@ describe("default saved kernel conversations", () => {
const { deps } = makeKernel(kernel().filter(row => row.logicalId !== "advisor.lead"));
const result = await new TerminalService(deps).openView({ view: "saved:kernel" });
expect(result.ok).toBe(true);
expect(result.opened).toEqual(["tui-bound", "operator-bound"]);
expect(result.opened).toEqual(["operator-bound", "tui-bound"]);
expect(result.absent.map(member => member.seat)).toEqual(["advisor.lead"]);
expect(result.notes?.join(" ")).toContain("runtime layout unverified");
});
@@ -532,7 +537,7 @@ describe("default saved kernel conversations", () => {
const result = await service.openView({ view: "saved:kernel" });
expect(result.code).toBe("kernel_seats_unavailable");
expect(result.opened).toEqual([]);
expect(result.absent.map(member => member.seat)).toEqual(["operator.human", "advisor.lead", "operator.agent"]);
expect(result.absent.map(member => member.seat)).toEqual(["operator.agent", "operator.human", "advisor.lead"]);
for (const member of result.absent) expect(result.error).toContain(member.seat);
expect(herdr.lastView).toBeNull();
});
+3 -2
View File
@@ -38,8 +38,9 @@ guidance in `AGENTS.md` or `CLAUDE.md` plus the team's skills and plugins. Say s
Keep it.
- **Open the welcome screen:** for “show me my agents”, “show me the terminals”, “see my agents” or “welcome screen”, run
`rig terminal open saved:kernel --window` on the daemon's desktop. Otherwise ask “Open the OpenRig view now?”
first. It opens a new terminal tab/window itself: TUI | advisor | operator, using herdr when installed or plain
tmux otherwise. Preserve the current terminal. Check the result and visible content, or report what cannot be
first. It opens a new terminal tab/window itself with one full-width operator pane, using herdr when installed
or plain tmux otherwise. The dashboard and advisor each have their own tab or tmux window; select one to switch.
Preserve the current terminal. Check the result and visible content, or report what cannot be
verified. On a Mac the first open can raise two macOS prompts: “… is an app downloaded from the Internet”
(Open) and “… wants access to control …” (Allow). Tell the person to expect them and accept both; if the open
timed out while a prompt was up, check the desktop and run it once more. If the view is already open, point them