@jmoyers/harness 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 +21 -0
- package/README.md +145 -0
- package/native/ptyd/Cargo.lock +16 -0
- package/native/ptyd/Cargo.toml +7 -0
- package/native/ptyd/src/main.rs +257 -0
- package/package.json +90 -0
- package/scripts/build-ptyd.sh +73 -0
- package/scripts/control-plane-daemon.ts +277 -0
- package/scripts/cursor-hook-relay.ts +82 -0
- package/scripts/harness-animate.ts +469 -0
- package/scripts/harness-bin.js +77 -0
- package/scripts/harness-core.ts +1 -0
- package/scripts/harness-inspector.ts +439 -0
- package/scripts/harness.ts +2493 -0
- package/src/adapters/agent-session-state.ts +390 -0
- package/src/cli/gateway-record.ts +173 -0
- package/src/codex/live-session.ts +872 -0
- package/src/config/config-core.ts +1359 -0
- package/src/config/secrets-core.ts +170 -0
- package/src/control-plane/agent-realtime-api.ts +2441 -0
- package/src/control-plane/codex-session-stream.ts +392 -0
- package/src/control-plane/codex-telemetry.ts +1325 -0
- package/src/control-plane/lifecycle-hooks.ts +706 -0
- package/src/control-plane/session-summary.ts +380 -0
- package/src/control-plane/status/agent-status-reducer.ts +21 -0
- package/src/control-plane/status/reducer-base.ts +170 -0
- package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
- package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
- package/src/control-plane/status/session-status-engine.ts +76 -0
- package/src/control-plane/stream-client.ts +396 -0
- package/src/control-plane/stream-command-parser.ts +1673 -0
- package/src/control-plane/stream-protocol.ts +1808 -0
- package/src/control-plane/stream-server-background.ts +486 -0
- package/src/control-plane/stream-server-command.ts +2557 -0
- package/src/control-plane/stream-server-connection.ts +234 -0
- package/src/control-plane/stream-server-observed-filter.ts +112 -0
- package/src/control-plane/stream-server-session-runtime.ts +566 -0
- package/src/control-plane/stream-server-state-store.ts +15 -0
- package/src/control-plane/stream-server.ts +3192 -0
- package/src/cursor/managed-hooks.ts +282 -0
- package/src/domain/conversations.ts +414 -0
- package/src/domain/directories.ts +78 -0
- package/src/domain/repositories.ts +123 -0
- package/src/domain/tasks.ts +148 -0
- package/src/domain/workspace.ts +156 -0
- package/src/events/normalized-events.ts +124 -0
- package/src/mux/ansi-integrity.ts +103 -0
- package/src/mux/control-plane-op-queue.ts +212 -0
- package/src/mux/conversation-rail.ts +339 -0
- package/src/mux/double-click.ts +78 -0
- package/src/mux/dual-pane-core.ts +435 -0
- package/src/mux/harness-core-ui.ts +817 -0
- package/src/mux/input-shortcuts.ts +667 -0
- package/src/mux/live-mux/actions-conversation.ts +344 -0
- package/src/mux/live-mux/actions-repository.ts +246 -0
- package/src/mux/live-mux/actions-task.ts +115 -0
- package/src/mux/live-mux/args.ts +142 -0
- package/src/mux/live-mux/command-menu.ts +298 -0
- package/src/mux/live-mux/control-plane-records.ts +546 -0
- package/src/mux/live-mux/conversation-state.ts +188 -0
- package/src/mux/live-mux/directory-resolution.ts +34 -0
- package/src/mux/live-mux/event-mapping.ts +96 -0
- package/src/mux/live-mux/gateway-profiler.ts +152 -0
- package/src/mux/live-mux/gateway-render-trace.ts +177 -0
- package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
- package/src/mux/live-mux/git-parsing.ts +131 -0
- package/src/mux/live-mux/git-snapshot.ts +263 -0
- package/src/mux/live-mux/git-state.ts +136 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
- package/src/mux/live-mux/home-pane-actions.ts +58 -0
- package/src/mux/live-mux/home-pane-drop.ts +44 -0
- package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
- package/src/mux/live-mux/home-pane-pointer.ts +96 -0
- package/src/mux/live-mux/input-forwarding.ts +112 -0
- package/src/mux/live-mux/layout.ts +30 -0
- package/src/mux/live-mux/left-nav-activation.ts +103 -0
- package/src/mux/live-mux/left-nav.ts +85 -0
- package/src/mux/live-mux/left-rail-actions.ts +118 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
- package/src/mux/live-mux/left-rail-pointer.ts +74 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
- package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
- package/src/mux/live-mux/modal-input-reducers.ts +94 -0
- package/src/mux/live-mux/modal-overlays.ts +287 -0
- package/src/mux/live-mux/modal-pointer.ts +70 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
- package/src/mux/live-mux/observed-stream.ts +87 -0
- package/src/mux/live-mux/palette-parsing.ts +128 -0
- package/src/mux/live-mux/pointer-routing.ts +108 -0
- package/src/mux/live-mux/process-usage.ts +53 -0
- package/src/mux/live-mux/project-pane-pointer.ts +44 -0
- package/src/mux/live-mux/rail-layout.ts +244 -0
- package/src/mux/live-mux/render-trace-analysis.ts +213 -0
- package/src/mux/live-mux/render-trace-state.ts +84 -0
- package/src/mux/live-mux/repository-folding.ts +207 -0
- package/src/mux/live-mux/runtime-shutdown.ts +51 -0
- package/src/mux/live-mux/selection.ts +411 -0
- package/src/mux/live-mux/startup-utils.ts +187 -0
- package/src/mux/live-mux/status-timeline-state.ts +82 -0
- package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
- package/src/mux/live-mux/terminal-palette.ts +79 -0
- package/src/mux/new-thread-prompt.ts +165 -0
- package/src/mux/project-tree.ts +295 -0
- package/src/mux/render-frame.ts +113 -0
- package/src/mux/runtime-wiring.ts +185 -0
- package/src/mux/selector-index.ts +160 -0
- package/src/mux/startup-sequencer.ts +238 -0
- package/src/mux/task-composer.ts +289 -0
- package/src/mux/task-focused-pane.ts +417 -0
- package/src/mux/task-screen-keybindings.ts +539 -0
- package/src/mux/terminal-input-modes.ts +35 -0
- package/src/mux/workspace-path.ts +55 -0
- package/src/mux/workspace-rail-model.ts +701 -0
- package/src/mux/workspace-rail.ts +247 -0
- package/src/perf/perf-core.ts +307 -0
- package/src/pty/pty_host.ts +217 -0
- package/src/pty/session-broker.ts +158 -0
- package/src/recording/terminal-recording.ts +383 -0
- package/src/services/control-plane.ts +567 -0
- package/src/services/conversation-lifecycle.ts +176 -0
- package/src/services/conversation-startup-hydration.ts +47 -0
- package/src/services/directory-hydration.ts +49 -0
- package/src/services/event-persistence.ts +104 -0
- package/src/services/mux-ui-state-persistence.ts +82 -0
- package/src/services/output-load-sampler.ts +231 -0
- package/src/services/process-usage-refresh.ts +88 -0
- package/src/services/recording.ts +75 -0
- package/src/services/render-trace-recorder.ts +177 -0
- package/src/services/runtime-control-actions.ts +123 -0
- package/src/services/runtime-control-plane-ops.ts +131 -0
- package/src/services/runtime-conversation-actions.ts +113 -0
- package/src/services/runtime-conversation-activation.ts +78 -0
- package/src/services/runtime-conversation-starter.ts +171 -0
- package/src/services/runtime-conversation-title-edit.ts +149 -0
- package/src/services/runtime-directory-actions.ts +164 -0
- package/src/services/runtime-envelope-handler.ts +198 -0
- package/src/services/runtime-git-state.ts +92 -0
- package/src/services/runtime-input-pipeline.ts +50 -0
- package/src/services/runtime-input-router.ts +202 -0
- package/src/services/runtime-layout-resize.ts +236 -0
- package/src/services/runtime-left-rail-render.ts +159 -0
- package/src/services/runtime-main-pane-input.ts +230 -0
- package/src/services/runtime-modal-input.ts +119 -0
- package/src/services/runtime-navigation-input.ts +207 -0
- package/src/services/runtime-process-wiring.ts +68 -0
- package/src/services/runtime-rail-input.ts +287 -0
- package/src/services/runtime-render-flush.ts +146 -0
- package/src/services/runtime-render-lifecycle.ts +104 -0
- package/src/services/runtime-render-orchestrator.ts +108 -0
- package/src/services/runtime-render-pipeline.ts +167 -0
- package/src/services/runtime-render-state.ts +72 -0
- package/src/services/runtime-repository-actions.ts +197 -0
- package/src/services/runtime-right-pane-render.ts +132 -0
- package/src/services/runtime-shutdown.ts +79 -0
- package/src/services/runtime-stream-subscriptions.ts +56 -0
- package/src/services/runtime-task-composer-persistence.ts +139 -0
- package/src/services/runtime-task-editor-actions.ts +83 -0
- package/src/services/runtime-task-pane-actions.ts +198 -0
- package/src/services/runtime-task-pane-shortcuts.ts +189 -0
- package/src/services/runtime-task-pane.ts +62 -0
- package/src/services/runtime-workspace-actions.ts +153 -0
- package/src/services/runtime-workspace-observed-events.ts +190 -0
- package/src/services/session-projection-instrumentation.ts +190 -0
- package/src/services/startup-background-probe.ts +91 -0
- package/src/services/startup-background-resume.ts +65 -0
- package/src/services/startup-orchestrator.ts +166 -0
- package/src/services/startup-output-tracker.ts +54 -0
- package/src/services/startup-paint-tracker.ts +115 -0
- package/src/services/startup-persisted-conversation-queue.ts +45 -0
- package/src/services/startup-settled-gate.ts +67 -0
- package/src/services/startup-shutdown.ts +53 -0
- package/src/services/startup-span-tracker.ts +77 -0
- package/src/services/startup-state-hydration.ts +94 -0
- package/src/services/startup-visibility.ts +35 -0
- package/src/services/status-timeline-recorder.ts +144 -0
- package/src/services/task-pane-selection-actions.ts +153 -0
- package/src/services/task-planning-hydration.ts +58 -0
- package/src/services/task-planning-observed-events.ts +89 -0
- package/src/services/workspace-observed-events.ts +113 -0
- package/src/store/control-plane-store-normalize.ts +760 -0
- package/src/store/control-plane-store-types.ts +224 -0
- package/src/store/control-plane-store.ts +2951 -0
- package/src/store/event-store.ts +253 -0
- package/src/store/sqlite.ts +81 -0
- package/src/terminal/compat-matrix.ts +345 -0
- package/src/terminal/differential-checkpoints.ts +132 -0
- package/src/terminal/parity-suite.ts +441 -0
- package/src/terminal/snapshot-oracle.ts +1840 -0
- package/src/ui/conversation-input-forwarder.ts +114 -0
- package/src/ui/conversation-selection-input.ts +103 -0
- package/src/ui/debug-footer-notice.ts +39 -0
- package/src/ui/global-shortcut-input.ts +126 -0
- package/src/ui/input-preflight.ts +68 -0
- package/src/ui/input-token-router.ts +312 -0
- package/src/ui/input.ts +238 -0
- package/src/ui/kit.ts +509 -0
- package/src/ui/left-nav-input.ts +80 -0
- package/src/ui/left-rail-pointer-input.ts +148 -0
- package/src/ui/main-pane-pointer-input.ts +150 -0
- package/src/ui/modals/manager.ts +192 -0
- package/src/ui/mux-theme.ts +529 -0
- package/src/ui/panes/conversation.ts +19 -0
- package/src/ui/panes/home-gridfire.ts +302 -0
- package/src/ui/panes/home.ts +109 -0
- package/src/ui/panes/left-rail.ts +12 -0
- package/src/ui/panes/project.ts +44 -0
- package/src/ui/pointer-routing-input.ts +158 -0
- package/src/ui/repository-fold-input.ts +91 -0
- package/src/ui/screen.ts +210 -0
- package/src/ui/surface.ts +224 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
type ScanResult =
|
|
2
|
+
| {
|
|
3
|
+
readonly valid: true;
|
|
4
|
+
}
|
|
5
|
+
| {
|
|
6
|
+
readonly valid: false;
|
|
7
|
+
readonly reason: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function scanAnsiText(text: string): ScanResult {
|
|
11
|
+
let index = 0;
|
|
12
|
+
while (index < text.length) {
|
|
13
|
+
const code = text.codePointAt(index)!;
|
|
14
|
+
const char = String.fromCodePoint(code);
|
|
15
|
+
const width = code > 0xffff ? 2 : 1;
|
|
16
|
+
if (char !== '\u001b') {
|
|
17
|
+
index += width;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const next = text[index + 1];
|
|
22
|
+
if (next === undefined) {
|
|
23
|
+
return {
|
|
24
|
+
valid: false,
|
|
25
|
+
reason: 'dangling ESC at end of row',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (next === '[') {
|
|
30
|
+
let csiIndex = index + 2;
|
|
31
|
+
let foundFinal = false;
|
|
32
|
+
while (csiIndex < text.length) {
|
|
33
|
+
const csiCode = text.codePointAt(csiIndex)!;
|
|
34
|
+
if (csiCode >= 0x40 && csiCode <= 0x7e) {
|
|
35
|
+
foundFinal = true;
|
|
36
|
+
csiIndex += 1;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (csiCode < 0x20 || csiCode > 0x3f) {
|
|
40
|
+
return {
|
|
41
|
+
valid: false,
|
|
42
|
+
reason: `invalid CSI byte 0x${csiCode.toString(16)}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
csiIndex += 1;
|
|
46
|
+
}
|
|
47
|
+
if (!foundFinal) {
|
|
48
|
+
return {
|
|
49
|
+
valid: false,
|
|
50
|
+
reason: 'unterminated CSI sequence',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
index = csiIndex;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (next === ']') {
|
|
58
|
+
let oscIndex = index + 2;
|
|
59
|
+
let terminated = false;
|
|
60
|
+
while (oscIndex < text.length) {
|
|
61
|
+
const oscCode = text.codePointAt(oscIndex)!;
|
|
62
|
+
if (oscCode === 0x07) {
|
|
63
|
+
terminated = true;
|
|
64
|
+
oscIndex += 1;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (oscCode === 0x1b && text[oscIndex + 1] === '\\') {
|
|
68
|
+
terminated = true;
|
|
69
|
+
oscIndex += 2;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
oscIndex += oscCode > 0xffff ? 2 : 1;
|
|
73
|
+
}
|
|
74
|
+
if (!terminated) {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
reason: 'unterminated OSC sequence',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
index = oscIndex;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Two-byte escape sequence.
|
|
85
|
+
index += 2;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
valid: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function findAnsiIntegrityIssues(rows: readonly string[]): readonly string[] {
|
|
94
|
+
const issues: string[] = [];
|
|
95
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
96
|
+
const row = rows[rowIndex] ?? '';
|
|
97
|
+
const result = scanAnsiText(row);
|
|
98
|
+
if (!result.valid) {
|
|
99
|
+
issues.push(`row ${String(rowIndex + 1)}: ${result.reason}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return issues;
|
|
103
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export type ControlPlaneOpPriority = 'interactive' | 'background';
|
|
2
|
+
|
|
3
|
+
interface QueuedControlPlaneOp {
|
|
4
|
+
readonly id: number;
|
|
5
|
+
readonly priority: ControlPlaneOpPriority;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly enqueuedAtMs: number;
|
|
8
|
+
readonly task: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ControlPlaneOpQueueMetrics {
|
|
12
|
+
readonly interactiveQueued: number;
|
|
13
|
+
readonly backgroundQueued: number;
|
|
14
|
+
readonly running: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ControlPlaneOpQueueEvent {
|
|
18
|
+
readonly id: number;
|
|
19
|
+
readonly priority: ControlPlaneOpPriority;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
readonly enqueuedAtMs: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ControlPlaneOpQueueStartEvent extends ControlPlaneOpQueueEvent {
|
|
25
|
+
readonly waitMs: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ControlPlaneOpQueueOptions {
|
|
29
|
+
readonly nowMs?: () => number;
|
|
30
|
+
readonly schedule?: (callback: () => void) => void;
|
|
31
|
+
readonly onEnqueued?: (
|
|
32
|
+
event: ControlPlaneOpQueueEvent,
|
|
33
|
+
metrics: ControlPlaneOpQueueMetrics,
|
|
34
|
+
) => void;
|
|
35
|
+
readonly onStart?: (
|
|
36
|
+
event: ControlPlaneOpQueueStartEvent,
|
|
37
|
+
metrics: ControlPlaneOpQueueMetrics,
|
|
38
|
+
) => void;
|
|
39
|
+
readonly onSuccess?: (
|
|
40
|
+
event: ControlPlaneOpQueueStartEvent,
|
|
41
|
+
metrics: ControlPlaneOpQueueMetrics,
|
|
42
|
+
) => void;
|
|
43
|
+
readonly onError?: (
|
|
44
|
+
event: ControlPlaneOpQueueStartEvent,
|
|
45
|
+
metrics: ControlPlaneOpQueueMetrics,
|
|
46
|
+
error: unknown,
|
|
47
|
+
) => void;
|
|
48
|
+
readonly onFatal?: (error: unknown) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultSchedule(callback: () => void): void {
|
|
52
|
+
setImmediate(callback);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ControlPlaneOpQueue {
|
|
56
|
+
private readonly nowMs: () => number;
|
|
57
|
+
private readonly schedule: (callback: () => void) => void;
|
|
58
|
+
private readonly onEnqueued: ControlPlaneOpQueueOptions['onEnqueued'];
|
|
59
|
+
private readonly onStart: ControlPlaneOpQueueOptions['onStart'];
|
|
60
|
+
private readonly onSuccess: ControlPlaneOpQueueOptions['onSuccess'];
|
|
61
|
+
private readonly onError: ControlPlaneOpQueueOptions['onError'];
|
|
62
|
+
private readonly onFatal: ControlPlaneOpQueueOptions['onFatal'];
|
|
63
|
+
|
|
64
|
+
private readonly interactiveQueue: QueuedControlPlaneOp[] = [];
|
|
65
|
+
private readonly backgroundQueue: QueuedControlPlaneOp[] = [];
|
|
66
|
+
private readonly drainWaiters: Array<() => void> = [];
|
|
67
|
+
private nextId = 1;
|
|
68
|
+
private running = false;
|
|
69
|
+
private pumpScheduled = false;
|
|
70
|
+
|
|
71
|
+
constructor(options: ControlPlaneOpQueueOptions = {}) {
|
|
72
|
+
this.nowMs = options.nowMs ?? Date.now;
|
|
73
|
+
this.schedule = options.schedule ?? defaultSchedule;
|
|
74
|
+
this.onEnqueued = options.onEnqueued;
|
|
75
|
+
this.onStart = options.onStart;
|
|
76
|
+
this.onSuccess = options.onSuccess;
|
|
77
|
+
this.onError = options.onError;
|
|
78
|
+
this.onFatal = options.onFatal;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
enqueueInteractive(task: () => Promise<void>, label = 'interactive-op'): void {
|
|
82
|
+
this.enqueue(task, 'interactive', label);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
enqueueBackground(task: () => Promise<void>, label = 'background-op'): void {
|
|
86
|
+
this.enqueue(task, 'background', label);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async waitForDrain(): Promise<void> {
|
|
90
|
+
if (!this.running && this.interactiveQueue.length === 0 && this.backgroundQueue.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await new Promise<void>((resolve) => {
|
|
94
|
+
this.drainWaiters.push(resolve);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
metrics(): {
|
|
99
|
+
readonly interactiveQueued: number;
|
|
100
|
+
readonly backgroundQueued: number;
|
|
101
|
+
readonly running: boolean;
|
|
102
|
+
} {
|
|
103
|
+
return this.metricsSnapshot();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private enqueue(
|
|
107
|
+
task: () => Promise<void>,
|
|
108
|
+
priority: ControlPlaneOpPriority,
|
|
109
|
+
label: string,
|
|
110
|
+
): void {
|
|
111
|
+
const op: QueuedControlPlaneOp = {
|
|
112
|
+
id: this.nextId,
|
|
113
|
+
priority,
|
|
114
|
+
label,
|
|
115
|
+
enqueuedAtMs: this.nowMs(),
|
|
116
|
+
task,
|
|
117
|
+
};
|
|
118
|
+
this.nextId += 1;
|
|
119
|
+
|
|
120
|
+
if (priority === 'interactive') {
|
|
121
|
+
this.interactiveQueue.push(op);
|
|
122
|
+
} else {
|
|
123
|
+
this.backgroundQueue.push(op);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.onEnqueued?.(
|
|
127
|
+
{
|
|
128
|
+
id: op.id,
|
|
129
|
+
priority: op.priority,
|
|
130
|
+
label: op.label,
|
|
131
|
+
enqueuedAtMs: op.enqueuedAtMs,
|
|
132
|
+
},
|
|
133
|
+
this.metricsSnapshot(),
|
|
134
|
+
);
|
|
135
|
+
this.schedulePump();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private metricsSnapshot(): ControlPlaneOpQueueMetrics {
|
|
139
|
+
return {
|
|
140
|
+
interactiveQueued: this.interactiveQueue.length,
|
|
141
|
+
backgroundQueued: this.backgroundQueue.length,
|
|
142
|
+
running: this.running,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private resolveDrainIfIdle(): void {
|
|
147
|
+
if (this.running || this.interactiveQueue.length > 0 || this.backgroundQueue.length > 0) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
while (this.drainWaiters.length > 0) {
|
|
151
|
+
const resolve = this.drainWaiters.shift();
|
|
152
|
+
resolve?.();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private pickNext(): QueuedControlPlaneOp | null {
|
|
157
|
+
const interactive = this.interactiveQueue.shift();
|
|
158
|
+
if (interactive !== undefined) {
|
|
159
|
+
return interactive;
|
|
160
|
+
}
|
|
161
|
+
const background = this.backgroundQueue.shift();
|
|
162
|
+
return background ?? null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private schedulePump(): void {
|
|
166
|
+
if (this.pumpScheduled) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.pumpScheduled = true;
|
|
170
|
+
this.schedule(() => {
|
|
171
|
+
this.pumpScheduled = false;
|
|
172
|
+
void this.runQueue().catch((error: unknown) => {
|
|
173
|
+
this.onFatal?.(error);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async runQueue(): Promise<void> {
|
|
179
|
+
if (this.running) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const next = this.pickNext();
|
|
183
|
+
if (next === null) {
|
|
184
|
+
this.resolveDrainIfIdle();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.running = true;
|
|
189
|
+
const waitMs = Math.max(0, this.nowMs() - next.enqueuedAtMs);
|
|
190
|
+
const startEvent: ControlPlaneOpQueueStartEvent = {
|
|
191
|
+
id: next.id,
|
|
192
|
+
priority: next.priority,
|
|
193
|
+
label: next.label,
|
|
194
|
+
enqueuedAtMs: next.enqueuedAtMs,
|
|
195
|
+
waitMs,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
this.onStart?.(startEvent, this.metricsSnapshot());
|
|
200
|
+
try {
|
|
201
|
+
await next.task();
|
|
202
|
+
this.onSuccess?.(startEvent, this.metricsSnapshot());
|
|
203
|
+
} catch (error: unknown) {
|
|
204
|
+
this.onError?.(startEvent, this.metricsSnapshot(), error);
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
this.running = false;
|
|
208
|
+
this.resolveDrainIfIdle();
|
|
209
|
+
this.schedulePump();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StreamSessionListSort,
|
|
3
|
+
StreamSessionRuntimeStatus,
|
|
4
|
+
StreamSessionStatusModel,
|
|
5
|
+
} from '../control-plane/stream-protocol.ts';
|
|
6
|
+
import { padOrTrimDisplay } from './dual-pane-core.ts';
|
|
7
|
+
import {
|
|
8
|
+
createUiSurface,
|
|
9
|
+
DEFAULT_UI_STYLE,
|
|
10
|
+
drawUiText,
|
|
11
|
+
fillUiRow,
|
|
12
|
+
renderUiSurfaceAnsiRows,
|
|
13
|
+
} from '../ui/surface.ts';
|
|
14
|
+
import { paintUiRow } from '../ui/kit.ts';
|
|
15
|
+
|
|
16
|
+
export interface ConversationRailSessionSummary {
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly status: StreamSessionRuntimeStatus;
|
|
19
|
+
readonly statusModel: StreamSessionStatusModel | null;
|
|
20
|
+
readonly attentionReason: string | null;
|
|
21
|
+
readonly live: boolean;
|
|
22
|
+
readonly startedAt: string;
|
|
23
|
+
readonly lastEventAt: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ConversationRailOrder = StreamSessionListSort | 'input-order';
|
|
27
|
+
|
|
28
|
+
interface ConversationRailRenderRow {
|
|
29
|
+
readonly kind: 'header' | 'session' | 'empty';
|
|
30
|
+
readonly text: string;
|
|
31
|
+
readonly session?: ConversationRailSessionSummary;
|
|
32
|
+
readonly active?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function compareIsoDesc(left: string | null, right: string | null): number {
|
|
36
|
+
if (left === right) {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
if (left === null) {
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
if (right === null) {
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
45
|
+
return right.localeCompare(left);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const STATUS_PRIORITY: Record<StreamSessionRuntimeStatus, number> = {
|
|
49
|
+
'needs-input': 0,
|
|
50
|
+
running: 1,
|
|
51
|
+
completed: 2,
|
|
52
|
+
exited: 3,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function statusPriority(status: StreamSessionRuntimeStatus): number {
|
|
56
|
+
return STATUS_PRIORITY[status];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function sortConversationRailSessions(
|
|
60
|
+
sessions: readonly ConversationRailSessionSummary[],
|
|
61
|
+
sort: StreamSessionListSort = 'attention-first',
|
|
62
|
+
): readonly ConversationRailSessionSummary[] {
|
|
63
|
+
const sorted = [...sessions];
|
|
64
|
+
if (sort === 'started-asc') {
|
|
65
|
+
sorted.sort(
|
|
66
|
+
(left, right) =>
|
|
67
|
+
left.startedAt.localeCompare(right.startedAt) ||
|
|
68
|
+
left.sessionId.localeCompare(right.sessionId),
|
|
69
|
+
);
|
|
70
|
+
return sorted;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (sort === 'started-desc') {
|
|
74
|
+
sorted.sort(
|
|
75
|
+
(left, right) =>
|
|
76
|
+
right.startedAt.localeCompare(left.startedAt) ||
|
|
77
|
+
left.sessionId.localeCompare(right.sessionId),
|
|
78
|
+
);
|
|
79
|
+
return sorted;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
sorted.sort(
|
|
83
|
+
(left, right) =>
|
|
84
|
+
statusPriority(left.status) - statusPriority(right.status) ||
|
|
85
|
+
compareIsoDesc(left.lastEventAt, right.lastEventAt) ||
|
|
86
|
+
right.startedAt.localeCompare(left.startedAt) ||
|
|
87
|
+
left.sessionId.localeCompare(right.sessionId),
|
|
88
|
+
);
|
|
89
|
+
return sorted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compactSessionId(sessionId: string): string {
|
|
93
|
+
if (sessionId.startsWith('conversation-')) {
|
|
94
|
+
const suffix = sessionId.slice('conversation-'.length);
|
|
95
|
+
if (suffix.length > 8) {
|
|
96
|
+
return suffix.slice(0, 8);
|
|
97
|
+
}
|
|
98
|
+
return suffix;
|
|
99
|
+
}
|
|
100
|
+
if (sessionId.length > 16) {
|
|
101
|
+
return `${sessionId.slice(0, 16)}…`;
|
|
102
|
+
}
|
|
103
|
+
return sessionId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function statusToken(status: StreamSessionRuntimeStatus): string {
|
|
107
|
+
if (status === 'needs-input') {
|
|
108
|
+
return '!';
|
|
109
|
+
}
|
|
110
|
+
if (status === 'running') {
|
|
111
|
+
return '~';
|
|
112
|
+
}
|
|
113
|
+
if (status === 'completed') {
|
|
114
|
+
return '+';
|
|
115
|
+
}
|
|
116
|
+
return 'x';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderConversationLine(
|
|
120
|
+
session: ConversationRailSessionSummary,
|
|
121
|
+
activeSessionId: string | null,
|
|
122
|
+
): string {
|
|
123
|
+
const activePrefix = session.sessionId === activeSessionId ? '>' : ' ';
|
|
124
|
+
const token = statusToken(session.status);
|
|
125
|
+
const shortId = compactSessionId(session.sessionId);
|
|
126
|
+
const liveState = session.live ? '' : ' (dead)';
|
|
127
|
+
const base = `${activePrefix} [${token}] ${shortId}${liveState}`;
|
|
128
|
+
if (session.attentionReason !== null && session.attentionReason.length > 0) {
|
|
129
|
+
return `${base} - ${session.attentionReason}`;
|
|
130
|
+
}
|
|
131
|
+
return base;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildConversationRailRows(
|
|
135
|
+
sessions: readonly ConversationRailSessionSummary[],
|
|
136
|
+
activeSessionId: string | null,
|
|
137
|
+
maxRows: number,
|
|
138
|
+
order: ConversationRailOrder,
|
|
139
|
+
): readonly ConversationRailRenderRow[] {
|
|
140
|
+
const safeMaxRows = Math.max(1, maxRows);
|
|
141
|
+
const sorted =
|
|
142
|
+
order === 'input-order' ? [...sessions] : sortConversationRailSessions(sessions, order);
|
|
143
|
+
const headerText = `conversations (${String(sorted.length)}) [ctrl-t new] [ctrl-n/p switch]`;
|
|
144
|
+
const rows: ConversationRailRenderRow[] = [
|
|
145
|
+
{
|
|
146
|
+
kind: 'header',
|
|
147
|
+
text: headerText,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
if (safeMaxRows === 1) {
|
|
152
|
+
return rows;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const maxConversationRows = safeMaxRows - 1;
|
|
156
|
+
const visible: ConversationRailSessionSummary[] = sorted.slice(0, maxConversationRows);
|
|
157
|
+
if (
|
|
158
|
+
activeSessionId !== null &&
|
|
159
|
+
!visible.some((session) => session.sessionId === activeSessionId)
|
|
160
|
+
) {
|
|
161
|
+
const active = sorted.find((session) => session.sessionId === activeSessionId);
|
|
162
|
+
if (active !== undefined) {
|
|
163
|
+
visible[visible.length - 1] = active;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const session of visible) {
|
|
168
|
+
rows.push({
|
|
169
|
+
kind: 'session',
|
|
170
|
+
text: renderConversationLine(session, activeSessionId),
|
|
171
|
+
session,
|
|
172
|
+
active: session.sessionId === activeSessionId,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
while (rows.length < safeMaxRows) {
|
|
177
|
+
rows.push({
|
|
178
|
+
kind: 'empty',
|
|
179
|
+
text: '',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return rows;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function buildConversationRailLines(
|
|
186
|
+
sessions: readonly ConversationRailSessionSummary[],
|
|
187
|
+
activeSessionId: string | null,
|
|
188
|
+
width: number,
|
|
189
|
+
maxRows: number,
|
|
190
|
+
order: ConversationRailOrder = 'attention-first',
|
|
191
|
+
): readonly string[] {
|
|
192
|
+
const safeWidth = Math.max(1, width);
|
|
193
|
+
const rows = buildConversationRailRows(sessions, activeSessionId, maxRows, order);
|
|
194
|
+
return rows.map((row) => padOrTrimDisplay(row.text, safeWidth));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const HEADER_ROW_STYLE = {
|
|
198
|
+
fg: { kind: 'indexed', index: 250 } as const,
|
|
199
|
+
bg: { kind: 'indexed', index: 236 } as const,
|
|
200
|
+
bold: false,
|
|
201
|
+
};
|
|
202
|
+
const NORMAL_ROW_STYLE = DEFAULT_UI_STYLE;
|
|
203
|
+
const ACTIVE_ROW_STYLE = {
|
|
204
|
+
fg: { kind: 'indexed', index: 255 } as const,
|
|
205
|
+
bg: { kind: 'indexed', index: 238 } as const,
|
|
206
|
+
bold: false,
|
|
207
|
+
};
|
|
208
|
+
const ACTIVE_INDICATOR_STYLE = {
|
|
209
|
+
fg: { kind: 'indexed', index: 231 } as const,
|
|
210
|
+
bg: { kind: 'indexed', index: 238 } as const,
|
|
211
|
+
bold: true,
|
|
212
|
+
};
|
|
213
|
+
const BADGE_STYLES = {
|
|
214
|
+
'needs-input': {
|
|
215
|
+
fg: { kind: 'indexed', index: 231 } as const,
|
|
216
|
+
bg: { kind: 'indexed', index: 166 } as const,
|
|
217
|
+
bold: true,
|
|
218
|
+
},
|
|
219
|
+
running: {
|
|
220
|
+
fg: { kind: 'indexed', index: 231 } as const,
|
|
221
|
+
bg: { kind: 'indexed', index: 24 } as const,
|
|
222
|
+
bold: true,
|
|
223
|
+
},
|
|
224
|
+
completed: {
|
|
225
|
+
fg: { kind: 'indexed', index: 231 } as const,
|
|
226
|
+
bg: { kind: 'indexed', index: 28 } as const,
|
|
227
|
+
bold: true,
|
|
228
|
+
},
|
|
229
|
+
exited: {
|
|
230
|
+
fg: { kind: 'indexed', index: 250 } as const,
|
|
231
|
+
bg: { kind: 'indexed', index: 239 } as const,
|
|
232
|
+
bold: true,
|
|
233
|
+
},
|
|
234
|
+
} as const;
|
|
235
|
+
const ACTIVE_TEXT_STYLE = {
|
|
236
|
+
fg: { kind: 'indexed', index: 255 } as const,
|
|
237
|
+
bg: { kind: 'indexed', index: 238 } as const,
|
|
238
|
+
bold: false,
|
|
239
|
+
};
|
|
240
|
+
const NORMAL_TEXT_STYLE = DEFAULT_UI_STYLE;
|
|
241
|
+
const MUTED_TEXT_STYLE = {
|
|
242
|
+
fg: { kind: 'indexed', index: 245 } as const,
|
|
243
|
+
bg: { kind: 'default' } as const,
|
|
244
|
+
bold: false,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
function badgeLabel(status: StreamSessionRuntimeStatus): string {
|
|
248
|
+
if (status === 'needs-input') {
|
|
249
|
+
return 'NEED';
|
|
250
|
+
}
|
|
251
|
+
if (status === 'running') {
|
|
252
|
+
return 'RUN ';
|
|
253
|
+
}
|
|
254
|
+
if (status === 'completed') {
|
|
255
|
+
return 'DONE';
|
|
256
|
+
}
|
|
257
|
+
return 'EXIT';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function rowBodyText(session: ConversationRailSessionSummary): string {
|
|
261
|
+
const idText = compactSessionId(session.sessionId);
|
|
262
|
+
const deadToken = session.live ? '' : ' (dead)';
|
|
263
|
+
const attentionText =
|
|
264
|
+
session.attentionReason !== null && session.attentionReason.length > 0
|
|
265
|
+
? ` - ${session.attentionReason}`
|
|
266
|
+
: '';
|
|
267
|
+
return `${idText}${deadToken}${attentionText}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function renderConversationRailAnsiRows(
|
|
271
|
+
sessions: readonly ConversationRailSessionSummary[],
|
|
272
|
+
activeSessionId: string | null,
|
|
273
|
+
width: number,
|
|
274
|
+
maxRows: number,
|
|
275
|
+
order: ConversationRailOrder = 'attention-first',
|
|
276
|
+
): readonly string[] {
|
|
277
|
+
const safeWidth = Math.max(1, width);
|
|
278
|
+
const rows = buildConversationRailRows(sessions, activeSessionId, maxRows, order);
|
|
279
|
+
const surface = createUiSurface(safeWidth, rows.length, DEFAULT_UI_STYLE);
|
|
280
|
+
|
|
281
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
282
|
+
const row = rows[rowIndex]!;
|
|
283
|
+
if (row.kind === 'header') {
|
|
284
|
+
paintUiRow(surface, rowIndex, row.text, HEADER_ROW_STYLE, HEADER_ROW_STYLE, 1);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (row.kind === 'empty') {
|
|
289
|
+
paintUiRow(surface, rowIndex, '', NORMAL_ROW_STYLE, NORMAL_ROW_STYLE);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const session = row.session!;
|
|
294
|
+
const active = row.active === true;
|
|
295
|
+
fillUiRow(surface, rowIndex, active ? ACTIVE_ROW_STYLE : NORMAL_ROW_STYLE);
|
|
296
|
+
drawUiText(
|
|
297
|
+
surface,
|
|
298
|
+
0,
|
|
299
|
+
rowIndex,
|
|
300
|
+
active ? '>' : ' ',
|
|
301
|
+
active ? ACTIVE_INDICATOR_STYLE : NORMAL_TEXT_STYLE,
|
|
302
|
+
);
|
|
303
|
+
drawUiText(surface, 2, rowIndex, badgeLabel(session.status), BADGE_STYLES[session.status]);
|
|
304
|
+
drawUiText(
|
|
305
|
+
surface,
|
|
306
|
+
7,
|
|
307
|
+
rowIndex,
|
|
308
|
+
rowBodyText(session),
|
|
309
|
+
active ? ACTIVE_TEXT_STYLE : NORMAL_TEXT_STYLE,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (!session.live && session.attentionReason === null) {
|
|
313
|
+
drawUiText(surface, 7, rowIndex, rowBodyText(session), MUTED_TEXT_STYLE);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return renderUiSurfaceAnsiRows(surface);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function cycleConversationId(
|
|
321
|
+
sessionIds: readonly string[],
|
|
322
|
+
activeSessionId: string | null,
|
|
323
|
+
direction: 'next' | 'previous',
|
|
324
|
+
): string | null {
|
|
325
|
+
if (sessionIds.length === 0) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (activeSessionId === null) {
|
|
329
|
+
return sessionIds[0]!;
|
|
330
|
+
}
|
|
331
|
+
const index = sessionIds.indexOf(activeSessionId);
|
|
332
|
+
if (index < 0) {
|
|
333
|
+
return sessionIds[0]!;
|
|
334
|
+
}
|
|
335
|
+
if (direction === 'next') {
|
|
336
|
+
return sessionIds[(index + 1) % sessionIds.length]!;
|
|
337
|
+
}
|
|
338
|
+
return sessionIds[(index - 1 + sessionIds.length) % sessionIds.length]!;
|
|
339
|
+
}
|