@bastani/atomic 0.6.6-1 → 0.6.7-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/README.md +22 -16
- package/dist/sdk/components/compact-switcher.d.ts.map +1 -1
- package/dist/sdk/components/connectors.d.ts +1 -0
- package/dist/sdk/components/connectors.d.ts.map +1 -1
- package/dist/sdk/components/edge.d.ts +1 -1
- package/dist/sdk/components/edge.d.ts.map +1 -1
- package/dist/sdk/components/graph-theme.d.ts.map +1 -1
- package/dist/sdk/components/header.d.ts.map +1 -1
- package/dist/sdk/components/node-card.d.ts.map +1 -1
- package/dist/sdk/components/orchestrator-panel.d.ts +7 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -1
- package/dist/sdk/components/renderer-background.d.ts +9 -0
- package/dist/sdk/components/renderer-background.d.ts.map +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts.map +1 -1
- package/dist/sdk/components/statusline.d.ts.map +1 -1
- package/dist/sdk/components/tui-diagnostics.d.ts +56 -0
- package/dist/sdk/components/tui-diagnostics.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +2 -1
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
- package/dist/sdk/providers/copilot.d.ts +3 -2
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/theme.d.ts +4 -0
- package/dist/sdk/runtime/theme.d.ts.map +1 -1
- package/dist/theme/colors.d.ts +2 -0
- package/dist/theme/colors.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +3 -3
- package/src/commands/cli/chat/index.ts +10 -4
- package/src/commands/cli/management-commands.ts +4 -3
- package/src/commands/cli/session.test.ts +79 -6
- package/src/commands/cli/session.ts +65 -9
- package/src/completions/fish.ts +9 -3
- package/src/completions/powershell.ts +27 -3
- package/src/completions/zsh.ts +9 -2
- package/src/sdk/components/compact-switcher.tsx +10 -5
- package/src/sdk/components/connectors.ts +4 -0
- package/src/sdk/components/edge.tsx +5 -3
- package/src/sdk/components/graph-theme.ts +2 -3
- package/src/sdk/components/header.tsx +21 -9
- package/src/sdk/components/node-card.tsx +13 -7
- package/src/sdk/components/orchestrator-panel.tsx +47 -2
- package/src/sdk/components/renderer-background.ts +49 -0
- package/src/sdk/components/session-graph-panel.tsx +9 -2
- package/src/sdk/components/statusline.tsx +26 -22
- package/src/sdk/components/tui-diagnostics.ts +273 -0
- package/src/sdk/components/workflow-picker-panel.tsx +33 -22
- package/src/sdk/providers/copilot.ts +10 -4
- package/src/sdk/runtime/executor.ts +28 -1
- package/src/sdk/runtime/theme.ts +28 -36
- package/src/services/system/install-ui.ts +16 -17
- package/src/theme/colors.ts +14 -9
- package/src/theme/logo.ts +23 -12
|
@@ -14,20 +14,39 @@ import { StoreContext, ThemeContext, TmuxSessionContext } from "./orchestrator-p
|
|
|
14
14
|
import type { PanelSession, PanelOptions, SessionData } from "./orchestrator-panel-types.ts";
|
|
15
15
|
import { SessionGraphPanel } from "./session-graph-panel.tsx";
|
|
16
16
|
import { ErrorBoundary } from "./error-boundary.tsx";
|
|
17
|
+
import {
|
|
18
|
+
requestRendererBackgroundRepaint,
|
|
19
|
+
resetRendererTerminalBackground,
|
|
20
|
+
setRendererBackground,
|
|
21
|
+
} from "./renderer-background.ts";
|
|
22
|
+
import { createTuiDiagnostics, type TuiDiagnostics } from "./tui-diagnostics.ts";
|
|
17
23
|
|
|
18
24
|
export class OrchestratorPanel {
|
|
19
25
|
private store: PanelStore;
|
|
20
26
|
private renderer: CliRenderer;
|
|
21
27
|
private destroyed = false;
|
|
28
|
+
private terminalBackgroundSynced: boolean;
|
|
29
|
+
private diagnostics: TuiDiagnostics | null = null;
|
|
30
|
+
private unsubscribeDiagnostics: (() => void) | null = null;
|
|
22
31
|
|
|
23
32
|
private constructor(
|
|
24
33
|
renderer: CliRenderer,
|
|
25
34
|
store: PanelStore,
|
|
26
35
|
graphTheme: GraphTheme,
|
|
27
36
|
tmuxSession: string,
|
|
37
|
+
terminalBackgroundSynced: boolean,
|
|
28
38
|
) {
|
|
29
39
|
this.renderer = renderer;
|
|
30
40
|
this.store = store;
|
|
41
|
+
this.terminalBackgroundSynced = terminalBackgroundSynced;
|
|
42
|
+
this.diagnostics = createTuiDiagnostics({
|
|
43
|
+
renderer,
|
|
44
|
+
graphTheme,
|
|
45
|
+
getSnapshot: () => this.getDiagnosticSnapshot(),
|
|
46
|
+
});
|
|
47
|
+
this.unsubscribeDiagnostics = this.diagnostics
|
|
48
|
+
? store.subscribe(() => this.diagnostics?.capture("store-update"))
|
|
49
|
+
: null;
|
|
31
50
|
|
|
32
51
|
createRoot(renderer).render(
|
|
33
52
|
<StoreContext.Provider value={store}>
|
|
@@ -56,6 +75,8 @@ export class OrchestratorPanel {
|
|
|
56
75
|
</ThemeContext.Provider>
|
|
57
76
|
</StoreContext.Provider>,
|
|
58
77
|
);
|
|
78
|
+
requestRendererBackgroundRepaint(this.renderer);
|
|
79
|
+
this.diagnostics?.capture("post-mount");
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
/**
|
|
@@ -69,18 +90,20 @@ export class OrchestratorPanel {
|
|
|
69
90
|
exitOnCtrlC: false,
|
|
70
91
|
exitSignals: ["SIGTERM", "SIGQUIT", "SIGABRT", "SIGHUP", "SIGPIPE", "SIGBUS", "SIGFPE"],
|
|
71
92
|
});
|
|
72
|
-
return OrchestratorPanel.createWithRenderer(renderer, options);
|
|
93
|
+
return OrchestratorPanel.createWithRenderer(renderer, options, { syncTerminalBackground: true });
|
|
73
94
|
}
|
|
74
95
|
|
|
75
96
|
/** Create with an externally-provided renderer (e.g. a test renderer). */
|
|
76
97
|
static createWithRenderer(
|
|
77
98
|
renderer: CliRenderer,
|
|
78
99
|
options: PanelOptions,
|
|
100
|
+
{ syncTerminalBackground = false }: { syncTerminalBackground?: boolean } = {},
|
|
79
101
|
): OrchestratorPanel {
|
|
80
102
|
const termTheme = resolveTheme(renderer.themeMode);
|
|
103
|
+
setRendererBackground(renderer, termTheme.bg, { syncTerminalDefault: syncTerminalBackground });
|
|
81
104
|
const graphTheme = deriveGraphTheme(termTheme);
|
|
82
105
|
const store = new PanelStore();
|
|
83
|
-
return new OrchestratorPanel(renderer, store, graphTheme, options.tmuxSession);
|
|
106
|
+
return new OrchestratorPanel(renderer, store, graphTheme, options.tmuxSession, syncTerminalBackground);
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
/**
|
|
@@ -175,7 +198,15 @@ export class OrchestratorPanel {
|
|
|
175
198
|
destroy(): void {
|
|
176
199
|
if (this.destroyed) return;
|
|
177
200
|
this.destroyed = true;
|
|
201
|
+
this.unsubscribeDiagnostics?.();
|
|
202
|
+
this.unsubscribeDiagnostics = null;
|
|
203
|
+
this.diagnostics?.capture("destroy");
|
|
204
|
+
this.diagnostics?.dispose();
|
|
205
|
+
this.diagnostics = null;
|
|
178
206
|
try {
|
|
207
|
+
if (this.terminalBackgroundSynced) {
|
|
208
|
+
resetRendererTerminalBackground(this.renderer);
|
|
209
|
+
}
|
|
179
210
|
this.renderer.destroy();
|
|
180
211
|
} catch {}
|
|
181
212
|
}
|
|
@@ -214,4 +245,18 @@ export class OrchestratorPanel {
|
|
|
214
245
|
sessions: this.store.sessions,
|
|
215
246
|
};
|
|
216
247
|
}
|
|
248
|
+
|
|
249
|
+
private getDiagnosticSnapshot() {
|
|
250
|
+
return {
|
|
251
|
+
workflowName: this.store.workflowName,
|
|
252
|
+
agent: this.store.agent,
|
|
253
|
+
prompt: this.store.prompt,
|
|
254
|
+
fatalError: this.store.fatalError,
|
|
255
|
+
completionReached: this.store.completionReached,
|
|
256
|
+
sessions: this.store.sessions,
|
|
257
|
+
backgroundTaskCount: this.store.backgroundTaskCount,
|
|
258
|
+
viewMode: this.store.viewMode,
|
|
259
|
+
activeAgentId: this.store.activeAgentId,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
217
262
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CliRenderer } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
export function setRendererBackground(
|
|
4
|
+
renderer: CliRenderer,
|
|
5
|
+
color: string,
|
|
6
|
+
{ syncTerminalDefault = false }: { syncTerminalDefault?: boolean } = {},
|
|
7
|
+
): void {
|
|
8
|
+
renderer.setBackgroundColor(color);
|
|
9
|
+
if (syncTerminalDefault) {
|
|
10
|
+
process.stdout.write(wrapForTmuxIfNeeded(terminalBackgroundColorSequence(color)));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function requestRendererBackgroundRepaint(renderer: CliRenderer): void {
|
|
15
|
+
// OpenTUI 0.1.103+ no longer syncs the renderer background to the terminal
|
|
16
|
+
// default via OSC 11. Force the next frame so blank cells with this background
|
|
17
|
+
// are emitted instead of being skipped as unchanged initial buffer contents.
|
|
18
|
+
Object.assign(renderer, { forceFullRepaintRequested: true });
|
|
19
|
+
renderer.requestRender();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resetRendererTerminalBackground(renderer: CliRenderer): void {
|
|
23
|
+
if (process.env.TMUX) {
|
|
24
|
+
process.stdout.write(wrapForTmuxIfNeeded("\x1b]111\x07"));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
renderer.resetTerminalBgColor();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function terminalBackgroundColorSequence(color: string): string {
|
|
32
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(color);
|
|
33
|
+
if (!match) {
|
|
34
|
+
throw new Error(`Cannot sync terminal background for non-hex color: ${color}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const hex = match[1]!;
|
|
38
|
+
return `\x1b]11;rgb:${hex.slice(0, 2)}/${hex.slice(2, 4)}/${hex.slice(4, 6)}\x07`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function wrapForTmuxIfNeeded(sequence: string): string {
|
|
42
|
+
if (!process.env.TMUX) return sequence;
|
|
43
|
+
|
|
44
|
+
let escaped = "";
|
|
45
|
+
for (const char of sequence) {
|
|
46
|
+
escaped += char === "\x1b" ? "\x1b\x1b" : char;
|
|
47
|
+
}
|
|
48
|
+
return `\x1bPtmux;${escaped}\x1b\\`;
|
|
49
|
+
}
|
|
@@ -433,9 +433,16 @@ export function SessionGraphPanel() {
|
|
|
433
433
|
},
|
|
434
434
|
}}
|
|
435
435
|
>
|
|
436
|
-
<box width={canvasW} height={canvasH} position="relative">
|
|
436
|
+
<box width={canvasW} height={canvasH} position="relative" backgroundColor={theme.background}>
|
|
437
437
|
{/* Offset all content by padding to center the graph */}
|
|
438
|
-
<box
|
|
438
|
+
<box
|
|
439
|
+
position="absolute"
|
|
440
|
+
left={padX}
|
|
441
|
+
top={padY}
|
|
442
|
+
width={layout.width}
|
|
443
|
+
height={layout.height}
|
|
444
|
+
backgroundColor={theme.background}
|
|
445
|
+
>
|
|
439
446
|
{/* Connectors (rendered behind nodes) */}
|
|
440
447
|
{connectors.map((conn, i) => (
|
|
441
448
|
<Edge key={`e${i}`} {...conn} />
|
|
@@ -15,17 +15,19 @@ export function Statusline({
|
|
|
15
15
|
<box height={1} flexDirection="row" backgroundColor={theme.backgroundElement}>
|
|
16
16
|
{/* Mode badge — always GRAPH since this bar is only visible in the orchestrator window */}
|
|
17
17
|
<box backgroundColor={theme.primary} paddingLeft={1} paddingRight={1} alignItems="center">
|
|
18
|
-
<text
|
|
19
|
-
<
|
|
18
|
+
<text>
|
|
19
|
+
<span fg={theme.backgroundElement} bg={theme.primary}>
|
|
20
|
+
<strong>GRAPH</strong>
|
|
21
|
+
</span>
|
|
20
22
|
</text>
|
|
21
23
|
</box>
|
|
22
24
|
|
|
23
25
|
{store.backgroundTaskCount > 0 ? (
|
|
24
|
-
<box backgroundColor=
|
|
26
|
+
<box backgroundColor={theme.backgroundElement} paddingLeft={1} alignItems="center">
|
|
25
27
|
<text>
|
|
26
|
-
<span fg={theme.textDim}>{"\u00B7"} </span>
|
|
27
|
-
<span fg={theme.warning}>{"\u25C6"} </span>
|
|
28
|
-
<span fg={theme.textMuted}>
|
|
28
|
+
<span fg={theme.textDim} bg={theme.backgroundElement}>{"\u00B7"} </span>
|
|
29
|
+
<span fg={theme.warning} bg={theme.backgroundElement}>{"\u25C6"} </span>
|
|
30
|
+
<span fg={theme.textMuted} bg={theme.backgroundElement}>
|
|
29
31
|
{store.backgroundTaskCount} background
|
|
30
32
|
</span>
|
|
31
33
|
</text>
|
|
@@ -37,25 +39,27 @@ export function Statusline({
|
|
|
37
39
|
{/* Navigation hints — always graph-mode (tmux status bar handles attached-mode hints) */}
|
|
38
40
|
<box paddingRight={2} alignItems="center">
|
|
39
41
|
{attachMsg ? (
|
|
40
|
-
<text
|
|
41
|
-
<
|
|
42
|
+
<text>
|
|
43
|
+
<span fg={theme.text} bg={theme.backgroundElement}>
|
|
44
|
+
<strong>{attachMsg}</strong>
|
|
45
|
+
</span>
|
|
42
46
|
</text>
|
|
43
47
|
) : (
|
|
44
48
|
<text>
|
|
45
|
-
<span fg={theme.text}>{"\u2191\u2193\u2190\u2192"}</span>
|
|
46
|
-
<span fg={theme.textMuted}> navigate</span>
|
|
47
|
-
<span fg={theme.textDim}> {"\u00B7"} </span>
|
|
48
|
-
<span fg={theme.text}>{"\u21B5"}</span>
|
|
49
|
-
<span fg={theme.textMuted}> attach</span>
|
|
50
|
-
<span fg={theme.textDim}> {"\u00B7"} </span>
|
|
51
|
-
<span fg={theme.text}>/</span>
|
|
52
|
-
<span fg={theme.textMuted}> stages</span>
|
|
53
|
-
<span fg={theme.textDim}> {"\u00B7"} </span>
|
|
54
|
-
<span fg={theme.text}>ctrl+b d</span>
|
|
55
|
-
<span fg={theme.textMuted}> detach</span>
|
|
56
|
-
<span fg={theme.textDim}> {"\u00B7"} </span>
|
|
57
|
-
<span fg={theme.text}>q</span>
|
|
58
|
-
<span fg={theme.textMuted}> quit</span>
|
|
49
|
+
<span fg={theme.text} bg={theme.backgroundElement}>{"\u2191\u2193\u2190\u2192"}</span>
|
|
50
|
+
<span fg={theme.textMuted} bg={theme.backgroundElement}> navigate</span>
|
|
51
|
+
<span fg={theme.textDim} bg={theme.backgroundElement}> {"\u00B7"} </span>
|
|
52
|
+
<span fg={theme.text} bg={theme.backgroundElement}>{"\u21B5"}</span>
|
|
53
|
+
<span fg={theme.textMuted} bg={theme.backgroundElement}> attach</span>
|
|
54
|
+
<span fg={theme.textDim} bg={theme.backgroundElement}> {"\u00B7"} </span>
|
|
55
|
+
<span fg={theme.text} bg={theme.backgroundElement}>/</span>
|
|
56
|
+
<span fg={theme.textMuted} bg={theme.backgroundElement}> stages</span>
|
|
57
|
+
<span fg={theme.textDim} bg={theme.backgroundElement}> {"\u00B7"} </span>
|
|
58
|
+
<span fg={theme.text} bg={theme.backgroundElement}>ctrl+b d</span>
|
|
59
|
+
<span fg={theme.textMuted} bg={theme.backgroundElement}> detach</span>
|
|
60
|
+
<span fg={theme.textDim} bg={theme.backgroundElement}> {"\u00B7"} </span>
|
|
61
|
+
<span fg={theme.text} bg={theme.backgroundElement}>q</span>
|
|
62
|
+
<span fg={theme.textMuted} bg={theme.backgroundElement}> quit</span>
|
|
59
63
|
</text>
|
|
60
64
|
)}
|
|
61
65
|
</box>
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import type { CliRenderer, OptimizedBuffer } from "@opentui/core";
|
|
5
|
+
import type { GraphTheme } from "./graph-theme.ts";
|
|
6
|
+
import type { SessionData } from "./orchestrator-panel-types.ts";
|
|
7
|
+
|
|
8
|
+
type BackgroundRun = {
|
|
9
|
+
x: number;
|
|
10
|
+
width: number;
|
|
11
|
+
color: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type BufferRowDiagnostic = {
|
|
15
|
+
y: number;
|
|
16
|
+
text: string;
|
|
17
|
+
backgrounds: BackgroundRun[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ColorCount = {
|
|
21
|
+
color: string;
|
|
22
|
+
count: number;
|
|
23
|
+
percent: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type BufferDiagnostic = {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
topBackgrounds: ColorCount[];
|
|
30
|
+
yellowHueCells: number;
|
|
31
|
+
yellowHueSamples: Array<{ x: number; y: number; color: string; char: string }>;
|
|
32
|
+
rows: BufferRowDiagnostic[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type WorkflowDiagnosticSnapshot = {
|
|
36
|
+
workflowName: string;
|
|
37
|
+
agent: string;
|
|
38
|
+
prompt: string;
|
|
39
|
+
fatalError: string | null;
|
|
40
|
+
completionReached: boolean;
|
|
41
|
+
sessions: readonly SessionData[];
|
|
42
|
+
backgroundTaskCount: number;
|
|
43
|
+
viewMode: string;
|
|
44
|
+
activeAgentId: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type TuiDiagnostics = {
|
|
48
|
+
capture: (reason: string) => void;
|
|
49
|
+
dispose: () => void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type TuiDiagnosticsOptions = {
|
|
53
|
+
renderer: CliRenderer;
|
|
54
|
+
graphTheme: GraphTheme;
|
|
55
|
+
getSnapshot: () => WorkflowDiagnosticSnapshot;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const DEFAULT_INTERVAL_MS = 1_000;
|
|
59
|
+
const DEFAULT_MAX_CAPTURES = 45;
|
|
60
|
+
|
|
61
|
+
export function isTuiDiagnosticsEnabled(): boolean {
|
|
62
|
+
const value = process.env.ATOMIC_TUI_DIAGNOSTICS;
|
|
63
|
+
return value === "1" || value === "true" || value === "yes";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createTuiDiagnostics({
|
|
67
|
+
renderer,
|
|
68
|
+
graphTheme,
|
|
69
|
+
getSnapshot,
|
|
70
|
+
}: TuiDiagnosticsOptions): TuiDiagnostics | null {
|
|
71
|
+
if (!isTuiDiagnosticsEnabled()) return null;
|
|
72
|
+
|
|
73
|
+
const directory = resolveDiagnosticsDirectory();
|
|
74
|
+
mkdirSync(directory, { recursive: true });
|
|
75
|
+
|
|
76
|
+
let sequence = 0;
|
|
77
|
+
let disposed = false;
|
|
78
|
+
const maxCaptures = readPositiveInt(process.env.ATOMIC_TUI_DIAGNOSTICS_MAX, DEFAULT_MAX_CAPTURES);
|
|
79
|
+
const intervalMs = readPositiveInt(process.env.ATOMIC_TUI_DIAGNOSTICS_INTERVAL_MS, DEFAULT_INTERVAL_MS);
|
|
80
|
+
|
|
81
|
+
writeJson(join(directory, "metadata.json"), {
|
|
82
|
+
directory,
|
|
83
|
+
pid: process.pid,
|
|
84
|
+
startedAt: new Date().toISOString(),
|
|
85
|
+
environment: diagnosticEnvironment(),
|
|
86
|
+
graphTheme,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const capture = (reason: string): void => {
|
|
90
|
+
if (disposed || sequence >= maxCaptures) return;
|
|
91
|
+
sequence++;
|
|
92
|
+
const timestamp = Date.now();
|
|
93
|
+
const payload = {
|
|
94
|
+
sequence,
|
|
95
|
+
reason,
|
|
96
|
+
capturedAt: new Date(timestamp).toISOString(),
|
|
97
|
+
environment: diagnosticEnvironment(),
|
|
98
|
+
renderer: {
|
|
99
|
+
width: renderer.width,
|
|
100
|
+
height: renderer.height,
|
|
101
|
+
terminalWidth: renderer.terminalWidth,
|
|
102
|
+
terminalHeight: renderer.terminalHeight,
|
|
103
|
+
themeMode: renderer.themeMode,
|
|
104
|
+
capabilities: cloneJson(renderer.capabilities),
|
|
105
|
+
},
|
|
106
|
+
graphTheme,
|
|
107
|
+
workflow: getSnapshot(),
|
|
108
|
+
currentRenderBuffer: summarizeBuffer(renderer.currentRenderBuffer),
|
|
109
|
+
nextRenderBuffer: summarizeBuffer(renderer.nextRenderBuffer),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
writeJson(join(directory, `${String(sequence).padStart(4, "0")}-${sanitizeReason(reason)}.json`), payload);
|
|
113
|
+
writeJson(join(directory, "latest.json"), payload);
|
|
114
|
+
|
|
115
|
+
if (process.env.ATOMIC_TUI_DIAGNOSTICS_OPENTUI_DUMP === "1") {
|
|
116
|
+
renderer.dumpBuffers(timestamp);
|
|
117
|
+
renderer.dumpStdoutBuffer(timestamp);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const timer = setInterval(() => capture("interval"), intervalMs);
|
|
122
|
+
capture("created");
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
capture,
|
|
126
|
+
dispose: () => {
|
|
127
|
+
if (disposed) return;
|
|
128
|
+
disposed = true;
|
|
129
|
+
clearInterval(timer);
|
|
130
|
+
writeJson(join(directory, "disposed.json"), {
|
|
131
|
+
disposedAt: new Date().toISOString(),
|
|
132
|
+
captures: sequence,
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function summarizeBuffer(buffer: OptimizedBuffer): BufferDiagnostic {
|
|
139
|
+
const raw = buffer.buffers;
|
|
140
|
+
const width = buffer.width;
|
|
141
|
+
const height = buffer.height;
|
|
142
|
+
const counts = new Map<string, number>();
|
|
143
|
+
const rows: BufferRowDiagnostic[] = [];
|
|
144
|
+
const yellowHueSamples: Array<{ x: number; y: number; color: string; char: string }> = [];
|
|
145
|
+
let yellowHueCells = 0;
|
|
146
|
+
|
|
147
|
+
for (let y = 0; y < height; y++) {
|
|
148
|
+
const backgrounds: BackgroundRun[] = [];
|
|
149
|
+
let text = "";
|
|
150
|
+
let currentColor = "";
|
|
151
|
+
let runStart = 0;
|
|
152
|
+
|
|
153
|
+
for (let x = 0; x < width; x++) {
|
|
154
|
+
const index = y * width + x;
|
|
155
|
+
const color = readColor(raw.bg, index);
|
|
156
|
+
const char = readChar(raw.char[index] ?? 0);
|
|
157
|
+
text += char;
|
|
158
|
+
|
|
159
|
+
counts.set(color, (counts.get(color) ?? 0) + 1);
|
|
160
|
+
if (isYellowHue(color)) {
|
|
161
|
+
yellowHueCells++;
|
|
162
|
+
if (yellowHueSamples.length < 40) {
|
|
163
|
+
yellowHueSamples.push({ x, y, color, char });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (x === 0) {
|
|
168
|
+
currentColor = color;
|
|
169
|
+
runStart = 0;
|
|
170
|
+
} else if (color !== currentColor) {
|
|
171
|
+
backgrounds.push({ x: runStart, width: x - runStart, color: currentColor });
|
|
172
|
+
currentColor = color;
|
|
173
|
+
runStart = x;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (width > 0) {
|
|
178
|
+
backgrounds.push({ x: runStart, width: width - runStart, color: currentColor });
|
|
179
|
+
}
|
|
180
|
+
rows.push({ y, text: text.trimEnd(), backgrounds });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const total = Math.max(1, width * height);
|
|
184
|
+
const topBackgrounds = Array.from(counts.entries())
|
|
185
|
+
.map(([color, count]) => ({
|
|
186
|
+
color,
|
|
187
|
+
count,
|
|
188
|
+
percent: Math.round((count / total) * 10_000) / 100,
|
|
189
|
+
}))
|
|
190
|
+
.sort((a, b) => b.count - a.count)
|
|
191
|
+
.slice(0, 16);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
width,
|
|
195
|
+
height,
|
|
196
|
+
topBackgrounds,
|
|
197
|
+
yellowHueCells,
|
|
198
|
+
yellowHueSamples,
|
|
199
|
+
rows,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveDiagnosticsDirectory(): string {
|
|
204
|
+
const explicit = process.env.ATOMIC_TUI_DIAGNOSTICS_DIR;
|
|
205
|
+
if (explicit && explicit.trim() !== "") return explicit;
|
|
206
|
+
return join(tmpdir(), `atomic-tui-diagnostics-${process.pid}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function diagnosticEnvironment(): Record<string, string | null> {
|
|
210
|
+
return {
|
|
211
|
+
TERM: process.env.TERM ?? null,
|
|
212
|
+
TERM_PROGRAM: process.env.TERM_PROGRAM ?? null,
|
|
213
|
+
COLORTERM: process.env.COLORTERM ?? null,
|
|
214
|
+
TMUX: process.env.TMUX ?? null,
|
|
215
|
+
SSH_TTY: process.env.SSH_TTY ?? null,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readPositiveInt(value: string | undefined, fallback: number): number {
|
|
220
|
+
if (value === undefined) return fallback;
|
|
221
|
+
const parsed = Number.parseInt(value, 10);
|
|
222
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function writeJson(path: string, value: object): void {
|
|
226
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sanitizeReason(reason: string): string {
|
|
230
|
+
const sanitized = reason.replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "");
|
|
231
|
+
return sanitized === "" ? "capture" : sanitized;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function readColor(buffer: Uint16Array, cellIndex: number): string {
|
|
235
|
+
const offset = cellIndex * 4;
|
|
236
|
+
return `#${toHex(buffer[offset] ?? 0)}${toHex(buffer[offset + 1] ?? 0)}${toHex(buffer[offset + 2] ?? 0)}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function toHex(value: number): string {
|
|
240
|
+
const clamped = Math.max(0, Math.min(255, value));
|
|
241
|
+
return clamped.toString(16).padStart(2, "0");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readChar(codePoint: number): string {
|
|
245
|
+
if (codePoint <= 0 || codePoint > 0x10ffff) return " ";
|
|
246
|
+
return String.fromCodePoint(codePoint);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isYellowHue(color: string): boolean {
|
|
250
|
+
const red = Number.parseInt(color.slice(1, 3), 16);
|
|
251
|
+
const green = Number.parseInt(color.slice(3, 5), 16);
|
|
252
|
+
const blue = Number.parseInt(color.slice(5, 7), 16);
|
|
253
|
+
const max = Math.max(red, green, blue);
|
|
254
|
+
const min = Math.min(red, green, blue);
|
|
255
|
+
if (max < 32 || max === min) return false;
|
|
256
|
+
|
|
257
|
+
const saturation = (max - min) / max;
|
|
258
|
+
if (saturation < 0.12) return false;
|
|
259
|
+
|
|
260
|
+
const hue =
|
|
261
|
+
max === red
|
|
262
|
+
? ((green - blue) / (max - min)) * 60
|
|
263
|
+
: max === green
|
|
264
|
+
? (2 + (blue - red) / (max - min)) * 60
|
|
265
|
+
: (4 + (red - green) / (max - min)) * 60;
|
|
266
|
+
const normalizedHue = hue < 0 ? hue + 360 : hue;
|
|
267
|
+
return normalizedHue >= 35 && normalizedHue <= 85;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function cloneJson(value: object | null): object | null {
|
|
271
|
+
if (value === null) return null;
|
|
272
|
+
return JSON.parse(JSON.stringify(value)) as object;
|
|
273
|
+
}
|