@gaia-ai/addon-herdr 0.9.1 → 0.10.0

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.
@@ -13,6 +13,12 @@ export interface StateConfig {
13
13
  */
14
14
  tabLabel?: string;
15
15
  workspaceLabel?: string;
16
+ /**
17
+ * Root-pane (panel) label template applied via `herdr pane rename` after the
18
+ * state tab is created (GAIA-374). Default `{title}`. Distinct from
19
+ * {@link tabLabel}: stop/state matching still anchors on the tab label.
20
+ */
21
+ panelLabel?: string;
16
22
  /** Extra layout panes (diff/logs); NOT the agent command. */
17
23
  panes?: PaneSpec[];
18
24
  }
@@ -47,6 +53,7 @@ export interface HerdrExecutorOptions {
47
53
  export interface ResolvedConfig {
48
54
  tabLabel: string;
49
55
  workspaceLabel: string;
56
+ panelLabel: string;
50
57
  panes: PaneSpec[];
51
58
  }
52
59
  export declare const BUILTIN_DEFAULTS: ResolvedConfig;
@@ -1,6 +1,7 @@
1
1
  export const BUILTIN_DEFAULTS = {
2
2
  tabLabel: '{identifier} · {titleSlug}',
3
3
  workspaceLabel: '{repoName}',
4
+ panelLabel: '{title}',
4
5
  panes: [],
5
6
  };
6
7
  export function resolveStateConfig(options, state) {
@@ -11,6 +12,7 @@ export function resolveStateConfig(options, state) {
11
12
  workspaceLabel: stateCfg.workspaceLabel ??
12
13
  base.workspaceLabel ??
13
14
  BUILTIN_DEFAULTS.workspaceLabel,
15
+ panelLabel: stateCfg.panelLabel ?? base.panelLabel ?? BUILTIN_DEFAULTS.panelLabel,
14
16
  panes: stateCfg.panes ?? base.panes ?? BUILTIN_DEFAULTS.panes,
15
17
  };
16
18
  }
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { exec, shellQuote, slugify } from '@gaia-ai/core';
3
- import { resolveStateConfig, } from './config.js';
3
+ import { renderTemplate, resolveStateConfig, } from './config.js';
4
4
  import { forceRemoveDir, restoreWritable } from './fs.js';
5
5
  import { applyPaneLayout, isRecord, parseJson, } from './pane-layout.js';
6
6
  import { AGENT_SHELL_ENV, envArgs } from './panes.js';
@@ -679,6 +679,15 @@ export class HerdrExecutor {
679
679
  '--no-focus',
680
680
  ...envArgs(AGENT_SHELL_ENV),
681
681
  ]));
