@gajae-code/coding-agent 0.2.5 → 0.3.1
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 +28 -0
- package/dist/types/async/job-manager.d.ts +91 -2
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +10 -4
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +23 -29
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/skill-state/active-state.d.ts +2 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +198 -14
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +26 -1
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +334 -6
- package/src/cli/args.ts +9 -2
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +862 -0
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +54 -39
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +13 -3
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +372 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +235 -43
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +179 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +1155 -46
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +749 -0
- package/src/gjc-runtime/team-runtime.ts +1255 -189
- package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
- package/src/gjc-runtime/workflow-manifest.ts +427 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +148 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +600 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +98 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +197 -64
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +21 -5
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +133 -20
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +43 -1
- package/src/modes/controllers/input-controller.ts +105 -9
- package/src/modes/controllers/selector-controller.ts +31 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +28 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +2 -0
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/tools/subagent.md +39 -4
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +8 -0
- package/src/session/agent-session.ts +445 -71
- package/src/session/session-manager.ts +13 -1
- package/src/skill-state/active-state.ts +58 -65
- package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +33 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/executor.ts +79 -13
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +376 -74
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +54 -134
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +104 -10
- package/src/tools/ask.ts +88 -27
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +423 -79
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
|
|
3
4
|
import type { AutocompleteProvider, SlashCommand } from "@gajae-code/tui";
|
|
4
5
|
import { $env, sanitizeText } from "@gajae-code/utils";
|
|
@@ -13,7 +14,7 @@ import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../sessio
|
|
|
13
14
|
import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
|
|
14
15
|
import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
|
|
15
16
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
16
|
-
import { ensureSupportedImageInput } from "../../utils/image-loading";
|
|
17
|
+
import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
|
|
17
18
|
import { resizeImage } from "../../utils/image-resize";
|
|
18
19
|
import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
|
|
19
20
|
|
|
@@ -22,6 +23,8 @@ interface Expandable {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
|
|
26
|
+
const CLIPBOARD_TEMP_IMAGE_FILE_PATTERN = /^clipboard-\d{4}-\d{2}-\d{2}-\d{6}-[A-Za-z0-9]+\.(?:png|jpe?g|gif|webp)$/i;
|
|
27
|
+
const MACOS_CLIPBOARD_TEMP_DIR_PATTERN = /^\/var\/folders\/[^/]+\/[^/]+\/T$/;
|
|
25
28
|
|
|
26
29
|
function isExpandable(obj: unknown): obj is Expandable {
|
|
27
30
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
@@ -30,8 +33,17 @@ function isExpandable(obj: unknown): obj is Expandable {
|
|
|
30
33
|
export class InputController {
|
|
31
34
|
constructor(private ctx: InteractiveModeContext) {}
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
/** Set after a first Esc silently consumes a queued steer. Kept until the
|
|
37
|
+
* queued steer is either cancelled by a second Esc or drained by continuation,
|
|
38
|
+
* so abort cleanup going idle cannot turn the second Esc into an idle action. */
|
|
39
|
+
#steerConsumePending = false;
|
|
40
|
+
|
|
41
|
+
#abortInteractive(options?: { silent?: boolean }): Promise<void> {
|
|
42
|
+
return this.ctx.session.abort({
|
|
43
|
+
timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS,
|
|
44
|
+
cause: "user_interrupt",
|
|
45
|
+
silent: options?.silent,
|
|
46
|
+
});
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
setupKeyHandlers(): void {
|
|
@@ -40,6 +52,7 @@ export class InputController {
|
|
|
40
52
|
Boolean(
|
|
41
53
|
this.ctx.loadingAnimation ||
|
|
42
54
|
this.ctx.hasActiveBtw() ||
|
|
55
|
+
(this.#steerConsumePending && this.ctx.session.hasQueuedSteering) ||
|
|
43
56
|
this.ctx.session.isStreaming ||
|
|
44
57
|
this.ctx.session.isCompacting ||
|
|
45
58
|
this.ctx.session.isGeneratingHandoff ||
|
|
@@ -54,6 +67,17 @@ export class InputController {
|
|
|
54
67
|
if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
|
|
55
68
|
return;
|
|
56
69
|
}
|
|
70
|
+
if (this.#steerConsumePending) {
|
|
71
|
+
if (this.ctx.session.hasQueuedSteering) {
|
|
72
|
+
// Second Esc before the scheduled steer continuation drains the
|
|
73
|
+
// queue: restore/drop the queued steer and perform a real abort,
|
|
74
|
+
// even if abort cleanup already made the session look idle.
|
|
75
|
+
this.#steerConsumePending = false;
|
|
76
|
+
this.restoreQueuedMessagesToEditor({ abort: true });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.#steerConsumePending = false;
|
|
80
|
+
}
|
|
57
81
|
if (this.ctx.loadingAnimation) {
|
|
58
82
|
if (this.ctx.cancelPendingSubmission()) {
|
|
59
83
|
return;
|
|
@@ -73,7 +97,15 @@ export class InputController {
|
|
|
73
97
|
this.ctx.isPythonMode = false;
|
|
74
98
|
this.ctx.updateEditorBorderColor();
|
|
75
99
|
} else if (this.ctx.session.isStreaming) {
|
|
76
|
-
|
|
100
|
+
if (this.ctx.session.hasQueuedSteering && !this.#steerConsumePending) {
|
|
101
|
+
// First Esc with a queued steer: silently consume it and
|
|
102
|
+
// auto-continue via steer-on-interrupt instead of stalling on
|
|
103
|
+
// "Operation aborted".
|
|
104
|
+
this.#steerConsumePending = true;
|
|
105
|
+
void this.#abortInteractive({ silent: true });
|
|
106
|
+
} else {
|
|
107
|
+
void this.#abortInteractive();
|
|
108
|
+
}
|
|
77
109
|
} else if (!this.ctx.editor.getText().trim()) {
|
|
78
110
|
// Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
|
|
79
111
|
const action = settings.get("doubleEscapeAction");
|
|
@@ -132,6 +164,13 @@ export class InputController {
|
|
|
132
164
|
this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
|
|
133
165
|
);
|
|
134
166
|
this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
|
|
167
|
+
this.ctx.editor.onPasteText = text => this.handleTextPaste(text);
|
|
168
|
+
this.ctx.editor.onPastePendingInputCleared = (reason, droppedInputCount) => {
|
|
169
|
+
const reasonText = reason === "timeout" ? "timed out" : "exceeded the input queue limit";
|
|
170
|
+
this.ctx.showWarning(
|
|
171
|
+
`Paste handling ${reasonText}; discarded ${droppedInputCount} buffered input event${droppedInputCount === 1 ? "" : "s"}.`,
|
|
172
|
+
);
|
|
173
|
+
};
|
|
135
174
|
this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
|
|
136
175
|
this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
|
|
137
176
|
this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
|
|
@@ -170,6 +209,9 @@ export class InputController {
|
|
|
170
209
|
for (const key of this.ctx.keybindings.getKeys("app.session.observe")) {
|
|
171
210
|
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionObserver());
|
|
172
211
|
}
|
|
212
|
+
for (const key of this.ctx.keybindings.getKeys("app.jobs.open")) {
|
|
213
|
+
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showJobsOverlay());
|
|
214
|
+
}
|
|
173
215
|
|
|
174
216
|
this.ctx.editor.onChange = (text: string) => {
|
|
175
217
|
const wasBashMode = this.ctx.isBashMode;
|
|
@@ -568,6 +610,14 @@ export class InputController {
|
|
|
568
610
|
this.ctx.retryLoader.stop();
|
|
569
611
|
this.ctx.retryLoader = undefined;
|
|
570
612
|
}
|
|
613
|
+
if (this.ctx.retryCountdownTimer) {
|
|
614
|
+
clearInterval(this.ctx.retryCountdownTimer);
|
|
615
|
+
this.ctx.retryCountdownTimer = undefined;
|
|
616
|
+
}
|
|
617
|
+
if (this.ctx.retryEscapeHandler) {
|
|
618
|
+
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
|
619
|
+
this.ctx.retryEscapeHandler = undefined;
|
|
620
|
+
}
|
|
571
621
|
this.ctx.statusContainer.clear();
|
|
572
622
|
this.ctx.statusLine.dispose();
|
|
573
623
|
|
|
@@ -594,6 +644,55 @@ export class InputController {
|
|
|
594
644
|
process.kill(0, "SIGTSTP");
|
|
595
645
|
}
|
|
596
646
|
|
|
647
|
+
handleTextPaste(text: string): boolean | Promise<boolean> {
|
|
648
|
+
const imagePath = this.#getPastedImagePathCandidate(text);
|
|
649
|
+
return imagePath ? this.#attachPastedImagePath(imagePath) : false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async #attachPastedImagePath(imagePath: string): Promise<boolean> {
|
|
653
|
+
try {
|
|
654
|
+
const image = await loadImageInput({
|
|
655
|
+
path: imagePath,
|
|
656
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
657
|
+
autoResize: this.ctx.settings.get("images.autoResize"),
|
|
658
|
+
});
|
|
659
|
+
if (!image) {
|
|
660
|
+
this.ctx.showStatus("Unsupported pasted clipboard image file");
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
this.ctx.pendingImages.push({
|
|
665
|
+
type: "image",
|
|
666
|
+
data: image.data,
|
|
667
|
+
mimeType: image.mimeType,
|
|
668
|
+
});
|
|
669
|
+
this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
|
|
670
|
+
this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
|
|
671
|
+
this.ctx.ui.requestRender();
|
|
672
|
+
return true;
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (error instanceof ImageInputTooLargeError) {
|
|
675
|
+
this.ctx.showStatus(error.message);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
this.ctx.showStatus("Failed to attach pasted clipboard image");
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#getPastedImagePathCandidate(text: string): string | undefined {
|
|
684
|
+
const resolvedPath = path.resolve(text.trim());
|
|
685
|
+
const parentDir = path.dirname(resolvedPath);
|
|
686
|
+
const isClipboardTempPath =
|
|
687
|
+
(parentDir === "/tmp" || MACOS_CLIPBOARD_TEMP_DIR_PATTERN.test(parentDir)) &&
|
|
688
|
+
CLIPBOARD_TEMP_IMAGE_FILE_PATTERN.test(path.basename(resolvedPath));
|
|
689
|
+
return isClipboardTempPath ? resolvedPath : undefined;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#nextImagePlaceholder(): string {
|
|
693
|
+
return `[image ${this.ctx.pendingImages.length}]`;
|
|
694
|
+
}
|
|
695
|
+
|
|
597
696
|
async handleImagePaste(): Promise<boolean> {
|
|
598
697
|
try {
|
|
599
698
|
const image = await readImageFromClipboard();
|
|
@@ -608,7 +707,7 @@ export class InputController {
|
|
|
608
707
|
this.ctx.showStatus(`Unsupported clipboard image format: ${image.mimeType}`);
|
|
609
708
|
return false;
|
|
610
709
|
}
|
|
611
|
-
if (settings.get("images.autoResize")) {
|
|
710
|
+
if (this.ctx.settings.get("images.autoResize")) {
|
|
612
711
|
try {
|
|
613
712
|
const resized = await resizeImage({
|
|
614
713
|
type: "image",
|
|
@@ -626,10 +725,7 @@ export class InputController {
|
|
|
626
725
|
data: imageData.data,
|
|
627
726
|
mimeType: imageData.mimeType,
|
|
628
727
|
});
|
|
629
|
-
|
|
630
|
-
const imageNum = this.ctx.pendingImages.length;
|
|
631
|
-
const placeholder = `[Image #${imageNum}]`;
|
|
632
|
-
this.ctx.editor.insertText(`${placeholder} `);
|
|
728
|
+
this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
|
|
633
729
|
this.ctx.ui.requestRender();
|
|
634
730
|
return true;
|
|
635
731
|
}
|
|
@@ -41,6 +41,7 @@ import { AgentDashboard } from "../components/agent-dashboard";
|
|
|
41
41
|
import { AssistantMessageComponent } from "../components/assistant-message";
|
|
42
42
|
import { ExtensionDashboard } from "../components/extensions";
|
|
43
43
|
import { HistorySearchComponent } from "../components/history-search";
|
|
44
|
+
import { JobsOverlayComponent } from "../components/jobs-overlay";
|
|
44
45
|
import { ModelSelectorComponent, type ModelSelectorSelection } from "../components/model-selector";
|
|
45
46
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
46
47
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
@@ -55,14 +56,16 @@ import { ThemeSelectorComponent } from "../components/theme-selector";
|
|
|
55
56
|
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
56
57
|
import { TreeSelectorComponent } from "../components/tree-selector";
|
|
57
58
|
import { UserMessageSelectorComponent } from "../components/user-message-selector";
|
|
59
|
+
import type { JobsObserver } from "../jobs-observer";
|
|
58
60
|
import type { SessionObserverRegistry } from "../session-observer-registry";
|
|
59
61
|
|
|
60
|
-
const CALLBACK_SERVER_PROVIDERS = new Set<
|
|
62
|
+
const CALLBACK_SERVER_PROVIDERS = new Set<string>([
|
|
61
63
|
"anthropic",
|
|
62
64
|
"openai-codex",
|
|
63
65
|
"gitlab-duo",
|
|
64
66
|
"google-gemini-cli",
|
|
65
67
|
"google-antigravity",
|
|
68
|
+
"xai",
|
|
66
69
|
]);
|
|
67
70
|
|
|
68
71
|
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
|
|
@@ -1149,4 +1152,31 @@ export class SelectorController {
|
|
|
1149
1152
|
this.ctx.ui.setFocus(selector);
|
|
1150
1153
|
this.ctx.ui.requestRender();
|
|
1151
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Jobs overlay: navigate ongoing monitor + cron jobs (Monitors then Crons,
|
|
1158
|
+
* newest-first), drill into per-type detail, and cancel/delete with a y/N
|
|
1159
|
+
* confirm. Built from nested SelectLists (list -> detail -> confirm) so focus
|
|
1160
|
+
* stays on the active SelectList.
|
|
1161
|
+
*/
|
|
1162
|
+
showJobsOverlay(observer: JobsObserver): void {
|
|
1163
|
+
let overlay: JobsOverlayComponent | undefined;
|
|
1164
|
+
const close = () => {
|
|
1165
|
+
this.ctx.editorContainer.clear();
|
|
1166
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
1167
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
1168
|
+
this.ctx.ui.requestRender();
|
|
1169
|
+
};
|
|
1170
|
+
overlay = new JobsOverlayComponent(observer, {
|
|
1171
|
+
close,
|
|
1172
|
+
requestRender: () => {
|
|
1173
|
+
if (overlay) this.ctx.ui.setFocus(overlay.getFocus());
|
|
1174
|
+
this.ctx.ui.requestRender();
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
this.ctx.editorContainer.clear();
|
|
1178
|
+
this.ctx.editorContainer.addChild(overlay);
|
|
1179
|
+
this.ctx.ui.setFocus(overlay.getFocus());
|
|
1180
|
+
this.ctx.ui.requestRender();
|
|
1181
|
+
}
|
|
1152
1182
|
}
|
package/src/modes/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { postmortem } from "@gajae-code/utils";
|
|
|
5
5
|
* Run modes for the coding agent.
|
|
6
6
|
*/
|
|
7
7
|
export { runAcpMode } from "./acp";
|
|
8
|
+
export { runBridgeMode } from "./bridge/bridge-mode";
|
|
8
9
|
export { InteractiveMode, type InteractiveModeOptions } from "./interactive-mode";
|
|
9
10
|
export { type PrintModeOptions, runPrintMode } from "./print-mode";
|
|
10
11
|
export {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "@gajae-code/tui";
|
|
29
29
|
import { APP_NAME, adjustHsv, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@gajae-code/utils";
|
|
30
30
|
import chalk from "chalk";
|
|
31
|
+
import { AsyncJobManager } from "../async";
|
|
31
32
|
import { KeybindingsManager } from "../config/keybindings";
|
|
32
33
|
import { isSettingsInitialized, type Settings, settings } from "../config/settings";
|
|
33
34
|
import { DEFAULT_GJC_DEFINITION_NAMES } from "../defaults/gjc-defaults";
|
|
@@ -88,6 +89,7 @@ import { InputController } from "./controllers/input-controller";
|
|
|
88
89
|
import { SelectorController } from "./controllers/selector-controller";
|
|
89
90
|
import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
90
91
|
import { TodoCommandController } from "./controllers/todo-command-controller";
|
|
92
|
+
import { JobsObserver } from "./jobs-observer";
|
|
91
93
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
92
94
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
93
95
|
import { interruptHint } from "./shared";
|
|
@@ -276,6 +278,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
276
278
|
}
|
|
277
279
|
autoCompactionEscapeHandler?: () => void;
|
|
278
280
|
retryEscapeHandler?: () => void;
|
|
281
|
+
retryCountdownTimer?: ReturnType<typeof setInterval>;
|
|
279
282
|
unsubscribe?: () => void;
|
|
280
283
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
281
284
|
optimisticUserMessageSignature: string | undefined = undefined;
|
|
@@ -329,6 +332,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
329
332
|
#voicePreviousUseTerminalCursor: boolean | null = null;
|
|
330
333
|
#resizeHandler?: () => void;
|
|
331
334
|
#observerRegistry: SessionObserverRegistry;
|
|
335
|
+
#jobsObserver?: JobsObserver;
|
|
332
336
|
#eventBus?: EventBus;
|
|
333
337
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
334
338
|
#welcomeComponent?: WelcomeComponent;
|
|
@@ -524,6 +528,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
524
528
|
this.ui.requestRender();
|
|
525
529
|
});
|
|
526
530
|
|
|
531
|
+
// Event-driven monitor/cron jobs widget. Scoped to this session's owner so
|
|
532
|
+
// overlay actions cannot mutate another agent's background work.
|
|
533
|
+
const jobManager = AsyncJobManager.instance();
|
|
534
|
+
if (jobManager) {
|
|
535
|
+
const jobsObserver = new JobsObserver(jobManager, this.session.getAgentId());
|
|
536
|
+
this.#jobsObserver = jobsObserver;
|
|
537
|
+
this.statusLine.setJobs(jobsObserver.getSnapshot());
|
|
538
|
+
jobsObserver.onChange(() => {
|
|
539
|
+
this.statusLine.setJobs(jobsObserver.getSnapshot());
|
|
540
|
+
this.ui.requestRender();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
527
544
|
// Load initial todos
|
|
528
545
|
await this.#loadTodoList();
|
|
529
546
|
|
|
@@ -1842,6 +1859,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1842
1859
|
this.#observerRegistry.dispose();
|
|
1843
1860
|
this.#eventController.dispose();
|
|
1844
1861
|
this.statusLine.dispose();
|
|
1862
|
+
this.#jobsObserver?.dispose();
|
|
1863
|
+
this.editor.dispose();
|
|
1845
1864
|
if (this.#resizeHandler) {
|
|
1846
1865
|
process.stdout.removeListener("resize", this.#resizeHandler);
|
|
1847
1866
|
this.#resizeHandler = undefined;
|
|
@@ -1943,6 +1962,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1943
1962
|
nextEditor.setHistoryStorage(this.historyStorage);
|
|
1944
1963
|
}
|
|
1945
1964
|
nextEditor.setText(previousText);
|
|
1965
|
+
previousEditor.dispose();
|
|
1946
1966
|
|
|
1947
1967
|
this.editorContainer.clear();
|
|
1948
1968
|
this.editor = nextEditor;
|
|
@@ -2316,6 +2336,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2316
2336
|
this.#selectorController.showSessionObserver(this.#observerRegistry);
|
|
2317
2337
|
}
|
|
2318
2338
|
|
|
2339
|
+
showJobsOverlay(): void {
|
|
2340
|
+
if (!this.#jobsObserver) {
|
|
2341
|
+
this.showStatus("Background jobs are unavailable in this session");
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
this.#selectorController.showJobsOverlay(this.#jobsObserver);
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2319
2347
|
resetObserverRegistry(): void {
|
|
2320
2348
|
this.#observerRegistry.resetSessions();
|
|
2321
2349
|
this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JobsObserver
|
|
3
|
+
*
|
|
4
|
+
* Single, event-driven aggregator over the two background-work sources surfaced
|
|
5
|
+
* by the status-line jobs widget and the jobs overlay:
|
|
6
|
+
* - monitor jobs (bash jobs started by the `monitor` tool, tracked in `AsyncJobManager`)
|
|
7
|
+
* - cron jobs (tracked in the cron module's owner-scoped schedule store)
|
|
8
|
+
*
|
|
9
|
+
* It subscribes to change hooks on both sources (no polling), debounces bursts
|
|
10
|
+
* to a microtask, and exposes a precomputed snapshot so the status-line render
|
|
11
|
+
* loop never scans the underlying stores. A failure latch keeps the widget red
|
|
12
|
+
* until `acknowledgeFailures()` is called (when the overlay opens), so a failed
|
|
13
|
+
* job that evicts before the user looks is not silently lost.
|
|
14
|
+
*/
|
|
15
|
+
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
16
|
+
import { deleteCronJobById, listCronSnapshots, onCronChange } from "../tools/cron";
|
|
17
|
+
|
|
18
|
+
export type JobsWorstState = "none" | "running" | "failed";
|
|
19
|
+
|
|
20
|
+
export interface MonitorJobView {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
status: AsyncJob["status"];
|
|
24
|
+
startTime: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CronJobView {
|
|
28
|
+
id: string;
|
|
29
|
+
humanSchedule: string;
|
|
30
|
+
cronExpression: string;
|
|
31
|
+
prompt: string;
|
|
32
|
+
recurring: boolean;
|
|
33
|
+
nextFireAt?: number;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface JobsSnapshot {
|
|
38
|
+
monitors: MonitorJobView[];
|
|
39
|
+
crons: CronJobView[];
|
|
40
|
+
activeMonitorCount: number;
|
|
41
|
+
activeCronCount: number;
|
|
42
|
+
worstState: JobsWorstState;
|
|
43
|
+
failedUnacknowledged: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const EMPTY_JOBS_SNAPSHOT: JobsSnapshot = {
|
|
47
|
+
monitors: [],
|
|
48
|
+
crons: [],
|
|
49
|
+
activeMonitorCount: 0,
|
|
50
|
+
activeCronCount: 0,
|
|
51
|
+
worstState: "none",
|
|
52
|
+
failedUnacknowledged: false,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export class JobsObserver {
|
|
56
|
+
readonly #manager: AsyncJobManager;
|
|
57
|
+
readonly #ownerId: string | undefined;
|
|
58
|
+
readonly #unsubscribers: Array<() => void> = [];
|
|
59
|
+
readonly #listeners = new Set<() => void>();
|
|
60
|
+
#failedUnacknowledged = false;
|
|
61
|
+
#notifyScheduled = false;
|
|
62
|
+
#disposed = false;
|
|
63
|
+
#snapshot: JobsSnapshot = EMPTY_JOBS_SNAPSHOT;
|
|
64
|
+
readonly #acknowledgedFailedIds = new Set<string>();
|
|
65
|
+
|
|
66
|
+
constructor(manager: AsyncJobManager, ownerId: string | undefined) {
|
|
67
|
+
this.#manager = manager;
|
|
68
|
+
this.#ownerId = ownerId;
|
|
69
|
+
this.#unsubscribers.push(manager.onChange(() => this.#onUpstreamChange()));
|
|
70
|
+
this.#unsubscribers.push(onCronChange(() => this.#onUpstreamChange()));
|
|
71
|
+
this.#recompute();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Subscribe to debounced change events. Returns an unsubscribe function. */
|
|
75
|
+
onChange(cb: () => void): () => void {
|
|
76
|
+
this.#listeners.add(cb);
|
|
77
|
+
return () => {
|
|
78
|
+
this.#listeners.delete(cb);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#onUpstreamChange(): void {
|
|
83
|
+
if (this.#disposed) return;
|
|
84
|
+
this.#recompute();
|
|
85
|
+
if (this.#notifyScheduled) return;
|
|
86
|
+
this.#notifyScheduled = true;
|
|
87
|
+
queueMicrotask(() => {
|
|
88
|
+
this.#notifyScheduled = false;
|
|
89
|
+
if (this.#disposed) return;
|
|
90
|
+
this.#emit();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#emit(): void {
|
|
95
|
+
for (const cb of this.#listeners) {
|
|
96
|
+
try {
|
|
97
|
+
cb();
|
|
98
|
+
} catch {
|
|
99
|
+
// Listener errors are isolated; a bad subscriber must not break others.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#listMonitorJobs(): AsyncJob[] {
|
|
105
|
+
const filter = this.#ownerId ? { ownerId: this.#ownerId } : undefined;
|
|
106
|
+
return this.#manager.getAllJobs(filter).filter(job => job.type === "bash" && job.metadata?.monitor === true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Recompute and store the snapshot. Called on construction and on every
|
|
111
|
+
* upstream change; the status-line render path only reads the stored
|
|
112
|
+
* snapshot (never scans the manager/cron stores).
|
|
113
|
+
*/
|
|
114
|
+
#recompute(): void {
|
|
115
|
+
const monitorJobs = this.#listMonitorJobs();
|
|
116
|
+
const presentIds = new Set(monitorJobs.map(job => job.id));
|
|
117
|
+
// Prune acknowledged ids whose jobs have been evicted.
|
|
118
|
+
for (const id of this.#acknowledgedFailedIds) {
|
|
119
|
+
if (!presentIds.has(id)) this.#acknowledgedFailedIds.delete(id);
|
|
120
|
+
}
|
|
121
|
+
// Sticky failure latch: set when an unacknowledged failed monitor is seen
|
|
122
|
+
// (including at construction); stays set even after the failed job evicts,
|
|
123
|
+
// until acknowledgeFailures() clears it.
|
|
124
|
+
const hasUnacknowledgedFailure = monitorJobs.some(
|
|
125
|
+
job => job.status === "failed" && !this.#acknowledgedFailedIds.has(job.id),
|
|
126
|
+
);
|
|
127
|
+
if (hasUnacknowledgedFailure) this.#failedUnacknowledged = true;
|
|
128
|
+
|
|
129
|
+
const activeMonitors = monitorJobs.filter(job => job.status === "running");
|
|
130
|
+
const cronSnapshots = listCronSnapshots(this.#ownerId);
|
|
131
|
+
const monitors: MonitorJobView[] = monitorJobs
|
|
132
|
+
.map(job => ({ id: job.id, label: job.label, status: job.status, startTime: job.startTime }))
|
|
133
|
+
.sort((a, b) => b.startTime - a.startTime);
|
|
134
|
+
const crons: CronJobView[] = cronSnapshots
|
|
135
|
+
.map(snapshot => ({
|
|
136
|
+
id: snapshot.id,
|
|
137
|
+
humanSchedule: snapshot.humanSchedule,
|
|
138
|
+
cronExpression: snapshot.cron_expression,
|
|
139
|
+
prompt: snapshot.prompt,
|
|
140
|
+
recurring: snapshot.recurring,
|
|
141
|
+
nextFireAt: snapshot.nextFireAt,
|
|
142
|
+
createdAt: snapshot.createdAt,
|
|
143
|
+
}))
|
|
144
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
145
|
+
const worstState: JobsWorstState = this.#failedUnacknowledged
|
|
146
|
+
? "failed"
|
|
147
|
+
: activeMonitors.length > 0 || crons.length > 0
|
|
148
|
+
? "running"
|
|
149
|
+
: "none";
|
|
150
|
+
this.#snapshot = {
|
|
151
|
+
monitors,
|
|
152
|
+
crons,
|
|
153
|
+
activeMonitorCount: activeMonitors.length,
|
|
154
|
+
activeCronCount: crons.length,
|
|
155
|
+
worstState,
|
|
156
|
+
failedUnacknowledged: this.#failedUnacknowledged,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Return the precomputed snapshot (recomputed on each upstream change). */
|
|
161
|
+
getSnapshot(): JobsSnapshot {
|
|
162
|
+
return this.#snapshot;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Clear the failure latch (called when the user opens the jobs overlay). */
|
|
166
|
+
acknowledgeFailures(): void {
|
|
167
|
+
for (const job of this.#listMonitorJobs()) {
|
|
168
|
+
if (job.status === "failed") this.#acknowledgedFailedIds.add(job.id);
|
|
169
|
+
}
|
|
170
|
+
if (!this.#failedUnacknowledged) return;
|
|
171
|
+
this.#failedUnacknowledged = false;
|
|
172
|
+
this.#recompute();
|
|
173
|
+
this.#emit();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Cancel a running monitor job. Returns true when the job was cancelled. */
|
|
177
|
+
cancelMonitor(id: string): boolean {
|
|
178
|
+
return this.#manager.cancel(id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Delete a visible scheduled cron job. Returns true when removed. */
|
|
182
|
+
deleteCron(id: string): boolean {
|
|
183
|
+
return deleteCronJobById(this.#ownerId, id);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Bounded tail of a monitor job's captured output (for the detail view). */
|
|
187
|
+
getMonitorOutput(id: string): string {
|
|
188
|
+
const slice = this.#manager.readOutputSince(id, 0, this.#ownerId ? { ownerId: this.#ownerId } : undefined);
|
|
189
|
+
return slice?.text ?? "";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
dispose(): void {
|
|
193
|
+
this.#disposed = true;
|
|
194
|
+
for (const unsubscribe of this.#unsubscribers) {
|
|
195
|
+
try {
|
|
196
|
+
unsubscribe();
|
|
197
|
+
} catch {
|
|
198
|
+
// best-effort teardown
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.#unsubscribers.length = 0;
|
|
202
|
+
this.#listeners.clear();
|
|
203
|
+
}
|
|
204
|
+
}
|