@agimon-ai/doompi-workflow 0.0.1-alpha.48 → 0.0.1-alpha.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-workflow",
3
- "version": "0.0.1-alpha.48",
3
+ "version": "0.0.1-alpha.49",
4
4
  "description": "GitHub Actions-style workflow graphs, artifacts, recovery, and asynchronous runs for DoomPi.",
5
5
  "keywords": [
6
6
  "ai",
@@ -68,12 +68,12 @@
68
68
  "@modelcontextprotocol/sdk": "1.30.0",
69
69
  "hono": "4.13.5",
70
70
  "zod": "4.4.3",
71
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.47",
72
- "@agimon-ai/doompi-ui": "0.0.1-alpha.48",
73
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.47",
74
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.8",
75
- "@agimon-ai/doompi-web-security": "0.0.1-alpha.9",
76
- "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.11"
71
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.48",
72
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.48",
73
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.49",
74
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.9",
75
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.12",
76
+ "@agimon-ai/doompi-web-security": "0.0.1-alpha.10"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@earendil-works/pi-coding-agent": "0.84.4",
@@ -1,10 +1,9 @@
1
- import { Badge, Button, StatusBadge, StreamCursor } from '@agimon-ai/doompi-web-components';
1
+ import { AnsiLine, Badge, Button, StatusBadge, StreamCursor } from '@agimon-ai/doompi-web-components';
2
2
  import type { TransientTab, WebPluginSlotProps } from '@agimon-ai/doompi-web-contracts';
3
3
  import { useStore } from '@tanstack/react-store';
4
4
  import { type KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
5
5
  import type { WorkflowRunView } from '../types/webWorkflows.ts';
6
6
  import type { WorkflowTerminalCapabilitiesView } from '../types/webWorkflowTerminal.ts';
7
- import { ansiSpans } from './ansiSpans.ts';
8
7
  import { followScreen, releaseControl, sendKeys, takeControl } from './terminalApi.ts';
9
8
  import { workflows } from './workflowsStore.ts';
10
9
 
@@ -53,23 +52,10 @@ export function stepTerminalTab(run: WorkflowRunView, job: string, step?: string
53
52
  }
54
53
 
55
54
  function ScreenLine({ line }: { line: string }) {
56
- const spans = ansiSpans(line);
57
- if (spans.length === 0) return <span className="block h-[15px]" />;
58
- return (
59
- <span className="block whitespace-pre">
60
- {spans.map((span, index) => (
61
- <span
62
- key={index}
63
- className={`${span.className ?? ''} ${span.bold ? 'font-bold' : ''} ${span.faint ? 'opacity-60' : ''} ${
64
- span.italic ? 'italic' : ''
65
- } ${span.underline ? 'underline' : ''} ${span.inverse ? 'bg-doom-text text-doom-deep' : ''}`}
66
- style={span.color === undefined ? undefined : { color: span.color }}
67
- >
68
- {span.text}
69
- </span>
70
- ))}
71
- </span>
72
- );
55
+ // A blank row still has to hold the grid open, so an empty line keeps its
56
+ // height rather than collapsing the screen by one row.
57
+ if (line.length === 0) return <span className="block h-[15px]" />;
58
+ return <AnsiLine line={line} className="block whitespace-pre" />;
73
59
  }
74
60
 
75
61
  /**
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AnsiLine,
2
3
  Button,
3
4
  ChevronDownIcon,
4
5
  cn,
@@ -35,7 +36,6 @@ import type {
35
36
  } from '../types/webWorkflows.ts';
36
37
  import type { WorkflowTerminalCapabilitiesView } from '../types/webWorkflowTerminal.ts';
37
38
  import { ArtifactsPane, artifactTab } from './ArtifactsPane.tsx';
38
- import { ansiSpans } from './ansiSpans.ts';
39
39
  import { catalog, closeCatalog, closeLaunch, openCatalog, openLaunch } from './catalogStore.ts';
40
40
  import { LaunchWorkflowDialog } from './LaunchWorkflowDialog.tsx';
41
41
  import { formatRunDuration } from './runDuration.ts';
@@ -331,23 +331,10 @@ function StepRow({
331
331
  }
332
332
 
333
333
  function ScreenLine({ line }: { line: string }) {
334
- const spans = ansiSpans(line);
335
- if (spans.length === 0) return <span className="block h-[15px]" />;
336
- return (
337
- <span className="block whitespace-pre">
338
- {spans.map((span, index) => (
339
- <span
340
- key={index}
341
- className={`${span.className ?? ''} ${span.bold ? 'font-bold' : ''} ${span.faint ? 'opacity-60' : ''} ${
342
- span.italic ? 'italic' : ''
343
- } ${span.underline ? 'underline' : ''} ${span.inverse ? 'bg-doom-text text-doom-deep' : ''}`}
344
- style={span.color === undefined ? undefined : { color: span.color }}
345
- >
346
- {span.text}
347
- </span>
348
- ))}
349
- </span>
350
- );
334
+ // A blank row still has to hold the grid open, so an empty line keeps its
335
+ // height rather than collapsing the screen by one row.
336
+ if (line.length === 0) return <span className="block h-[15px]" />;
337
+ return <AnsiLine line={line} className="block whitespace-pre" />;
351
338
  }
352
339
 
353
340
  function InlineStepOutput({
@@ -1,230 +0,0 @@
1
- /**
2
- * The escapes a captured terminal screen carries, turned into spans.
3
- *
4
- * WHY THIS EXISTS:
5
- * tmux captures a pane with `-e`, and cmux's render grid is rebuilt into the
6
- * same escapes, so what arrives at the cockpit is a terminal's own colour. The
7
- * browser has no terminal, and the cockpit carries no terminal emulator, so
8
- * the sequences are read here and rendered as ordinary spans.
9
- *
10
- * WHAT IT UNDERSTANDS:
11
- * SGR (`ESC [ … m`) only: the eight base colours and their bright forms, 256
12
- * colour and truecolour, and bold, faint, italic, underline and inverse. Every
13
- * other sequence is dropped rather than printed, because a cursor move or a
14
- * clear that arrives as visible junk is worse than one that arrives as
15
- * nothing: the screen is a snapshot, and the next one repaints it anyway.
16
- *
17
- * The named colours become theme tokens rather than the child's own palette,
18
- * so a run's output sits in the cockpit's colours instead of fighting them.
19
- * Only 256-colour and truecolour, which name a colour no token can stand in
20
- * for, are rendered as themselves.
21
- */
22
-
23
- /** One run of text with the attributes in force when it was printed. */
24
- export interface AnsiSpan {
25
- text: string;
26
- /** Theme class for a named colour, absent when the colour is the default. */
27
- className?: string;
28
- /** An exact colour the child asked for, as `rgb(r g b)`; only 256/truecolour set this. */
29
- color?: string;
30
- bold?: boolean;
31
- faint?: boolean;
32
- italic?: boolean;
33
- underline?: boolean;
34
- /** Foreground and background swapped, which a TUI uses for selection and cursors. */
35
- inverse?: boolean;
36
- }
37
-
38
- // oxlint-disable-next-line no-control-regex -- reading terminal escapes is the point
39
- const ANSI_PATTERN = /\x1b\[([0-9;:]*)([A-Za-z])|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][A-Za-z0-9]|\x1b[A-Za-z]/g;
40
- const SGR_FINAL = 'm';
41
- const RESET_CODE = 0;
42
- const DEFAULT_FOREGROUND = 39;
43
- const EXTENDED_FOREGROUND = 38;
44
- const EXTENDED_BACKGROUND = 48;
45
- const TRUECOLOR_SELECTOR = 2;
46
- const PALETTE_SELECTOR = 5;
47
- const BASE_FOREGROUND_START = 30;
48
- const BASE_FOREGROUND_END = 37;
49
- const BRIGHT_FOREGROUND_START = 90;
50
- const BRIGHT_FOREGROUND_END = 97;
51
- const BACKGROUND_START = 40;
52
- const BACKGROUND_END = 49;
53
- const BRIGHT_BACKGROUND_START = 100;
54
- const BRIGHT_BACKGROUND_END = 107;
55
- const RGB_CHANNELS = 3;
56
- const PALETTE_SIZE = 256;
57
- const PALETTE_CUBE_START = 16;
58
- const PALETTE_GREY_START = 232;
59
- const CUBE_SIDE = 6;
60
- const CUBE_STEP = 40;
61
- const CUBE_OFFSET = 55;
62
- const GREY_STEP = 10;
63
- const GREY_OFFSET = 8;
64
- const MAX_CHANNEL = 255;
65
-
66
- const BOLD = 1;
67
- const FAINT = 2;
68
- const ITALIC = 3;
69
- const UNDERLINE = 4;
70
- const INVERSE = 7;
71
- const NOT_BOLD = 22;
72
- const NOT_ITALIC = 23;
73
- const NOT_UNDERLINE = 24;
74
- const NOT_INVERSE = 27;
75
-
76
- /**
77
- * The eight terminal colours as theme tokens, listed as whole class strings so
78
- * the host's class scanner can see them.
79
- */
80
- const FOREGROUND_CLASS: Readonly<Record<number, string>> = {
81
- 30: 'text-doom-faint',
82
- 31: 'text-doom-red',
83
- 32: 'text-doom-green',
84
- 33: 'text-doom-yellow',
85
- 34: 'text-doom-blue',
86
- 35: 'text-doom-magenta',
87
- 36: 'text-doom-cyan',
88
- 37: 'text-doom-text',
89
- 90: 'text-doom-dim',
90
- 91: 'text-doom-red',
91
- 92: 'text-doom-green',
92
- 93: 'text-doom-orange',
93
- 94: 'text-doom-blue',
94
- 95: 'text-doom-violet',
95
- 96: 'text-doom-cyan',
96
- 97: 'text-doom-hi',
97
- };
98
-
99
- interface Attributes {
100
- className?: string;
101
- color?: string;
102
- bold: boolean;
103
- faint: boolean;
104
- italic: boolean;
105
- underline: boolean;
106
- inverse: boolean;
107
- }
108
-
109
- function blank(): Attributes {
110
- return { bold: false, faint: false, italic: false, underline: false, inverse: false };
111
- }
112
-
113
- /** One of the 256 palette entries as an exact colour, since no token stands for it. */
114
- function paletteColor(index: number): string | undefined {
115
- if (index < 0 || index >= PALETTE_SIZE) return undefined;
116
- if (index < PALETTE_CUBE_START) return undefined; // The first sixteen are the named ones.
117
- if (index >= PALETTE_GREY_START) {
118
- const level = (index - PALETTE_GREY_START) * GREY_STEP + GREY_OFFSET;
119
- return rgb(level, level, level);
120
- }
121
- const offset = index - PALETTE_CUBE_START;
122
- const channel = (value: number): number => (value === 0 ? 0 : value * CUBE_STEP + CUBE_OFFSET);
123
- return rgb(
124
- channel(Math.floor(offset / (CUBE_SIDE * CUBE_SIDE)) % CUBE_SIDE),
125
- channel(Math.floor(offset / CUBE_SIDE) % CUBE_SIDE),
126
- channel(offset % CUBE_SIDE),
127
- );
128
- }
129
-
130
- function rgb(red: number, green: number, blue: number): string {
131
- const clamp = (value: number): number => Math.max(0, Math.min(MAX_CHANNEL, Math.round(value)));
132
- return `rgb(${clamp(red)} ${clamp(green)} ${clamp(blue)})`;
133
- }
134
-
135
- /** Reads one extended-colour run (`38;5;n` or `38;2;r;g;b`), answering how many codes it ate. */
136
- function extendedColor(codes: readonly number[], index: number): { color?: string; consumed: number } {
137
- const selector = codes[index + 1];
138
- if (selector === TRUECOLOR_SELECTOR) {
139
- const [red, green, blue] = codes.slice(index + 2, index + 2 + RGB_CHANNELS);
140
- if (red === undefined || green === undefined || blue === undefined) return { consumed: codes.length };
141
- return { color: rgb(red, green, blue), consumed: 1 + 1 + RGB_CHANNELS };
142
- }
143
- if (selector === PALETTE_SELECTOR) {
144
- const entry = codes[index + 2];
145
- return { ...(entry === undefined ? {} : { color: paletteColor(entry) }), consumed: 3 };
146
- }
147
- return { consumed: 1 };
148
- }
149
-
150
- function applySgr(current: Attributes, parameters: string): Attributes {
151
- // An empty parameter list is a reset, which is what a bare `ESC[m` means.
152
- const codes = parameters === '' ? [RESET_CODE] : parameters.split(/[;:]/).map((part) => Number(part) || 0);
153
- let next = { ...current };
154
- for (let index = 0; index < codes.length; index += 1) {
155
- const code = codes[index] as number;
156
- if (code === RESET_CODE) {
157
- next = blank();
158
- } else if (code === BOLD) next.bold = true;
159
- else if (code === FAINT) next.faint = true;
160
- else if (code === ITALIC) next.italic = true;
161
- else if (code === UNDERLINE) next.underline = true;
162
- else if (code === INVERSE) next.inverse = true;
163
- else if (code === NOT_BOLD) {
164
- next.bold = false;
165
- next.faint = false;
166
- } else if (code === NOT_ITALIC) next.italic = false;
167
- else if (code === NOT_UNDERLINE) next.underline = false;
168
- else if (code === NOT_INVERSE) next.inverse = false;
169
- else if (code === DEFAULT_FOREGROUND) {
170
- delete next.className;
171
- delete next.color;
172
- } else if (code === EXTENDED_FOREGROUND) {
173
- const { color, consumed } = extendedColor(codes, index);
174
- delete next.className;
175
- if (color === undefined) delete next.color;
176
- else next.color = color;
177
- index += consumed - 1;
178
- } else if (code === EXTENDED_BACKGROUND) {
179
- // Backgrounds are dropped: a child painting its own background over the
180
- // cockpit's panel is what makes captured output unreadable in a theme.
181
- index += extendedColor(codes, index).consumed - 1;
182
- } else if (
183
- (code >= BASE_FOREGROUND_START && code <= BASE_FOREGROUND_END) ||
184
- (code >= BRIGHT_FOREGROUND_START && code <= BRIGHT_FOREGROUND_END)
185
- ) {
186
- const className = FOREGROUND_CLASS[code];
187
- delete next.color;
188
- if (className === undefined) delete next.className;
189
- else next.className = className;
190
- }
191
- // Background and everything else is read and dropped.
192
- else if (
193
- (code >= BACKGROUND_START && code <= BACKGROUND_END) ||
194
- (code >= BRIGHT_BACKGROUND_START && code <= BRIGHT_BACKGROUND_END)
195
- ) {
196
- next.inverse = false;
197
- }
198
- }
199
- return next;
200
- }
201
-
202
- function spanOf(text: string, attributes: Attributes): AnsiSpan {
203
- return {
204
- text,
205
- ...(attributes.className === undefined ? {} : { className: attributes.className }),
206
- ...(attributes.color === undefined ? {} : { color: attributes.color }),
207
- ...(attributes.bold ? { bold: true } : {}),
208
- ...(attributes.faint ? { faint: true } : {}),
209
- ...(attributes.italic ? { italic: true } : {}),
210
- ...(attributes.underline ? { underline: true } : {}),
211
- ...(attributes.inverse ? { inverse: true } : {}),
212
- };
213
- }
214
-
215
- /** One line of captured screen as spans, with every unreadable sequence dropped. */
216
- export function ansiSpans(line: string): AnsiSpan[] {
217
- const spans: AnsiSpan[] = [];
218
- let attributes = blank();
219
- let cursor = 0;
220
- ANSI_PATTERN.lastIndex = 0;
221
- let match = ANSI_PATTERN.exec(line);
222
- while (match !== null) {
223
- if (match.index > cursor) spans.push(spanOf(line.slice(cursor, match.index), attributes));
224
- if (match[2] === SGR_FINAL) attributes = applySgr(attributes, match[1] ?? '');
225
- cursor = match.index + match[0].length;
226
- match = ANSI_PATTERN.exec(line);
227
- }
228
- if (cursor < line.length) spans.push(spanOf(line.slice(cursor), attributes));
229
- return spans;
230
- }