diff --git a/CHANGELOG.md b/CHANGELOG.md index b046a65..d37cc8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [Unreleased] + +### Added + +- `[ui].placement` for how Sessionizer / Worktree pickers open in Herdr: `overlay`, `split`, or `popup` (popup requires Herdr `>= 0.7.4`) +- Optional `[ui].width` / `[ui].height` for popup outer size (terminal cells or percentages such as `"80%"`) + +### Changed + +- Picker placement moves from `[layout].placement` to `[ui]`; workspace bootstrap `[layout]` now only carries `focus` (no fallback for the old key) +- New default config uses `[ui].placement = "popup"` with `width` / `height` = `"80%"` +- Placement is no longer required when `[tabs]` are defined (omitted `[ui]` still falls back to `overlay`) +- Minimum Herdr version is now `0.7.4` (`min_herdr_version` + README prerequisites; required for popup picker placement) + ## [0.6.2] - 2026-07-26 ### Added diff --git a/README.md b/README.md index 40714c3..750d482 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Inspired by [ThePrimeagen's tmux-sessionizer](https://github.com/ThePrimeagen/tm Sessionizer does not install system tools for you. -- [Herdr](https://herdr.dev/) `>= 0.7.0` +- [Herdr](https://herdr.dev/) `>= 0.7.4` - [Bun](https://bun.sh/) — plugin build and runtime - [fzf](https://github.com/junegunn/fzf) — interactive pickers @@ -123,10 +123,14 @@ When Sessionizer **creates** a new project or worktree workspace, it applies the Created automatically on first run if missing. -`[layout]` and `[tabs]` are optional. A config with only `[projects]` is +`[ui]`, `[layout]`, and `[tabs]` are optional. A config with only `[projects]` is valid: new workspaces then open with a plain shell and no layout is applied. -When `[tabs]` sections exist, `[layout].placement` and `[layout].focus` are -required. +When `[tabs]` sections exist, `[layout].focus` is required. + +`[ui]` controls how Sessionizer / Worktree **pickers** open inside Herdr (not +workspace bootstrap). New configs default to a session-modal `popup` at `80%` +width/height (Herdr `>= 0.7.4`). You can switch to `overlay` or `split`, or omit +`[ui]` entirely to fall back to `overlay`. If you want an agent to help edit either the global config or a repo-local override, see [Agent skills](#agent-skills). @@ -138,8 +142,12 @@ roots = ["~/Projects", "~/Workspace"] git_only = true depth = 1 +[ui] +placement = "popup" # overlay | split | popup (popup needs Herdr >= 0.7.4) +width = "80%" +height = "80%" + [layout] -placement = "overlay" focus = "editor" [tabs.dev] @@ -202,7 +210,8 @@ Second tab shape: - `[projects].roots` — parent folders scanned by both pickers (plain paths; optional globs — see [Glob roots](#glob-roots-optional) below) - `[projects].git_only` — `true` returns only directories with `.git` metadata; `false` lists all immediate child folders - `[projects].depth` — maximum levels below each root to scan when `git_only = true`; `1` means immediate children -- `[layout].placement` — how plugin panes open (`overlay` or `split`) +- `[ui].placement` — how Sessionizer / Worktree pickers open in Herdr (`overlay`, `split`, or `popup`; new configs default to `popup`, which needs Herdr `>= 0.7.4`) +- `[ui].width` / `[ui].height` — popup outer size (cells or `"80%"`); only with `placement = "popup"` (new configs default to `"80%"`) - `[layout].focus` — which tab or pane to focus after layout bootstrap - `[tabs.]` — one Herdr tab to create per section - `[[tabs..panes]]` — panes inside the tab; `from` + `split` (`right` or `down`) define the split tree @@ -264,7 +273,7 @@ When Sessionizer or Worktree creates a new workspace at `cwd`, Sessionizer check 1. `/.sessionizer/config.toml` — if present, use its `[layout].focus` and `[tabs.*]` (full replacement; no merge with global tabs) 2. Global `config.toml` — default layout -`[projects].roots` and `[layout].placement` always come from the global config. Repo-local files may include those sections, but they are ignored. Invalid repo-local config fails with an error that names the file path. +`[projects].roots` and `[ui]` (picker placement/size) always come from the global config. Repo-local files may include those sections, but they are ignored. Invalid repo-local config fails with an error that names the file path. | Event | Layout source | | ------------------------------------------- | ---------------------------------------------------- | diff --git a/docs/prd/per-repo-layout.md b/docs/prd/per-repo-layout.md index 99eab05..b05269a 100644 --- a/docs/prd/per-repo-layout.md +++ b/docs/prd/per-repo-layout.md @@ -8,7 +8,7 @@ Related: #12, ADR-0001, `src/config.ts`, `src/sessionizer.ts`, `src/worktree.ts` Today every newly created workspace uses the same global layout from the plugin config dir (`HERDR_PLUGIN_CONFIG_DIR/config.toml`). This PRD adds optional **repo-local layout overrides** at `/.sessionizer/config.toml` so each repository can declare its own tabs, panes, and focus target when Sessionizer or Worktree bootstraps a **new** workspace. -Global config continues to own `[projects].roots` and `[layout].placement`. Repo-local config owns only the layout slice: `[layout].focus` and `[tabs.*]`. +Global config continues to own `[projects].roots` and picker UI (`[ui].placement`). Repo-local config owns only the layout slice: `[layout].focus` and `[tabs.*]`. ## Goals @@ -38,7 +38,7 @@ $HERDR_PLUGIN_CONFIG_DIR/config.toml Required global fields (unchanged): - `[projects].roots` -- `[layout].placement` +- `[ui].placement` (optional; defaults to `overlay`) - `[layout].focus` and `[tabs.*]` — used as **default layout** when no repo override exists ### Repo-local override @@ -55,7 +55,7 @@ Allowed fields in repo-local file: Ignored if present in repo-local file (read from global only): - `[projects]` -- `[layout].placement` +- `[ui]` ### Lookup order at bootstrap time @@ -64,7 +64,7 @@ Given `layoutCwd` (absolute project or worktree checkout path): 1. If `join(layoutCwd, '.sessionizer', 'config.toml')` exists → parse as layout override 2. Else → use `focus` + `tabs` from global `config.toml` -`projects.roots` and `layout.placement` always come from global config. +`projects.roots` and `ui.placement` always come from global config. ### API shape (proposed) @@ -72,7 +72,12 @@ Given `layoutCwd` (absolute project or worktree checkout path): // src/config.ts export interface SessionizerConfig { projects: { roots: string[] }; - layout: { placement: PanePlacement; focus: string }; + ui: { + placement: PanePlacement; + width?: number | string; + height?: number | string; + }; + layout: { focus: string }; tabs: TabConfig[]; } @@ -81,8 +86,8 @@ export function loadConfig(): SessionizerConfig; // global; unchanged call sites export function resolveLayoutConfig( layoutCwd: string, global?: SessionizerConfig -): Pick; -// Returns merged config: global.placement + resolved focus/tabs +): SessionizerConfig; +// Returns merged config: global.ui + resolved focus/tabs ``` `resolveLayoutConfig` throws with a message that includes the repo-local path on parse/validation failure. @@ -208,4 +213,4 @@ Use `mkdtemp` fixtures; no dependency on real `llm-wiki` path in unit tests. | Partial overrides | No — full layout slice replacement | | Invalid repo config | Fail loud | | `[projects].roots` in repo file | Ignored | -| `layout.placement` in repo file | Ignored; global only | +| `ui` in repo file | Ignored; global only | diff --git a/docs/prd/worktree-close.md b/docs/prd/worktree-close.md index 17bc784..bd6c9b5 100644 --- a/docs/prd/worktree-close.md +++ b/docs/prd/worktree-close.md @@ -123,7 +123,7 @@ The action entry point follows the same thin-wrapper pattern as `src/open-worktr - `src/worktree-close.ts` — the core flow, runtime-injected for testability. Mirrors the structure of `src/worktree.ts`. - `src/worktree-close-pane.ts` — one-liner pane entry point: imports `runWorktreeClose`, calls it with the default runtime, logs and exits 1 on failure. Mirrors `src/sessionizer-pane.ts` (no env-var scaffolding, since the close flow takes no caller-supplied arguments). -- `src/open-worktree-close-pane.ts` — the action entry point. Mirrors `src/open-worktree-pane.ts`; opens the `worktree-close` pane using `config.layout.placement`. +- `src/open-worktree-close-pane.ts` — the action entry point. Mirrors `src/open-worktree-pane.ts`; opens the `worktree-close` pane using `config.ui.placement`. - `src/ui/workspace-row.ts` — extracted `workspaceRow` and `extractWorkspaceId` (currently private to `src/sessionizer.ts`). Adding `repo_name` brings the column count from 7 to 8. ### Modified files diff --git a/herdr-plugin.toml b/herdr-plugin.toml index e12c404..95b38d2 100644 --- a/herdr-plugin.toml +++ b/herdr-plugin.toml @@ -1,7 +1,7 @@ id = "sessionizer" name = "Sessionizer" version = "0.6.2" -min_herdr_version = "0.7.0" +min_herdr_version = "0.7.4" description = "Inspired by ThePrimeagen's tmux-sessionizer: fuzzy pickers to open projects and Git worktrees into Herdr workspaces." platforms = ["macos", "linux"] diff --git a/skills/sessionizer-layout-editor/SKILL.md b/skills/sessionizer-layout-editor/SKILL.md index 2ee9ebe..a2f271f 100644 --- a/skills/sessionizer-layout-editor/SKILL.md +++ b/skills/sessionizer-layout-editor/SKILL.md @@ -1,6 +1,6 @@ --- name: sessionizer-layout-editor -description: Sessionizer config edits. Use when the user wants project roots, git_only, or depth; global tabs, panes, focus, or split ratios; or a repo-local .sessionizer override. +description: Sessionizer config edits. Use when the user wants project roots, git_only, or depth; global tabs, panes, focus, or split ratios; picker [ui].placement (overlay|split|popup) or size; or a repo-local .sessionizer override. --- # Sessionizer Layout Editor @@ -12,31 +12,48 @@ description: Sessionizer config edits. Use when the user wants project roots, gi | global | `~/.config/herdr/plugins/config/sessionizer/config.toml` | | repo-local | `/.sessionizer/config.toml` | -**Bootstrap** — layout changes apply only when Sessionizer or Worktree creates a **new** workspace, not on reopen. +**Bootstrap** — layout changes (`[layout].focus`, `[tabs.*]`) apply only when Sessionizer or Worktree creates a **new** workspace, not on reopen. **Picker UI** (`[ui]`) applies every time a picker opens and is global-only. ## Workflow 1. Read the existing target file if present. 2. Enforce **scope**: - - **repo-local**: `[layout].focus` and `[tabs.*]` only — no `[projects]`, no `[layout].placement`; tabs fully replace global (no merge) - - **global**: `[projects]`, `[layout]`, and tabs/panes + - **repo-local**: `[layout].focus` and `[tabs.*]` only — no `[projects]`, no `[ui]`; tabs fully replace global (no merge) + - **global**: `[projects]`, `[ui]`, `[layout]`, and tabs/panes 3. Build or edit layout: - - `[layout].focus` required + - `[layout].focus` required when tabs exist - `[tabs.]` with `label` and `[[tabs..panes]]` - first pane: no `from`; later panes: `from` + `split` (`right` or `down`); optional `ratio` in `(0, 1)` on the split axis - pane ids unique per tab; `focus` must name an existing tab or pane -4. **Discovery** edits (global only): follow [references/discovery.md](references/discovery.md) -5. Minimal diff — change only what the user asked for; stay in the chosen **scope** -6. Done when: TOML is valid, scope rules hold, and you summarized the layout plus the **bootstrap** reminder +4. **Picker UI** edits (global only): + - `[ui].placement` is `overlay` | `split` | `popup` (popup needs Herdr `>= 0.7.4`) + - optional `[ui].width` / `[ui].height` only with `popup` (cells or `"80%"`) + - **do not** put placement under `[layout]` + - if the file still has legacy `[layout].placement`, **move it to `[ui].placement` and remove the old key** (no dual-write) + - new seeded configs default to `placement = "popup"`, `width = "80%"`, `height = "80%"`; omitted `[ui]` falls back to `overlay` at runtime +5. **Discovery** edits (global only): follow [references/discovery.md](references/discovery.md) +6. Minimal diff — change only what the user asked for; stay in the chosen **scope**. When editing global layout or discovery, **preserve existing `[ui]`** (and `[projects]` for layout edits) unless the user asked to change them. +7. Done when: TOML is valid, scope rules hold, and you summarized the layout/UI change plus the **bootstrap** reminder when layout tabs changed ## Examples - "Add `~/Work` to my project roots" → global `[projects].roots` - "Add `~/Projects/github.com/*` to my project roots" → global `[projects].roots`; globs expand at use-time — see reference - "Set `git_only = false`" → global discovery; see reference +- "Open pickers as a popup" → global `[ui].placement = "popup"` (optional width/height; default size is Herdr half-size if omitted) +- "Use the default popup size" → `placement = "popup"`, `width = "80%"`, `height = "80%"` - "Add a repo-local override with lazygit + copilot" → repo-local file - "Make the right pane 30% with `ratio = 0.3`" → layout pane edit in the active **scope** +## Global `[ui]` snippet + +```toml +[ui] +placement = "popup" # overlay | split | popup +width = "80%" +height = "80%" +``` + ## Repo-local template ```toml @@ -59,4 +76,4 @@ split = "right" command = "copilot" ``` -For global layout edits, preserve the existing `[projects]` section unless discovery is in **scope**. +For global layout edits, preserve the existing `[projects]` and `[ui]` sections unless those are in **scope**. diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 0cfbe03..3ad1d9b 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -14,7 +14,8 @@ import { function globalConfig(): SessionizerConfig { return { projects: { roots: ["/projects"], git_only: false, depth: 1 }, - layout: { placement: "overlay", focus: "editor" }, + ui: { placement: "overlay" }, + layout: { focus: "editor" }, tabs: [ { id: "dev", @@ -47,8 +48,10 @@ function minimalGlobalConfig(projects: string[]): string { "[projects]", ...projects, "", - "[layout]", + "[ui]", 'placement = "overlay"', + "", + "[layout]", 'focus = "editor"', "", "[tabs.dev]", @@ -100,6 +103,11 @@ describe("loadConfig", () => { expect(config.projects.git_only).toBe(true); expect(config.projects.depth).toBe(1); + expect(config.ui).toEqual({ + placement: "popup", + width: "80%", + height: "80%", + }); }); }); @@ -135,7 +143,7 @@ describe("resolveLayoutConfig", () => { const config = loadConfig(); expect(config.tabs).toEqual([]); - expect(config.layout.placement).toBe("overlay"); + expect(config.ui.placement).toBe("overlay"); expect(config.layout.focus).toBe(""); }); }); @@ -148,7 +156,7 @@ describe("resolveLayoutConfig", () => { "[projects]", 'roots = ["~/Projects"]', "", - "[layout]", + "[ui]", 'placement = "overlay"', "", "[tabs.dev]", @@ -166,7 +174,70 @@ describe("resolveLayoutConfig", () => { }); }); - it("still requires [layout].placement when [tabs] sections exist", () => { + it("defaults [ui].placement to overlay when omitted", () => { + withPluginConfigDir((dir) => { + writeFileSync( + join(dir, "config.toml"), + [ + "[projects]", + 'roots = ["~/Projects"]', + "", + "[layout]", + 'focus = "editor"', + "", + "[tabs.dev]", + 'label = "dev"', + "", + "[[tabs.dev.panes]]", + 'id = "editor"', + 'title = "nvim"', + 'command = "nvim"', + "", + ].join("\n"), + "utf-8" + ); + + expect(loadConfig().ui.placement).toBe("overlay"); + }); + }); + + it("reads [ui].placement including popup sizing", () => { + withPluginConfigDir((dir) => { + writeFileSync( + join(dir, "config.toml"), + [ + "[projects]", + 'roots = ["~/Projects"]', + "", + "[ui]", + 'placement = "popup"', + 'width = "80%"', + "height = 24", + "", + "[layout]", + 'focus = "editor"', + "", + "[tabs.dev]", + 'label = "dev"', + "", + "[[tabs.dev.panes]]", + 'id = "editor"', + 'title = "nvim"', + 'command = "nvim"', + "", + ].join("\n"), + "utf-8" + ); + + expect(loadConfig().ui).toEqual({ + placement: "popup", + width: "80%", + height: 24, + }); + }); + }); + + it("ignores legacy [layout].placement and defaults to overlay", () => { withPluginConfigDir((dir) => { writeFileSync( join(dir, "config.toml"), @@ -175,6 +246,39 @@ describe("resolveLayoutConfig", () => { 'roots = ["~/Projects"]', "", "[layout]", + 'placement = "split"', + 'focus = "editor"', + "", + "[tabs.dev]", + 'label = "dev"', + "", + "[[tabs.dev.panes]]", + 'id = "editor"', + 'title = "nvim"', + 'command = "nvim"', + "", + ].join("\n"), + "utf-8" + ); + + // Unknown keys under [layout] are ignored; placement only lives under [ui]. + expect(loadConfig().ui.placement).toBe("overlay"); + }); + }); + + it("rejects popup size when placement is not popup", () => { + withPluginConfigDir((dir) => { + writeFileSync( + join(dir, "config.toml"), + [ + "[projects]", + 'roots = ["~/Projects"]', + "", + "[ui]", + 'placement = "overlay"', + 'width = "80%"', + "", + "[layout]", 'focus = "editor"', "", "[tabs.dev]", @@ -190,7 +294,7 @@ describe("resolveLayoutConfig", () => { ); expect(() => loadConfig()).toThrow( - "Config must define [layout].placement as 'overlay' or 'split'." + 'Config [ui].width and [ui].height are only valid when [ui].placement = "popup".' ); }); }); @@ -224,7 +328,7 @@ describe("resolveLayoutConfig", () => { const resolved = resolveLayoutConfig(repoRoot, globalConfig()); - expect(resolved.layout.placement).toBe("overlay"); + expect(resolved.ui.placement).toBe("overlay"); expect(resolved.layout.focus).toBe("wiki"); expect(resolved.tabs).toEqual([ { diff --git a/src/config/config.ts b/src/config/config.ts index c1ba2b8..0ea84fb 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -6,7 +6,7 @@ import { expandHome } from "../discovery/discovery.ts"; import { parse } from "smol-toml"; -type PanePlacement = "overlay" | "split"; +export type PanePlacement = "overlay" | "split" | "popup"; type SplitDirection = "right" | "down"; interface RawPaneConfig { @@ -31,8 +31,12 @@ interface RawConfig { git_only?: boolean; depth?: unknown; }; - layout?: { + ui?: { placement?: string; + width?: unknown; + height?: unknown; + }; + layout?: { focus?: string; }; tabs?: Record; @@ -54,14 +58,23 @@ export interface TabConfig { panes: PaneConfig[]; } +export interface UiConfig { + /** How Sessionizer / Worktree pickers open inside Herdr. */ + placement: PanePlacement; + /** Outer popup width (cells or `"80%"`). Only used when placement is `popup`. */ + width?: number | string; + /** Outer popup height (cells or `"80%"`). Only used when placement is `popup`. */ + height?: number | string; +} + export interface SessionizerConfig { projects: { roots: string[]; git_only: boolean; depth: number; }; + ui: UiConfig; layout: { - placement: PanePlacement; focus: string; }; tabs: TabConfig[]; @@ -93,8 +106,8 @@ export function resolveLayoutConfig( return { projects: globalConfig.projects, + ui: globalConfig.ui, layout: { - placement: globalConfig.layout.placement, focus, }, tabs: buildTabs(raw), @@ -137,10 +150,8 @@ export function loadConfig(): SessionizerConfig { git_only: gitOnly, depth, }, + ui: resolveUiConfig(pluginConfig), layout: { - placement: asPlacement(pluginConfig?.layout?.placement, { - required: tabs.length > 0, - }), focus: focus ?? "", }, tabs, @@ -221,9 +232,14 @@ function defaultConfigToml(): string { "# Only used when git_only = true; 1 means immediate children", "depth = 1", "", + "[ui]", + "# How Sessionizer / Worktree pickers open in Herdr: overlay | split | popup", + "# popup requires Herdr >= 0.7.4 (width/height only apply to popup)", + 'placement = "popup"', + 'width = "80%"', + 'height = "80%"', + "", "[layout]", - "# How the plugin pane itself opens: overlay | split", - 'placement = "overlay"', "# Which pane or tab to focus after layout creation", 'focus = "editor"', "", @@ -258,14 +274,61 @@ function defaultConfigToml(): string { ].join("\n"); } -function asPlacement( - value: string | undefined, - options: { required: boolean } = { required: true } -): PanePlacement { - if (value === "overlay" || value === "split") return value; - if (value === undefined && !options.required) return "overlay"; +function resolveUiConfig(config: RawConfig | undefined): UiConfig { + const placement = asPlacement(config?.ui?.placement); + const width = asOptionalPopupSize(config?.ui?.width, "width"); + const height = asOptionalPopupSize(config?.ui?.height, "height"); + + if ((width !== undefined || height !== undefined) && placement !== "popup") { + throw new Error( + 'Config [ui].width and [ui].height are only valid when [ui].placement = "popup".' + ); + } + + return { + placement, + ...(width !== undefined ? { width } : {}), + ...(height !== undefined ? { height } : {}), + }; +} + +function asPlacement(value: string | undefined): PanePlacement { + if (value === undefined) return "overlay"; + if (value === "overlay" || value === "split" || value === "popup") { + return value; + } + throw new Error( + "Config must define [ui].placement as 'overlay', 'split', or 'popup'." + ); +} + +function asOptionalPopupSize( + value: unknown, + field: "width" | "height" +): number | string | undefined { + if (value === undefined) return undefined; + + if (typeof value === "number") { + if (!Number.isInteger(value) || value < 0 || value > 65535) { + throw new Error( + `Config [ui].${field} must be an integer cell count from 0 to 65535, or a percentage like "80%".` + ); + } + return value; + } + + if (typeof value === "string") { + const trimmed = value.trim(); + if (/^(100|[1-9][0-9]?)%$/.test(trimmed)) { + return trimmed; + } + throw new Error( + `Config [ui].${field} must be an integer cell count from 0 to 65535, or a percentage like "80%".` + ); + } + throw new Error( - "Config must define [layout].placement as 'overlay' or 'split'." + `Config [ui].${field} must be an integer cell count from 0 to 65535, or a percentage like "80%".` ); } diff --git a/src/layouts/project.test.ts b/src/layouts/project.test.ts index 2aae2c6..166f7af 100644 --- a/src/layouts/project.test.ts +++ b/src/layouts/project.test.ts @@ -11,7 +11,8 @@ import { function testConfig(overrides?: Partial): SessionizerConfig { return { projects: { roots: ["/tmp"], git_only: false, depth: 1 }, - layout: { placement: "overlay", focus: "assistant" }, + ui: { placement: "overlay" }, + layout: { focus: "assistant" }, tabs: [ { id: "terminal", diff --git a/src/sessionizer/open-pane.ts b/src/sessionizer/open-pane.ts index 64bdc89..7859ee5 100644 --- a/src/sessionizer/open-pane.ts +++ b/src/sessionizer/open-pane.ts @@ -14,7 +14,7 @@ async function run(): Promise { const herdr = new Herdr(); const config = loadConfig(); const placement = - process.env.SESSIONIZER_PANE_PLACEMENT ?? config.layout.placement; + process.env.SESSIONIZER_PANE_PLACEMENT ?? config.ui.placement; const args = [ "plugin", "pane", @@ -28,7 +28,12 @@ async function run(): Promise { "--focus", ]; - if (placement !== "overlay") { + if (placement === "popup") { + const width = process.env.SESSIONIZER_PANE_WIDTH ?? config.ui.width; + const height = process.env.SESSIONIZER_PANE_HEIGHT ?? config.ui.height; + if (width !== undefined) args.push("--width", String(width)); + if (height !== undefined) args.push("--height", String(height)); + } else if (placement !== "overlay") { if (process.env.HERDR_PANE_ID) { args.push("--target-pane", process.env.HERDR_PANE_ID); } else if (process.env.HERDR_WORKSPACE_ID) { diff --git a/src/sessionizer/sessionizer.test.ts b/src/sessionizer/sessionizer.test.ts index 39417d8..a489f15 100644 --- a/src/sessionizer/sessionizer.test.ts +++ b/src/sessionizer/sessionizer.test.ts @@ -11,7 +11,8 @@ import { runSessionizer } from "./sessionizer.ts"; function testConfig(): SessionizerConfig { return { projects: { roots: ["/projects"], git_only: false, depth: 1 }, - layout: { placement: "overlay", focus: "assistant" }, + ui: { placement: "overlay" }, + layout: { focus: "assistant" }, tabs: [], }; } @@ -282,7 +283,8 @@ describe("runSessionizer", () => { projectRoot, { ...config, - layout: { placement: "overlay", focus: "wiki" }, + ui: { placement: "overlay" }, + layout: { focus: "wiki" }, tabs: [ { id: "wiki", diff --git a/src/worktree/open-worktree-pane.ts b/src/worktree/open-worktree-pane.ts index dbc55fb..304c5ac 100644 --- a/src/worktree/open-worktree-pane.ts +++ b/src/worktree/open-worktree-pane.ts @@ -13,6 +13,7 @@ export async function openWorktreePane( const herdr = new Herdr(); const config = loadConfig(); + const placement = config.ui.placement; const args = [ "plugin", "pane", @@ -22,10 +23,19 @@ export async function openWorktreePane( "--entrypoint", "worktree", "--placement", - config.layout.placement, + placement, "--focus", ]; + if (placement === "popup") { + if (config.ui.width !== undefined) { + args.push("--width", String(config.ui.width)); + } + if (config.ui.height !== undefined) { + args.push("--height", String(config.ui.height)); + } + } + for (const [key, value] of Object.entries(extraEnv)) { args.push("--env", `${key}=${value}`); } diff --git a/src/worktree/worktree.test.ts b/src/worktree/worktree.test.ts index 7b38698..d95be17 100644 --- a/src/worktree/worktree.test.ts +++ b/src/worktree/worktree.test.ts @@ -38,7 +38,8 @@ function testRuntime( panes: {}, config: { projects: { roots: ["/repo"], git_only: false, depth: 1 }, - layout: { placement: "overlay", focus: "terminal" }, + ui: { placement: "overlay" }, + layout: { focus: "terminal" }, tabs: [], }, resolver: { resolveExisting: mock(async () => undefined) }, @@ -264,7 +265,8 @@ describe("runWorktree", () => { "/Users/mac/.herdr/worktrees/repo/feature-test-flow", { projects: { roots: ["/repo"], git_only: false, depth: 1 }, - layout: { placement: "overlay", focus: "terminal" }, + ui: { placement: "overlay" }, + layout: { focus: "terminal" }, tabs: [], }, {}, @@ -429,7 +431,8 @@ describe("runWorktree", () => { "/worktrees/repo/feature-test-flow", { projects: { roots: ["/repo"], git_only: false, depth: 1 }, - layout: { placement: "overlay", focus: "terminal" }, + ui: { placement: "overlay" }, + layout: { focus: "terminal" }, tabs: [], }, {},