@siuver/omp-debug-mode 0.1.5 โ 0.1.7
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 +82 -48
- package/README.md +88 -31
- package/package.json +41 -41
- package/src/debug-mode.ts +285 -92
- package/src/evidence.ts +325 -169
- package/src/gate.ts +115 -51
- package/src/log-files.ts +161 -115
- package/src/machine.ts +136 -49
- package/src/main.ts +24 -24
- package/src/methodology.ts +56 -15
- package/src/state.ts +191 -42
- package/src/tools.ts +195 -9
- package/src/ui.ts +145 -71
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,54 @@ 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
|
|
|
31
|
+
/**
|
|
32
|
+
* Clip to terminal columns, not to code units. Reproduction steps are written
|
|
33
|
+
* in whatever language the user reports the bug in, and a CJK step counts one
|
|
34
|
+
* unit per character while occupying two columns โ measured by `length` a line
|
|
35
|
+
* of Chinese fits the budget at twice the real width and the host wraps or
|
|
36
|
+
* truncates it instead.
|
|
37
|
+
*/
|
|
25
38
|
function clip(text: string): string {
|
|
26
|
-
|
|
39
|
+
if (Bun.stringWidth(text) <= WIDGET_MAX_WIDTH) return text;
|
|
40
|
+
let width = 0;
|
|
41
|
+
let kept = "";
|
|
42
|
+
for (const char of text) {
|
|
43
|
+
const next = width + Bun.stringWidth(char);
|
|
44
|
+
if (next > WIDGET_MAX_WIDTH - 1) break;
|
|
45
|
+
width = next;
|
|
46
|
+
kept += char;
|
|
47
|
+
}
|
|
48
|
+
return `${kept}โฆ`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function paint(ctx: ExtensionContext, lines: WidgetLine[]): string[] {
|
|
52
|
+
return lines.map(line => ctx.ui.theme.fg(line.tone, line.text));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function clearWidgets(ctx: ExtensionContext): void {
|
|
56
|
+
ctx.ui.setWidget(WIDGET_CONTEXT_KEY, undefined);
|
|
57
|
+
ctx.ui.setWidget(WIDGET_ACTIONS_KEY, undefined);
|
|
27
58
|
}
|
|
28
59
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
const TIPS_HEADER = "Tips:";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remaining commands after the exits have their own lines. The `-`
|
|
64
|
+
* sentinel is deliberately not spelled out here: `<request-id|->` renders as an
|
|
65
|
+
* arrow next to lines that use `โ` to mean "and then this happens", and
|
|
66
|
+
* the command's own completion offers `-` as its first entry anyway.
|
|
67
|
+
*/
|
|
68
|
+
const OTHER_COMMANDS = "/debug-evidence <request-id> <path> ยท /debug-clear ยท /debug-abort ยท /debug-status";
|
|
35
69
|
|
|
36
70
|
function plural(count: number, singular: string): string {
|
|
37
71
|
return `${count} ${singular}${count === 1 ? "" : "s"}`;
|
|
@@ -42,86 +76,126 @@ function logEntries(count: number): string {
|
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* nothing here is what makes an unclosed round look like the gate.
|
|
79
|
+
* What this round wants from the user, without naming a command: the actions
|
|
80
|
+
* widget sits directly under the editor and names all of them already.
|
|
48
81
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
function callToAction(mode: HandoffMode): string {
|
|
83
|
+
if (mode === "reproduce") return "Reproduce the bug now.";
|
|
84
|
+
if (mode === "capture") return "Supply the requested report or file.";
|
|
85
|
+
if (mode === "question") return "Reply in the editor. The agent is waiting on an answer, not a reproduction.";
|
|
86
|
+
return "The agent stopped without saying what to reproduce or capture.";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What `/debug-proceed` does right now, with the numbers it would act on.
|
|
91
|
+
* The actions strip is a dim cheat-sheet under the editor, so this line never
|
|
92
|
+
* lights up: captured vs empty is in the wording, not the tone.
|
|
93
|
+
*/
|
|
94
|
+
export function proceedLine(session: DebugSession, logCount: number): WidgetLine {
|
|
95
|
+
const captured: string[] = [];
|
|
96
|
+
if (logCount > 0) captured.push(logEntries(logCount));
|
|
97
|
+
if (session.observations.length > 0) captured.push(plural(session.observations.length, "observation"));
|
|
98
|
+
if (session.artifacts.length > 0) captured.push(plural(session.artifacts.length, "artifact"));
|
|
99
|
+
return {
|
|
100
|
+
text: clip(
|
|
101
|
+
captured.length > 0
|
|
102
|
+
? `/debug-proceed โ close this round, analyze ${captured.join(", ")}`
|
|
103
|
+
: "/debug-proceed โ close this round with nothing captured yet",
|
|
104
|
+
),
|
|
105
|
+
tone: "dim",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The other exit: the bug is actually gone. */
|
|
110
|
+
export function doneLine(): WidgetLine {
|
|
111
|
+
return {
|
|
112
|
+
text: clip("/debug-done โ bug is fixed: remove probes and summarize"),
|
|
113
|
+
tone: "dim",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A model ordinal in any of the shapes the prompt's numbered list produces. */
|
|
118
|
+
const LEADING_ORDINAL = /^\s*\d+\s*[.)ใ]\s*/;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Every step carries the widget's own ordinal. The model numbers its list about
|
|
122
|
+
* as often as it does not, and an unnumbered run of dim lines reads as one
|
|
123
|
+
* paragraph rather than as a sequence to work through.
|
|
124
|
+
*/
|
|
125
|
+
export function numberedSteps(steps: readonly string[]): string[] {
|
|
126
|
+
const kept: string[] = [];
|
|
127
|
+
for (const step of steps) {
|
|
128
|
+
// The commands are named once, under the editor. A step that repeats them
|
|
129
|
+
// is the third copy on one screen, and the prompt forbids writing it.
|
|
130
|
+
if (/\/debug-(?:proceed|done)\b/i.test(step)) continue;
|
|
131
|
+
const body = step.replace(LEADING_ORDINAL, "").trim();
|
|
132
|
+
if (body.length === 0) continue;
|
|
133
|
+
kept.push(`${kept.length + 1}. ${body}`);
|
|
134
|
+
}
|
|
135
|
+
return kept;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function appendFoldedSteps(lines: WidgetLine[], steps: readonly string[]): void {
|
|
139
|
+
const stepBudget = Math.max(0, WIDGET_MAX_LINES - lines.length);
|
|
140
|
+
if (stepBudget === 0 || steps.length === 0) return;
|
|
141
|
+
const shownCount = steps.length > stepBudget ? Math.max(0, stepBudget - 1) : stepBudget;
|
|
142
|
+
for (const step of steps.slice(0, shownCount)) lines.push({ text: clip(step), tone: "dim" });
|
|
143
|
+
const hidden = steps.length - shownCount;
|
|
144
|
+
if (hidden > 0) lines.push({ text: `โฆ +${plural(hidden, "more step")} in the transcript`, tone: "dim" });
|
|
57
145
|
}
|
|
58
146
|
|
|
59
147
|
/**
|
|
60
|
-
*
|
|
61
|
-
* reproduction
|
|
62
|
-
*
|
|
63
|
-
* live log counter is always the final line within the host's line budget.
|
|
148
|
+
* Above the editor: the work this round asked for. Mode changes the wording
|
|
149
|
+
* and whether stale reproduction steps are worth repeating, never which
|
|
150
|
+
* commands the actions widget offers.
|
|
64
151
|
*/
|
|
65
|
-
export function
|
|
152
|
+
export function userTurnContextLines(
|
|
66
153
|
session: DebugSession,
|
|
67
|
-
logCount: number,
|
|
68
154
|
pendingEvidence: EvidenceRequest[] = pendingRequests(session),
|
|
69
155
|
): WidgetLine[] {
|
|
70
156
|
const round = currentRound(session);
|
|
71
|
-
const
|
|
157
|
+
const mode = round.handoff ?? "incomplete";
|
|
158
|
+
const lines: WidgetLine[] = [{ text: clip(callToAction(mode)), tone: mode === "incomplete" ? "warning" : "accent" }];
|
|
72
159
|
for (const request of pendingEvidence.slice(0, 2)) {
|
|
73
160
|
lines.push({ text: clip(`โช ${request.id} ${request.title}: ${request.instructions[0] ?? ""}`), tone: "accent" });
|
|
74
161
|
}
|
|
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
|
-
});
|
|
162
|
+
const steps = mode === "reproduce" || mode === "capture" ? numberedSteps(round.reproductionSteps) : [];
|
|
163
|
+
appendFoldedSteps(lines, steps);
|
|
98
164
|
return lines.slice(0, WIDGET_MAX_LINES);
|
|
99
165
|
}
|
|
100
166
|
|
|
101
167
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
168
|
+
* Below the editor: a dim cheat-sheet next to the prompt so the user does not
|
|
169
|
+
* have to hunt above the transcript for what to type. A reply in the editor is
|
|
170
|
+
* still a legal exit โ it just is not listed here, because the editor is
|
|
171
|
+
* already where that line would have pointed.
|
|
172
|
+
*/
|
|
173
|
+
export function userTurnActionLines(session: DebugSession, logCount: number): WidgetLine[] {
|
|
174
|
+
return [
|
|
175
|
+
{ text: TIPS_HEADER, tone: "dim" },
|
|
176
|
+
proceedLine(session, logCount),
|
|
177
|
+
doneLine(),
|
|
178
|
+
{ text: clip(OTHER_COMMANDS), tone: "dim" },
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Render the footer status and the two stage widgets. `getLogCount` is only
|
|
184
|
+
* consulted on the user's turn so idle stages do not touch the log files.
|
|
104
185
|
*/
|
|
105
186
|
export function applyUi(ctx: ExtensionContext | null, state: DebugState, getLogCount: () => number): void {
|
|
106
187
|
if (!ctx?.hasUI) return;
|
|
107
188
|
if (!state.active) {
|
|
108
189
|
ctx.ui.setStatus("debug-mode", undefined);
|
|
109
|
-
ctx
|
|
190
|
+
clearWidgets(ctx);
|
|
110
191
|
return;
|
|
111
192
|
}
|
|
112
193
|
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);
|
|
194
|
+
if (state.stage !== "user_turn") {
|
|
195
|
+
clearWidgets(ctx);
|
|
121
196
|
return;
|
|
122
197
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
198
|
+
const logCount = getLogCount();
|
|
199
|
+
ctx.ui.setWidget(WIDGET_CONTEXT_KEY, paint(ctx, userTurnContextLines(state)));
|
|
200
|
+
ctx.ui.setWidget(WIDGET_ACTIONS_KEY, paint(ctx, userTurnActionLines(state, logCount)), { placement: "belowEditor" });
|
|
127
201
|
}
|