@ian-pascoe/pi-dap 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ian Pascoe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # @ian-pascoe/pi-dap
2
+
3
+ Configured [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/)
4
+ (DAP) sessions for [Pi](https://github.com/earendil-works/pi).
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pi install npm:@ian-pascoe/pi-dap
10
+ # or
11
+ pi install git:github.com/ian-pascoe/pi-extensions
12
+ ```
13
+
14
+ For a local checkout, run `pi -e ./packages/pi-dap/src/index.ts`.
15
+
16
+ Debug Adapter executables are user-managed. Pi DAP has no adapter discovery,
17
+ installer, or catalog. The repository-only `vscode-js-debug` development
18
+ dependency supports this repository's Node smoke test; its files are not packed
19
+ or installed with the package.
20
+
21
+ ## Settings
22
+
23
+ Pi DAP reads only the `dap` key from Pi's global `settings.json` and trusted
24
+ project `.pi/settings.json`:
25
+
26
+ ```json
27
+ {
28
+ "dap": {
29
+ "timeouts": {
30
+ "startupMs": 10000,
31
+ "requestMs": 10000,
32
+ "executionMs": 30000,
33
+ "shutdownMs": 5000
34
+ },
35
+ "adapters": {
36
+ "node": {
37
+ "command": "node",
38
+ "args": ["/absolute/path/to/dapDebugServer.js", "$PORT", "127.0.0.1"],
39
+ "environment": {},
40
+ "transport": {
41
+ "type": "tcp",
42
+ "host": "127.0.0.1",
43
+ "port": 0
44
+ }
45
+ }
46
+ },
47
+ "profiles": {
48
+ "node": {
49
+ "adapter": "node",
50
+ "arguments": {
51
+ "type": "pwa-node",
52
+ "console": "internalConsole",
53
+ "stopOnEntry": true
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ An Adapter Definition needs a non-empty `command` and `transport`; `args` and
62
+ `environment` default to an empty array and object, respectively. `transport` is either `"stdio"` or a
63
+ TCP object with `type: "tcp"`; its host defaults to `127.0.0.1`, and a missing
64
+ or zero port selects a local port. TCP arguments may use `$PORT` anywhere in an
65
+ argument, and Pi DAP also supplies that selected port as `PORT` in the adapter
66
+ environment. `$PORT` is invalid for stdio adapters. Environment strings overlay
67
+ the inherited environment; `null` removes an inherited variable.
68
+
69
+ A Launch Profile needs an existing Adapter Definition ID and opaque JSON-object
70
+ `arguments`. Global and trusted-project timeouts merge by field. Adapter and
71
+ profile maps merge by ID: a project entry replaces the complete global entry;
72
+ `null` removes it. Invalid project replacements still shadow global entries.
73
+ Invalid entries are quarantined independently and produce path-qualified
74
+ warnings at session start, while unrelated valid entries stay available.
75
+ Untrusted project settings are ignored. Use Pi `/reload` to reload settings.
76
+
77
+ ### Node and TypeScript
78
+
79
+ Node/TypeScript through Microsoft `vscode-js-debug` is the Supported Adapter
80
+ workflow. For example, set the Node adapter command to `node` and point its TCP
81
+ arguments at `dapDebugServer.js` followed by `$PORT`, as above. Other
82
+ standards-based adapters are Experimental: they may work through DAP but have
83
+ no compatibility promise.
84
+
85
+ ## `dap` tool
86
+
87
+ Pi DAP registers one strict `dap` tool with exactly these operations:
88
+
89
+ ```text
90
+ launch set_breakpoints continue next
91
+ step_in step_out pause stack
92
+ variables evaluate status stop
93
+ ```
94
+
95
+ `launch` selects a profile (it may be omitted only when exactly one valid
96
+ profile exists). `program`, `args`, and `cwd` replace the same profile
97
+ arguments; relative `program` and `cwd` paths resolve from Pi's project working
98
+ directory. A Debug Session is single-active: launching while one is active
99
+ fails. Desired Breakpoints are complete per-file lists and survive `stop` and
100
+ later launches in the same Pi conversation session; `[]` clears a file.
101
+ Relative breakpoint paths also resolve from Pi's project working directory.
102
+
103
+ Execution and inspection require a stopped Debuggee: `continue`, `next`,
104
+ `step_in`, `step_out`, `stack`, `variables`, and `evaluate`. `pause` requires a
105
+ running Debuggee. `status` and `stop` are idempotent. `stack` defaults to the
106
+ stopped thread, offset `0`, and count `20`; `variables` takes exactly one
107
+ `frame_id` or `variables_reference` and defaults its count to `100`; `evaluate`
108
+ defaults to the top Stack Frame.
109
+
110
+ Execution waits end on a stop, exit, cancellation, or `executionMs`; an
111
+ execution timeout reports `running`. Request, startup, and shutdown timeouts
112
+ are errors. A natural exit leaves a terminal snapshot available from `status`
113
+ until the next launch.
114
+
115
+ ## Output and lifecycle
116
+
117
+ Each successful operation drains currently unread Debuggee output. Pi DAP
118
+ retains at most 1 MiB of unread output, reporting discarded older bytes. Tool
119
+ text follows Pi's 2,000-line/50-KB visible limit; when truncated, the retained
120
+ complete result is written to a Result Spill and its path appears in the
121
+ result. Adapter stderr retains its newest 1 MiB in the session directory and
122
+ process or protocol failures name that path.
123
+
124
+ Adapters start lazily at `launch`, use the project working directory, and are
125
+ owned by one Pi conversation session. `stop`, launch cancellation, adapter
126
+ failure, and session shutdown attempt DAP termination and disconnect before
127
+ terminating owned Linux process groups. Session shutdown removes session files.
128
+ Cancelling an execution wait only ends that wait; the live Debug Session remains
129
+ recoverable.
130
+
131
+ ### Observer UI
132
+
133
+ In TUI mode, calls and results use compact semantic transcript rows. Expanding a
134
+ row shows only explicitly supplied arguments and bounded Breakpoint, Stack Frame,
135
+ variable, or evaluation details. Long execution waits update once per second.
136
+ Malformed or historical rows fall back to their original tool text.
137
+
138
+ One widget above the editor follows launching, running, stopped, and terminated
139
+ activity. It is derived only from lifecycle transitions and successful results
140
+ Pi DAP has already received; it sends no additional DAP request and provides no
141
+ human debugger controls. Stopped source locations clear on resume. The terminal
142
+ snapshot remains for ten seconds, while idle sessions have no widget. RPC, JSON,
143
+ and print modes do not mount it.
144
+
145
+ The model still receives the unchanged raw `DAP <operation>: <JSON>` text,
146
+ Debuggee output, truncation, and Result Spill notice. Only the human-visible copy
147
+ of Debuggee output is stripped of terminal sequences and unsafe controls; the raw
148
+ tool result and Result Spill retain the original bytes.
149
+
150
+ ## V1 boundary
151
+
152
+ V1 supports configured stdio and TCP adapters on Linux, one active Debug
153
+ Session, source breakpoints, core execution control, stack/variables/evaluation,
154
+ and headless `runInTerminal`. The Supported `vscode-js-debug` workflow uses one
155
+ adapter-owned primary target channel; it is not a second model-facing Debug
156
+ Session, and unrelated, second, or nested `startDebugging` requests are rejected.
157
+ V1 excludes attach, restart, function/data/instruction breakpoints, hit counts,
158
+ logpoints, memory, disassembly, modules, user-requested child Debug Sessions, raw
159
+ DAP requests, `launch.json`, WebSocket, persistence, and a directly operated
160
+ debugger UI.
161
+
162
+ Trusted project settings can run arbitrary local executables with Pi's
163
+ permissions. Review adapter commands and configuration before trusting a
164
+ project.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@ian-pascoe/pi-dap",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Configured Debug Adapter Protocol sessions for Pi",
6
+ "keywords": [
7
+ "dap",
8
+ "debug-adapter-protocol",
9
+ "pi",
10
+ "pi-extension",
11
+ "pi-package"
12
+ ],
13
+ "homepage": "https://github.com/ian-pascoe/pi-extensions#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/ian-pascoe/pi-extensions/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Ian Pascoe <ian.g.pascoe@gmail.com>",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ian-pascoe/pi-extensions.git",
22
+ "directory": "packages/pi-dap"
23
+ },
24
+ "files": [
25
+ "src",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "type": "module",
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "provenance": true
33
+ },
34
+ "scripts": {
35
+ "test": "vitest run --config ../../vitest.config.ts --root .",
36
+ "typecheck": "tsc --noEmit -p tsconfig.json"
37
+ },
38
+ "dependencies": {
39
+ "@vscode/debugprotocol": "^1.68.0"
40
+ },
41
+ "devDependencies": {
42
+ "vscode-js-debug": "https://github.com/microsoft/vscode-js-debug/releases/download/v1.117.0/js-debug-dap-v1.117.0.tar.gz"
43
+ },
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-coding-agent": "*",
46
+ "@earendil-works/pi-tui": "*",
47
+ "typebox": "*"
48
+ },
49
+ "engines": {
50
+ "node": ">=22.19.0"
51
+ },
52
+ "pi": {
53
+ "extensions": [
54
+ "./src/index.ts"
55
+ ]
56
+ }
57
+ }
@@ -0,0 +1,337 @@
1
+ import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ sliceByColumn,
4
+ truncateToWidth,
5
+ visibleWidth,
6
+ type Component,
7
+ type TUI,
8
+ } from "@earendil-works/pi-tui";
9
+ import type { DapSessionResult, DapSessionSnapshot } from "./dap-session.js";
10
+ import type { DapToolParameters } from "./dap-tool-contract.js";
11
+ import { workspaceRelativeDapPath } from "./dap-tool-rendering.js";
12
+ import type { DapToolObserver } from "./dap-tool.js";
13
+
14
+ const DAP_OBSERVER_UI_KEY = "pi-dap";
15
+ const DAP_OBSERVER_REFRESH_MS = 1_000;
16
+ const DAP_OBSERVER_TERMINAL_COOLDOWN_MS = 10_000;
17
+ const DAP_OBSERVER_SEPARATOR = " ";
18
+
19
+ /** Theme operations used by the one-line Pi DAP Observer widget. */
20
+ export type DapObserverWidgetTheme = Pick<Theme, "bold" | "fg">;
21
+
22
+ /** Projected values rendered by the non-authoritative Observer widget. */
23
+ export interface DapObserverWidgetView {
24
+ /** Current observed lifecycle state; idle has no widget view. */
25
+ readonly state: "launching" | "running" | "stopped" | "terminated";
26
+ readonly adapterId?: string;
27
+ readonly profileId?: string;
28
+ readonly stopReason?: string;
29
+ /** Current stopped Stack Frame location, falling back to the explicitly launched program. */
30
+ readonly path?: string;
31
+ /** Milliseconds since launch, frozen when termination is observed. */
32
+ readonly elapsedMs?: number;
33
+ readonly exitCode?: number;
34
+ }
35
+
36
+ /** Narrow Pi context needed to mount and notify the Observer UI. */
37
+ export interface DapObserverUiContext {
38
+ readonly mode: "tui" | "rpc" | "json" | "print";
39
+ readonly cwd: string;
40
+ readonly ui: Pick<ExtensionUIContext, "notify" | "setWidget">;
41
+ }
42
+
43
+ interface DapObserverStateParts {
44
+ readonly full: string;
45
+ readonly short: string;
46
+ }
47
+
48
+ function dapObserverStateParts(
49
+ view: DapObserverWidgetView,
50
+ theme: DapObserverWidgetTheme,
51
+ ): DapObserverStateParts {
52
+ switch (view.state) {
53
+ case "launching":
54
+ return {
55
+ full: theme.fg("accent", "▶ launching"),
56
+ short: theme.fg("accent", "▶ launching"),
57
+ };
58
+ case "running":
59
+ return {
60
+ full: theme.fg("accent", "▶ running"),
61
+ short: theme.fg("accent", "▶ running"),
62
+ };
63
+ case "stopped": {
64
+ const short = theme.fg("accent", "● stopped");
65
+ return {
66
+ full:
67
+ view.stopReason === undefined
68
+ ? short
69
+ : `${short}${theme.fg("dim", ` · ${view.stopReason}`)}`,
70
+ short,
71
+ };
72
+ }
73
+ case "terminated": {
74
+ const short = theme.fg("success", "■ terminated");
75
+ return {
76
+ full:
77
+ view.exitCode === undefined
78
+ ? short
79
+ : `${short}${theme.fg("dim", ` · exit ${view.exitCode}`)}`,
80
+ short,
81
+ };
82
+ }
83
+ }
84
+ }
85
+
86
+ function formatDapObserverDuration(elapsedMs: number | undefined): string | undefined {
87
+ if (elapsedMs === undefined) return undefined;
88
+ const seconds = Math.max(0, Math.floor(elapsedMs / 1_000));
89
+ if (seconds < 60) return `${seconds}s`;
90
+ const minutes = Math.floor(seconds / 60);
91
+ return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
92
+ }
93
+
94
+ function joinDapObserverParts(parts: readonly (string | undefined)[]): string {
95
+ return parts.filter((part): part is string => part !== undefined).join(DAP_OBSERVER_SEPARATOR);
96
+ }
97
+
98
+ /** Render one responsive Observer snapshot without exceeding the terminal width. */
99
+ export function renderDapObserverWidgetLine(
100
+ view: DapObserverWidgetView,
101
+ width: number,
102
+ theme: DapObserverWidgetTheme,
103
+ ): string {
104
+ if (width <= 0) return "";
105
+ const title = theme.fg("toolTitle", theme.bold("DAP"));
106
+ const state = dapObserverStateParts(view, theme);
107
+ const profile =
108
+ view.adapterId === undefined && view.profileId === undefined
109
+ ? undefined
110
+ : theme.fg("muted", `${view.adapterId ?? "?"}/${view.profileId ?? "?"}`);
111
+ const path = view.path === undefined ? undefined : theme.fg("muted", view.path);
112
+ const durationValue = formatDapObserverDuration(view.elapsedMs);
113
+ const duration = durationValue === undefined ? undefined : theme.fg("muted", durationValue);
114
+ const candidates = [
115
+ joinDapObserverParts([title, state.full, profile, path, duration]),
116
+ joinDapObserverParts([title, state.full, profile, path]),
117
+ joinDapObserverParts([title, state.full, profile]),
118
+ joinDapObserverParts([title, state.full]),
119
+ joinDapObserverParts([title, state.short]),
120
+ ];
121
+ const fitting = candidates.find((candidate) => visibleWidth(candidate) <= width);
122
+ if (fitting !== undefined) return fitting;
123
+
124
+ if (visibleWidth(title) >= width) return truncateToWidth(title, width, "…");
125
+ const stateWidth = Math.max(
126
+ 0,
127
+ width - visibleWidth(title) - visibleWidth(DAP_OBSERVER_SEPARATOR),
128
+ );
129
+ const shortenedState = sliceByColumn(state.short, 0, stateWidth, true);
130
+ return truncateToWidth(joinDapObserverParts([title, shortenedState || undefined]), width, "…");
131
+ }
132
+
133
+ class DapObserverWidgetComponent implements Component {
134
+ constructor(
135
+ private view: DapObserverWidgetView,
136
+ private readonly tui: TUI,
137
+ private readonly theme: DapObserverWidgetTheme,
138
+ ) {}
139
+
140
+ update(view: DapObserverWidgetView): void {
141
+ this.view = view;
142
+ this.tui.requestRender();
143
+ }
144
+
145
+ render(width: number): string[] {
146
+ return [renderDapObserverWidgetLine(this.view, width, this.theme)];
147
+ }
148
+
149
+ invalidate(): void {}
150
+ }
151
+
152
+ /** Own one TUI-only Observer snapshot, widget, refresh interval, cooldown, and cleanup. */
153
+ export class DapObserverUiController implements DapToolObserver {
154
+ private activeToolCalls = 0;
155
+ private disposed = false;
156
+ private launchStartedAt: number | undefined;
157
+ private terminalElapsedMs: number | undefined;
158
+ private launchedProgram: string | undefined;
159
+ private sourceLocation: string | undefined;
160
+ private snapshot: DapSessionSnapshot = { state: "idle" };
161
+ private currentView: DapObserverWidgetView | undefined;
162
+ private widgetComponent: DapObserverWidgetComponent | undefined;
163
+ private widgetMounted = false;
164
+ private refreshInterval: ReturnType<typeof setInterval> | undefined;
165
+ private cooldownTimeout: ReturnType<typeof setTimeout> | undefined;
166
+
167
+ /** Construct an inert Observer UI; the first launch tool call mounts its widget. */
168
+ constructor(private readonly context: DapObserverUiContext) {}
169
+
170
+ /** Record tool activity without dispatching any Debug Adapter operation. */
171
+ onToolStart(parameters: DapToolParameters): void {
172
+ if (this.disposed) return;
173
+ this.activeToolCalls++;
174
+ if (parameters.operation !== "launch") return;
175
+ this.clearCooldown();
176
+ this.launchStartedAt = Date.now();
177
+ this.terminalElapsedMs = undefined;
178
+ this.launchedProgram =
179
+ parameters.program === undefined
180
+ ? undefined
181
+ : workspaceRelativeDapPath(this.context.cwd, parameters.program);
182
+ this.sourceLocation = undefined;
183
+ this.snapshot = { state: "launching", adapterId: "?", profileId: parameters.profile ?? "?" };
184
+ this.ensureRefreshInterval();
185
+ this.refreshWidget();
186
+ }
187
+
188
+ /** Learn source location only from a successful result already returned by the tool. */
189
+ onToolSuccess(_parameters: DapToolParameters, result: DapSessionResult): void {
190
+ if (this.disposed) return;
191
+ this.activeToolCalls = Math.max(0, this.activeToolCalls - 1);
192
+ if (this.snapshot.state === "stopped") {
193
+ const frame = result.stackFrames?.[0];
194
+ const source = frame?.source?.path ?? frame?.source?.name;
195
+ if (frame !== undefined && source !== undefined) {
196
+ this.sourceLocation = `${workspaceRelativeDapPath(this.context.cwd, source)}:${frame.line}`;
197
+ }
198
+ }
199
+ this.refreshWidget();
200
+ }
201
+
202
+ /** Complete failed tool activity while leaving model-facing failure behavior unchanged. */
203
+ onToolFailure(parameters: DapToolParameters, _error: Error): void {
204
+ if (this.disposed) return;
205
+ this.activeToolCalls = Math.max(0, this.activeToolCalls - 1);
206
+ if (parameters.operation === "launch" && this.snapshot.state === "launching") {
207
+ this.snapshot = { state: "idle" };
208
+ this.clearRefreshInterval();
209
+ this.hideWidget();
210
+ }
211
+ }
212
+
213
+ /** Project one actual Debug Session lifecycle transition into the Observer snapshot. */
214
+ onSessionSnapshot(snapshot: DapSessionSnapshot): void {
215
+ if (this.disposed) return;
216
+ if (snapshot.state !== "stopped" || this.snapshot.state !== "stopped") {
217
+ this.sourceLocation = undefined;
218
+ }
219
+ this.snapshot = snapshot;
220
+ if (snapshot.state === "idle") {
221
+ this.clearRefreshInterval();
222
+ this.clearCooldown();
223
+ this.hideWidget();
224
+ return;
225
+ }
226
+ if (snapshot.state === "terminated") {
227
+ this.terminalElapsedMs =
228
+ this.launchStartedAt === undefined ? undefined : Date.now() - this.launchStartedAt;
229
+ this.clearRefreshInterval();
230
+ this.refreshWidget();
231
+ this.ensureCooldown();
232
+ return;
233
+ }
234
+ this.clearCooldown();
235
+ this.ensureRefreshInterval();
236
+ this.refreshWidget();
237
+ }
238
+
239
+ /** Notify only an actionable asynchronous failure not represented by an active tool result. */
240
+ onUnexpectedFailure(error: Error): void {
241
+ if (this.disposed || this.activeToolCalls > 0) return;
242
+ const message = error.message.startsWith("Pi DAP:")
243
+ ? error.message
244
+ : `Pi DAP: ${error.message}`;
245
+ this.context.ui.notify(message, "error");
246
+ }
247
+
248
+ /** Dispose timers and the widget immediately; repeated disposal is inert. */
249
+ dispose(): void {
250
+ if (this.disposed) return;
251
+ this.disposed = true;
252
+ this.clearRefreshInterval();
253
+ this.clearCooldown();
254
+ this.hideWidget();
255
+ }
256
+
257
+ private observerWidgetView(): DapObserverWidgetView | undefined {
258
+ if (this.snapshot.state === "idle") return undefined;
259
+ const elapsedMs =
260
+ this.snapshot.state === "terminated"
261
+ ? this.terminalElapsedMs
262
+ : this.launchStartedAt === undefined
263
+ ? undefined
264
+ : Date.now() - this.launchStartedAt;
265
+ const path = this.sourceLocation ?? this.launchedProgram;
266
+ let view: DapObserverWidgetView = { state: this.snapshot.state };
267
+ if ("adapterId" in this.snapshot) view = { ...view, adapterId: this.snapshot.adapterId };
268
+ if ("profileId" in this.snapshot) view = { ...view, profileId: this.snapshot.profileId };
269
+ if (this.snapshot.state === "stopped") {
270
+ view = { ...view, stopReason: this.snapshot.stopReason };
271
+ }
272
+ if (this.snapshot.state === "terminated" && this.snapshot.exitCode !== undefined) {
273
+ view = { ...view, exitCode: this.snapshot.exitCode };
274
+ }
275
+ if (path !== undefined) view = { ...view, path };
276
+ if (elapsedMs !== undefined) view = { ...view, elapsedMs };
277
+ return view;
278
+ }
279
+
280
+ private refreshWidget(): void {
281
+ if (this.context.mode !== "tui") return;
282
+ const view = this.observerWidgetView();
283
+ this.currentView = view;
284
+ if (view === undefined) {
285
+ this.hideWidget();
286
+ return;
287
+ }
288
+ if (this.widgetMounted) {
289
+ this.widgetComponent?.update(view);
290
+ return;
291
+ }
292
+ this.context.ui.setWidget(
293
+ DAP_OBSERVER_UI_KEY,
294
+ (tui, theme) => {
295
+ this.widgetComponent = new DapObserverWidgetComponent(this.currentView ?? view, tui, theme);
296
+ return this.widgetComponent;
297
+ },
298
+ { placement: "aboveEditor" },
299
+ );
300
+ this.widgetMounted = true;
301
+ }
302
+
303
+ private hideWidget(): void {
304
+ if (this.context.mode !== "tui" || !this.widgetMounted) return;
305
+ this.context.ui.setWidget(DAP_OBSERVER_UI_KEY, undefined);
306
+ this.widgetMounted = false;
307
+ this.widgetComponent = undefined;
308
+ }
309
+
310
+ private ensureRefreshInterval(): void {
311
+ if (this.context.mode !== "tui" || this.refreshInterval !== undefined) return;
312
+ this.refreshInterval = setInterval(() => this.refreshWidget(), DAP_OBSERVER_REFRESH_MS);
313
+ this.refreshInterval.unref?.();
314
+ }
315
+
316
+ private clearRefreshInterval(): void {
317
+ if (this.refreshInterval === undefined) return;
318
+ clearInterval(this.refreshInterval);
319
+ this.refreshInterval = undefined;
320
+ }
321
+
322
+ private ensureCooldown(): void {
323
+ if (this.context.mode !== "tui") return;
324
+ this.clearCooldown();
325
+ this.cooldownTimeout = setTimeout(() => {
326
+ this.cooldownTimeout = undefined;
327
+ this.hideWidget();
328
+ }, DAP_OBSERVER_TERMINAL_COOLDOWN_MS);
329
+ this.cooldownTimeout.unref?.();
330
+ }
331
+
332
+ private clearCooldown(): void {
333
+ if (this.cooldownTimeout === undefined) return;
334
+ clearTimeout(this.cooldownTimeout);
335
+ this.cooldownTimeout = undefined;
336
+ }
337
+ }