aboutsummaryrefslogtreecommitdiffstats
path: root/cli
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-13 22:07:45 +0200
committeruser <user@clank>2026-08-13 22:07:45 +0200
commita8ea4e888938e562885b1a0f009f5743939a879b (patch)
tree2ae5df21d4919cfe128830376b55dca814418496 /cli
parentchecks: ignore __pycache__ (the up check imports a sibling check) (diff)
downloadsl0ppi-a8ea4e888938e562885b1a0f009f5743939a879b.tar.gz
sl0ppi-a8ea4e888938e562885b1a0f009f5743939a879b.tar.xz
sl0ppi-a8ea4e888938e562885b1a0f009f5743939a879b.zip
sl0p: keep a templated tab's purpose, so runtime tabs get a status bar
Clicking the bar's `+` made a tab with no status bar. The button only exposed it: pressing `n` in TAB mode did the same, in any `sl0ppi up` session. The chain: a generated layout had no default_tab_template, so a tab created at runtime had nothing to take a bar from. It had none because parse_tab_node_with_template read a tab node's name, cwd and focus but never its `purpose` -- so templating erased the purpose that IS identity here (`sl0ppi ls` finds project tabs by it), and the CLI worked around that by inlining the template into every tab instead of emitting it. Fixed at the root: a templated tab keeps its own purpose, and the CLI emits `default_tab_template` again. The generated layout also shrinks -- one copy of the bar's configuration instead of one per tab. The two properties trade off against each other, so sl0ppi-up-live.py now asserts both at once: `sl0ppi ls` still sees a tab per project, and every tab including one created at runtime has exactly one status bar. Confirmed by reverting each half separately: without the parser fix the tabs lose their purpose and `ls` shows "alpha —"; without the CLI change the runtime tab has zero bars, which is the reported bug. Two measurement traps cost an hour here and are now written down: counting bars by grepping dump-layout also counts the template and every swap layout ("4 bars for 3 tabs"), and list-panes stops reporting borderless plugin panes for a tab once a client has rendered it. Also in here, from the same session: - the split button check found buttons by searching for " + " on any row with a frame glyph, which matches the stack list's "+N more" once two panes sit side by side, and then reports the product broken. It now requires the frame corner the button is drawn against. - the checks killed their sessions but did not delete them, so each run left an "EXITED - attach to resurrect" entry: 35 had quietly filled the session picker. They now delete-session by name. - and the trap that cost the user their running client: I killed a bare `sl0ppij` because "there are no user sessions on this box". That was a guess, not evidence, and wrong. A check's processes always carry that run's own socket dir or `attach sl0ptest-...`; a bare `sl0ppij` with a cwd in the repo is a human's. Written into the handover next to the `pkill -f` trap, which is the same mistake in a different costume.
Diffstat (limited to 'cli')
-rw-r--r--cli/src/core/layout.ts44
-rw-r--r--cli/test/layout.test.ts34
2 files changed, 56 insertions, 22 deletions
diff --git a/cli/src/core/layout.ts b/cli/src/core/layout.ts
index 6c2319e..0aa8e5c 100644
--- a/cli/src/core/layout.ts
+++ b/cli/src/core/layout.ts
@@ -161,16 +161,8 @@ function indent(text: string, spaces: number): string {
/**
* Fold a tab template around a project's panes.
*
- * We do NOT emit `default_tab_template` into the generated layout, even though
- * that is what it is for: a templated tab loses its `purpose=`, because the
- * fork hangs a tab's purpose on the tab's root layout and templating replaces
- * it. Verified -- with a template every tab came back `purpose=None`, which
- * breaks identity outright: `sl0ppi ls` sees no project tabs and `up` would
- * cheerfully create a second set.
- *
- * So we inline instead: take the template body and substitute the project's
- * panes for its `children` node. The tab node keeps its own name/purpose/cwd,
- * and the status bar comes along.
+ * Kept for callers that hand a layout STRING to a running session. The session
+ * layout emits `default_tab_template` instead -- see `sessionLayoutKdl`.
*/
export function applyTemplate(fragment: string, template: string): string | undefined {
const body = template.slice(template.indexOf("{") + 1, template.lastIndexOf("}"));
@@ -219,17 +211,41 @@ function preamble(paneTemplates?: string[]): string {
return `${paneTemplates.map((template) => indent(template, 4)).join("\n")}\n`;
}
-/** A whole-session layout: what `up` passes to `--layout` when creating. */
+/**
+ * A whole-session layout: what `up` passes to `--layout` when creating.
+ *
+ * The tab template is EMITTED, not inlined into each tab. It used to be
+ * inlined, because a templated tab came back with `purpose=None` and purpose is
+ * identity here -- `sl0ppi ls` finds project tabs by it. The fork now keeps a
+ * tab's own purpose when a template is applied
+ * (`sl0p_a_templated_tab_keeps_its_own_purpose`), which removes that reason and
+ * fixes the thing the workaround cost us: a tab created at RUNTIME -- `new-tab`
+ * from a keybinding, or the bar's `+` button -- had no template to take a
+ * status bar from, and came up bare.
+ */
export function sessionLayoutKdl(
specs: TabSpec[],
template?: string,
paneTemplates?: string[],
): string {
- const tabs = specs.map((spec) => tabKdl(spec, 4, template)).join("\n");
- return `layout {\n${preamble(paneTemplates)}${tabs}\n}\n`;
+ const tabs = specs.map((spec) => tabKdl(spec, 4)).join("\n");
+ return `layout {\n${preamble(paneTemplates)}${templateBlock(template)}${tabs}\n}\n`;
}
-/** A one-tab layout: what `up` passes to `new-tab --layout-string`. */
+/** The `default_tab_template { ... }` node, indented into the layout block. */
+function templateBlock(template?: string): string {
+ if (!template) return "";
+ return `${indent(template, 4)}\n`;
+}
+
+/**
+ * A one-tab layout: what `open` passes to `new-tab --layout-string`.
+ *
+ * Still inlined: this layout describes ONE tab being added to a session that
+ * already exists, and a `default_tab_template` in it would apply to that tab
+ * anyway -- but the session's own template (if it has one) would apply first,
+ * and the tab would come up with two status bars.
+ */
export function singleTabLayoutKdl(
spec: TabSpec,
template?: string,
diff --git a/cli/test/layout.test.ts b/cli/test/layout.test.ts
index 1910529..01b59da 100644
--- a/cli/test/layout.test.ts
+++ b/cli/test/layout.test.ts
@@ -146,19 +146,37 @@ describe("extractDefaultTabTemplate", () => {
expect(extractDefaultTabTemplate("layout {\n default_tab_template {\n children\n")).toBeUndefined();
});
- // A templated tab loses its purpose (the fork hangs tab purpose on the tab's
- // root layout, which templating replaces), and purpose IS identity here. So
- // the template is inlined into each tab rather than emitted as
- // default_tab_template.
- test("the template is inlined, and the tab keeps its identity", () => {
+ // This used to assert the opposite -- that the template was INLINED into each
+ // tab -- because a templated tab came back with purpose=None and purpose is
+ // identity here. The fork now keeps a tab's own purpose through templating
+ // (sl0p_a_templated_tab_keeps_its_own_purpose), and inlining had a cost: a
+ // tab created at runtime (`new-tab`, or the bar's + button) had no template
+ // to take a status bar from and came up bare.
+ test("the session layout emits the template, and the tabs keep their identity", () => {
const template = 'default_tab_template {\n children\n pane size=1 { plugin location="file:~/z.wasm" }\n}';
const kdl = sessionLayoutKdl([spec], template);
- expect(kdl).not.toContain("default_tab_template");
- expect(kdl).toContain('purpose="project:foo"'); // identity survives
+ expect(kdl).toContain("default_tab_template"); // runtime tabs get a bar
+ expect(kdl).toContain('purpose="project:foo"'); // identity survives
expect(kdl).toContain("z.wasm"); // status bar came along
expect(kdl).toContain('purpose="agent:main"'); // the project's own panes
- expect(kdl.indexOf('purpose="project:foo"')).toBeLessThan(kdl.indexOf("z.wasm"));
+ // the template is declared before the tabs that use it
+ expect(kdl.indexOf("default_tab_template")).toBeLessThan(kdl.indexOf('name="foo"'));
+ // ...and exactly once, however many tabs there are
+ const twoTabs = sessionLayoutKdl([spec, { ...spec, name: "bar", relPath: "bar" }], template);
+ expect(twoTabs.split("z.wasm").length - 1).toBe(1);
+ });
+
+ // A layout string handed to a RUNNING session is a different case: the
+ // session's own template applies to the new tab, so emitting another one
+ // would give it two status bars.
+ test("a single-tab layout string still inlines", () => {
+ const template = 'default_tab_template {\n children\n pane size=1 { plugin location="file:~/z.wasm" }\n}';
+ const kdl = singleTabLayoutKdl(spec, template);
+
+ expect(kdl).not.toContain("default_tab_template");
+ expect(kdl).toContain("z.wasm");
+ expect(kdl).toContain('purpose="project:foo"');
});
test("a template without a children node is skipped rather than mangled", async () => {