@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure model helpers for the jobs overlay.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of UI/Component dependencies so the grouping/ordering and
|
|
5
|
+
* detail-formatting logic is unit-testable. The selector controller wires these
|
|
6
|
+
* SelectItem lists into nested SelectLists (list -> detail -> confirm).
|
|
7
|
+
*/
|
|
8
|
+
import type { SelectItem } from "@gajae-code/tui";
|
|
9
|
+
import type { JobsSnapshot } from "../jobs-observer";
|
|
10
|
+
|
|
11
|
+
export type JobRefKind = "monitor" | "cron";
|
|
12
|
+
|
|
13
|
+
export interface JobRef {
|
|
14
|
+
kind: JobRefKind;
|
|
15
|
+
id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PROMPT_PREVIEW_MAX = 60;
|
|
19
|
+
|
|
20
|
+
function preview(text: string, max = PROMPT_PREVIEW_MAX): string {
|
|
21
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
22
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Compact relative time, e.g. "in 5m", "2m ago", "now". */
|
|
26
|
+
export function formatRelative(targetMs: number | undefined, nowMs = Date.now()): string {
|
|
27
|
+
if (targetMs === undefined) return "—";
|
|
28
|
+
const deltaMs = targetMs - nowMs;
|
|
29
|
+
const abs = Math.abs(deltaMs);
|
|
30
|
+
const mins = Math.round(abs / 60_000);
|
|
31
|
+
if (mins < 1) return "now";
|
|
32
|
+
const unit = mins >= 60 ? `${Math.round(mins / 60)}h` : `${mins}m`;
|
|
33
|
+
return deltaMs >= 0 ? `in ${unit}` : `${unit} ago`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse a list item value back into a job reference. */
|
|
37
|
+
export function parseJobRef(value: string): JobRef | null {
|
|
38
|
+
const sep = value.indexOf(":");
|
|
39
|
+
if (sep === -1) return null;
|
|
40
|
+
const kind = value.slice(0, sep);
|
|
41
|
+
const id = value.slice(sep + 1);
|
|
42
|
+
if ((kind === "monitor" || kind === "cron") && id.length > 0) {
|
|
43
|
+
return { kind, id };
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the grouped jobs list: monitors first (newest-first), then crons
|
|
50
|
+
* (newest-first). The snapshot arrays are already sorted newest-first.
|
|
51
|
+
*/
|
|
52
|
+
export function buildJobsListItems(snapshot: JobsSnapshot): SelectItem[] {
|
|
53
|
+
const items: SelectItem[] = [];
|
|
54
|
+
for (const monitor of snapshot.monitors) {
|
|
55
|
+
items.push({
|
|
56
|
+
value: `monitor:${monitor.id}`,
|
|
57
|
+
label: `monitor · ${preview(monitor.label, 40)}`,
|
|
58
|
+
description: monitor.status,
|
|
59
|
+
hint: monitor.status === "failed" ? "failed" : undefined,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const cron of snapshot.crons) {
|
|
63
|
+
items.push({
|
|
64
|
+
value: `cron:${cron.id}`,
|
|
65
|
+
label: `cron · ${cron.humanSchedule}`,
|
|
66
|
+
description: preview(cron.prompt),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return items;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the detail-level items for a job: read-only info rows (value "noop"),
|
|
74
|
+
* then the destructive action, then a back row. `output` is the bounded monitor
|
|
75
|
+
* output tail (ignored for cron jobs).
|
|
76
|
+
*/
|
|
77
|
+
export function buildJobDetailItems(snapshot: JobsSnapshot, ref: JobRef, output = ""): SelectItem[] {
|
|
78
|
+
if (ref.kind === "monitor") {
|
|
79
|
+
const monitor = snapshot.monitors.find(m => m.id === ref.id);
|
|
80
|
+
if (!monitor) return [{ value: "back", label: "Back (job no longer present)" }];
|
|
81
|
+
const lastOutput = output.trim().split("\n").filter(Boolean).slice(-1)[0] ?? "(no output captured)";
|
|
82
|
+
return [
|
|
83
|
+
{ value: "noop", label: "Status", description: monitor.status },
|
|
84
|
+
{ value: "noop", label: "Label", description: preview(monitor.label) },
|
|
85
|
+
{ value: "noop", label: "Started", description: formatRelative(monitor.startTime) },
|
|
86
|
+
{ value: "noop", label: "Output", description: preview(lastOutput, 80) },
|
|
87
|
+
{ value: "action:cancel", label: "Cancel this monitor", hint: "stops the running job" },
|
|
88
|
+
{ value: "back", label: "Back" },
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
const cron = snapshot.crons.find(c => c.id === ref.id);
|
|
92
|
+
if (!cron) return [{ value: "back", label: "Back (job no longer present)" }];
|
|
93
|
+
return [
|
|
94
|
+
{ value: "noop", label: "Schedule", description: `${cron.humanSchedule} (${cron.cronExpression})` },
|
|
95
|
+
{ value: "noop", label: "Recurring", description: cron.recurring ? "yes" : "no" },
|
|
96
|
+
{ value: "noop", label: "Next fire", description: formatRelative(cron.nextFireAt) },
|
|
97
|
+
{ value: "noop", label: "Prompt", description: preview(cron.prompt, 80) },
|
|
98
|
+
{ value: "action:delete", label: "Delete this cron", hint: "removes the schedule" },
|
|
99
|
+
{ value: "back", label: "Back" },
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Yes/No confirm items for a destructive action. */
|
|
104
|
+
export function buildConfirmItems(actionLabel: string): SelectItem[] {
|
|
105
|
+
return [
|
|
106
|
+
{ value: "no", label: `No, keep it` },
|
|
107
|
+
{ value: "yes", label: `Yes, ${actionLabel}` },
|
|
108
|
+
];
|
|
109
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Container, type SelectItem, SelectList } from "@gajae-code/tui";
|
|
2
|
+
import type { JobsSnapshot } from "../jobs-observer";
|
|
3
|
+
import { getSelectListTheme } from "../theme/theme";
|
|
4
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
5
|
+
import {
|
|
6
|
+
buildConfirmItems,
|
|
7
|
+
buildJobDetailItems,
|
|
8
|
+
buildJobsListItems,
|
|
9
|
+
type JobRef,
|
|
10
|
+
parseJobRef,
|
|
11
|
+
} from "./jobs-overlay-model";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generic single-level selector used by the jobs overlay. The selector
|
|
15
|
+
* controller mounts a fresh instance per navigation level (list -> detail ->
|
|
16
|
+
* confirm); focus is placed on the inner SelectList, matching the existing
|
|
17
|
+
* selector components (e.g. ThemeSelectorComponent).
|
|
18
|
+
*/
|
|
19
|
+
export class JobsSelectorComponent extends Container {
|
|
20
|
+
#selectList: SelectList;
|
|
21
|
+
|
|
22
|
+
constructor(items: SelectItem[], onSelect: (item: SelectItem) => void, onCancel: () => void, maxVisible = 12) {
|
|
23
|
+
super();
|
|
24
|
+
this.addChild(new DynamicBorder());
|
|
25
|
+
this.#selectList = new SelectList(items, maxVisible, getSelectListTheme());
|
|
26
|
+
this.#selectList.onSelect = onSelect;
|
|
27
|
+
this.#selectList.onCancel = onCancel;
|
|
28
|
+
this.addChild(this.#selectList);
|
|
29
|
+
this.addChild(new DynamicBorder());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getSelectList(): SelectList {
|
|
33
|
+
return this.#selectList;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface JobsOverlayController {
|
|
38
|
+
acknowledgeFailures(): void;
|
|
39
|
+
getSnapshot(): JobsSnapshot;
|
|
40
|
+
getMonitorOutput(id: string): string;
|
|
41
|
+
cancelMonitor(id: string): boolean;
|
|
42
|
+
deleteCron(id: string): boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface JobsOverlayCallbacks {
|
|
46
|
+
close(): void;
|
|
47
|
+
requestRender(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type JobsOverlayView = "list" | "detail" | "confirm";
|
|
51
|
+
type JobsOverlayAction = "cancel" | "delete";
|
|
52
|
+
|
|
53
|
+
export class JobsOverlayComponent extends Container {
|
|
54
|
+
readonly #controller: JobsOverlayController;
|
|
55
|
+
readonly #callbacks: JobsOverlayCallbacks;
|
|
56
|
+
#view: JobsOverlayView = "list";
|
|
57
|
+
#ref: JobRef | undefined;
|
|
58
|
+
#action: JobsOverlayAction | undefined;
|
|
59
|
+
#selectList: SelectList | undefined;
|
|
60
|
+
|
|
61
|
+
constructor(controller: JobsOverlayController, callbacks: JobsOverlayCallbacks) {
|
|
62
|
+
super();
|
|
63
|
+
this.#controller = controller;
|
|
64
|
+
this.#callbacks = callbacks;
|
|
65
|
+
this.#controller.acknowledgeFailures();
|
|
66
|
+
this.#renderList();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getFocus(): SelectList {
|
|
70
|
+
if (!this.#selectList) throw new Error("Jobs overlay has no focusable list");
|
|
71
|
+
return this.#selectList;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleInput(data: string): void {
|
|
75
|
+
if (this.#view === "confirm") {
|
|
76
|
+
const key = data.toLowerCase();
|
|
77
|
+
if (key === "y") {
|
|
78
|
+
this.#confirmYes();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key === "n") {
|
|
82
|
+
this.#renderDetail();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.#selectList?.handleInput(data);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#replaceList(
|
|
90
|
+
items: SelectItem[],
|
|
91
|
+
onSelect: (item: SelectItem) => void,
|
|
92
|
+
onCancel: () => void,
|
|
93
|
+
maxVisible = 12,
|
|
94
|
+
): void {
|
|
95
|
+
this.clear();
|
|
96
|
+
this.addChild(new DynamicBorder());
|
|
97
|
+
this.#selectList = new SelectList(items, maxVisible, getSelectListTheme());
|
|
98
|
+
this.#selectList.onSelect = onSelect;
|
|
99
|
+
this.#selectList.onCancel = onCancel;
|
|
100
|
+
this.addChild(this.#selectList);
|
|
101
|
+
this.addChild(new DynamicBorder());
|
|
102
|
+
this.#callbacks.requestRender();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#renderList(): void {
|
|
106
|
+
this.#view = "list";
|
|
107
|
+
this.#ref = undefined;
|
|
108
|
+
this.#action = undefined;
|
|
109
|
+
const snapshot = this.#controller.getSnapshot();
|
|
110
|
+
const built = buildJobsListItems(snapshot);
|
|
111
|
+
const items = built.length > 0 ? built : [{ value: "close", label: "No active monitor or cron jobs" }];
|
|
112
|
+
this.#replaceList(
|
|
113
|
+
items,
|
|
114
|
+
item => {
|
|
115
|
+
const ref = parseJobRef(item.value);
|
|
116
|
+
if (ref) this.#renderDetail(ref);
|
|
117
|
+
else this.#callbacks.close();
|
|
118
|
+
},
|
|
119
|
+
() => this.#callbacks.close(),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#renderDetail(ref = this.#ref): void {
|
|
124
|
+
if (!ref) {
|
|
125
|
+
this.#renderList();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.#view = "detail";
|
|
129
|
+
this.#ref = ref;
|
|
130
|
+
this.#action = undefined;
|
|
131
|
+
const output = ref.kind === "monitor" ? this.#controller.getMonitorOutput(ref.id) : "";
|
|
132
|
+
const items = buildJobDetailItems(this.#controller.getSnapshot(), ref, output);
|
|
133
|
+
this.#replaceList(
|
|
134
|
+
items,
|
|
135
|
+
item => {
|
|
136
|
+
if (item.value === "action:cancel") this.#renderConfirm("cancel");
|
|
137
|
+
else if (item.value === "action:delete") this.#renderConfirm("delete");
|
|
138
|
+
else if (item.value === "back") this.#renderList();
|
|
139
|
+
},
|
|
140
|
+
() => this.#callbacks.close(),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#renderConfirm(action: JobsOverlayAction): void {
|
|
145
|
+
if (!this.#ref) {
|
|
146
|
+
this.#renderList();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.#view = "confirm";
|
|
150
|
+
this.#action = action;
|
|
151
|
+
const label = action === "cancel" ? "cancel this monitor" : "delete this cron";
|
|
152
|
+
this.#replaceList(
|
|
153
|
+
buildConfirmItems(label),
|
|
154
|
+
item => {
|
|
155
|
+
if (item.value === "yes") this.#confirmYes();
|
|
156
|
+
else this.#renderDetail();
|
|
157
|
+
},
|
|
158
|
+
() => this.#renderDetail(),
|
|
159
|
+
4,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#confirmYes(): void {
|
|
164
|
+
if (!this.#ref || !this.#action) {
|
|
165
|
+
this.#renderList();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (this.#action === "cancel") this.#controller.cancelMonitor(this.#ref.id);
|
|
169
|
+
else this.#controller.deleteCron(this.#ref.id);
|
|
170
|
+
this.#renderList();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
|
|
|
3
3
|
export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
4
4
|
default: {
|
|
5
5
|
leftSegments: ["model", "mode", "git", "pr", "path"],
|
|
6
|
-
rightSegments: ["session_name", "token_rate", "context_pct", "cost"],
|
|
6
|
+
rightSegments: ["session_name", "jobs", "token_rate", "context_pct", "cost"],
|
|
7
7
|
separator: "slash",
|
|
8
8
|
segmentOptions: {
|
|
9
9
|
model: { showThinkingLevel: true },
|
|
@@ -14,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
14
14
|
|
|
15
15
|
minimal: {
|
|
16
16
|
leftSegments: ["path", "git"],
|
|
17
|
-
rightSegments: ["session_name", "mode", "context_pct"],
|
|
17
|
+
rightSegments: ["session_name", "jobs", "mode", "context_pct"],
|
|
18
18
|
separator: "slash",
|
|
19
19
|
segmentOptions: {
|
|
20
20
|
path: { abbreviate: true, maxLength: 30 },
|
|
@@ -24,7 +24,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
24
24
|
|
|
25
25
|
compact: {
|
|
26
26
|
leftSegments: ["model", "mode", "git", "pr"],
|
|
27
|
-
rightSegments: ["session_name", "cost", "context_pct"],
|
|
27
|
+
rightSegments: ["session_name", "jobs", "cost", "context_pct"],
|
|
28
28
|
separator: "slash",
|
|
29
29
|
segmentOptions: {
|
|
30
30
|
model: { showThinkingLevel: false },
|
|
@@ -36,6 +36,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
36
36
|
leftSegments: ["gajae", "hostname", "model", "mode", "path", "git", "pr", "subagents"],
|
|
37
37
|
rightSegments: [
|
|
38
38
|
"session_name",
|
|
39
|
+
"jobs",
|
|
39
40
|
"token_in",
|
|
40
41
|
"token_out",
|
|
41
42
|
"token_rate",
|
|
@@ -59,6 +60,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
59
60
|
leftSegments: ["gajae", "hostname", "model", "mode", "path", "git", "pr", "session", "subagents"],
|
|
60
61
|
rightSegments: [
|
|
61
62
|
"session_name",
|
|
63
|
+
"jobs",
|
|
62
64
|
"token_in",
|
|
63
65
|
"token_out",
|
|
64
66
|
"cache_read",
|
|
@@ -82,7 +84,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
82
84
|
ascii: {
|
|
83
85
|
// No Nerd Font dependencies
|
|
84
86
|
leftSegments: ["model", "mode", "path", "git", "pr"],
|
|
85
|
-
rightSegments: ["session_name", "token_total", "cost", "context_pct"],
|
|
87
|
+
rightSegments: ["session_name", "jobs", "token_total", "cost", "context_pct"],
|
|
86
88
|
separator: "ascii",
|
|
87
89
|
segmentOptions: {
|
|
88
90
|
model: { showThinkingLevel: true },
|
|
@@ -94,7 +96,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
94
96
|
custom: {
|
|
95
97
|
// User-defined - these are just defaults that get overridden
|
|
96
98
|
leftSegments: ["model", "mode", "path", "git", "pr"],
|
|
97
|
-
rightSegments: ["session_name", "token_total", "cost", "context_pct"],
|
|
99
|
+
rightSegments: ["session_name", "jobs", "token_total", "cost", "context_pct"],
|
|
98
100
|
separator: "slash",
|
|
99
101
|
segmentOptions: {},
|
|
100
102
|
},
|
|
@@ -270,6 +270,30 @@ const subagentsSegment: StatusLineSegment = {
|
|
|
270
270
|
},
|
|
271
271
|
};
|
|
272
272
|
|
|
273
|
+
const jobsSegment: StatusLineSegment = {
|
|
274
|
+
id: "jobs",
|
|
275
|
+
render(ctx) {
|
|
276
|
+
const { jobs } = ctx;
|
|
277
|
+
const visible = jobs.activeMonitorCount > 0 || jobs.activeCronCount > 0 || jobs.worstState === "failed";
|
|
278
|
+
if (!visible) {
|
|
279
|
+
return { content: "", visible: false };
|
|
280
|
+
}
|
|
281
|
+
const parts: string[] = [];
|
|
282
|
+
if (jobs.activeMonitorCount > 0) {
|
|
283
|
+
parts.push(withIcon(theme.icon.agents, `${jobs.activeMonitorCount}`));
|
|
284
|
+
}
|
|
285
|
+
if (jobs.activeCronCount > 0) {
|
|
286
|
+
parts.push(withIcon(theme.icon.time, `${jobs.activeCronCount}`));
|
|
287
|
+
}
|
|
288
|
+
if (parts.length === 0) {
|
|
289
|
+
// Nothing active but a failure is unacknowledged — keep a drill-in marker.
|
|
290
|
+
parts.push(withIcon(theme.icon.warning, "jobs"));
|
|
291
|
+
}
|
|
292
|
+
const color: ThemeColor = jobs.worstState === "failed" ? "error" : "statusLineSubagents";
|
|
293
|
+
return { content: theme.fg(color, parts.join(" ")), visible: true };
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
273
297
|
const tokenInSegment: StatusLineSegment = {
|
|
274
298
|
id: "token_in",
|
|
275
299
|
render(ctx) {
|
|
@@ -521,6 +545,7 @@ export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
|
|
|
521
545
|
git: gitSegment,
|
|
522
546
|
pr: prSegment,
|
|
523
547
|
subagents: subagentsSegment,
|
|
548
|
+
jobs: jobsSegment,
|
|
524
549
|
token_in: tokenInSegment,
|
|
525
550
|
token_out: tokenOutSegment,
|
|
526
551
|
token_total: tokenTotalSegment,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../../config/settings-schema";
|
|
2
2
|
import type { AgentSession } from "../../../session/agent-session";
|
|
3
|
+
import type { JobsSnapshot } from "../../jobs-observer";
|
|
3
4
|
import type { StatusLineSegmentOptions, StatusLineSettings } from "../status-line";
|
|
4
5
|
|
|
5
6
|
export type {
|
|
@@ -42,6 +43,7 @@ export interface SegmentContext {
|
|
|
42
43
|
contextWindow: number;
|
|
43
44
|
autoCompactEnabled: boolean;
|
|
44
45
|
subagentCount: number;
|
|
46
|
+
jobs: JobsSnapshot;
|
|
45
47
|
sessionStartTime: number;
|
|
46
48
|
git: {
|
|
47
49
|
branch: string | null;
|
|
@@ -11,6 +11,7 @@ import type { AgentSession } from "../../session/agent-session";
|
|
|
11
11
|
import { readVisibleSkillActiveState, type SkillActiveEntry } from "../../skill-state/active-state";
|
|
12
12
|
import * as git from "../../utils/git";
|
|
13
13
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../utils/session-color";
|
|
14
|
+
import { EMPTY_JOBS_SNAPSHOT, type JobsSnapshot } from "../jobs-observer";
|
|
14
15
|
import { sanitizeStatusText } from "../shared";
|
|
15
16
|
import { computeNonMessageTokens } from "../utils/context-usage";
|
|
16
17
|
import { renderSkillHudBar } from "./skill-hud/render";
|
|
@@ -153,6 +154,7 @@ export class StatusLineComponent implements Component {
|
|
|
153
154
|
#autoCompactEnabled: boolean = true;
|
|
154
155
|
#hookStatuses: Map<string, string> = new Map();
|
|
155
156
|
#subagentCount: number = 0;
|
|
157
|
+
#jobs: JobsSnapshot = EMPTY_JOBS_SNAPSHOT;
|
|
156
158
|
#sessionStartTime: number = Date.now();
|
|
157
159
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
158
160
|
#goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
@@ -220,6 +222,10 @@ export class StatusLineComponent implements Component {
|
|
|
220
222
|
this.#subagentCount = count;
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
setJobs(jobs: JobsSnapshot): void {
|
|
226
|
+
this.#jobs = jobs;
|
|
227
|
+
}
|
|
228
|
+
|
|
223
229
|
setSessionStartTime(time: number): void {
|
|
224
230
|
this.#sessionStartTime = time;
|
|
225
231
|
}
|
|
@@ -612,6 +618,7 @@ export class StatusLineComponent implements Component {
|
|
|
612
618
|
contextWindow,
|
|
613
619
|
autoCompactEnabled: this.#autoCompactEnabled,
|
|
614
620
|
subagentCount: this.#subagentCount,
|
|
621
|
+
jobs: this.#jobs,
|
|
615
622
|
sessionStartTime: this.#sessionStartTime,
|
|
616
623
|
git: {
|
|
617
624
|
branch: this.#getCurrentBranch(),
|
|
@@ -687,7 +694,8 @@ export class StatusLineComponent implements Component {
|
|
|
687
694
|
}
|
|
688
695
|
}
|
|
689
696
|
|
|
690
|
-
const runningBackgroundJobs =
|
|
697
|
+
const runningBackgroundJobs =
|
|
698
|
+
this.session.getAsyncJobSnapshot()?.running.filter(job => job.metadata?.monitor !== true).length ?? 0;
|
|
691
699
|
if (runningBackgroundJobs > 0) {
|
|
692
700
|
const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
|
|
693
701
|
const label = `${formatCount("job", runningBackgroundJobs)} running`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { INTENT_FIELD } from "@gajae-code/agent-core";
|
|
2
2
|
import { calculatePromptTokens } from "@gajae-code/agent-core/compaction/compaction";
|
|
3
3
|
import type { AssistantMessage, ImageContent } from "@gajae-code/ai";
|
|
4
|
+
import { parseRateLimitReason } from "@gajae-code/ai";
|
|
4
5
|
import { type Component, Loader, TERMINAL, Text } from "@gajae-code/tui";
|
|
5
6
|
import { settings } from "../../config/settings";
|
|
6
7
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
@@ -24,6 +25,24 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
|
24
25
|
|
|
25
26
|
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
26
27
|
|
|
28
|
+
function friendlyRetryReason(errorMessage: string | undefined): string {
|
|
29
|
+
if (!errorMessage) return "";
|
|
30
|
+
switch (parseRateLimitReason(errorMessage)) {
|
|
31
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
32
|
+
return "rate limited";
|
|
33
|
+
case "QUOTA_EXHAUSTED":
|
|
34
|
+
return "usage limit";
|
|
35
|
+
case "MODEL_CAPACITY_EXHAUSTED":
|
|
36
|
+
return "overloaded";
|
|
37
|
+
case "SERVER_ERROR":
|
|
38
|
+
return "server error";
|
|
39
|
+
default:
|
|
40
|
+
return /network|connection|socket|fetch failed|terminated|timeout|timed out|stream/i.test(errorMessage)
|
|
41
|
+
? "connection error"
|
|
42
|
+
: "transient error";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
type AgentSessionEventHandlers = {
|
|
28
47
|
[E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
|
|
29
48
|
};
|
|
@@ -71,6 +90,15 @@ export class EventController {
|
|
|
71
90
|
|
|
72
91
|
dispose(): void {
|
|
73
92
|
this.#cancelIdleCompaction();
|
|
93
|
+
this.#clearRetryCountdown();
|
|
94
|
+
if (this.ctx.retryEscapeHandler) {
|
|
95
|
+
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
|
96
|
+
this.ctx.retryEscapeHandler = undefined;
|
|
97
|
+
}
|
|
98
|
+
if (this.ctx.retryLoader) {
|
|
99
|
+
this.ctx.retryLoader.stop();
|
|
100
|
+
this.ctx.retryLoader = undefined;
|
|
101
|
+
}
|
|
74
102
|
for (const timer of this.#ircExpiryTimers.values()) {
|
|
75
103
|
clearTimeout(timer);
|
|
76
104
|
}
|
|
@@ -166,6 +194,7 @@ export class EventController {
|
|
|
166
194
|
}
|
|
167
195
|
if (this.ctx.retryLoader) {
|
|
168
196
|
this.ctx.retryLoader.stop();
|
|
197
|
+
this.#clearRetryCountdown();
|
|
169
198
|
this.ctx.retryLoader = undefined;
|
|
170
199
|
this.ctx.statusContainer.clear();
|
|
171
200
|
}
|
|
@@ -648,21 +677,56 @@ export class EventController {
|
|
|
648
677
|
this.ctx.ui.requestRender();
|
|
649
678
|
}
|
|
650
679
|
|
|
680
|
+
#clearRetryCountdown(): void {
|
|
681
|
+
if (this.ctx.retryCountdownTimer) {
|
|
682
|
+
clearInterval(this.ctx.retryCountdownTimer);
|
|
683
|
+
this.ctx.retryCountdownTimer = undefined;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
651
687
|
async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
|
|
652
|
-
|
|
688
|
+
// Preserve the ORIGINAL editor Escape handler across repeated retry
|
|
689
|
+
// starts: auto_retry_end only fires at final success/failure, so a
|
|
690
|
+
// second auto_retry_start must not snapshot the prior retry handler.
|
|
691
|
+
if (!this.ctx.retryEscapeHandler) {
|
|
692
|
+
this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
|
|
693
|
+
}
|
|
694
|
+
let escPressed = false;
|
|
653
695
|
this.ctx.editor.onEscape = () => {
|
|
654
|
-
|
|
696
|
+
if (!escPressed) {
|
|
697
|
+
// First Esc: skip the backoff and retry immediately.
|
|
698
|
+
escPressed = true;
|
|
699
|
+
this.ctx.session.retryNow();
|
|
700
|
+
} else {
|
|
701
|
+
// Second Esc: cancel the retry entirely.
|
|
702
|
+
this.ctx.session.abortRetry();
|
|
703
|
+
}
|
|
655
704
|
};
|
|
656
705
|
this.ctx.statusContainer.clear();
|
|
657
|
-
|
|
658
|
-
this.ctx.retryLoader
|
|
706
|
+
// Stop any prior retry loader/timer before installing a new one.
|
|
707
|
+
this.ctx.retryLoader?.stop();
|
|
708
|
+
this.#clearRetryCountdown();
|
|
709
|
+
const reason = friendlyRetryReason(event.errorMessage);
|
|
710
|
+
const attemptLabel = event.unbounded ? `attempt ${event.attempt}` : `${event.attempt}/${event.maxAttempts}`;
|
|
711
|
+
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
712
|
+
const deadline = Date.now() + event.delayMs;
|
|
713
|
+
const buildMessage = () => {
|
|
714
|
+
const remainingSeconds = Math.max(0, Math.round((deadline - Date.now()) / 1000));
|
|
715
|
+
// First Esc retries immediately; a second Esc cancels.
|
|
716
|
+
return `Retrying (${attemptLabel})${reasonSuffix}, next in ${remainingSeconds}s… (esc to retry now)`;
|
|
717
|
+
};
|
|
718
|
+
const retryLoader = new Loader(
|
|
659
719
|
this.ctx.ui,
|
|
660
720
|
spinner => theme.fg("warning", spinner),
|
|
661
721
|
text => theme.fg("muted", text),
|
|
662
|
-
|
|
722
|
+
buildMessage(),
|
|
663
723
|
getSymbolTheme().spinnerFrames,
|
|
664
724
|
);
|
|
665
|
-
this.ctx.
|
|
725
|
+
this.ctx.retryLoader = retryLoader;
|
|
726
|
+
this.ctx.retryCountdownTimer = setInterval(() => {
|
|
727
|
+
retryLoader.setMessage(buildMessage());
|
|
728
|
+
}, 1000);
|
|
729
|
+
this.ctx.statusContainer.addChild(retryLoader);
|
|
666
730
|
this.ctx.ui.requestRender();
|
|
667
731
|
}
|
|
668
732
|
|
|
@@ -673,6 +737,7 @@ export class EventController {
|
|
|
673
737
|
}
|
|
674
738
|
if (this.ctx.retryLoader) {
|
|
675
739
|
this.ctx.retryLoader.stop();
|
|
740
|
+
this.#clearRetryCountdown();
|
|
676
741
|
this.ctx.retryLoader = undefined;
|
|
677
742
|
this.ctx.statusContainer.clear();
|
|
678
743
|
}
|
|
@@ -25,11 +25,17 @@ import type { InteractiveModeContext } from "../../modes/types";
|
|
|
25
25
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
26
26
|
|
|
27
27
|
const MAX_WIDGET_LINES = 10;
|
|
28
|
+
const HOOK_SELECTOR_MOUSE_REPORTING_ENABLE = "\x1b[?1006h\x1b[?1000h";
|
|
29
|
+
const HOOK_SELECTOR_MOUSE_REPORTING_DISABLE = "\x1b[?1000l\x1b[?1006l";
|
|
30
|
+
const HOOK_SELECTOR_CHROME_ROWS = 7;
|
|
31
|
+
const HOOK_SELECTOR_OUTLINE_ROWS = 2;
|
|
28
32
|
|
|
29
33
|
export class ExtensionUiController {
|
|
30
34
|
#extensionTerminalInputUnsubscribers = new Set<() => void>();
|
|
31
35
|
#hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
|
|
32
36
|
#hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
|
|
37
|
+
#hookSelectorMouseReportingEnabled = false;
|
|
38
|
+
|
|
33
39
|
constructor(private ctx: InteractiveModeContext) {}
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -589,7 +595,20 @@ export class ExtensionUiController {
|
|
|
589
595
|
() => this.hideHookSelector(),
|
|
590
596
|
dialogOptions?.signal,
|
|
591
597
|
);
|
|
592
|
-
const
|
|
598
|
+
const requestedTitleRows = dialogOptions?.scrollTitleRows;
|
|
599
|
+
const baseMaxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
|
|
600
|
+
const scrollOptionRows = Math.max(1, Math.min(baseMaxVisible, options.length));
|
|
601
|
+
const maxVisible =
|
|
602
|
+
requestedTitleRows === undefined ? baseMaxVisible : Math.min(15, Math.max(3, scrollOptionRows + 1));
|
|
603
|
+
const listChromeRows = dialogOptions?.outline === true ? HOOK_SELECTOR_OUTLINE_ROWS : 0;
|
|
604
|
+
const availableTitleRows =
|
|
605
|
+
this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - HOOK_SELECTOR_CHROME_ROWS;
|
|
606
|
+
const scrollTitleRows =
|
|
607
|
+
requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
|
|
608
|
+
if (scrollTitleRows !== undefined) {
|
|
609
|
+
this.#enableHookSelectorMouseReporting();
|
|
610
|
+
}
|
|
611
|
+
|
|
593
612
|
this.ctx.hookSelector = new HookSelectorComponent(
|
|
594
613
|
title,
|
|
595
614
|
options,
|
|
@@ -624,6 +643,7 @@ export class ExtensionUiController {
|
|
|
624
643
|
tui: this.ctx.ui,
|
|
625
644
|
outline: dialogOptions?.outline,
|
|
626
645
|
wrapFocused: dialogOptions?.wrapFocused,
|
|
646
|
+
scrollTitleRows,
|
|
627
647
|
maxVisible,
|
|
628
648
|
},
|
|
629
649
|
);
|
|
@@ -634,10 +654,32 @@ export class ExtensionUiController {
|
|
|
634
654
|
attachAbort();
|
|
635
655
|
return promise;
|
|
636
656
|
}
|
|
657
|
+
|
|
658
|
+
#enableHookSelectorMouseReporting(): void {
|
|
659
|
+
if (this.#hookSelectorMouseReportingEnabled) return;
|
|
660
|
+
this.#hookSelectorMouseReportingEnabled = true;
|
|
661
|
+
this.#writeTerminalControl(HOOK_SELECTOR_MOUSE_REPORTING_ENABLE);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
#disableHookSelectorMouseReporting(): void {
|
|
665
|
+
if (!this.#hookSelectorMouseReportingEnabled) return;
|
|
666
|
+
this.#hookSelectorMouseReportingEnabled = false;
|
|
667
|
+
this.#writeTerminalControl(HOOK_SELECTOR_MOUSE_REPORTING_DISABLE);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
#writeTerminalControl(sequence: string): void {
|
|
671
|
+
try {
|
|
672
|
+
this.ctx.ui.terminal.write(sequence);
|
|
673
|
+
} catch {
|
|
674
|
+
// Terminal teardown can race selector cleanup; normal shutdown restores modes.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
637
678
|
/**
|
|
638
679
|
* Hide the hook selector.
|
|
639
680
|
*/
|
|
640
681
|
hideHookSelector(): void {
|
|
682
|
+
this.#disableHookSelectorMouseReporting();
|
|
641
683
|
this.ctx.hookSelector?.dispose();
|
|
642
684
|
this.ctx.editorContainer.clear();
|
|
643
685
|
this.ctx.editorContainer.addChild(this.ctx.editor);
|