@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,185 @@
|
|
|
1
|
+
import type { ControlPlaneKeyEvent } from '../control-plane/codex-session-stream.ts';
|
|
2
|
+
import type {
|
|
3
|
+
StreamSessionController,
|
|
4
|
+
StreamSessionRuntimeStatus,
|
|
5
|
+
StreamSessionStatusModel,
|
|
6
|
+
} from '../control-plane/stream-protocol.ts';
|
|
7
|
+
|
|
8
|
+
export interface MuxRuntimeConversationState {
|
|
9
|
+
directoryId: string | null;
|
|
10
|
+
status: StreamSessionRuntimeStatus;
|
|
11
|
+
statusModel: StreamSessionStatusModel | null;
|
|
12
|
+
attentionReason: string | null;
|
|
13
|
+
live: boolean;
|
|
14
|
+
controller: StreamSessionController | null;
|
|
15
|
+
lastEventAt: string | null;
|
|
16
|
+
lastKnownWork: string | null;
|
|
17
|
+
lastKnownWorkAt: string | null;
|
|
18
|
+
lastTelemetrySource: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface EnsureConversationSeed {
|
|
22
|
+
directoryId?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ApplyMuxControlPlaneKeyEventOptions<TConversation extends MuxRuntimeConversationState> {
|
|
26
|
+
readonly removedConversationIds: ReadonlySet<string>;
|
|
27
|
+
ensureConversation: (sessionId: string, seed?: EnsureConversationSeed) => TConversation;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface MuxTelemetrySummaryInput {
|
|
31
|
+
readonly source: string;
|
|
32
|
+
readonly eventName: string | null;
|
|
33
|
+
readonly summary: string | null;
|
|
34
|
+
readonly observedAt: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ProjectedTelemetrySummary {
|
|
38
|
+
readonly text: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseIsoMs(value: string | null): number {
|
|
42
|
+
if (value === null) {
|
|
43
|
+
return Number.NaN;
|
|
44
|
+
}
|
|
45
|
+
return Date.parse(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeEventName(value: string | null): string {
|
|
49
|
+
return (value ?? '').trim().toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeSummary(value: string | null): string {
|
|
53
|
+
return (value ?? '').trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function projectTelemetrySummary(
|
|
57
|
+
telemetry: Omit<MuxTelemetrySummaryInput, 'observedAt'>,
|
|
58
|
+
): ProjectedTelemetrySummary {
|
|
59
|
+
const eventName = normalizeEventName(telemetry.eventName);
|
|
60
|
+
const summary = normalizeSummary(telemetry.summary);
|
|
61
|
+
const summaryLower = summary.toLowerCase();
|
|
62
|
+
if (telemetry.source === 'otlp-metric') {
|
|
63
|
+
if (eventName === 'codex.turn.e2e_duration_ms') {
|
|
64
|
+
return {
|
|
65
|
+
text: 'inactive',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
text: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (telemetry.source === 'otlp-log' && eventName === 'codex.sse_event') {
|
|
73
|
+
if (
|
|
74
|
+
summaryLower.includes('response.created') ||
|
|
75
|
+
summaryLower.includes('response.in_progress') ||
|
|
76
|
+
summaryLower.includes('response.output_text.delta') ||
|
|
77
|
+
summaryLower.includes('response.output_item.added') ||
|
|
78
|
+
summaryLower.includes('response.function_call_arguments.delta')
|
|
79
|
+
) {
|
|
80
|
+
return {
|
|
81
|
+
text: 'active',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (eventName === 'codex.user_prompt') {
|
|
86
|
+
return {
|
|
87
|
+
text: 'active',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (eventName === 'claude.userpromptsubmit' || eventName === 'claude.pretooluse') {
|
|
91
|
+
return {
|
|
92
|
+
text: 'active',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
eventName === 'claude.stop' ||
|
|
97
|
+
eventName === 'claude.subagentstop' ||
|
|
98
|
+
eventName === 'claude.sessionend'
|
|
99
|
+
) {
|
|
100
|
+
return {
|
|
101
|
+
text: 'inactive',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
eventName === 'cursor.beforesubmitprompt' ||
|
|
106
|
+
eventName === 'cursor.beforeshellexecution' ||
|
|
107
|
+
eventName === 'cursor.beforemcptool'
|
|
108
|
+
) {
|
|
109
|
+
return {
|
|
110
|
+
text: 'active',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (eventName === 'cursor.stop' || eventName === 'cursor.sessionend') {
|
|
114
|
+
return {
|
|
115
|
+
text: 'inactive',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
text: null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function telemetrySummaryText(
|
|
124
|
+
summary: Omit<MuxTelemetrySummaryInput, 'observedAt'>,
|
|
125
|
+
): string | null {
|
|
126
|
+
const projected = projectTelemetrySummary(summary);
|
|
127
|
+
return projected.text;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function applyTelemetrySummaryToConversation<
|
|
131
|
+
TConversation extends MuxRuntimeConversationState,
|
|
132
|
+
>(target: TConversation, telemetry: MuxTelemetrySummaryInput | null): void {
|
|
133
|
+
if (telemetry === null) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const observedAtMs = parseIsoMs(telemetry.observedAt);
|
|
137
|
+
const currentAtMs = parseIsoMs(target.lastKnownWorkAt);
|
|
138
|
+
if (Number.isFinite(currentAtMs) && Number.isFinite(observedAtMs) && observedAtMs < currentAtMs) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const projected = projectTelemetrySummary(telemetry);
|
|
142
|
+
if (projected.text !== null) {
|
|
143
|
+
target.lastKnownWork = projected.text;
|
|
144
|
+
target.lastKnownWorkAt = telemetry.observedAt;
|
|
145
|
+
target.lastTelemetrySource = telemetry.source;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function applyMuxControlPlaneKeyEvent<TConversation extends MuxRuntimeConversationState>(
|
|
150
|
+
event: ControlPlaneKeyEvent,
|
|
151
|
+
options: ApplyMuxControlPlaneKeyEventOptions<TConversation>,
|
|
152
|
+
): TConversation | null {
|
|
153
|
+
if (options.removedConversationIds.has(event.sessionId)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const conversation = options.ensureConversation(event.sessionId, {
|
|
157
|
+
directoryId: event.directoryId,
|
|
158
|
+
});
|
|
159
|
+
if (event.directoryId !== null) {
|
|
160
|
+
conversation.directoryId = event.directoryId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (event.type === 'session-status') {
|
|
164
|
+
conversation.status = event.status;
|
|
165
|
+
conversation.statusModel = event.statusModel;
|
|
166
|
+
conversation.attentionReason = event.attentionReason;
|
|
167
|
+
conversation.live = event.live;
|
|
168
|
+
conversation.controller = event.controller;
|
|
169
|
+
conversation.lastEventAt = event.ts;
|
|
170
|
+
conversation.lastKnownWork = event.statusModel?.lastKnownWork ?? null;
|
|
171
|
+
conversation.lastKnownWorkAt = event.statusModel?.lastKnownWorkAt ?? null;
|
|
172
|
+
conversation.lastTelemetrySource = event.telemetry?.source ?? null;
|
|
173
|
+
return conversation;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (event.type === 'session-control') {
|
|
177
|
+
conversation.controller = event.controller;
|
|
178
|
+
conversation.lastEventAt = event.ts;
|
|
179
|
+
return conversation;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
conversation.lastEventAt = event.keyEvent.observedAt;
|
|
183
|
+
conversation.lastTelemetrySource = event.keyEvent.source;
|
|
184
|
+
return conversation;
|
|
185
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
interface SelectorIndexDirectory {
|
|
2
|
+
readonly directoryId: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface SelectorIndexConversation {
|
|
6
|
+
readonly sessionId: string;
|
|
7
|
+
readonly directoryId: string | null;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly agentType: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SelectorIndexEntry {
|
|
13
|
+
readonly selectorIndex: number;
|
|
14
|
+
readonly directoryIndex: number;
|
|
15
|
+
readonly directoryId: string;
|
|
16
|
+
readonly sessionId: string;
|
|
17
|
+
readonly title: string;
|
|
18
|
+
readonly agentType: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type LeftNavSelectorEntry =
|
|
22
|
+
| {
|
|
23
|
+
readonly selectorIndex: number;
|
|
24
|
+
readonly key: 'home';
|
|
25
|
+
readonly kind: 'home';
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
readonly selectorIndex: number;
|
|
29
|
+
readonly key: `directory:${string}`;
|
|
30
|
+
readonly kind: 'directory';
|
|
31
|
+
readonly directoryId: string;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
readonly selectorIndex: number;
|
|
35
|
+
readonly key: `conversation:${string}`;
|
|
36
|
+
readonly kind: 'conversation';
|
|
37
|
+
readonly directoryId: string;
|
|
38
|
+
readonly sessionId: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function normalizedDirectoryId(directoryId: string | null): string {
|
|
42
|
+
if (directoryId === null) {
|
|
43
|
+
return 'directory-missing';
|
|
44
|
+
}
|
|
45
|
+
const trimmed = directoryId.trim();
|
|
46
|
+
return trimmed.length === 0 ? 'directory-missing' : trimmed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function orderedDirectoryIds(
|
|
50
|
+
directories: ReadonlyMap<string, SelectorIndexDirectory>,
|
|
51
|
+
conversationById: ReadonlyMap<string, SelectorIndexConversation>,
|
|
52
|
+
orderedSessionIds: readonly string[],
|
|
53
|
+
): readonly string[] {
|
|
54
|
+
const orderedDirectoryIds: string[] = [...directories.keys()];
|
|
55
|
+
const seenDirectoryIds = new Set(orderedDirectoryIds);
|
|
56
|
+
|
|
57
|
+
for (const sessionId of orderedSessionIds) {
|
|
58
|
+
const conversation = conversationById.get(sessionId);
|
|
59
|
+
if (conversation === undefined) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const directoryId = normalizedDirectoryId(conversation.directoryId);
|
|
63
|
+
if (seenDirectoryIds.has(directoryId)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
seenDirectoryIds.add(directoryId);
|
|
67
|
+
orderedDirectoryIds.push(directoryId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return orderedDirectoryIds;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildSelectorIndexEntries(
|
|
74
|
+
directories: ReadonlyMap<string, SelectorIndexDirectory>,
|
|
75
|
+
conversationById: ReadonlyMap<string, SelectorIndexConversation>,
|
|
76
|
+
orderedSessionIds: readonly string[],
|
|
77
|
+
): readonly SelectorIndexEntry[] {
|
|
78
|
+
const entries: SelectorIndexEntry[] = [];
|
|
79
|
+
let selectorIndex = 1;
|
|
80
|
+
for (const directoryId of orderedDirectoryIds(directories, conversationById, orderedSessionIds)) {
|
|
81
|
+
let directoryIndex = 0;
|
|
82
|
+
for (const sessionId of orderedSessionIds) {
|
|
83
|
+
const conversation = conversationById.get(sessionId);
|
|
84
|
+
if (conversation === undefined) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (normalizedDirectoryId(conversation.directoryId) !== directoryId) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
directoryIndex += 1;
|
|
91
|
+
entries.push({
|
|
92
|
+
selectorIndex,
|
|
93
|
+
directoryIndex,
|
|
94
|
+
directoryId,
|
|
95
|
+
sessionId: conversation.sessionId,
|
|
96
|
+
title: conversation.title,
|
|
97
|
+
agentType: conversation.agentType,
|
|
98
|
+
});
|
|
99
|
+
selectorIndex += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return entries;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function visualConversationOrder(
|
|
106
|
+
directories: ReadonlyMap<string, SelectorIndexDirectory>,
|
|
107
|
+
conversationById: ReadonlyMap<string, SelectorIndexConversation>,
|
|
108
|
+
orderedSessionIds: readonly string[],
|
|
109
|
+
): readonly string[] {
|
|
110
|
+
return buildSelectorIndexEntries(directories, conversationById, orderedSessionIds).map(
|
|
111
|
+
(entry) => entry.sessionId,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildLeftNavSelectorEntries(
|
|
116
|
+
directories: ReadonlyMap<string, SelectorIndexDirectory>,
|
|
117
|
+
conversationById: ReadonlyMap<string, SelectorIndexConversation>,
|
|
118
|
+
orderedSessionIds: readonly string[],
|
|
119
|
+
options: {
|
|
120
|
+
readonly includeHome?: boolean;
|
|
121
|
+
} = {},
|
|
122
|
+
): readonly LeftNavSelectorEntry[] {
|
|
123
|
+
const entries: LeftNavSelectorEntry[] = [];
|
|
124
|
+
let selectorIndex = 1;
|
|
125
|
+
if (options.includeHome ?? false) {
|
|
126
|
+
entries.push({
|
|
127
|
+
selectorIndex,
|
|
128
|
+
key: 'home',
|
|
129
|
+
kind: 'home',
|
|
130
|
+
});
|
|
131
|
+
selectorIndex += 1;
|
|
132
|
+
}
|
|
133
|
+
for (const directoryId of orderedDirectoryIds(directories, conversationById, orderedSessionIds)) {
|
|
134
|
+
entries.push({
|
|
135
|
+
selectorIndex,
|
|
136
|
+
key: `directory:${directoryId}`,
|
|
137
|
+
kind: 'directory',
|
|
138
|
+
directoryId,
|
|
139
|
+
});
|
|
140
|
+
selectorIndex += 1;
|
|
141
|
+
for (const sessionId of orderedSessionIds) {
|
|
142
|
+
const conversation = conversationById.get(sessionId);
|
|
143
|
+
if (conversation === undefined) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (normalizedDirectoryId(conversation.directoryId) !== directoryId) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
entries.push({
|
|
150
|
+
selectorIndex,
|
|
151
|
+
key: `conversation:${conversation.sessionId}`,
|
|
152
|
+
kind: 'conversation',
|
|
153
|
+
directoryId,
|
|
154
|
+
sessionId: conversation.sessionId,
|
|
155
|
+
});
|
|
156
|
+
selectorIndex += 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return entries;
|
|
160
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
type StartupSettleGate = 'header' | 'nonempty';
|
|
2
|
+
|
|
3
|
+
type StartupSequencerPhase =
|
|
4
|
+
| 'inactive'
|
|
5
|
+
| 'waiting-for-output'
|
|
6
|
+
| 'waiting-for-paint'
|
|
7
|
+
| 'waiting-for-header'
|
|
8
|
+
| 'settling'
|
|
9
|
+
| 'settled';
|
|
10
|
+
|
|
11
|
+
interface StartupSequencerOptions {
|
|
12
|
+
readonly quietMs: number;
|
|
13
|
+
readonly nonemptyFallbackMs: number;
|
|
14
|
+
readonly nowMs?: () => number;
|
|
15
|
+
readonly setTimer?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
|
16
|
+
readonly clearTimer?: (handle: ReturnType<typeof setTimeout>) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface StartupSequencerSnapshot {
|
|
20
|
+
readonly targetSessionId: string | null;
|
|
21
|
+
readonly phase: StartupSequencerPhase;
|
|
22
|
+
readonly firstOutputObserved: boolean;
|
|
23
|
+
readonly firstOutputAtMs: number | null;
|
|
24
|
+
readonly firstPaintObserved: boolean;
|
|
25
|
+
readonly headerObserved: boolean;
|
|
26
|
+
readonly settleGate: StartupSettleGate | null;
|
|
27
|
+
readonly settledObserved: boolean;
|
|
28
|
+
readonly settledSignaled: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface StartupSettledEvent {
|
|
32
|
+
readonly sessionId: string;
|
|
33
|
+
readonly gate: StartupSettleGate;
|
|
34
|
+
readonly quietMs: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function defaultSetTimer(callback: () => void, delayMs: number): ReturnType<typeof setTimeout> {
|
|
38
|
+
return setTimeout(callback, delayMs);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultClearTimer(handle: ReturnType<typeof setTimeout>): void {
|
|
42
|
+
clearTimeout(handle);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class StartupSequencer {
|
|
46
|
+
private readonly quietMs: number;
|
|
47
|
+
private readonly nonemptyFallbackMs: number;
|
|
48
|
+
private readonly nowMs: () => number;
|
|
49
|
+
private readonly setTimer: (
|
|
50
|
+
callback: () => void,
|
|
51
|
+
delayMs: number,
|
|
52
|
+
) => ReturnType<typeof setTimeout>;
|
|
53
|
+
private readonly clearTimer: (handle: ReturnType<typeof setTimeout>) => void;
|
|
54
|
+
|
|
55
|
+
private targetSessionId: string | null = null;
|
|
56
|
+
private phase: StartupSequencerPhase = 'inactive';
|
|
57
|
+
private firstOutputObserved = false;
|
|
58
|
+
private firstOutputAtMs: number | null = null;
|
|
59
|
+
private firstPaintObserved = false;
|
|
60
|
+
private headerObserved = false;
|
|
61
|
+
private settleGate: StartupSettleGate | null = null;
|
|
62
|
+
private settledObserved = false;
|
|
63
|
+
private settledSignaled = false;
|
|
64
|
+
private settledTimer: ReturnType<typeof setTimeout> | null = null;
|
|
65
|
+
private settledWait: Promise<void>;
|
|
66
|
+
private resolveSettledWait: (() => void) | null = null;
|
|
67
|
+
|
|
68
|
+
constructor(options: StartupSequencerOptions) {
|
|
69
|
+
this.quietMs = Math.max(0, Math.floor(options.quietMs));
|
|
70
|
+
this.nonemptyFallbackMs = Math.max(0, Math.floor(options.nonemptyFallbackMs));
|
|
71
|
+
this.nowMs = options.nowMs ?? Date.now;
|
|
72
|
+
this.setTimer = options.setTimer ?? defaultSetTimer;
|
|
73
|
+
this.clearTimer = options.clearTimer ?? defaultClearTimer;
|
|
74
|
+
this.settledWait = this.createSettledWait();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
waitForSettled(): Promise<void> {
|
|
78
|
+
return this.settledWait;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setTargetSession(sessionId: string | null): void {
|
|
82
|
+
this.clearSettledTimer();
|
|
83
|
+
this.targetSessionId = sessionId;
|
|
84
|
+
this.phase = sessionId === null ? 'inactive' : 'waiting-for-output';
|
|
85
|
+
this.firstOutputObserved = false;
|
|
86
|
+
this.firstOutputAtMs = null;
|
|
87
|
+
this.firstPaintObserved = false;
|
|
88
|
+
this.headerObserved = false;
|
|
89
|
+
this.settleGate = null;
|
|
90
|
+
this.settledObserved = false;
|
|
91
|
+
this.settledSignaled = false;
|
|
92
|
+
this.settledWait = this.createSettledWait();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
snapshot(): StartupSequencerSnapshot {
|
|
96
|
+
return {
|
|
97
|
+
targetSessionId: this.targetSessionId,
|
|
98
|
+
phase: this.phase,
|
|
99
|
+
firstOutputObserved: this.firstOutputObserved,
|
|
100
|
+
firstOutputAtMs: this.firstOutputAtMs,
|
|
101
|
+
firstPaintObserved: this.firstPaintObserved,
|
|
102
|
+
headerObserved: this.headerObserved,
|
|
103
|
+
settleGate: this.settleGate,
|
|
104
|
+
settledObserved: this.settledObserved,
|
|
105
|
+
settledSignaled: this.settledSignaled,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
markFirstOutput(sessionId: string): boolean {
|
|
110
|
+
if (!this.hasTargetSession(sessionId) || this.firstOutputObserved) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
this.firstOutputObserved = true;
|
|
114
|
+
this.firstOutputAtMs = this.nowMs();
|
|
115
|
+
if (!this.settledObserved) {
|
|
116
|
+
this.phase = 'waiting-for-paint';
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
markFirstPaintVisible(sessionId: string, glyphCells: number): boolean {
|
|
122
|
+
if (
|
|
123
|
+
!this.hasTargetSession(sessionId) ||
|
|
124
|
+
this.firstPaintObserved ||
|
|
125
|
+
!this.firstOutputObserved ||
|
|
126
|
+
glyphCells <= 0
|
|
127
|
+
) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
this.firstPaintObserved = true;
|
|
131
|
+
if (!this.settledObserved) {
|
|
132
|
+
this.phase = 'waiting-for-header';
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
markHeaderVisible(sessionId: string, visible: boolean): boolean {
|
|
138
|
+
if (!this.hasTargetSession(sessionId) || !visible || this.headerObserved) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
this.headerObserved = true;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
maybeSelectSettleGate(sessionId: string, glyphCells: number): StartupSettleGate | null {
|
|
146
|
+
if (!this.hasTargetSession(sessionId) || this.settleGate !== null) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (this.headerObserved) {
|
|
150
|
+
this.settleGate = 'header';
|
|
151
|
+
} else if (
|
|
152
|
+
glyphCells > 0 &&
|
|
153
|
+
this.firstOutputAtMs !== null &&
|
|
154
|
+
this.nowMs() - this.firstOutputAtMs >= this.nonemptyFallbackMs
|
|
155
|
+
) {
|
|
156
|
+
this.settleGate = 'nonempty';
|
|
157
|
+
}
|
|
158
|
+
if (this.settleGate !== null && !this.settledObserved) {
|
|
159
|
+
this.phase = 'settling';
|
|
160
|
+
}
|
|
161
|
+
return this.settleGate;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
scheduleSettledProbe(
|
|
165
|
+
sessionId: string,
|
|
166
|
+
onSettled: (event: StartupSettledEvent) => void,
|
|
167
|
+
): boolean {
|
|
168
|
+
if (
|
|
169
|
+
!this.hasTargetSession(sessionId) ||
|
|
170
|
+
!this.firstOutputObserved ||
|
|
171
|
+
!this.firstPaintObserved ||
|
|
172
|
+
this.settleGate === null ||
|
|
173
|
+
this.settledObserved
|
|
174
|
+
) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (this.settleGate === 'header') {
|
|
178
|
+
if (this.settledTimer !== null) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
this.clearSettledTimer();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.settledTimer = this.setTimer(() => {
|
|
186
|
+
this.settledTimer = null;
|
|
187
|
+
if (!this.hasTargetSession(sessionId) || this.settledObserved || this.settleGate === null) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.settledObserved = true;
|
|
191
|
+
this.phase = 'settled';
|
|
192
|
+
onSettled({
|
|
193
|
+
sessionId,
|
|
194
|
+
gate: this.settleGate,
|
|
195
|
+
quietMs: this.quietMs,
|
|
196
|
+
});
|
|
197
|
+
this.signalSettled();
|
|
198
|
+
}, this.quietMs);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
clearSettledTimer(): boolean {
|
|
203
|
+
if (this.settledTimer === null) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
this.clearTimer(this.settledTimer);
|
|
207
|
+
this.settledTimer = null;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
signalSettled(): boolean {
|
|
212
|
+
if (this.settledSignaled) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
this.settledSignaled = true;
|
|
216
|
+
this.resolveSettledWait?.();
|
|
217
|
+
this.resolveSettledWait = null;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
finalize(): void {
|
|
222
|
+
this.clearSettledTimer();
|
|
223
|
+
this.signalSettled();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private hasTargetSession(sessionId: string): boolean {
|
|
227
|
+
return this.targetSessionId !== null && this.targetSessionId === sessionId;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private createSettledWait(): Promise<void> {
|
|
231
|
+
let resolveFn: (() => void) | null = null;
|
|
232
|
+
const wait = new Promise<void>((resolve) => {
|
|
233
|
+
resolveFn = resolve;
|
|
234
|
+
});
|
|
235
|
+
this.resolveSettledWait = resolveFn;
|
|
236
|
+
return wait;
|
|
237
|
+
}
|
|
238
|
+
}
|