@prads01/blackbox 0.1.1 → 0.1.2
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/commands/analyze.js +2 -2
- package/dist/commands/setup.js +1 -0
- package/dist/commands/status.js +3 -2
- package/dist/commands/watch.js +2 -2
- package/dist/config.js +4 -4
- package/dist/linking.js +71 -0
- package/dist/tui/app.js +26 -7
- package/dist/tui/data.js +75 -2
- package/dist/tui/keybindings.js +26 -0
- package/dist/tui/render.js +98 -4
- package/package.json +1 -1
package/dist/commands/analyze.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { callAnalyzeApi } from "../api.js";
|
|
2
|
-
import { readConfig } from "../config.js";
|
|
3
2
|
import { loadEvidenceFromConfig } from "../files.js";
|
|
3
|
+
import { ensureRepoLinkedForCli } from "../linking.js";
|
|
4
4
|
import { printTruthReport } from "../report.js";
|
|
5
5
|
import { colors, section } from "../utils/format.js";
|
|
6
6
|
export async function runAnalyzeOnce() {
|
|
7
|
-
const config = await
|
|
7
|
+
const config = await ensureRepoLinkedForCli();
|
|
8
8
|
const evidence = await loadEvidenceFromConfig(config);
|
|
9
9
|
const payload = {
|
|
10
10
|
project: {
|
package/dist/commands/setup.js
CHANGED
|
@@ -139,6 +139,7 @@ export async function runSetup() {
|
|
|
139
139
|
expected_file: expectedFile,
|
|
140
140
|
config_file: configFile,
|
|
141
141
|
commit_file: base.commit_file || defaults.commit_file,
|
|
142
|
+
linked_repo: base.linked_repo,
|
|
142
143
|
};
|
|
143
144
|
const writeSpinner = ora("Writing .blackbox.yaml").start();
|
|
144
145
|
const configPath = await writeConfig(configToWrite);
|
package/dist/commands/status.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isApiReachable } from "../api.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ensureRepoLinkedForCli } from "../linking.js";
|
|
3
3
|
import { colors, row, section } from "../utils/format.js";
|
|
4
4
|
export async function runStatus() {
|
|
5
5
|
try {
|
|
6
|
-
const config = await
|
|
6
|
+
const config = await ensureRepoLinkedForCli();
|
|
7
7
|
section("BlackBox Status");
|
|
8
8
|
row("Project:", config.project_name);
|
|
9
9
|
row("Environment:", config.environment);
|
|
@@ -12,6 +12,7 @@ export async function runStatus() {
|
|
|
12
12
|
row("Expected file:", config.expected_file);
|
|
13
13
|
row("Config file:", config.config_file);
|
|
14
14
|
row("Commit file:", config.commit_file);
|
|
15
|
+
row("Linked repo:", config.linked_repo ?? colors.warn("not linked"));
|
|
15
16
|
const reachable = await isApiReachable(config);
|
|
16
17
|
row("API reachable:", reachable ? colors.ok("yes") : colors.bad("no"));
|
|
17
18
|
}
|
package/dist/commands/watch.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import chokidar from "chokidar";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureRepoLinkedForCli } from "../linking.js";
|
|
4
4
|
import { runAnalyzeOnce } from "./analyze.js";
|
|
5
5
|
import { colors, row, section } from "../utils/format.js";
|
|
6
6
|
const WATCH_DEBOUNCE_MS = 1200;
|
|
7
7
|
const WATCH_COOLDOWN_MS = 2000;
|
|
8
8
|
export async function runWatch() {
|
|
9
9
|
try {
|
|
10
|
-
const config = await
|
|
10
|
+
const config = await ensureRepoLinkedForCli();
|
|
11
11
|
const absoluteLogPath = path.resolve(process.cwd(), config.log_file);
|
|
12
12
|
let isAnalyzing = false;
|
|
13
13
|
let debounceTimer = null;
|
package/dist/config.js
CHANGED
|
@@ -10,10 +10,10 @@ export function createDefaultConfig(cwd = process.cwd()) {
|
|
|
10
10
|
project_name: path.basename(cwd),
|
|
11
11
|
environment: "local",
|
|
12
12
|
api_base_url: "http://localhost:3000",
|
|
13
|
-
log_file: "
|
|
14
|
-
expected_file: "
|
|
15
|
-
config_file: "
|
|
16
|
-
commit_file: "
|
|
13
|
+
log_file: "demo-system/logs/app.log",
|
|
14
|
+
expected_file: "demo-system/config/service-config.json",
|
|
15
|
+
config_file: "demo-system/state/system-state.json",
|
|
16
|
+
commit_file: "demo-system/config/recent-change.json",
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
function assertConfigShape(value) {
|
package/dist/linking.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readConfig, writeConfig } from "./config.js";
|
|
3
|
+
import { colors } from "./utils/format.js";
|
|
4
|
+
function openBrowser(url) {
|
|
5
|
+
const platform = process.platform;
|
|
6
|
+
if (platform === "darwin") {
|
|
7
|
+
const child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
8
|
+
child.unref();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (platform === "win32") {
|
|
12
|
+
const child = spawn("cmd", ["/c", "start", "", url], {
|
|
13
|
+
detached: true,
|
|
14
|
+
stdio: "ignore",
|
|
15
|
+
});
|
|
16
|
+
child.unref();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
20
|
+
child.unref();
|
|
21
|
+
}
|
|
22
|
+
async function startLinkFlow(config) {
|
|
23
|
+
const response = await fetch(`${config.api_base_url}/api/cli/link/start`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Unable to start CLI link flow (${response.status}).`);
|
|
28
|
+
}
|
|
29
|
+
return (await response.json());
|
|
30
|
+
}
|
|
31
|
+
async function getLinkStatus(config, token) {
|
|
32
|
+
const response = await fetch(`${config.api_base_url}/api/cli/link/${token}`, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Unable to fetch CLI link status (${response.status}).`);
|
|
37
|
+
}
|
|
38
|
+
return (await response.json());
|
|
39
|
+
}
|
|
40
|
+
export async function ensureRepoLinkedForCli() {
|
|
41
|
+
const config = await readConfig();
|
|
42
|
+
if (config.linked_repo && config.linked_repo.trim().length > 0) {
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
console.log(colors.warn("No linked GitHub repo found for this CLI config."));
|
|
46
|
+
console.log(colors.muted("Starting browser-based auth and repo linking flow..."));
|
|
47
|
+
const start = await startLinkFlow(config);
|
|
48
|
+
console.log(colors.title(`Open this URL if browser does not open:\n${start.linkUrl}`));
|
|
49
|
+
try {
|
|
50
|
+
openBrowser(start.linkUrl);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// no-op, URL is already printed
|
|
54
|
+
}
|
|
55
|
+
const maxAttempts = 120;
|
|
56
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
58
|
+
const status = await getLinkStatus(config, start.token);
|
|
59
|
+
if (status.status !== "completed" || !status.repo)
|
|
60
|
+
continue;
|
|
61
|
+
const linkedRepo = status.repo.fullName;
|
|
62
|
+
const nextConfig = {
|
|
63
|
+
...config,
|
|
64
|
+
linked_repo: linkedRepo,
|
|
65
|
+
};
|
|
66
|
+
await writeConfig(nextConfig);
|
|
67
|
+
console.log(colors.ok(`Linked repository: ${linkedRepo}`));
|
|
68
|
+
return nextConfig;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("CLI link timed out. Complete browser auth and retry.");
|
|
71
|
+
}
|
package/dist/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import blessed from "blessed";
|
|
2
|
-
import { fetchApiReachability, fetchIncidents, loadTuiConfig,
|
|
2
|
+
import { fetchAnalysisRun, fetchApiReachability, fetchIncidents, loadTuiConfig, startAnalysisRun, triggerCreatePr, triggerSandboxFix, updateIncidentStatus, } from "./data.js";
|
|
3
3
|
import { attachKeybindings } from "./keybindings.js";
|
|
4
4
|
import { indexToView, palette, renderFooter, renderMain, renderSidebar, viewToIndex, } from "./render.js";
|
|
5
5
|
import { TuiWatchService } from "./watch-service.js";
|
|
@@ -27,6 +27,7 @@ function createInitialState(config) {
|
|
|
27
27
|
incidents: [],
|
|
28
28
|
selectedIncidentIndex: 0,
|
|
29
29
|
incidentsMode: "list",
|
|
30
|
+
contextSourceView: "logs",
|
|
30
31
|
sandboxMode: "list",
|
|
31
32
|
selectedSandboxIndex: 0,
|
|
32
33
|
lastSandboxIncidentId: null,
|
|
@@ -39,6 +40,7 @@ function createInitialState(config) {
|
|
|
39
40
|
sandboxPrError: null,
|
|
40
41
|
sandboxPrInFlight: false,
|
|
41
42
|
analyzeInFlight: false,
|
|
43
|
+
currentAnalysisRun: null,
|
|
42
44
|
busyMessage: null,
|
|
43
45
|
statusMessage: null,
|
|
44
46
|
errorMessage: null,
|
|
@@ -220,12 +222,29 @@ export async function runTui() {
|
|
|
220
222
|
state.analyzeInFlight = true;
|
|
221
223
|
let didSucceed = false;
|
|
222
224
|
await withBusy("Analyzing...", async () => {
|
|
223
|
-
await
|
|
224
|
-
state.
|
|
225
|
-
|
|
226
|
-
state.
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
const run = await startAnalysisRun(config);
|
|
226
|
+
state.currentAnalysisRun = run;
|
|
227
|
+
state.view = "analyze";
|
|
228
|
+
state.selectedNavIndex = viewToIndex("analyze");
|
|
229
|
+
render();
|
|
230
|
+
while (true) {
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
232
|
+
const next = await fetchAnalysisRun(config, run.id);
|
|
233
|
+
state.currentAnalysisRun = next;
|
|
234
|
+
render();
|
|
235
|
+
if (next.status === "running")
|
|
236
|
+
continue;
|
|
237
|
+
if (next.status === "failed") {
|
|
238
|
+
state.errorMessage = next.error ?? "Analysis run failed.";
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
didSucceed = true;
|
|
242
|
+
state.lastAnalysisAt = new Date().toISOString();
|
|
243
|
+
await refreshData();
|
|
244
|
+
state.view = "dashboard";
|
|
245
|
+
state.selectedNavIndex = viewToIndex("dashboard");
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
229
248
|
});
|
|
230
249
|
state.analyzeInFlight = false;
|
|
231
250
|
if (didSucceed) {
|
package/dist/tui/data.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isApiReachable } from "../api.js";
|
|
2
|
-
import { readConfig } from "../config.js";
|
|
3
2
|
import { loadEvidenceFromConfig } from "../files.js";
|
|
3
|
+
import { ensureRepoLinkedForCli } from "../linking.js";
|
|
4
4
|
function assertIncidentsResponse(value) {
|
|
5
5
|
if (!value || typeof value !== "object")
|
|
6
6
|
return false;
|
|
@@ -25,11 +25,21 @@ function assertIncidentsResponse(value) {
|
|
|
25
25
|
typeof i.impact === "string" &&
|
|
26
26
|
typeof i.nextAction === "string" &&
|
|
27
27
|
(i.evidencePoints === undefined || Array.isArray(i.evidencePoints)) &&
|
|
28
|
+
(i.investigation === undefined ||
|
|
29
|
+
(typeof i.investigation === "object" &&
|
|
30
|
+
i.investigation !== null &&
|
|
31
|
+
typeof i.investigation.investigationSummary ===
|
|
32
|
+
"string" &&
|
|
33
|
+
Array.isArray(i.investigation.evidence) &&
|
|
34
|
+
typeof i.investigation.confidence === "number")) &&
|
|
35
|
+
(i.investigationTimeline === undefined || Array.isArray(i.investigationTimeline)) &&
|
|
36
|
+
(i.investigationTools === undefined || Array.isArray(i.investigationTools)) &&
|
|
37
|
+
(i.toolOutputsSummary === undefined || Array.isArray(i.toolOutputsSummary)) &&
|
|
28
38
|
typeof i.timestamp === "string");
|
|
29
39
|
});
|
|
30
40
|
}
|
|
31
41
|
export async function loadTuiConfig() {
|
|
32
|
-
return
|
|
42
|
+
return ensureRepoLinkedForCli();
|
|
33
43
|
}
|
|
34
44
|
export async function fetchApiReachability(config) {
|
|
35
45
|
return isApiReachable(config);
|
|
@@ -80,6 +90,69 @@ export async function triggerAnalysis(config) {
|
|
|
80
90
|
}
|
|
81
91
|
return result;
|
|
82
92
|
}
|
|
93
|
+
function assertAnalysisRun(value) {
|
|
94
|
+
if (!value || typeof value !== "object")
|
|
95
|
+
return false;
|
|
96
|
+
const candidate = value;
|
|
97
|
+
if (typeof candidate.id !== "string" ||
|
|
98
|
+
(candidate.status !== "running" &&
|
|
99
|
+
candidate.status !== "completed" &&
|
|
100
|
+
candidate.status !== "failed") ||
|
|
101
|
+
typeof candidate.startedAt !== "string" ||
|
|
102
|
+
!Array.isArray(candidate.steps)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return candidate.steps.every((step) => {
|
|
106
|
+
if (!step || typeof step !== "object")
|
|
107
|
+
return false;
|
|
108
|
+
const s = step;
|
|
109
|
+
return (typeof s.id === "string" &&
|
|
110
|
+
typeof s.label === "string" &&
|
|
111
|
+
(s.status === "pending" || s.status === "running" || s.status === "completed"));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export async function startAnalysisRun(config) {
|
|
115
|
+
const evidence = await loadEvidenceFromConfig(config);
|
|
116
|
+
const payload = {
|
|
117
|
+
project: {
|
|
118
|
+
name: config.project_name,
|
|
119
|
+
environment: config.environment,
|
|
120
|
+
},
|
|
121
|
+
evidence,
|
|
122
|
+
};
|
|
123
|
+
const response = await fetch(`${config.api_base_url}/api/analyze/run`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify(payload),
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const body = await response.text();
|
|
132
|
+
throw new Error(`Analyze run start failed (${response.status}): ${body}`);
|
|
133
|
+
}
|
|
134
|
+
const payloadJson = (await response.json());
|
|
135
|
+
const run = payloadJson.run;
|
|
136
|
+
if (!assertAnalysisRun(run)) {
|
|
137
|
+
throw new Error("Analyze run start returned invalid payload.");
|
|
138
|
+
}
|
|
139
|
+
return run;
|
|
140
|
+
}
|
|
141
|
+
export async function fetchAnalysisRun(config, runId) {
|
|
142
|
+
const response = await fetch(`${config.api_base_url}/api/analyze/run/${runId}`, {
|
|
143
|
+
method: "GET",
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const body = await response.text();
|
|
147
|
+
throw new Error(`Analyze run fetch failed (${response.status}): ${body}`);
|
|
148
|
+
}
|
|
149
|
+
const payload = (await response.json());
|
|
150
|
+
const run = payload.run;
|
|
151
|
+
if (!assertAnalysisRun(run)) {
|
|
152
|
+
throw new Error("Analyze run status payload invalid.");
|
|
153
|
+
}
|
|
154
|
+
return run;
|
|
155
|
+
}
|
|
83
156
|
function assertSandboxResult(value) {
|
|
84
157
|
if (!value || typeof value !== "object")
|
|
85
158
|
return false;
|
package/dist/tui/keybindings.js
CHANGED
|
@@ -56,6 +56,7 @@ export function attachKeybindings(screen, state, actions) {
|
|
|
56
56
|
}
|
|
57
57
|
if (state.view === "incidents" && state.incidentsMode === "list" && state.incidents.length > 0) {
|
|
58
58
|
state.incidentsMode = "detail";
|
|
59
|
+
state.contextSourceView = "logs";
|
|
59
60
|
actions.render();
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
@@ -82,9 +83,34 @@ export function attachKeybindings(screen, state, actions) {
|
|
|
82
83
|
screen.key(["i"], () => {
|
|
83
84
|
state.view = "incidents";
|
|
84
85
|
state.incidentsMode = "list";
|
|
86
|
+
state.contextSourceView = "logs";
|
|
85
87
|
state.selectedNavIndex = 1;
|
|
86
88
|
actions.render();
|
|
87
89
|
});
|
|
90
|
+
screen.key(["1"], () => {
|
|
91
|
+
if (state.view === "incidents" && state.incidentsMode === "detail") {
|
|
92
|
+
state.contextSourceView = "logs";
|
|
93
|
+
actions.render();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
screen.key(["2"], () => {
|
|
97
|
+
if (state.view === "incidents" && state.incidentsMode === "detail") {
|
|
98
|
+
state.contextSourceView = "config";
|
|
99
|
+
actions.render();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
screen.key(["3"], () => {
|
|
103
|
+
if (state.view === "incidents" && state.incidentsMode === "detail") {
|
|
104
|
+
state.contextSourceView = "commits";
|
|
105
|
+
actions.render();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
screen.key(["4"], () => {
|
|
109
|
+
if (state.view === "incidents" && state.incidentsMode === "detail") {
|
|
110
|
+
state.contextSourceView = "system_state";
|
|
111
|
+
actions.render();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
88
114
|
screen.key(["s"], () => {
|
|
89
115
|
state.view = "sandbox";
|
|
90
116
|
state.sandboxMode = "list";
|
package/dist/tui/render.js
CHANGED
|
@@ -108,6 +108,48 @@ function renderIncidentDetail(state) {
|
|
|
108
108
|
if (!incident) {
|
|
109
109
|
return "Incident not found.";
|
|
110
110
|
}
|
|
111
|
+
const investigation = incident.investigation;
|
|
112
|
+
const evidence = investigation?.evidence && investigation.evidence.length > 0
|
|
113
|
+
? investigation.evidence
|
|
114
|
+
: incident.evidencePoints ?? [];
|
|
115
|
+
const timeline = incident.investigationTimeline ?? [];
|
|
116
|
+
const investigationTools = incident.investigationTools ?? [];
|
|
117
|
+
const context = incident.context;
|
|
118
|
+
const contextHeader = [
|
|
119
|
+
"{bold}{white-fg}CONTEXT SOURCES{/white-fg}{/bold}",
|
|
120
|
+
`[1] Logs [2] Config [3] Commits [4] System State (active: ${state.contextSourceView})`,
|
|
121
|
+
];
|
|
122
|
+
const contextBody = (() => {
|
|
123
|
+
if (!context)
|
|
124
|
+
return ["No context data available for this incident."];
|
|
125
|
+
if (state.contextSourceView === "logs") {
|
|
126
|
+
const logs = context.logs?.slice(-12) ?? [];
|
|
127
|
+
return logs.length > 0 ? logs : ["No log lines available."];
|
|
128
|
+
}
|
|
129
|
+
if (state.contextSourceView === "config") {
|
|
130
|
+
return [
|
|
131
|
+
`Service: ${context.systemState.service || "N/A"}`,
|
|
132
|
+
`Environment: ${context.systemState.environment || "N/A"}`,
|
|
133
|
+
`Expected Topic: ${context.systemState.invalidationTopicExpected || "N/A"}`,
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
if (state.contextSourceView === "commits") {
|
|
137
|
+
const commits = context.recentCommits ?? [];
|
|
138
|
+
if (commits.length === 0)
|
|
139
|
+
return ["No commit context available."];
|
|
140
|
+
return commits.slice(0, 5).flatMap((commit) => [
|
|
141
|
+
`${commit.sha.slice(0, 7)} ${commit.message}`,
|
|
142
|
+
` ${commit.summary}`,
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
return [
|
|
146
|
+
`Cache Version: ${context.systemState.cacheVersion || "N/A"}`,
|
|
147
|
+
`DB Version: ${context.systemState.dbVersion || "N/A"}`,
|
|
148
|
+
`Observed Topic: ${context.systemState.invalidationTopicObserved || "N/A"}`,
|
|
149
|
+
`Expected Topic: ${context.systemState.invalidationTopicExpected || "N/A"}`,
|
|
150
|
+
`Error Rate: ${context.systemState.recentErrorRate || "N/A"}`,
|
|
151
|
+
];
|
|
152
|
+
})();
|
|
111
153
|
return [
|
|
112
154
|
"{bold}{white-fg}Incident Report{/white-fg}{/bold}",
|
|
113
155
|
"{gray-fg}Backspace to return to incidents list.{/gray-fg}",
|
|
@@ -123,6 +165,10 @@ function renderIncidentDetail(state) {
|
|
|
123
165
|
"{bold}{white-fg}Root Cause{/white-fg}{/bold}",
|
|
124
166
|
incident.rootCause,
|
|
125
167
|
"",
|
|
168
|
+
"{bold}{white-fg}Investigation Summary{/white-fg}{/bold}",
|
|
169
|
+
investigation?.investigationSummary ?? "No synthesized investigation summary available.",
|
|
170
|
+
`{bold}Confidence{/bold}: ${investigation ? `${Math.round(investigation.confidence * 100)}%` : "N/A"}`,
|
|
171
|
+
"",
|
|
126
172
|
"{bold}{white-fg}Impact{/white-fg}{/bold}",
|
|
127
173
|
incident.impact,
|
|
128
174
|
"",
|
|
@@ -130,15 +176,37 @@ function renderIncidentDetail(state) {
|
|
|
130
176
|
incident.fixSuggestion?.trim() || "No fix suggestion available.",
|
|
131
177
|
"",
|
|
132
178
|
"{bold}{white-fg}Evidence Points{/white-fg}{/bold}",
|
|
133
|
-
...(
|
|
134
|
-
?
|
|
179
|
+
...(evidence.length
|
|
180
|
+
? evidence.slice(0, 5).map((point) => `- ${point}`)
|
|
135
181
|
: ["- No evidence points available."]),
|
|
182
|
+
"",
|
|
183
|
+
"{bold}{white-fg}Investigation Timeline{/white-fg}{/bold}",
|
|
184
|
+
...(timeline.length
|
|
185
|
+
? timeline.slice(0, 8).map((entry) => {
|
|
186
|
+
const source = entry.source ? ` [${entry.source}]` : "";
|
|
187
|
+
return `${entry.step}. ${entry.title}${source}\n ${entry.detail}`;
|
|
188
|
+
})
|
|
189
|
+
: ["No timeline steps available."]),
|
|
190
|
+
"",
|
|
191
|
+
"{bold}{white-fg}Investigation Tools{/white-fg}{/bold}",
|
|
192
|
+
...(investigationTools.length
|
|
193
|
+
? investigationTools.map((tool) => {
|
|
194
|
+
const status = tool.status === "completed"
|
|
195
|
+
? "{green-fg}completed{/green-fg}"
|
|
196
|
+
: "{red-fg}failed{/red-fg}";
|
|
197
|
+
return `- ${tool.name} [${status}]\n ${tool.summary}`;
|
|
198
|
+
})
|
|
199
|
+
: ["No tool execution records available."]),
|
|
200
|
+
"",
|
|
201
|
+
...contextHeader,
|
|
202
|
+
...contextBody,
|
|
136
203
|
].join("\n");
|
|
137
204
|
}
|
|
138
205
|
function renderIncidents(state) {
|
|
139
206
|
return state.incidentsMode === "detail" ? renderIncidentDetail(state) : renderIncidentList(state);
|
|
140
207
|
}
|
|
141
208
|
function renderAnalyze(state) {
|
|
209
|
+
const run = state.currentAnalysisRun;
|
|
142
210
|
return [
|
|
143
211
|
"{bold}{white-fg}Analyze{/white-fg}{/bold}",
|
|
144
212
|
"",
|
|
@@ -147,6 +215,29 @@ function renderAnalyze(state) {
|
|
|
147
215
|
"",
|
|
148
216
|
"Press {bold}a{/bold} to trigger analysis once.",
|
|
149
217
|
"Repeated keypresses are ignored while analysis is running.",
|
|
218
|
+
"",
|
|
219
|
+
"{bold}{white-fg}Investigation Activity{/white-fg}{/bold}",
|
|
220
|
+
...(run
|
|
221
|
+
? [
|
|
222
|
+
`Run ID: ${run.id}`,
|
|
223
|
+
`Run Status: ${run.status === "completed"
|
|
224
|
+
? "{green-fg}COMPLETED{/green-fg}"
|
|
225
|
+
: run.status === "failed"
|
|
226
|
+
? "{red-fg}FAILED{/red-fg}"
|
|
227
|
+
: "{yellow-fg}RUNNING{/yellow-fg}"}`,
|
|
228
|
+
...(run.incidentId ? [`Incident: ${run.incidentId}`] : []),
|
|
229
|
+
"",
|
|
230
|
+
...run.steps.map((step, index) => {
|
|
231
|
+
const status = step.status === "completed"
|
|
232
|
+
? "{green-fg}completed{/green-fg}"
|
|
233
|
+
: step.status === "running"
|
|
234
|
+
? "{yellow-fg}running{/yellow-fg}"
|
|
235
|
+
: "{gray-fg}pending{/gray-fg}";
|
|
236
|
+
const detail = step.detail ? `\n ${step.detail}` : "";
|
|
237
|
+
return `${index + 1}. ${step.label} [${status}]${detail}`;
|
|
238
|
+
}),
|
|
239
|
+
]
|
|
240
|
+
: ["No active investigation run."]),
|
|
150
241
|
].join("\n");
|
|
151
242
|
}
|
|
152
243
|
function renderWatch(state) {
|
|
@@ -269,9 +360,12 @@ export function renderMain(main, state) {
|
|
|
269
360
|
}
|
|
270
361
|
export function renderFooter(footer, state) {
|
|
271
362
|
const hotkeysBase = "a analyze | w watch | i incidents | d dashboard | s sandbox | x sandbox-fix | f mark-fixed | v mark-investigating";
|
|
363
|
+
const contextHotkeys = state.view === "incidents" && state.incidentsMode === "detail"
|
|
364
|
+
? " | 1 logs | 2 config | 3 commits | 4 system"
|
|
365
|
+
: "";
|
|
272
366
|
const hotkeys = canCreatePrFromSandbox(state)
|
|
273
|
-
? `${hotkeysBase} | c create-pr | r refresh | q quit`
|
|
274
|
-
: `${hotkeysBase} | r refresh | q quit`;
|
|
367
|
+
? `${hotkeysBase}${contextHotkeys} | c create-pr | r refresh | q quit`
|
|
368
|
+
: `${hotkeysBase}${contextHotkeys} | r refresh | q quit`;
|
|
275
369
|
const status = [
|
|
276
370
|
`focus:${state.focusZone.toUpperCase()}`,
|
|
277
371
|
`api:${state.apiReachable ? "up" : "down"}`,
|