@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.
- package/dist/src/config.d.ts +7 -0
- package/dist/src/config.js +2 -0
- package/dist/src/index.js +10 -1
- package/dist/src/workspace.d.ts +40 -1
- package/dist/src/workspace.js +95 -14
- package/package.json +4 -4
package/dist/src/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/config.js
CHANGED
|
@@ -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
|
package/dist/src/workspace.d.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|
package/dist/src/workspace.js
CHANGED
|
@@ -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` →
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
258
|
-
if (
|
|
259
|
-
this.pendingRootPanes.set(path,
|
|
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
|
|
283
|
-
if (!
|
|
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.
|
|
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.
|
|
23
|
+
"@gaia-ai/addon-workspace-git": "^0.10.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@gaia-ai/conductor": "^0.
|
|
27
|
-
"@gaia-ai/core": "^0.
|
|
26
|
+
"@gaia-ai/conductor": "^0.10.0",
|
|
27
|
+
"@gaia-ai/core": "^0.10.0"
|
|
28
28
|
}
|
|
29
29
|
}
|