@gaia-ai/addon-gaia-ui-artifacts 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 keytec GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @gaia-ai/addon-gaia-ui-artifacts
2
+
3
+ GAIA TUI: an Artifacts tab on ticket detail, reading assets-ai/<GAIA-nnn>/.
4
+
5
+ Part of the GAIA CLI. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all addons. Source: https://git.key-tec.de/keytec/gaia (gaia-cli/).
@@ -0,0 +1,6 @@
1
+ import type { UiPanel } from '@gaia-ai/addon-gaia-ui/ui';
2
+ /**
3
+ * The ticket's generated artifacts — the tree GAIA-371 D3 defines, which D1 left
4
+ * with no reader at all: transient, gitignored, and listed nowhere else.
5
+ */
6
+ export declare const artifactsPanel: UiPanel;
@@ -0,0 +1,53 @@
1
+ import { artifactRoots, artifactSections } from './tree.js';
2
+ /** How many lines of one artifact an expanded entry shows (spec D9). */
3
+ const HEAD_LINES = 200;
4
+ const attribute = (doc, name) => {
5
+ const value = doc?.data?.attributes?.[name];
6
+ return typeof value === 'string' ? value : '';
7
+ };
8
+ /**
9
+ * The ticket's generated artifacts — the tree GAIA-371 D3 defines, which D1 left
10
+ * with no reader at all: transient, gitignored, and listed nowhere else.
11
+ */
12
+ export const artifactsPanel = {
13
+ type: 'panel',
14
+ id: 'gaia/artifacts',
15
+ title: 'Artifacts',
16
+ // Present on every open ticket: an absent tree is an EMPTY STATE, not a
17
+ // hidden tab. Whether artifacts exist is exactly what the operator opens this
18
+ // to find out — a tab that vanished when there were none would answer the
19
+ // question by disappearing.
20
+ match: (ctx) => ctx.routeId === 'ticket.detail',
21
+ sections: (doc, ctx) => {
22
+ const identifier = attribute(doc, 'identifier');
23
+ if (ctx.cwd === undefined || identifier === '') {
24
+ return [
25
+ {
26
+ heading: 'Artifacts',
27
+ lines: [
28
+ 'This cockpit has no checkout to read artifacts from.',
29
+ 'Artifacts are transient and local (GAIA-371 D1/D10).',
30
+ ],
31
+ },
32
+ ];
33
+ }
34
+ const roots = artifactRoots(ctx.cwd, identifier, attribute(doc, 'branch_name'));
35
+ const sections = artifactSections(roots, { headLines: HEAD_LINES });
36
+ // The empty state NAMES BOTH PROBED PATHS. GAIA-371 D10 is a real hole —
37
+ // on another machine the worktree is gone — and naming the paths is what
38
+ // turns a blank tab into a diagnosis.
39
+ return sections.length > 0
40
+ ? sections
41
+ : [
42
+ {
43
+ heading: 'Artifacts',
44
+ lines: [
45
+ 'No artifacts in this checkout. Probed:',
46
+ ...roots.map((root) => ` ${root}`),
47
+ '',
48
+ 'Artifacts die with the worktree (GAIA-371 D1/D10).',
49
+ ],
50
+ },
51
+ ];
52
+ },
53
+ };
@@ -0,0 +1,2 @@
1
+ import type { GaiaPreset } from '@gaia-ai/core';
2
+ export declare const uiEntries: GaiaPreset['uiEntries'];
@@ -0,0 +1,4 @@
1
+ export const uiEntries = (acc) => [
2
+ ...acc,
3
+ '@gaia-ai/addon-gaia-ui-artifacts/ui',
4
+ ];
@@ -0,0 +1,30 @@
1
+ import type { PanelSection } from '@gaia-ai/addon-gaia-ui/ui';
2
+ /**
3
+ * The roots to probe, in order (spec D9).
4
+ *
5
+ * Pure path joins: `cwd` comes from `TuiOptions`, `identifier` and `branch` off
6
+ * the open document, so there is no `git` call and nothing async. The second
7
+ * root is not a nicety — an operator starts `gaia ui` in the main checkout while
8
+ * a run writes its artifacts inside the worktree, so probing only the first
9
+ * would show nothing, systematically.
10
+ */
11
+ export declare function artifactRoots(cwd: string, identifier: string, branch: string): string[];
12
+ /**
13
+ * One section per ARTIFACT, headed by its path under the ticket's tree so the
14
+ * producing tool leads every heading (`vitest/store-green.txt`,
15
+ * `playwright/logs/walk.txt`) and one tool's artifacts stand together.
16
+ *
17
+ * Why per artifact and not per tool directory: the panel contract is
18
+ * `PanelSection[]` with one collapsible entry per section (spec D4), and D9 asks
19
+ * for each FILE to be the expandable entry — its collapsed row the producing
20
+ * command, its expanded body the head of the capture. A section per tool could
21
+ * carry only one of those two, so the grouping lives in the heading instead,
22
+ * where it costs the narrow contract nothing.
23
+ *
24
+ * The first row of every text artifact is its own first line, which by GAIA-371
25
+ * D3 is the command that produced it — so the collapsed panel reads as the list
26
+ * of commands this ticket ran.
27
+ */
28
+ export declare function artifactSections(roots: readonly string[], { headLines }: {
29
+ headLines: number;
30
+ }): readonly PanelSection[];
@@ -0,0 +1,141 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * The roots to probe, in order (spec D9).
5
+ *
6
+ * Pure path joins: `cwd` comes from `TuiOptions`, `identifier` and `branch` off
7
+ * the open document, so there is no `git` call and nothing async. The second
8
+ * root is not a nicety — an operator starts `gaia ui` in the main checkout while
9
+ * a run writes its artifacts inside the worktree, so probing only the first
10
+ * would show nothing, systematically.
11
+ */
12
+ export function artifactRoots(cwd, identifier, branch) {
13
+ const own = join(cwd, 'assets-ai', identifier);
14
+ return branch.trim() === ''
15
+ ? [own]
16
+ : [own, join(cwd, '.gaia-worktrees', branch, 'assets-ai', identifier)];
17
+ }
18
+ /**
19
+ * Text file heads, memoised per `(path, mtime, size)` — spec D9.
20
+ *
21
+ * That key is what makes the cache CORRECT rather than merely fast: keying on
22
+ * the directory's own mtime would go stale the moment a re-run overwrites a log
23
+ * in place, because rewriting a file does not touch its directory. There is no
24
+ * reliance on the `R` key either — it reloads the document and knows nothing
25
+ * about this tree.
26
+ */
27
+ const heads = new Map();
28
+ const head = (path, lines) => {
29
+ const stat = statSync(path);
30
+ const key = `${path}:${stat.mtimeMs}:${stat.size}`;
31
+ const hit = heads.get(key);
32
+ if (hit)
33
+ return hit;
34
+ const all = readFileSync(path, 'utf8').replace(/\n$/, '').split('\n');
35
+ const kept = all.slice(0, lines);
36
+ if (all.length > lines)
37
+ kept.push(`… ${all.length - lines} more lines`);
38
+ heads.set(key, kept);
39
+ return kept;
40
+ };
41
+ /** Extensions never read: their bytes say nothing a terminal can show. */
42
+ const BINARY = /\.(png|jpe?g|gif|webp|zip|pdf|webm|mp4)$/i;
43
+ /**
44
+ * One directory's entries, or none when it cannot be read.
45
+ *
46
+ * A directory that vanished between the walk and the read is not an error worth
47
+ * a stack trace — this runs on the DRAW path, and a run writing into the tree is
48
+ * exactly the situation the panel exists for.
49
+ */
50
+ const entriesOf = (dir) => {
51
+ try {
52
+ return readdirSync(dir, { withFileTypes: true });
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ };
58
+ /**
59
+ * One FILE's section — and the same reasoning `entriesOf` states, applied to the
60
+ * calls that actually touch bytes.
61
+ *
62
+ * The race that comment names is not hypothetical for a file: a run unlinks or
63
+ * rewrites a capture between `readdirSync` and this read and the `stat`/`open`
64
+ * comes back ENOENT; a log written by a container as root comes back EACCES. On
65
+ * the DRAW path either one used to escape as a stack trace, and the host's guard
66
+ * would then have degraded the WHOLE listing to a single failure row — so one
67
+ * unreadable file would hide every artifact beside it. It costs its own row
68
+ * instead: the file stays listed under its real heading, the row says plainly
69
+ * that it could not be read, and it is NOT expandable, because there is nothing
70
+ * behind the caret to show. The errno leads, since `ENOENT`/`EACCES` is the part
71
+ * that tells the operator whether to re-run or to fix permissions.
72
+ */
73
+ const fileSection = (heading, path, headLines) => {
74
+ try {
75
+ return BINARY.test(heading)
76
+ ? // Listed, never read — and NOT expandable, because there is nothing
77
+ // behind the caret a terminal could show.
78
+ { heading, lines: [`${statSync(path).size} bytes`] }
79
+ : { heading, expandable: true, lines: [...head(path, headLines)] };
80
+ }
81
+ catch (caught) {
82
+ const code = caught?.code;
83
+ return {
84
+ heading,
85
+ lines: [
86
+ `could not be read: ${typeof code === 'string'
87
+ ? code
88
+ : caught instanceof Error
89
+ ? caught.message
90
+ : String(caught)}`,
91
+ ],
92
+ };
93
+ }
94
+ };
95
+ /**
96
+ * One section per ARTIFACT, headed by its path under the ticket's tree so the
97
+ * producing tool leads every heading (`vitest/store-green.txt`,
98
+ * `playwright/logs/walk.txt`) and one tool's artifacts stand together.
99
+ *
100
+ * Why per artifact and not per tool directory: the panel contract is
101
+ * `PanelSection[]` with one collapsible entry per section (spec D4), and D9 asks
102
+ * for each FILE to be the expandable entry — its collapsed row the producing
103
+ * command, its expanded body the head of the capture. A section per tool could
104
+ * carry only one of those two, so the grouping lives in the heading instead,
105
+ * where it costs the narrow contract nothing.
106
+ *
107
+ * The first row of every text artifact is its own first line, which by GAIA-371
108
+ * D3 is the command that produced it — so the collapsed panel reads as the list
109
+ * of commands this ticket ran.
110
+ */
111
+ export function artifactSections(roots, { headLines }) {
112
+ const root = roots.find((candidate) => {
113
+ try {
114
+ return statSync(candidate).isDirectory();
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ });
120
+ if (root === undefined)
121
+ return [];
122
+ const sections = [];
123
+ const walk = (dir, prefix) => {
124
+ // Sorted by name, so the listing is the same on every draw and on every
125
+ // filesystem — `readdirSync` promises no order of its own.
126
+ const sorted = [...entriesOf(dir)].sort((a, b) => a.name.localeCompare(b.name));
127
+ for (const entry of sorted) {
128
+ const path = join(dir, entry.name);
129
+ const heading = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
130
+ if (entry.isDirectory()) {
131
+ walk(path, heading);
132
+ continue;
133
+ }
134
+ if (!entry.isFile())
135
+ continue;
136
+ sections.push(fileSection(heading, path, headLines));
137
+ }
138
+ };
139
+ walk(root, '');
140
+ return sections;
141
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/src/ui.js ADDED
@@ -0,0 +1,6 @@
1
+ // The entry module: it registers at IMPORT time, which is Storybook-faithful for
2
+ // the addon author and is why the kernel drains the collector once rather than
3
+ // handing a store around (spec D3).
4
+ import { register } from '@gaia-ai/addon-gaia-ui/ui';
5
+ import { artifactsPanel } from './panel.js';
6
+ register('gaia/artifacts', (api) => api.add(artifactsPanel.id, artifactsPanel));
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@gaia-ai/addon-gaia-ui-artifacts",
3
+ "version": "0.10.0",
4
+ "description": "GAIA TUI: an Artifacts tab on ticket detail, reading assets-ai/<GAIA-nnn>/.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": "./dist/src/panel.js",
9
+ "./preset": "./dist/src/preset.js",
10
+ "./ui": "./dist/src/ui.js"
11
+ },
12
+ "files": [
13
+ "dist/src"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://git.key-tec.de/keytec/gaia.git",
21
+ "directory": "gaia-cli/addons/gaia-ui-artifacts"
22
+ },
23
+ "peerDependencies": {
24
+ "@gaia-ai/addon-gaia-ui": "^0.10.0",
25
+ "@gaia-ai/core": "^0.10.0"
26
+ }
27
+ }