682
+ // GAIA-374: panel label on the state root pane (best-effort). Tab --label
683
+ // above stays the stop/state matching string and is intentionally not the
684
+ // resolved StateConfig.tabLabel path.
685
+ await this.execHerdr([
686
+ 'pane',
687
+ 'rename',
688
+ rootPaneId,
689
+ renderTemplate(cfg.panelLabel, vars),
690
+ ]).catch(() => { });
682
691
  try {
683
692
  // 4+5. Run the agent command in the root pane, then split each configured
684
693
  // pane — the shared pane-layout primitive (same code path as the workspace
@@ -19,13 +19,39 @@ export type GitExec = (args: string[]) => Promise<string>;
19
19
  * (the executor) pre-assembles `KEY='value' <agent>` env prefixes whose values
20
20
  * may legitimately contain braces (spec D2).
21
21
  *
22
+ * GAIA-374: `tabLabel` / `panelLabel` are chrome strings applied after the
23
+ * layout via `herdr tab rename` / `herdr pane rename` ("panel label" = pane
24
+ * label). Defaults ship when keys are absent on an active layout; `open: {}`
25
+ * remains the full opt-out (no command, no splits, no labels). The overview
26
+ * **tab** default is `{identifier} · {title}` with `{title}` capped at
27
+ * {@link OPEN_TAB_TITLE_MAX} so the always-visible tab bar stays readable;
28
+ * the pane/panel label still gets the full title.
29
+ *
22
30
  * Unset → {@link DEFAULT_OPEN_LAYOUT}, the ticket deep link. `{}` → the bare
23
31
  * empty tab (`applyPaneLayout` issues no calls for an empty layout).
24
32
  */
25
33
  export interface OpenLayout {
26
34
  command?: string;
27
35
  panes?: PaneSpec[];
36
+ /**
37
+ * Overview tab label template. Default: `{identifier} · {title}` (title
38
+ * truncated to {@link OPEN_TAB_TITLE_MAX} when rendering).
39
+ */
40
+ tabLabel?: string;
41
+ /**
42
+ * Overview root-pane label template (`herdr pane rename`). Default:
43
+ * `{identifier} · {title}` (full title).
44
+ */
45
+ panelLabel?: string;
28
46
  }
47
+ /** Max `{title}` length substituted into the overview **tab** label (GAIA-374). */
48
+ export declare const OPEN_TAB_TITLE_MAX = 30;
49
+ /** Cap overview-tab `{title}` and append `...` when truncated. */
50
+ export declare function truncateOpenTabTitle(title: string): string;
51
+ /** Default overview tab chrome (GAIA-374) — always-visible tab bar identity. */
52
+ export declare const DEFAULT_OPEN_TAB_LABEL = "{identifier} \u00B7 {title}";
53
+ /** Default overview pane/panel label template (GAIA-374). */
54
+ export declare const DEFAULT_OPEN_PANEL_LABEL = "{identifier} \u00B7 {title}";
29
55
  /**
30
56
  * The shipped default open layout (GAIA-234): a fresh worktree's initial pane
31
57
  * renders the ticket it belongs to instead of an anonymous shell prompt.
@@ -72,8 +98,9 @@ export declare class HerdrWorkspace implements GaiaWorkspace {
72
98
  private readonly open;
73
99
  private readonly logger;
74
100
  /**
75
- * Root pane ids captured by a FRESH `ensure` create, keyed by worktree path,
101
+ * Root panes captured by a FRESH `ensure` create, keyed by worktree path,
76
102
  * awaiting {@link applyOpenLayout} (GAIA-237). One-shot: consumed on apply.
103
+ * Optional `tabId` powers the GAIA-374 overview tab rename.
77
104
  */
78
105
  private readonly pendingRootPanes;
79
106
  /** Herdr-backed intent-level agent host for `gaia ui` (GAIA-190). */
@@ -115,6 +142,10 @@ export declare class HerdrWorkspace implements GaiaWorkspace {
115
142
  * The pane's cwd is the worktree checkout, passed explicitly on the splits
116
143
  * (the root pane's cwd is already the checkout).
117
144
  *
145
+ * GAIA-374 then renames the overview tab (`{identifier} · {title}` with title
146
+ * capped at {@link OPEN_TAB_TITLE_MAX}) and root pane (full
147
+ * `{identifier} · {title}`) — fresh-create only, same gate as the layout.
148
+ *
118
149
  * NO stored pane ⇒ silent no-op. That is the reused-worktree case: `ensure`
119
150
  * took the reuse branch, there is no fresh root pane, and re-applying would
120
151
  * start a duplicate editor process on reattach (GAIA-139). The capture is
@@ -127,7 +158,15 @@ export declare class HerdrWorkspace implements GaiaWorkspace {
127
158
  applyOpenLayout(path: string, ctx: {
128
159
  identifier: string;
129
160
  branch: string;
161
+ title: string;
130
162
  }): Promise<void>;
163
+ /**
164
+ * Best-effort overview tab + pane renames (GAIA-374). Skipped for the
165
+ * `open: {}` opt-out. A missing create-time tab id falls back to `pane get`.
166
+ */
167
+ private applyOverviewLabels;
168
+ /** `pane get <id>` → `result.pane.tab_id` when present. */
169
+ private resolveTabIdForPane;
131
170
  }
132
171
  export declare function herdrWorkspace(opts?: {
133
172
  /**
@@ -5,6 +5,19 @@ import { herdrAgentHost } from './agents.js';
5
5
  import { renderTemplate } from './config.js';
6
6
  import { applyPaneLayout, isRecord, parseJson } from './pane-layout.js';
7
7
  import { deriveHerdrRoot } from './root-anchor.js';
8
+ /** Max `{title}` length substituted into the overview **tab** label (GAIA-374). */
9
+ export const OPEN_TAB_TITLE_MAX = 30;
10
+ /** Cap overview-tab `{title}` and append `...` when truncated. */
11
+ export function truncateOpenTabTitle(title) {
12
+ if (title.length <= OPEN_TAB_TITLE_MAX) {
13
+ return title;
14
+ }
15
+ return `${title.slice(0, OPEN_TAB_TITLE_MAX)}...`;
16
+ }
17
+ /** Default overview tab chrome (GAIA-374) — always-visible tab bar identity. */
18
+ export const DEFAULT_OPEN_TAB_LABEL = '{identifier} · {title}';
19
+ /** Default overview pane/panel label template (GAIA-374). */
20
+ export const DEFAULT_OPEN_PANEL_LABEL = '{identifier} · {title}';
8
21
  /**
9
22
  * The shipped default open layout (GAIA-234): a fresh worktree's initial pane
10
23
  * renders the ticket it belongs to instead of an anonymous shell prompt.
@@ -19,6 +32,8 @@ import { deriveHerdrRoot } from './root-anchor.js';
19
32
  */
20
33
  export const DEFAULT_OPEN_LAYOUT = {
21
34
  command: 'gaia ui --ticket {identifier}',
35
+ tabLabel: DEFAULT_OPEN_TAB_LABEL,
36
+ panelLabel: DEFAULT_OPEN_PANEL_LABEL,
22
37
  };
23
38
  const defaultExec = (args) => exec('herdr', args);
24
39
  const defaultGitExec = (args) => exec('git', args);
@@ -61,18 +76,22 @@ function parseWorktreePath(output, command) {
61
76
  return path;
62
77
  }
63
78
  /**
64
- * Parse `worktree create` → the created tab's root pane id
65
- * (`result.root_pane.pane_id`), sibling of {@link parseWorktreePath}. Returns
66
- * null when the create JSON carries none, so the open-layout caller can skip
79
+ * Parse `worktree create` → root pane id (+ optional tab id for GAIA-374
80
+ * overview tab rename). Sibling of {@link parseWorktreePath}. Returns null when
81
+ * the create JSON carries no pane id, so the open-layout caller can skip
67
82
  * silently rather than throw (best-effort).
68
83
  */
69
84
  function parseWorktreeRootPane(output) {
70
85
  const parsed = parseJson(output, 'worktree create');
71
- const paneId = isRecord(parsed)
72
- ? parsed.result
73
- ?.root_pane?.pane_id
74
- : undefined;
75
- return typeof paneId === 'string' && paneId ? paneId : null;
86
+ if (!isRecord(parsed) || !isRecord(parsed.result))
87
+ return null;
88
+ const result = parsed.result;
89
+ const paneId = result.root_pane?.pane_id;
90
+ if (typeof paneId !== 'string' || !paneId)
91
+ return null;
92
+ const tabIdRaw = result.root_pane?.tab_id ?? result.tab?.tab_id;
93
+ const tabId = typeof tabIdRaw === 'string' && tabIdRaw ? tabIdRaw : undefined;
94
+ return tabId !== undefined ? { paneId, tabId } : { paneId };
76
95
  }
77
96
  /** Git-safe branch validation: safe ref chars only, no leading dot, no `..`. */
78
97
  function validateBranch(branch) {
@@ -91,8 +110,9 @@ export class HerdrWorkspace {
91
110
  open;
92
111
  logger;
93
112
  /**
94
- * Root pane ids captured by a FRESH `ensure` create, keyed by worktree path,
113
+ * Root panes captured by a FRESH `ensure` create, keyed by worktree path,
95
114
  * awaiting {@link applyOpenLayout} (GAIA-237). One-shot: consumed on apply.
115
+ * Optional `tabId` powers the GAIA-374 overview tab rename.
96
116
  */
97
117
  pendingRootPanes = new Map();
98
118
  /** Herdr-backed intent-level agent host for `gaia ui` (GAIA-190). */
@@ -254,9 +274,9 @@ export class HerdrWorkspace {
254
274
  // pane command needed a sentinel wait loop. The conductor now calls
255
275
  // `applyOpenLayout` after the hook; a create with no root pane in its output
256
276
  // stores nothing, so the layout is a no-op exactly as before.
257
- const rootPaneId = parseWorktreeRootPane(createOutput);
258
- if (rootPaneId) {
259
- this.pendingRootPanes.set(path, rootPaneId);
277
+ const pending = parseWorktreeRootPane(createOutput);
278
+ if (pending) {
279
+ this.pendingRootPanes.set(path, pending);
260
280
  }
261
281
  // The `after_create` hook is run by the executor (GAIA-84), not here — the
262
282
  // workspace only reports that it created a fresh worktree.
@@ -269,6 +289,10 @@ export class HerdrWorkspace {
269
289
  * The pane's cwd is the worktree checkout, passed explicitly on the splits
270
290
  * (the root pane's cwd is already the checkout).
271
291
  *
292
+ * GAIA-374 then renames the overview tab (`{identifier} · {title}` with title
293
+ * capped at {@link OPEN_TAB_TITLE_MAX}) and root pane (full
294
+ * `{identifier} · {title}`) — fresh-create only, same gate as the layout.
295
+ *
272
296
  * NO stored pane ⇒ silent no-op. That is the reused-worktree case: `ensure`
273
297
  * took the reuse branch, there is no fresh root pane, and re-applying would
274
298
  * start a duplicate editor process on reattach (GAIA-139). The capture is
@@ -279,11 +303,12 @@ export class HerdrWorkspace {
279
303
  * and swallowed so a startup-command failure can NEVER wedge dispatch.
280
304
  */
281
305
  async applyOpenLayout(path, ctx) {
282
- const rootPaneId = this.pendingRootPanes.get(path);
283
- if (!rootPaneId) {
306
+ const pending = this.pendingRootPanes.get(path);
307
+ if (!pending) {
284
308
  return;
285
309
  }
286
310
  this.pendingRootPanes.delete(path);
311
+ const { paneId: rootPaneId } = pending;
287
312
  try {
288
313
  // Render the ROOT command here (spec D2). `applyPaneLayout` types its
289
314
  // root command verbatim for the executor's brace-bearing env-prefixed
@@ -292,6 +317,7 @@ export class HerdrWorkspace {
292
317
  const vars = {
293
318
  identifier: ctx.identifier,
294
319
  branch: ctx.branch,
320
+ title: ctx.title,
295
321
  worktreePath: path,
296
322
  };
297
323
  const layout = {
@@ -301,11 +327,66 @@ export class HerdrWorkspace {
301
327
  : {}),
302
328
  };
303
329
  await applyPaneLayout(this.execHerdr, rootPaneId, layout, path, vars);
330
+ await this.applyOverviewLabels(rootPaneId, pending.tabId, vars);
304
331
  }
305
332
  catch (err) {
306
333
  this.logger.warn({ branch: ctx.branch, worktree: path, err: String(err) }, 'herdr open-layout failed (best-effort, ignored)');
307
334
  }
308
335
  }
336
+ /**
337
+ * Best-effort overview tab + pane renames (GAIA-374). Skipped for the
338
+ * `open: {}` opt-out. A missing create-time tab id falls back to `pane get`.
339
+ */
340
+ async applyOverviewLabels(rootPaneId, createTabId, vars) {
341
+ const emptyOptOut = this.open.command === undefined &&
342
+ !this.open.panes?.length &&
343
+ this.open.tabLabel === undefined &&
344
+ this.open.panelLabel === undefined;
345
+ if (emptyOptOut) {
346
+ return;
347
+ }
348
+ // Tab bar is always visible; cap title length and mark truncation with
349
+ // `...`. Pane/panel label keeps the full title for pane borders.
350
+ const tabVars = {
351
+ ...vars,
352
+ title: truncateOpenTabTitle(vars.title ?? ''),
353
+ };
354
+ const tabLabel = renderTemplate(this.open.tabLabel ?? DEFAULT_OPEN_TAB_LABEL, tabVars);
355
+ const panelLabel = renderTemplate(this.open.panelLabel ?? DEFAULT_OPEN_PANEL_LABEL, vars);
356
+ let tabId = createTabId;
357
+ if (!tabId) {
358
+ try {
359
+ tabId = (await this.resolveTabIdForPane(rootPaneId)) ?? undefined;
360
+ }
361
+ catch (err) {
362
+ this.logger.warn({ paneId: rootPaneId, err: String(err) }, 'herdr overview tab id lookup failed (best-effort, ignored)');
363
+ }
364
+ }
365
+ if (tabId) {
366
+ try {
367
+ await this.execHerdr(['tab', 'rename', tabId, tabLabel]);
368
+ }
369
+ catch (err) {
370
+ this.logger.warn({ tabId, err: String(err) }, 'herdr overview tab rename failed (best-effort, ignored)');
371
+ }
372
+ }
373
+ try {
374
+ await this.execHerdr(['pane', 'rename', rootPaneId, panelLabel]);
375
+ }
376
+ catch (err) {
377
+ this.logger.warn({ paneId: rootPaneId, err: String(err) }, 'herdr overview pane rename failed (best-effort, ignored)');
378
+ }
379
+ }
380
+ /** `pane get <id>` → `result.pane.tab_id` when present. */
381
+ async resolveTabIdForPane(paneId) {
382
+ const output = await this.execHerdr(['pane', 'get', paneId, '--json']);
383
+ const parsed = parseJson(output, 'pane get');
384
+ const tabId = isRecord(parsed)
385
+ ? parsed.result?.pane
386
+ ?.tab_id
387
+ : undefined;
388
+ return typeof tabId === 'string' && tabId ? tabId : null;
389
+ }
309
390
  }
310
391
  export function herdrWorkspace(opts = {}) {
311
392
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/addon-herdr",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "GAIA herdr integration: spawn executor + per-ticket worktree workspace.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,10 +20,10 @@
20
20
  "directory": "gaia-cli/addons/herdr"
21
21
  },
22
22
  "dependencies": {
23
- "@gaia-ai/addon-workspace-git": "^0.9.1"
23
+ "@gaia-ai/addon-workspace-git": "^0.10.0"
24
24
  },
25
25
  "peerDependencies": {
26
- "@gaia-ai/conductor": "^0.9.1",
27
- "@gaia-ai/core": "^0.9.1"
26
+ "@gaia-ai/conductor": "^0.10.0",
27
+ "@gaia-ai/core": "^0.10.0"
28
28
  }
29
29
  }