@siuver/omp-debug-mode 0.1.5 โ 0.1.6
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/CHANGELOG.md +73 -48
- package/README.md +84 -31
- package/package.json +41 -41
- package/src/debug-mode.ts +240 -92
- package/src/evidence.ts +325 -169
- package/src/gate.ts +115 -51
- package/src/log-files.ts +154 -115
- package/src/machine.ts +136 -49
- package/src/main.ts +24 -24
- package/src/methodology.ts +46 -15
- package/src/state.ts +191 -42
- package/src/tools.ts +195 -9
- package/src/ui.ts +109 -69
package/src/ui.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import { PROCEED_REMINDER } from "./methodology";
|
|
4
|
-
import type { DebugSession, DebugState, EvidenceRequest } from "./state";
|
|
2
|
+
import type { DebugSession, DebugState, EvidenceRequest, HandoffMode } from "./state";
|
|
5
3
|
import { currentRound, pendingRequests } from "./state";
|
|
6
4
|
|
|
7
5
|
export const WIDGET_MAX_LINES = 10;
|
|
8
6
|
const WIDGET_MAX_WIDTH = 90;
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
/** Above the editor: what this round asked the user to do. */
|
|
9
|
+
export const WIDGET_CONTEXT_KEY = "debug-mode";
|
|
10
|
+
/** Below the editor: the commands that finish or keep the round. */
|
|
11
|
+
export const WIDGET_ACTIONS_KEY = "debug-mode-actions";
|
|
12
|
+
|
|
13
|
+
export type WidgetTone = "accent" | "dim" | "warning";
|
|
11
14
|
|
|
12
15
|
export interface WidgetLine {
|
|
13
16
|
text: string;
|
|
@@ -15,23 +18,31 @@ export interface WidgetLine {
|
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export function statusLabel(session: DebugSession): string {
|
|
18
|
-
const round = currentRound(session)
|
|
19
|
-
if (session.stage === "awaiting_evidence") return "๐ waiting-repro";
|
|
20
|
-
if (session.stage === "open") return `๐ round ${round} ยท your turn`;
|
|
21
|
+
const round = currentRound(session);
|
|
21
22
|
if (session.stage === "cleaning_up") return "๐ cleanup";
|
|
22
|
-
return `๐ round ${round}`;
|
|
23
|
+
if (session.stage === "investigating") return `๐ round ${round.index}`;
|
|
24
|
+
const mode = round.handoff ?? "incomplete";
|
|
25
|
+
if (mode === "reproduce") return `๐ round ${round.index} ยท reproduce`;
|
|
26
|
+
if (mode === "capture") return `๐ round ${round.index} ยท capture`;
|
|
27
|
+
if (mode === "question") return `๐ round ${round.index} ยท your reply`;
|
|
28
|
+
return `๐ round ${round.index} ยท needs input`;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
function clip(text: string): string {
|
|
26
32
|
return text.length > WIDGET_MAX_WIDTH ? `${text.slice(0, WIDGET_MAX_WIDTH - 1)}โฆ` : text;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
function paint(ctx: ExtensionContext, lines: WidgetLine[]): string[] {
|
|
36
|
+
return lines.map(line => ctx.ui.theme.fg(line.tone, line.text));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clearWidgets(ctx: ExtensionContext): void {
|
|
40
|
+
ctx.ui.setWidget(WIDGET_CONTEXT_KEY, undefined);
|
|
41
|
+
ctx.ui.setWidget(WIDGET_ACTIONS_KEY, undefined);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Remaining commands after the three exits have their own lines. */
|
|
45
|
+
const OTHER_COMMANDS = "/debug-evidence <request-id|-> <path> ยท /debug-abort ยท /debug-status";
|
|
35
46
|
|
|
36
47
|
function plural(count: number, singular: string): string {
|
|
37
48
|
return `${count} ${singular}${count === 1 ? "" : "s"}`;
|
|
@@ -41,87 +52,116 @@ function logEntries(count: number): string {
|
|
|
41
52
|
return count === 1 ? "1 log entry" : `${count} log entries`;
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
function callToAction(mode: HandoffMode): string {
|
|
56
|
+
if (mode === "reproduce") return "Reproduce the bug now, then /debug-proceed.";
|
|
57
|
+
if (mode === "capture") return "Supply the requested report or file, then /debug-proceed.";
|
|
58
|
+
if (mode === "question") return "Reply in the editor. The agent is waiting on an answer, not a reproduction.";
|
|
59
|
+
return "The agent stopped without saying what to reproduce or capture.";
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
/**
|
|
45
|
-
*
|
|
46
|
-
* the
|
|
47
|
-
*
|
|
63
|
+
* What `/debug-proceed` does right now, with the numbers it would act on.
|
|
64
|
+
* Naming the real effect is the only way to tell it apart from an ordinary
|
|
65
|
+
* reply, and that difference is what the two former user stages never explained.
|
|
48
66
|
*/
|
|
49
|
-
export function
|
|
67
|
+
export function proceedLine(session: DebugSession, logCount: number): WidgetLine {
|
|
50
68
|
const round = currentRound(session);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
const captured: string[] = [];
|
|
70
|
+
if (logCount > 0) captured.push(logEntries(logCount));
|
|
71
|
+
if (session.observations.length > 0) captured.push(plural(session.observations.length, "observation"));
|
|
72
|
+
if (session.artifacts.length > 0) captured.push(plural(session.artifacts.length, "artifact"));
|
|
73
|
+
return {
|
|
74
|
+
text: clip(
|
|
75
|
+
captured.length > 0
|
|
76
|
+
? `/debug-proceed โ close round ${round.index}, analyze ${captured.join(", ")}`
|
|
77
|
+
: `/debug-proceed โ close round ${round.index} with nothing captured yet`,
|
|
78
|
+
),
|
|
79
|
+
tone: captured.length > 0 ? "accent" : "warning",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The other half of the same choice, so declining `/debug-proceed` is informed too. */
|
|
84
|
+
export function replyLine(session: DebugSession): WidgetLine {
|
|
85
|
+
const round = currentRound(session);
|
|
86
|
+
return {
|
|
87
|
+
text: clip(`Reply in the editor โ round ${round.index} stays open, the run keeps recording`),
|
|
88
|
+
tone: "dim",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The third exit: the bug is actually gone. */
|
|
93
|
+
export function doneLine(): WidgetLine {
|
|
94
|
+
return {
|
|
95
|
+
text: clip("/debug-done โ bug is fixed: remove probes and summarize"),
|
|
96
|
+
tone: "dim",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function appendFoldedSteps(lines: WidgetLine[], steps: readonly string[]): void {
|
|
101
|
+
const stepBudget = Math.max(0, WIDGET_MAX_LINES - lines.length);
|
|
102
|
+
if (stepBudget === 0 || steps.length === 0) return;
|
|
103
|
+
const shownCount = steps.length > stepBudget ? Math.max(0, stepBudget - 1) : stepBudget;
|
|
104
|
+
for (const step of steps.slice(0, shownCount)) lines.push({ text: clip(step), tone: "dim" });
|
|
105
|
+
const hidden = steps.length - shownCount;
|
|
106
|
+
if (hidden > 0) lines.push({ text: `โฆ +${plural(hidden, "more step")} in the transcript`, tone: "dim" });
|
|
57
107
|
}
|
|
58
108
|
|
|
59
109
|
/**
|
|
60
|
-
*
|
|
61
|
-
* reproduction
|
|
62
|
-
*
|
|
63
|
-
* live log counter is always the final line within the host's line budget.
|
|
110
|
+
* Above the editor: the work this round asked for. Mode changes the wording
|
|
111
|
+
* and whether stale reproduction steps are worth repeating, never which
|
|
112
|
+
* commands the actions widget offers.
|
|
64
113
|
*/
|
|
65
|
-
export function
|
|
114
|
+
export function userTurnContextLines(
|
|
66
115
|
session: DebugSession,
|
|
67
|
-
logCount: number,
|
|
68
116
|
pendingEvidence: EvidenceRequest[] = pendingRequests(session),
|
|
69
117
|
): WidgetLine[] {
|
|
70
118
|
const round = currentRound(session);
|
|
71
|
-
const
|
|
119
|
+
const mode = round.handoff ?? "incomplete";
|
|
120
|
+
const lines: WidgetLine[] = [{ text: clip(callToAction(mode)), tone: mode === "incomplete" ? "warning" : "accent" }];
|
|
72
121
|
for (const request of pendingEvidence.slice(0, 2)) {
|
|
73
122
|
lines.push({ text: clip(`โช ${request.id} ${request.title}: ${request.instructions[0] ?? ""}`), tone: "accent" });
|
|
74
123
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Reproduction details are useful context, but command discoverability and
|
|
78
|
-
// the live log counter must survive the host's ten-line widget limit.
|
|
79
|
-
const reserved = lines.length + 2;
|
|
80
|
-
const stepBudget = Math.max(0, WIDGET_MAX_LINES - reserved);
|
|
81
|
-
if (stepBudget > 0 && round.reproductionSteps.length > 0) {
|
|
82
|
-
const showMoreLine = round.reproductionSteps.length > stepBudget;
|
|
83
|
-
const shownCount = showMoreLine ? Math.max(0, stepBudget - 1) : stepBudget;
|
|
84
|
-
for (const step of round.reproductionSteps.slice(0, shownCount)) {
|
|
85
|
-
lines.push({ text: clip(step), tone: "dim" });
|
|
86
|
-
}
|
|
87
|
-
const hidden = round.reproductionSteps.length - shownCount;
|
|
88
|
-
if (hidden > 0) lines.push({ text: `โฆ +${plural(hidden, "more step")} in the transcript`, tone: "dim" });
|
|
89
|
-
}
|
|
90
|
-
lines.push({
|
|
91
|
-
text: `evidence: ${plural(pendingEvidence.length, "pending request")}, ${plural(session.artifacts.length, "attached artifact")}`,
|
|
92
|
-
tone: pendingEvidence.length > 0 || session.artifacts.length > 0 ? "accent" : "dim",
|
|
93
|
-
});
|
|
94
|
-
lines.push({
|
|
95
|
-
text: `run ${round.runId ?? "none"} โ ${logEntries(logCount)}`,
|
|
96
|
-
tone: logCount > 0 ? "accent" : "dim",
|
|
97
|
-
});
|
|
124
|
+
const steps = mode === "reproduce" || mode === "capture" ? round.reproductionSteps : [];
|
|
125
|
+
appendFoldedSteps(lines, steps);
|
|
98
126
|
return lines.slice(0, WIDGET_MAX_LINES);
|
|
99
127
|
}
|
|
100
128
|
|
|
101
129
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
130
|
+
* Below the editor: the three exits plus the live log counter, sitting next to
|
|
131
|
+
* the prompt so the user does not have to hunt above the transcript for what
|
|
132
|
+
* to type.
|
|
133
|
+
*/
|
|
134
|
+
export function userTurnActionLines(session: DebugSession, logCount: number): WidgetLine[] {
|
|
135
|
+
const round = currentRound(session);
|
|
136
|
+
return [
|
|
137
|
+
proceedLine(session, logCount),
|
|
138
|
+
replyLine(session),
|
|
139
|
+
doneLine(),
|
|
140
|
+
{ text: clip(OTHER_COMMANDS), tone: "accent" },
|
|
141
|
+
{
|
|
142
|
+
text: `run ${round.runId ?? "none"} โ ${logEntries(logCount)}`,
|
|
143
|
+
tone: logCount > 0 ? "accent" : "dim",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Render the footer status and the two stage widgets. `getLogCount` is only
|
|
150
|
+
* consulted on the user's turn so idle stages do not touch the log files.
|
|
104
151
|
*/
|
|
105
152
|
export function applyUi(ctx: ExtensionContext | null, state: DebugState, getLogCount: () => number): void {
|
|
106
153
|
if (!ctx?.hasUI) return;
|
|
107
154
|
if (!state.active) {
|
|
108
155
|
ctx.ui.setStatus("debug-mode", undefined);
|
|
109
|
-
ctx
|
|
156
|
+
clearWidgets(ctx);
|
|
110
157
|
return;
|
|
111
158
|
}
|
|
112
159
|
ctx.ui.setStatus("debug-mode", ctx.ui.theme.fg("warning", statusLabel(state)));
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
? waitingWidgetLines(state, getLogCount())
|
|
116
|
-
: state.stage === "open"
|
|
117
|
-
? openWidgetLines(state)
|
|
118
|
-
: null;
|
|
119
|
-
if (!lines) {
|
|
120
|
-
ctx.ui.setWidget("debug-mode", undefined);
|
|
160
|
+
if (state.stage !== "user_turn") {
|
|
161
|
+
clearWidgets(ctx);
|
|
121
162
|
return;
|
|
122
163
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
164
|
+
const logCount = getLogCount();
|
|
165
|
+
ctx.ui.setWidget(WIDGET_CONTEXT_KEY, paint(ctx, userTurnContextLines(state)));
|
|
166
|
+
ctx.ui.setWidget(WIDGET_ACTIONS_KEY, paint(ctx, userTurnActionLines(state, logCount)), { placement: "belowEditor" });
|
|
127
167
|
}
|