@elench/testkit 0.1.89 → 0.1.90
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 +16 -7
- package/lib/cli/agents/index.mjs +48 -9
- package/lib/cli/agents/providers/claude.mjs +3 -3
- package/lib/cli/agents/providers/codex.mjs +3 -3
- package/lib/cli/assistant/bootstrap.mjs +248 -0
- package/lib/cli/assistant/interactive.mjs +52 -0
- package/lib/cli/assistant/prompt-builder.mjs +3 -7
- package/lib/cli/assistant/session.mjs +3 -1
- package/lib/cli/assistant/state.mjs +4 -2
- package/lib/cli/assistant/tool-registry.mjs +17 -17
- package/lib/cli/commands/assistant.mjs +44 -34
- package/lib/cli/{tui/detail-pane.mjs → context-resources.mjs} +81 -21
- package/lib/cli/entrypoint.mjs +12 -4
- package/lib/cli/presentation/tree-reporter.mjs +0 -101
- package/lib/cli/tui/inspect-app.mjs +7 -88
- package/lib/cli/tui/inspect-state.mjs +0 -117
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/lib/cli/agents/investigate.mjs +0 -75
- package/lib/cli/agents/investigation-context.mjs +0 -102
- package/lib/cli/agents/investigation-interpreter.mjs +0 -320
- package/lib/cli/agents/investigation-log.mjs +0 -37
- package/lib/cli/agents/prompt-builder.mjs +0 -25
- package/lib/cli/assistant/content.mjs +0 -60
- package/lib/cli/tui/assistant-app.mjs +0 -82
- package/lib/cli/tui/assistant-render.mjs +0 -99
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import React, { createElement } from "react";
|
|
2
1
|
import { Command, Flags } from "@oclif/core";
|
|
3
|
-
import { render } from "ink";
|
|
4
2
|
import { sharedFlags, resolveConfigsForCommand } from "../command-helpers.mjs";
|
|
5
3
|
import { createAssistantState } from "../assistant/state.mjs";
|
|
6
|
-
import {
|
|
4
|
+
import { runInteractiveAssistant } from "../assistant/interactive.mjs";
|
|
7
5
|
|
|
8
6
|
export default class AssistantCommand extends Command {
|
|
9
|
-
static summary = "Launch
|
|
7
|
+
static summary = "Launch a native Codex or Claude session with testkit context";
|
|
10
8
|
|
|
11
9
|
static enableJsonFlag = true;
|
|
12
10
|
|
|
@@ -20,6 +18,9 @@ export default class AssistantCommand extends Command {
|
|
|
20
18
|
file: Flags.string({
|
|
21
19
|
description: "Initial file selection",
|
|
22
20
|
}),
|
|
21
|
+
prompt: Flags.string({
|
|
22
|
+
description: "Initial interactive prompt for the provider session",
|
|
23
|
+
}),
|
|
23
24
|
message: Flags.string({
|
|
24
25
|
description: "Run one assistant turn non-interactively",
|
|
25
26
|
}),
|
|
@@ -27,29 +28,41 @@ export default class AssistantCommand extends Command {
|
|
|
27
28
|
|
|
28
29
|
async run() {
|
|
29
30
|
const { flags } = await this.parse(AssistantCommand);
|
|
31
|
+
if (flags.message && flags.prompt) {
|
|
32
|
+
this.error("Use either --message or --prompt, not both.");
|
|
33
|
+
}
|
|
30
34
|
const { allConfigs } = await resolveConfigsForCommand(flags);
|
|
31
35
|
const productDir = allConfigs[0]?.productDir || process.cwd();
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
await assistantState.loadLatestArtifact();
|
|
39
|
-
if (flags.file) {
|
|
40
|
-
try {
|
|
41
|
-
const artifact = loadLatestRunArtifact(productDir);
|
|
42
|
-
const subject = resolveFileSubject(artifact, flags.file, flags.service || null);
|
|
43
|
-
assistantState.revealFile(subject.service.name, subject.file.path);
|
|
44
|
-
} catch {
|
|
45
|
-
// Ignore missing initial selection.
|
|
46
|
-
}
|
|
47
|
-
} else if (flags.service) {
|
|
48
|
-
assistantState.revealService(flags.service);
|
|
36
|
+
const interactive =
|
|
37
|
+
(process.stdout.isTTY || process.env.TESTKIT_FORCE_INTERACTIVE_ASSISTANT === "1") &&
|
|
38
|
+
!this.jsonEnabled() &&
|
|
39
|
+
!flags.message;
|
|
40
|
+
if (flags.prompt && !interactive) {
|
|
41
|
+
this.error("--prompt requires an interactive assistant session.");
|
|
49
42
|
}
|
|
50
|
-
|
|
51
|
-
const interactive = process.stdout.isTTY && !this.jsonEnabled() && !flags.message;
|
|
52
43
|
if (!interactive) {
|
|
44
|
+
if (!flags.message) {
|
|
45
|
+
this.error("assistant requires an interactive tty; use --message for one non-interactive turn.");
|
|
46
|
+
}
|
|
47
|
+
const assistantState = createAssistantState({
|
|
48
|
+
productDir,
|
|
49
|
+
provider: flags.provider,
|
|
50
|
+
configs: allConfigs,
|
|
51
|
+
env: process.env,
|
|
52
|
+
});
|
|
53
|
+
await assistantState.loadLatestArtifact();
|
|
54
|
+
if (flags.file) {
|
|
55
|
+
try {
|
|
56
|
+
const { loadLatestRunArtifact, resolveFileSubject } = await import("../viewer.mjs");
|
|
57
|
+
const artifact = loadLatestRunArtifact(productDir);
|
|
58
|
+
const subject = resolveFileSubject(artifact, flags.file, flags.service || null);
|
|
59
|
+
assistantState.revealFile(subject.service.name, subject.file.path);
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore missing initial selection.
|
|
62
|
+
}
|
|
63
|
+
} else if (flags.service) {
|
|
64
|
+
assistantState.revealService(flags.service);
|
|
65
|
+
}
|
|
53
66
|
if (flags.message) {
|
|
54
67
|
await assistantState.submitInput(flags.message);
|
|
55
68
|
}
|
|
@@ -62,16 +75,13 @@ export default class AssistantCommand extends Command {
|
|
|
62
75
|
return snapshot;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
await app.waitUntilExit();
|
|
75
|
-
return assistantState.getSnapshot();
|
|
78
|
+
return runInteractiveAssistant({
|
|
79
|
+
productDir,
|
|
80
|
+
provider: flags.provider,
|
|
81
|
+
file: flags.file || null,
|
|
82
|
+
service: flags.service || null,
|
|
83
|
+
prompt: flags.prompt || null,
|
|
84
|
+
env: process.env,
|
|
85
|
+
});
|
|
76
86
|
}
|
|
77
87
|
}
|
|
@@ -7,32 +7,87 @@ import {
|
|
|
7
7
|
getSetupOperationsForService,
|
|
8
8
|
loadCurrentRunArtifact,
|
|
9
9
|
resolveFileSubject,
|
|
10
|
-
} from "
|
|
11
|
-
import { formatDuration } from "
|
|
12
|
-
import { readLogTail } from "
|
|
10
|
+
} from "./viewer.mjs";
|
|
11
|
+
import { formatDuration } from "../runner/formatting.mjs";
|
|
12
|
+
import { readLogTail } from "../runner/logs.mjs";
|
|
13
13
|
|
|
14
|
-
export function
|
|
15
|
-
|
|
14
|
+
export function readContextContent({
|
|
15
|
+
productDir,
|
|
16
|
+
snapshot,
|
|
17
|
+
mode = "detail",
|
|
18
|
+
logTail = 12,
|
|
19
|
+
previewLength = 6,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const normalizedMode = normalizeMode(mode);
|
|
22
|
+
const selectedEntry = snapshot?.selectedEntry || null;
|
|
23
|
+
const summaryRows = snapshot?.summaryData?.rows || [];
|
|
16
24
|
if (!selectedEntry) {
|
|
17
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
mode: normalizedMode,
|
|
27
|
+
title: "Selection",
|
|
28
|
+
lines: ["No entry selected."],
|
|
29
|
+
data: null,
|
|
30
|
+
selection: null,
|
|
31
|
+
summaryRows,
|
|
32
|
+
};
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
const runArtifact = resolveArtifact(productDir, snapshot);
|
|
21
36
|
const subject = resolveSubject(runArtifact, selectedEntry);
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
let content = null;
|
|
39
|
+
if (normalizedMode === "artifacts") {
|
|
40
|
+
content = buildArtifactsContent(productDir, runArtifact, selectedEntry, subject, previewLength);
|
|
41
|
+
} else if (normalizedMode === "logs") {
|
|
42
|
+
content = buildLogsContent(productDir, runArtifact, selectedEntry, logTail);
|
|
43
|
+
} else if (normalizedMode === "setup") {
|
|
44
|
+
content = buildSetupContent(runArtifact, selectedEntry);
|
|
45
|
+
} else {
|
|
46
|
+
content = buildDetailContent(productDir, runArtifact, selectedEntry, subject, logTail);
|
|
31
47
|
}
|
|
32
|
-
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
mode: normalizedMode,
|
|
51
|
+
title: content.title,
|
|
52
|
+
lines: content.lines || [],
|
|
53
|
+
data: content.data ?? null,
|
|
54
|
+
selection: selectedEntry,
|
|
55
|
+
summaryRows,
|
|
56
|
+
};
|
|
33
57
|
}
|
|
34
58
|
|
|
35
|
-
function
|
|
59
|
+
export function buildContextSelection(snapshot) {
|
|
60
|
+
const selectedEntry = snapshot?.selectedEntry || null;
|
|
61
|
+
const summaryRows = snapshot?.summaryData?.rows || [];
|
|
62
|
+
return {
|
|
63
|
+
selection: selectedEntry
|
|
64
|
+
? {
|
|
65
|
+
kind: selectedEntry.kind,
|
|
66
|
+
label: selectedEntry.label || selectedEntry.filePath || selectedEntry.serviceName || "selection",
|
|
67
|
+
serviceName: selectedEntry.serviceName || null,
|
|
68
|
+
type: selectedEntry.type || null,
|
|
69
|
+
suiteName: selectedEntry.suiteName || null,
|
|
70
|
+
filePath: selectedEntry.filePath || null,
|
|
71
|
+
status: selectedEntry.status || null,
|
|
72
|
+
}
|
|
73
|
+
: null,
|
|
74
|
+
summaryRows,
|
|
75
|
+
phase: snapshot?.phase || null,
|
|
76
|
+
hasArtifact: Boolean(snapshot?.runArtifact),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatContextToolText(title, lines) {
|
|
81
|
+
const normalizedTitle = String(title || "").trim();
|
|
82
|
+
const normalizedLines = Array.isArray(lines) ? lines.filter((line) => String(line).length > 0) : [];
|
|
83
|
+
if (normalizedTitle && normalizedLines.length > 0) {
|
|
84
|
+
return [normalizedTitle, ...normalizedLines].join("\n");
|
|
85
|
+
}
|
|
86
|
+
if (normalizedTitle) return normalizedTitle;
|
|
87
|
+
return normalizedLines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildDetailContent(productDir, runArtifact, entry, subject, logTail) {
|
|
36
91
|
if (subject && runArtifact) {
|
|
37
92
|
return {
|
|
38
93
|
title: "Detail",
|
|
@@ -48,7 +103,7 @@ function buildDetailPane(productDir, runArtifact, entry, subject, logTail) {
|
|
|
48
103
|
};
|
|
49
104
|
}
|
|
50
105
|
|
|
51
|
-
function
|
|
106
|
+
function buildArtifactsContent(productDir, runArtifact, entry, subject, previewLength) {
|
|
52
107
|
if (!runArtifact || !subject) {
|
|
53
108
|
return {
|
|
54
109
|
title: "Artifacts",
|
|
@@ -65,7 +120,7 @@ function buildArtifactsPane(productDir, runArtifact, entry, subject) {
|
|
|
65
120
|
kind: item.artifactRef.kind,
|
|
66
121
|
summary: item.artifactRef.summary,
|
|
67
122
|
path: item.artifactRef.path,
|
|
68
|
-
preview: formatArtifactPreview(item.payload,
|
|
123
|
+
preview: formatArtifactPreview(item.payload, previewLength),
|
|
69
124
|
}));
|
|
70
125
|
|
|
71
126
|
if (entries.length === 0) {
|
|
@@ -82,7 +137,7 @@ function buildArtifactsPane(productDir, runArtifact, entry, subject) {
|
|
|
82
137
|
return { title: "Artifacts", lines, data: entries };
|
|
83
138
|
}
|
|
84
139
|
|
|
85
|
-
function
|
|
140
|
+
function buildLogsContent(productDir, runArtifact, entry, tail) {
|
|
86
141
|
if (!runArtifact) {
|
|
87
142
|
return { title: "Logs", lines: ["Backend logs are available only from persisted run artifacts."], data: [] };
|
|
88
143
|
}
|
|
@@ -103,7 +158,7 @@ function buildLogsPane(productDir, runArtifact, entry, tail) {
|
|
|
103
158
|
return { title: "Logs", lines, data: logs };
|
|
104
159
|
}
|
|
105
160
|
|
|
106
|
-
function
|
|
161
|
+
function buildSetupContent(runArtifact, entry) {
|
|
107
162
|
if (!runArtifact) {
|
|
108
163
|
return { title: "Setup", lines: ["Setup operations are available only from persisted run artifacts."], data: [] };
|
|
109
164
|
}
|
|
@@ -123,7 +178,7 @@ function buildSetupPane(productDir, runArtifact, entry) {
|
|
|
123
178
|
}
|
|
124
179
|
|
|
125
180
|
function resolveArtifact(productDir, snapshot) {
|
|
126
|
-
if (snapshot
|
|
181
|
+
if (snapshot?.runArtifact) return snapshot.runArtifact;
|
|
127
182
|
try {
|
|
128
183
|
return loadCurrentRunArtifact(productDir);
|
|
129
184
|
} catch {
|
|
@@ -159,3 +214,8 @@ function formatAggregateDetail(entry) {
|
|
|
159
214
|
if (entry.error) lines.push(`Error: ${entry.error}`);
|
|
160
215
|
return lines;
|
|
161
216
|
}
|
|
217
|
+
|
|
218
|
+
function normalizeMode(mode) {
|
|
219
|
+
if (mode === "logs" || mode === "artifacts" || mode === "setup") return mode;
|
|
220
|
+
return "detail";
|
|
221
|
+
}
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -36,10 +36,13 @@ export function normalizeCliArgs(argv) {
|
|
|
36
36
|
"--log-tail",
|
|
37
37
|
"--provider",
|
|
38
38
|
"--message",
|
|
39
|
+
"--prompt",
|
|
39
40
|
]);
|
|
40
41
|
const positionals = findPositionals(argv, valueFlags);
|
|
41
42
|
const firstPositional = positionals[0] || null;
|
|
42
|
-
const
|
|
43
|
+
const forcedInteractiveAssistant = process.env.TESTKIT_FORCE_INTERACTIVE_ASSISTANT === "1";
|
|
44
|
+
const interactiveTty = process.stdout.isTTY || forcedInteractiveAssistant;
|
|
45
|
+
const assistantDefaultDisabled = process.env.TESTKIT_NO_ASSISTANT_DEFAULT === "1";
|
|
43
46
|
const runFlagPresent = argv.some((value) =>
|
|
44
47
|
[
|
|
45
48
|
"--type",
|
|
@@ -65,7 +68,7 @@ export function normalizeCliArgs(argv) {
|
|
|
65
68
|
runFlagPresent ||
|
|
66
69
|
!topLevelCommands.has(firstPositional?.value);
|
|
67
70
|
|
|
68
|
-
if (!firstPositional && interactiveTty && !runFlagPresent) {
|
|
71
|
+
if (!firstPositional && interactiveTty && !runFlagPresent && !assistantDefaultDisabled) {
|
|
69
72
|
return ["assistant", ...argv];
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -73,8 +76,13 @@ export function normalizeCliArgs(argv) {
|
|
|
73
76
|
return reorderCommandArgs(argv, positionals);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
if (!topLevelCommands.has(firstPositional?.value) && interactiveTty && !
|
|
77
|
-
|
|
79
|
+
if (!topLevelCommands.has(firstPositional?.value) && interactiveTty && !assistantDefaultDisabled) {
|
|
80
|
+
const shouldOpenAssistantPrompt =
|
|
81
|
+
forcedInteractiveAssistant ||
|
|
82
|
+
(!runFlagPresent && !runTypeShortcuts.has(firstPositional?.value));
|
|
83
|
+
if (shouldOpenAssistantPrompt) {
|
|
84
|
+
return ["assistant", "--prompt", argv.join(" ")];
|
|
85
|
+
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
if (shouldPrefixRun) {
|
|
@@ -9,24 +9,16 @@ import {
|
|
|
9
9
|
applyReporterTaskStarted,
|
|
10
10
|
} from "../tui/inspect-live-adapter.mjs";
|
|
11
11
|
import { suiteSelectionType } from "../../runner/suite-selection.mjs";
|
|
12
|
-
import { startHostedInvestigation } from "../agents/investigate.mjs";
|
|
13
|
-
import { createInvestigationInterpreter } from "../agents/investigation-interpreter.mjs";
|
|
14
|
-
import { writeInvestigationLog } from "../agents/investigation-log.mjs";
|
|
15
12
|
|
|
16
13
|
export function createTreeReporter({ stdout = process.stdout, stderr = process.stderr, productDir } = {}) {
|
|
17
14
|
const inspectState = createInspectState({ dataSource: "live" });
|
|
18
|
-
let activeAgentSession = null;
|
|
19
|
-
let activeInterpreter = null;
|
|
20
|
-
let investigationToken = 0;
|
|
21
15
|
|
|
22
16
|
const app = render(
|
|
23
17
|
createElement(InspectApp, {
|
|
24
18
|
inspectState,
|
|
25
19
|
stdout,
|
|
26
20
|
productDir,
|
|
27
|
-
onInvestigate: startInvestigation,
|
|
28
21
|
onRequestClose: close,
|
|
29
|
-
onCancelInvestigation: cancelInvestigation,
|
|
30
22
|
}),
|
|
31
23
|
{ stdout, exitOnCtrlC: false }
|
|
32
24
|
);
|
|
@@ -98,100 +90,7 @@ export function createTreeReporter({ stdout = process.stdout, stderr = process.s
|
|
|
98
90
|
close,
|
|
99
91
|
};
|
|
100
92
|
|
|
101
|
-
async function startInvestigation({ provider = "auto", userMessage } = {}) {
|
|
102
|
-
const snapshot = inspectState.getSnapshot();
|
|
103
|
-
if (!snapshot.selectedFailure) {
|
|
104
|
-
inspectState.setNotice("No failed file is selected for investigation.");
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (activeAgentSession) {
|
|
108
|
-
inspectState.setNotice("An investigation is already running.");
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const token = ++investigationToken;
|
|
113
|
-
let finalDelivered = false;
|
|
114
|
-
inspectState.beginInvestigation({ provider, userMessage });
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
activeInterpreter = createInvestigationInterpreter();
|
|
118
|
-
activeAgentSession = startHostedInvestigation({
|
|
119
|
-
productDir,
|
|
120
|
-
serviceName: snapshot.selectedFailure.serviceName,
|
|
121
|
-
filePath: snapshot.selectedFailure.filePath,
|
|
122
|
-
provider,
|
|
123
|
-
userMessage,
|
|
124
|
-
onEvent(event) {
|
|
125
|
-
if (token !== investigationToken) return;
|
|
126
|
-
if (event.type === "final") finalDelivered = true;
|
|
127
|
-
const presentation = activeInterpreter?.consumeProviderEvent(event) || null;
|
|
128
|
-
inspectState.recordInvestigationProgress(event, presentation);
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
const result = await activeAgentSession.completion;
|
|
132
|
-
if (token !== investigationToken) return;
|
|
133
|
-
activeAgentSession = null;
|
|
134
|
-
if (result.finalText && !finalDelivered) {
|
|
135
|
-
const finalEvent = { type: "final", text: result.finalText };
|
|
136
|
-
const presentation = activeInterpreter?.consumeProviderEvent(finalEvent) || null;
|
|
137
|
-
inspectState.recordInvestigationProgress(finalEvent, presentation);
|
|
138
|
-
}
|
|
139
|
-
if (result.cancelled) {
|
|
140
|
-
inspectState.cancelAgentSession("Cancelled investigation.");
|
|
141
|
-
persistInvestigationLog();
|
|
142
|
-
activeInterpreter = null;
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (result.exitCode !== 0 && !result.finalText) {
|
|
146
|
-
inspectState.failAgentSession(result.stderr || `Agent exited with code ${result.exitCode}`);
|
|
147
|
-
persistInvestigationLog();
|
|
148
|
-
activeInterpreter = null;
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
inspectState.completeAgentSession({
|
|
152
|
-
finalText: result.finalText,
|
|
153
|
-
exitCode: result.exitCode,
|
|
154
|
-
});
|
|
155
|
-
persistInvestigationLog();
|
|
156
|
-
activeInterpreter = null;
|
|
157
|
-
} catch (error) {
|
|
158
|
-
if (token !== investigationToken) return;
|
|
159
|
-
activeAgentSession = null;
|
|
160
|
-
inspectState.failAgentSession(error);
|
|
161
|
-
persistInvestigationLog();
|
|
162
|
-
activeInterpreter = null;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function cancelInvestigation() {
|
|
167
|
-
if (!activeAgentSession) {
|
|
168
|
-
inspectState.returnToSummary();
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
investigationToken += 1;
|
|
172
|
-
activeAgentSession.cancel();
|
|
173
|
-
activeAgentSession = null;
|
|
174
|
-
inspectState.cancelAgentSession("Cancelled investigation.");
|
|
175
|
-
persistInvestigationLog();
|
|
176
|
-
activeInterpreter = null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
93
|
function close() {
|
|
180
|
-
if (activeAgentSession) {
|
|
181
|
-
investigationToken += 1;
|
|
182
|
-
activeAgentSession.cancel();
|
|
183
|
-
activeAgentSession = null;
|
|
184
|
-
}
|
|
185
94
|
app.unmount();
|
|
186
95
|
}
|
|
187
|
-
|
|
188
|
-
function persistInvestigationLog() {
|
|
189
|
-
const snapshot = inspectState.getSnapshot();
|
|
190
|
-
if (!snapshot.selectedFailure || !snapshot.agentSession) return;
|
|
191
|
-
writeInvestigationLog({
|
|
192
|
-
productDir,
|
|
193
|
-
selectedFailure: snapshot.selectedFailure,
|
|
194
|
-
agentSession: snapshot.agentSession,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
96
|
}
|
|
@@ -13,9 +13,8 @@ import {
|
|
|
13
13
|
} from "../presentation/colors.mjs";
|
|
14
14
|
import { renderSummaryBox } from "../presentation/summary-box.mjs";
|
|
15
15
|
import { getTerminalWidth } from "../presentation/terminal-layout.mjs";
|
|
16
|
-
import {
|
|
16
|
+
import { readContextContent } from "../context-resources.mjs";
|
|
17
17
|
import { applyHighlight } from "./fuzzy-match.mjs";
|
|
18
|
-
import { buildInspectPaneContent } from "./detail-pane.mjs";
|
|
19
18
|
import { FilterBar } from "./filter-bar.mjs";
|
|
20
19
|
|
|
21
20
|
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
@@ -24,9 +23,7 @@ export function InspectApp({
|
|
|
24
23
|
inspectState,
|
|
25
24
|
stdout,
|
|
26
25
|
productDir,
|
|
27
|
-
onInvestigate,
|
|
28
26
|
onRequestClose,
|
|
29
|
-
onCancelInvestigation,
|
|
30
27
|
} = {}) {
|
|
31
28
|
const { exit } = useApp();
|
|
32
29
|
const [snapshot, setSnapshot] = useState(() => inspectState.getSnapshot());
|
|
@@ -41,25 +38,6 @@ export function InspectApp({
|
|
|
41
38
|
}, [inspectState]);
|
|
42
39
|
|
|
43
40
|
useInput((input, key) => {
|
|
44
|
-
if (snapshot.mode === "investigating") {
|
|
45
|
-
if (input === "q") {
|
|
46
|
-
(onRequestClose || exit)();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (input === "t") {
|
|
50
|
-
inspectState.toggleInvestigationViewMode();
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (input === "x") {
|
|
54
|
-
onCancelInvestigation?.();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (input === "b" && snapshot.agentSession?.status !== "running" && snapshot.agentSession?.status !== "starting") {
|
|
58
|
-
inspectState.returnToSummary();
|
|
59
|
-
}
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
41
|
if (!snapshot.finished) {
|
|
64
42
|
if (input === "q") {
|
|
65
43
|
inspectState.setNotice("Run is still in progress. Wait for completion before closing.");
|
|
@@ -115,20 +93,6 @@ export function InspectApp({
|
|
|
115
93
|
inspectState.cyclePaneMode();
|
|
116
94
|
return;
|
|
117
95
|
}
|
|
118
|
-
if ((input === "y" || input === "i") && snapshot.selectedFailure) {
|
|
119
|
-
onInvestigate?.({
|
|
120
|
-
provider: "auto",
|
|
121
|
-
userMessage: defaultInvestigationMessage(),
|
|
122
|
-
});
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (input === "c" && snapshot.selectedFailure) {
|
|
126
|
-
onInvestigate?.({ provider: "claude", userMessage: defaultInvestigationMessage() });
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (input === "o" && snapshot.selectedFailure) {
|
|
130
|
-
onInvestigate?.({ provider: "codex", userMessage: defaultInvestigationMessage() });
|
|
131
|
-
}
|
|
132
96
|
});
|
|
133
97
|
|
|
134
98
|
const terminalWidth = getTerminalWidth(stdout, 100);
|
|
@@ -140,13 +104,11 @@ export function InspectApp({
|
|
|
140
104
|
);
|
|
141
105
|
const paneContent = useMemo(
|
|
142
106
|
() =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
paneMode: snapshot.paneMode,
|
|
149
|
-
}),
|
|
107
|
+
readContextContent({
|
|
108
|
+
productDir,
|
|
109
|
+
snapshot,
|
|
110
|
+
mode: snapshot.paneMode,
|
|
111
|
+
}),
|
|
150
112
|
[productDir, snapshot]
|
|
151
113
|
);
|
|
152
114
|
const summaryLines = snapshot.finished && snapshot.summaryData
|
|
@@ -188,55 +150,12 @@ export function buildHeaderText(snapshot) {
|
|
|
188
150
|
}
|
|
189
151
|
|
|
190
152
|
export function buildFooterText(snapshot) {
|
|
191
|
-
if (snapshot.mode === "investigating") {
|
|
192
|
-
if (snapshot.agentSession?.status === "running" || snapshot.agentSession?.status === "starting") {
|
|
193
|
-
return "t transcript · x cancel investigation · q quit";
|
|
194
|
-
}
|
|
195
|
-
return "t transcript · b back · q quit";
|
|
196
|
-
}
|
|
197
153
|
if (!snapshot.finished) return "Run in progress";
|
|
198
154
|
if (snapshot.filter.active) {
|
|
199
155
|
return "type to filter · ↑/↓ move · Esc clear filter · q quit";
|
|
200
156
|
}
|
|
201
157
|
const inspectKeys = "↑/↓ move · Enter collapse/expand · Tab cycle pane · / filter";
|
|
202
|
-
|
|
203
|
-
return `${inspectKeys} · y investigate · c Claude · o Codex · q quit`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function formatAgentEntry(entry) {
|
|
207
|
-
if (!entry) return "";
|
|
208
|
-
if (entry.kind === "status") return `[status] ${entry.text}`;
|
|
209
|
-
if (entry.kind === "tool") return `[tool] ${entry.text}`;
|
|
210
|
-
if (entry.kind === "error") return `[error] ${entry.text}`;
|
|
211
|
-
return entry.text;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function formatTimelineEntry(entry) {
|
|
215
|
-
if (!entry) return "";
|
|
216
|
-
if (entry.kind === "result") return entry.summary || "";
|
|
217
|
-
if (entry.kind === "notice") return `${entry.severity || "info"}: ${entry.message}`;
|
|
218
|
-
return entry.message || entry.summary || "";
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function buildInvestigationLines(snapshot) {
|
|
222
|
-
const transcriptEntries = snapshot.agentSession?.transcriptEntries || [];
|
|
223
|
-
const timeline = snapshot.agentSession?.timeline || [];
|
|
224
|
-
const viewMode = snapshot.agentSession?.viewMode || "summary";
|
|
225
|
-
const status = snapshot.agentSession?.status || "idle";
|
|
226
|
-
const lines = [
|
|
227
|
-
`Status: ${status}`,
|
|
228
|
-
`Phase: ${snapshot.agentSession?.activePhase || "planning"}`,
|
|
229
|
-
"",
|
|
230
|
-
];
|
|
231
|
-
if (viewMode === "transcript") {
|
|
232
|
-
for (const entry of transcriptEntries.slice(-28)) lines.push(formatAgentEntry(entry));
|
|
233
|
-
return lines;
|
|
234
|
-
}
|
|
235
|
-
for (const entry of timeline.slice(-16)) lines.push(formatTimelineEntry(entry));
|
|
236
|
-
if (timeline.length === 0 && snapshot.agentSession?.finalText) {
|
|
237
|
-
lines.push(snapshot.agentSession.finalText);
|
|
238
|
-
}
|
|
239
|
-
return lines;
|
|
158
|
+
return `${inspectKeys} · q quit`;
|
|
240
159
|
}
|
|
241
160
|
|
|
242
161
|
function buildTreeViewport(entries, selectedEntryId, radius) {
|