diff options
| author | user <user@clank> | 2026-08-13 22:07:45 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-13 22:07:45 +0200 |
| commit | a8ea4e888938e562885b1a0f009f5743939a879b (patch) | |
| tree | 2ae5df21d4919cfe128830376b55dca814418496 /cli | |
| parent | checks: ignore __pycache__ (the up check imports a sibling check) (diff) | |
| download | sl0ppi-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.ts | 44 | ||||
| -rw-r--r-- | cli/test/layout.test.ts | 34 |
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 () => { |
