@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/src/ui.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
2
- import { describeOpenReason } from "./gate";
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
- export type WidgetTone = "accent" | "dim";
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).index;
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
- /** The command surface is the only interaction path at the gate. */
30
- const COMMAND_LINES = [
31
- "/debug-proceed [details]",
32
- "/debug-evidence <request-id> <path>",
33
- "/debug-done ยท /debug-abort ยท /debug-status",
34
- ] as const;
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
- * A round that settled without closing needs its own affordance: the user has
46
- * the turn, but the answer is an ordinary reply, not a reproduction. Showing
47
- * nothing here is what makes an unclosed round look like the gate.
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 openWidgetLines(session: DebugSession): WidgetLine[] {
67
+ export function proceedLine(session: DebugSession, logCount: number): WidgetLine {
50
68
  const round = currentRound(session);
51
- const reason = round.openReason ?? "awaiting_reply";
52
- return [
53
- { text: "This round is not closed โ€” reply to continue.", tone: "accent" },
54
- { text: clip(describeOpenReason(reason, round.index)), tone: "dim" },
55
- { text: "/debug-proceed closes it anyway ยท /debug-status ยท /debug-abort", tone: "dim" },
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
- * Reproduction-gate widget: the call to action, pending user evidence,
61
- * reproduction-step context, the command surface, and the live evidence
62
- * counter. Commands are the only interaction path โ€” there is no menu. The
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 waitingWidgetLines(
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 lines: WidgetLine[] = [{ text: PROCEED_REMINDER, tone: "accent" }];
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
- for (const command of COMMAND_LINES) lines.push({ text: command, tone: "accent" });
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
- * Render the status entry and the stage widget. `getLogCount` is only consulted
103
- * at the reproduction gate so idle stages do not touch the log files.
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.ui.setWidget("debug-mode", undefined);
156
+ clearWidgets(ctx);
110
157
  return;
111
158
  }
112
159
  ctx.ui.setStatus("debug-mode", ctx.ui.theme.fg("warning", statusLabel(state)));
113
- const lines =
114
- state.stage === "awaiting_evidence"
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
- ctx.ui.setWidget(
124
- "debug-mode",
125
- lines.map(line => ctx.ui.theme.fg(line.tone, line.text)),
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
  }