@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,78 @@
|
|
|
1
|
+
interface RuntimeActivationConversationRecord {
|
|
2
|
+
readonly directoryId: string | null;
|
|
3
|
+
readonly live: boolean;
|
|
4
|
+
readonly status: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RuntimeConversationActivationOptions {
|
|
8
|
+
readonly getActiveConversationId: () => string | null;
|
|
9
|
+
readonly setActiveConversationId: (sessionId: string) => void;
|
|
10
|
+
readonly isConversationPaneMode: () => boolean;
|
|
11
|
+
readonly enterConversationPaneForActiveSession: (sessionId: string) => void;
|
|
12
|
+
readonly enterConversationPaneForSessionSwitch: (sessionId: string) => void;
|
|
13
|
+
readonly stopConversationTitleEditForOtherSession: (sessionId: string) => void;
|
|
14
|
+
readonly clearSelectionState: () => void;
|
|
15
|
+
readonly detachConversation: (sessionId: string) => Promise<void>;
|
|
16
|
+
readonly conversationById: (sessionId: string) => RuntimeActivationConversationRecord | undefined;
|
|
17
|
+
readonly noteGitActivity: (directoryId: string | null) => void;
|
|
18
|
+
readonly startConversation: (sessionId: string) => Promise<unknown>;
|
|
19
|
+
readonly attachConversation: (sessionId: string) => Promise<void>;
|
|
20
|
+
readonly isSessionNotFoundError: (error: unknown) => boolean;
|
|
21
|
+
readonly isSessionNotLiveError: (error: unknown) => boolean;
|
|
22
|
+
readonly markSessionUnavailable: (sessionId: string) => void;
|
|
23
|
+
readonly schedulePtyResizeImmediate: () => void;
|
|
24
|
+
readonly markDirty: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class RuntimeConversationActivation {
|
|
28
|
+
constructor(private readonly options: RuntimeConversationActivationOptions) {}
|
|
29
|
+
|
|
30
|
+
async activateConversation(sessionId: string): Promise<void> {
|
|
31
|
+
if (this.options.getActiveConversationId() === sessionId) {
|
|
32
|
+
if (!this.options.isConversationPaneMode()) {
|
|
33
|
+
this.options.enterConversationPaneForActiveSession(sessionId);
|
|
34
|
+
this.options.markDirty();
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.options.stopConversationTitleEditForOtherSession(sessionId);
|
|
40
|
+
const previousActiveId = this.options.getActiveConversationId();
|
|
41
|
+
this.options.clearSelectionState();
|
|
42
|
+
if (previousActiveId !== null) {
|
|
43
|
+
await this.options.detachConversation(previousActiveId);
|
|
44
|
+
}
|
|
45
|
+
this.options.setActiveConversationId(sessionId);
|
|
46
|
+
this.options.enterConversationPaneForSessionSwitch(sessionId);
|
|
47
|
+
|
|
48
|
+
const targetConversation = this.options.conversationById(sessionId);
|
|
49
|
+
this.options.noteGitActivity(targetConversation?.directoryId ?? null);
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
targetConversation !== undefined &&
|
|
53
|
+
!targetConversation.live &&
|
|
54
|
+
targetConversation.status !== 'exited'
|
|
55
|
+
) {
|
|
56
|
+
await this.options.startConversation(sessionId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (targetConversation?.status !== 'exited') {
|
|
60
|
+
try {
|
|
61
|
+
await this.options.attachConversation(sessionId);
|
|
62
|
+
} catch (error: unknown) {
|
|
63
|
+
if (
|
|
64
|
+
!this.options.isSessionNotFoundError(error) &&
|
|
65
|
+
!this.options.isSessionNotLiveError(error)
|
|
66
|
+
) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
this.options.markSessionUnavailable(sessionId);
|
|
70
|
+
await this.options.startConversation(sessionId);
|
|
71
|
+
await this.options.attachConversation(sessionId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.options.schedulePtyResizeImmediate();
|
|
76
|
+
this.options.markDirty();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export interface RuntimeConversationStarterConversationRecord {
|
|
2
|
+
readonly sessionId: string;
|
|
3
|
+
readonly directoryId: string | null;
|
|
4
|
+
readonly agentType: string;
|
|
5
|
+
adapterState: Record<string, unknown>;
|
|
6
|
+
live: boolean;
|
|
7
|
+
lastOutputCursor: number;
|
|
8
|
+
launchCommand: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RuntimeConversationStarterPerfSpan {
|
|
12
|
+
end(input?: Record<string, unknown>): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RuntimeConversationStarterPtyStartInput {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
args: readonly string[];
|
|
18
|
+
env: Record<string, string>;
|
|
19
|
+
cwd: string;
|
|
20
|
+
initialCols: number;
|
|
21
|
+
initialRows: number;
|
|
22
|
+
worktreeId?: string;
|
|
23
|
+
terminalForegroundHex?: string;
|
|
24
|
+
terminalBackgroundHex?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RuntimeConversationStarterLaunchArgsInput {
|
|
28
|
+
readonly agentType: string;
|
|
29
|
+
readonly baseArgsForAgent: readonly string[];
|
|
30
|
+
readonly adapterState: Record<string, unknown>;
|
|
31
|
+
readonly sessionCwd: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type RuntimeConversationStarterSpanAttributes = Record<string, string | number | boolean>;
|
|
35
|
+
|
|
36
|
+
export interface RuntimeConversationStarterOptions<
|
|
37
|
+
TConversation extends RuntimeConversationStarterConversationRecord,
|
|
38
|
+
TSessionSummary,
|
|
39
|
+
> {
|
|
40
|
+
readonly runWithStartInFlight: (
|
|
41
|
+
sessionId: string,
|
|
42
|
+
run: () => Promise<TConversation>,
|
|
43
|
+
) => Promise<TConversation>;
|
|
44
|
+
readonly conversationById: (sessionId: string) => TConversation | undefined;
|
|
45
|
+
readonly ensureConversation: (sessionId: string) => TConversation;
|
|
46
|
+
readonly normalizeThreadAgentType: (agentType: string) => string;
|
|
47
|
+
readonly codexArgs: readonly string[];
|
|
48
|
+
readonly critiqueDefaultArgs: readonly string[];
|
|
49
|
+
readonly sessionCwdForConversation: (conversation: TConversation) => string;
|
|
50
|
+
readonly buildLaunchArgs: (input: RuntimeConversationStarterLaunchArgsInput) => readonly string[];
|
|
51
|
+
readonly launchCommandForAgent: (agentType: string) => string;
|
|
52
|
+
readonly formatCommandForDebugBar: (command: string, args: readonly string[]) => string;
|
|
53
|
+
readonly startConversationSpan: (sessionId: string) => RuntimeConversationStarterPerfSpan;
|
|
54
|
+
readonly firstPaintTargetSessionId: () => string | null;
|
|
55
|
+
readonly endStartCommandSpan: (input: RuntimeConversationStarterSpanAttributes) => void;
|
|
56
|
+
readonly layout: () => {
|
|
57
|
+
rightCols: number;
|
|
58
|
+
paneRows: number;
|
|
59
|
+
};
|
|
60
|
+
readonly startPtySession: (input: RuntimeConversationStarterPtyStartInput) => Promise<void>;
|
|
61
|
+
readonly setPtySize: (
|
|
62
|
+
sessionId: string,
|
|
63
|
+
size: {
|
|
64
|
+
cols: number;
|
|
65
|
+
rows: number;
|
|
66
|
+
},
|
|
67
|
+
) => void;
|
|
68
|
+
readonly sendResize: (sessionId: string, cols: number, rows: number) => void;
|
|
69
|
+
readonly sessionEnv: Record<string, string>;
|
|
70
|
+
readonly worktreeId: string | undefined;
|
|
71
|
+
readonly terminalForegroundHex: string | undefined;
|
|
72
|
+
readonly terminalBackgroundHex: string | undefined;
|
|
73
|
+
readonly recordStartCommand: (sessionId: string, launchArgs: readonly string[]) => void;
|
|
74
|
+
readonly getSessionStatus: (sessionId: string) => Promise<TSessionSummary | null>;
|
|
75
|
+
readonly upsertFromSessionSummary: (summary: TSessionSummary) => void;
|
|
76
|
+
readonly subscribeConversationEvents: (sessionId: string) => Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class RuntimeConversationStarter<
|
|
80
|
+
TConversation extends RuntimeConversationStarterConversationRecord,
|
|
81
|
+
TSessionSummary,
|
|
82
|
+
> {
|
|
83
|
+
constructor(
|
|
84
|
+
private readonly options: RuntimeConversationStarterOptions<TConversation, TSessionSummary>,
|
|
85
|
+
) {}
|
|
86
|
+
|
|
87
|
+
async startConversation(sessionId: string): Promise<TConversation> {
|
|
88
|
+
return await this.options.runWithStartInFlight(sessionId, async () => {
|
|
89
|
+
const existing = this.options.conversationById(sessionId);
|
|
90
|
+
const targetConversation = existing ?? this.options.ensureConversation(sessionId);
|
|
91
|
+
const agentType = this.options.normalizeThreadAgentType(targetConversation.agentType);
|
|
92
|
+
const baseArgsForAgent =
|
|
93
|
+
agentType === 'codex'
|
|
94
|
+
? this.options.codexArgs
|
|
95
|
+
: agentType === 'critique'
|
|
96
|
+
? this.options.critiqueDefaultArgs
|
|
97
|
+
: [];
|
|
98
|
+
const sessionCwd = this.options.sessionCwdForConversation(targetConversation);
|
|
99
|
+
const launchArgs = this.options.buildLaunchArgs({
|
|
100
|
+
agentType,
|
|
101
|
+
baseArgsForAgent,
|
|
102
|
+
adapterState: targetConversation.adapterState,
|
|
103
|
+
sessionCwd,
|
|
104
|
+
});
|
|
105
|
+
targetConversation.launchCommand = this.options.formatCommandForDebugBar(
|
|
106
|
+
this.options.launchCommandForAgent(agentType),
|
|
107
|
+
launchArgs,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (existing?.live === true) {
|
|
111
|
+
this.endStartCommandSpanIfTarget(sessionId, {
|
|
112
|
+
alreadyLive: true,
|
|
113
|
+
});
|
|
114
|
+
return existing;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const startSpan = this.options.startConversationSpan(sessionId);
|
|
118
|
+
targetConversation.lastOutputCursor = 0;
|
|
119
|
+
const layout = this.options.layout();
|
|
120
|
+
const ptyStartInput: RuntimeConversationStarterPtyStartInput = {
|
|
121
|
+
sessionId,
|
|
122
|
+
args: launchArgs,
|
|
123
|
+
env: this.options.sessionEnv,
|
|
124
|
+
cwd: sessionCwd,
|
|
125
|
+
initialCols: layout.rightCols,
|
|
126
|
+
initialRows: layout.paneRows,
|
|
127
|
+
};
|
|
128
|
+
if (this.options.worktreeId !== undefined) {
|
|
129
|
+
ptyStartInput.worktreeId = this.options.worktreeId;
|
|
130
|
+
}
|
|
131
|
+
if (this.options.terminalForegroundHex !== undefined) {
|
|
132
|
+
ptyStartInput.terminalForegroundHex = this.options.terminalForegroundHex;
|
|
133
|
+
}
|
|
134
|
+
if (this.options.terminalBackgroundHex !== undefined) {
|
|
135
|
+
ptyStartInput.terminalBackgroundHex = this.options.terminalBackgroundHex;
|
|
136
|
+
}
|
|
137
|
+
await this.options.startPtySession(ptyStartInput);
|
|
138
|
+
this.options.setPtySize(sessionId, {
|
|
139
|
+
cols: layout.rightCols,
|
|
140
|
+
rows: layout.paneRows,
|
|
141
|
+
});
|
|
142
|
+
this.options.sendResize(sessionId, layout.rightCols, layout.paneRows);
|
|
143
|
+
this.endStartCommandSpanIfTarget(sessionId, {
|
|
144
|
+
alreadyLive: false,
|
|
145
|
+
argCount: launchArgs.length,
|
|
146
|
+
resumed: launchArgs[0] === 'resume',
|
|
147
|
+
});
|
|
148
|
+
const state = this.options.ensureConversation(sessionId);
|
|
149
|
+
this.options.recordStartCommand(sessionId, launchArgs);
|
|
150
|
+
const statusSummary = await this.options.getSessionStatus(sessionId);
|
|
151
|
+
if (statusSummary !== null) {
|
|
152
|
+
this.options.upsertFromSessionSummary(statusSummary);
|
|
153
|
+
}
|
|
154
|
+
await this.options.subscribeConversationEvents(sessionId);
|
|
155
|
+
startSpan.end({
|
|
156
|
+
live: state.live,
|
|
157
|
+
});
|
|
158
|
+
return state;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private endStartCommandSpanIfTarget(
|
|
163
|
+
sessionId: string,
|
|
164
|
+
payload: RuntimeConversationStarterSpanAttributes,
|
|
165
|
+
): void {
|
|
166
|
+
if (this.options.firstPaintTargetSessionId() !== sessionId) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.options.endStartCommandSpan(payload);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ConversationTitleEditState, WorkspaceModel } from '../domain/workspace.ts';
|
|
2
|
+
|
|
3
|
+
interface ConversationTitleRecordLike {
|
|
4
|
+
title: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RuntimeConversationTitleEditServiceOptions<
|
|
8
|
+
TConversation extends ConversationTitleRecordLike,
|
|
9
|
+
> {
|
|
10
|
+
readonly workspace: WorkspaceModel;
|
|
11
|
+
readonly updateConversationTitle: (input: {
|
|
12
|
+
conversationId: string;
|
|
13
|
+
title: string;
|
|
14
|
+
}) => Promise<{ title: string } | null>;
|
|
15
|
+
readonly conversationById: (conversationId: string) => TConversation | undefined;
|
|
16
|
+
readonly markDirty: () => void;
|
|
17
|
+
readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
|
|
18
|
+
readonly debounceMs: number;
|
|
19
|
+
readonly setDebounceTimer?: (callback: () => void, ms: number) => NodeJS.Timeout;
|
|
20
|
+
readonly clearDebounceTimer?: (timer: NodeJS.Timeout) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class RuntimeConversationTitleEditService<
|
|
24
|
+
TConversation extends ConversationTitleRecordLike,
|
|
25
|
+
> {
|
|
26
|
+
private readonly setDebounceTimer: (callback: () => void, ms: number) => NodeJS.Timeout;
|
|
27
|
+
private readonly clearDebounceTimer: (timer: NodeJS.Timeout) => void;
|
|
28
|
+
|
|
29
|
+
constructor(private readonly options: RuntimeConversationTitleEditServiceOptions<TConversation>) {
|
|
30
|
+
this.setDebounceTimer = options.setDebounceTimer ?? setTimeout;
|
|
31
|
+
this.clearDebounceTimer = options.clearDebounceTimer ?? clearTimeout;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
clearCurrentTimer(): void {
|
|
35
|
+
const edit = this.options.workspace.conversationTitleEdit;
|
|
36
|
+
if (edit === null) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.clearTimer(edit);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
schedulePersist(): void {
|
|
43
|
+
const edit = this.options.workspace.conversationTitleEdit;
|
|
44
|
+
if (edit === null) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.clearTimer(edit);
|
|
48
|
+
edit.debounceTimer = this.setDebounceTimer(() => {
|
|
49
|
+
const latestEdit = this.options.workspace.conversationTitleEdit;
|
|
50
|
+
if (latestEdit === null || latestEdit.conversationId !== edit.conversationId) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
latestEdit.debounceTimer = null;
|
|
54
|
+
this.queuePersist(latestEdit, 'debounced');
|
|
55
|
+
}, this.options.debounceMs);
|
|
56
|
+
edit.debounceTimer.unref?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stop(persistPending: boolean): void {
|
|
60
|
+
const edit = this.options.workspace.conversationTitleEdit;
|
|
61
|
+
if (edit === null) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.clearTimer(edit);
|
|
65
|
+
if (persistPending) {
|
|
66
|
+
this.queuePersist(edit, 'flush');
|
|
67
|
+
}
|
|
68
|
+
this.options.workspace.conversationTitleEdit = null;
|
|
69
|
+
this.options.markDirty();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
begin(conversationId: string): void {
|
|
73
|
+
const target = this.options.conversationById(conversationId);
|
|
74
|
+
if (target === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (this.options.workspace.conversationTitleEdit?.conversationId === conversationId) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (this.options.workspace.conversationTitleEdit !== null) {
|
|
81
|
+
this.stop(true);
|
|
82
|
+
}
|
|
83
|
+
this.options.workspace.conversationTitleEdit = {
|
|
84
|
+
conversationId,
|
|
85
|
+
value: target.title,
|
|
86
|
+
lastSavedValue: target.title,
|
|
87
|
+
error: null,
|
|
88
|
+
persistInFlight: false,
|
|
89
|
+
debounceTimer: null,
|
|
90
|
+
};
|
|
91
|
+
this.options.markDirty();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private clearTimer(edit: ConversationTitleEditState): void {
|
|
95
|
+
if (edit.debounceTimer !== null) {
|
|
96
|
+
this.clearDebounceTimer(edit.debounceTimer);
|
|
97
|
+
edit.debounceTimer = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private queuePersist(edit: ConversationTitleEditState, reason: 'debounced' | 'flush'): void {
|
|
102
|
+
const titleToPersist = edit.value;
|
|
103
|
+
if (titleToPersist === edit.lastSavedValue) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
edit.persistInFlight = true;
|
|
107
|
+
this.options.markDirty();
|
|
108
|
+
this.options.queueControlPlaneOp(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = await this.options.updateConversationTitle({
|
|
111
|
+
conversationId: edit.conversationId,
|
|
112
|
+
title: titleToPersist,
|
|
113
|
+
});
|
|
114
|
+
const persistedTitle = parsed?.title ?? titleToPersist;
|
|
115
|
+
const latestConversation = this.options.conversationById(edit.conversationId);
|
|
116
|
+
const latestEdit = this.options.workspace.conversationTitleEdit;
|
|
117
|
+
const shouldApplyToConversation =
|
|
118
|
+
latestEdit === null ||
|
|
119
|
+
latestEdit.conversationId !== edit.conversationId ||
|
|
120
|
+
latestEdit.value === titleToPersist;
|
|
121
|
+
if (latestConversation !== undefined && shouldApplyToConversation) {
|
|
122
|
+
latestConversation.title = persistedTitle;
|
|
123
|
+
}
|
|
124
|
+
if (latestEdit !== null && latestEdit.conversationId === edit.conversationId) {
|
|
125
|
+
latestEdit.lastSavedValue = persistedTitle;
|
|
126
|
+
if (latestEdit.value === titleToPersist) {
|
|
127
|
+
latestEdit.error = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch (error: unknown) {
|
|
131
|
+
const latestEdit = this.options.workspace.conversationTitleEdit;
|
|
132
|
+
if (
|
|
133
|
+
latestEdit !== null &&
|
|
134
|
+
latestEdit.conversationId === edit.conversationId &&
|
|
135
|
+
latestEdit.value === titleToPersist
|
|
136
|
+
) {
|
|
137
|
+
latestEdit.error = error instanceof Error ? error.message : String(error);
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
} finally {
|
|
141
|
+
const latestEdit = this.options.workspace.conversationTitleEdit;
|
|
142
|
+
if (latestEdit !== null && latestEdit.conversationId === edit.conversationId) {
|
|
143
|
+
latestEdit.persistInFlight = false;
|
|
144
|
+
}
|
|
145
|
+
this.options.markDirty();
|
|
146
|
+
}
|
|
147
|
+
}, `title-edit-${reason}:${edit.conversationId}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addDirectoryByPath as addDirectoryByPathFn,
|
|
3
|
+
archiveConversation as archiveConversationFn,
|
|
4
|
+
closeDirectory as closeDirectoryFn,
|
|
5
|
+
} from '../mux/live-mux/actions-conversation.ts';
|
|
6
|
+
|
|
7
|
+
interface RuntimeConversationStateLike {
|
|
8
|
+
readonly directoryId: string | null;
|
|
9
|
+
readonly live: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RuntimeDirectoryRecordLike {
|
|
13
|
+
readonly directoryId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RuntimeDirectoryActionService<TDirectoryRecord extends RuntimeDirectoryRecordLike> {
|
|
17
|
+
closePtySession(sessionId: string): Promise<unknown>;
|
|
18
|
+
removeSession(sessionId: string): Promise<unknown>;
|
|
19
|
+
archiveConversation(sessionId: string): Promise<unknown>;
|
|
20
|
+
upsertDirectory(input: { directoryId: string; path: string }): Promise<TDirectoryRecord | null>;
|
|
21
|
+
archiveDirectory(directoryId: string): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RuntimeDirectoryActionsOptions<
|
|
25
|
+
TDirectoryRecord extends RuntimeDirectoryRecordLike,
|
|
26
|
+
TConversationState extends RuntimeConversationStateLike,
|
|
27
|
+
> {
|
|
28
|
+
readonly controlPlaneService: RuntimeDirectoryActionService<TDirectoryRecord>;
|
|
29
|
+
readonly conversations: () => ReadonlyMap<string, TConversationState>;
|
|
30
|
+
readonly orderedConversationIds: () => readonly string[];
|
|
31
|
+
readonly conversationDirectoryId: (sessionId: string) => string | null;
|
|
32
|
+
readonly conversationLive: (sessionId: string) => boolean;
|
|
33
|
+
readonly removeConversationState: (sessionId: string) => void;
|
|
34
|
+
readonly unsubscribeConversationEvents: (sessionId: string) => Promise<void>;
|
|
35
|
+
readonly activeConversationId: () => string | null;
|
|
36
|
+
readonly setActiveConversationId: (sessionId: string | null) => void;
|
|
37
|
+
readonly activateConversation: (sessionId: string) => Promise<unknown>;
|
|
38
|
+
readonly resolveActiveDirectoryId: () => string | null;
|
|
39
|
+
readonly enterProjectPane: (directoryId: string) => void;
|
|
40
|
+
readonly markDirty: () => void;
|
|
41
|
+
readonly isSessionNotFoundError: (error: unknown) => boolean;
|
|
42
|
+
readonly isConversationNotFoundError: (error: unknown) => boolean;
|
|
43
|
+
readonly createDirectoryId: () => string;
|
|
44
|
+
readonly resolveWorkspacePathForMux: (rawPath: string) => string;
|
|
45
|
+
readonly setDirectory: (directory: TDirectoryRecord) => void;
|
|
46
|
+
readonly directoryIdOf: (directory: TDirectoryRecord) => string;
|
|
47
|
+
readonly setActiveDirectoryId: (directoryId: string | null) => void;
|
|
48
|
+
readonly syncGitStateWithDirectories: () => void;
|
|
49
|
+
readonly noteGitActivity: (directoryId: string) => void;
|
|
50
|
+
readonly hydratePersistedConversationsForDirectory: (directoryId: string) => Promise<unknown>;
|
|
51
|
+
readonly findConversationIdByDirectory: (directoryId: string) => string | null;
|
|
52
|
+
readonly directoriesHas: (directoryId: string) => boolean;
|
|
53
|
+
readonly deleteDirectory: (directoryId: string) => void;
|
|
54
|
+
readonly deleteDirectoryGitState: (directoryId: string) => void;
|
|
55
|
+
readonly projectPaneSnapshotDirectoryId: () => string | null;
|
|
56
|
+
readonly clearProjectPaneSnapshot: () => void;
|
|
57
|
+
readonly directoriesSize: () => number;
|
|
58
|
+
readonly invocationDirectory: string;
|
|
59
|
+
readonly activeDirectoryId: () => string | null;
|
|
60
|
+
readonly firstDirectoryId: () => string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class RuntimeDirectoryActions<
|
|
64
|
+
TDirectoryRecord extends RuntimeDirectoryRecordLike,
|
|
65
|
+
TConversationState extends RuntimeConversationStateLike,
|
|
66
|
+
> {
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly options: RuntimeDirectoryActionsOptions<TDirectoryRecord, TConversationState>,
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
async archiveConversation(sessionId: string): Promise<void> {
|
|
72
|
+
await archiveConversationFn({
|
|
73
|
+
sessionId,
|
|
74
|
+
conversations: this.options.conversations(),
|
|
75
|
+
closePtySession: async (targetSessionId) => {
|
|
76
|
+
await this.options.controlPlaneService.closePtySession(targetSessionId);
|
|
77
|
+
},
|
|
78
|
+
removeSession: async (targetSessionId) => {
|
|
79
|
+
await this.options.controlPlaneService.removeSession(targetSessionId);
|
|
80
|
+
},
|
|
81
|
+
isSessionNotFoundError: this.options.isSessionNotFoundError,
|
|
82
|
+
archiveConversationRecord: async (targetSessionId) => {
|
|
83
|
+
await this.options.controlPlaneService.archiveConversation(targetSessionId);
|
|
84
|
+
},
|
|
85
|
+
isConversationNotFoundError: this.options.isConversationNotFoundError,
|
|
86
|
+
unsubscribeConversationEvents: this.options.unsubscribeConversationEvents,
|
|
87
|
+
removeConversationState: this.options.removeConversationState,
|
|
88
|
+
activeConversationId: this.options.activeConversationId(),
|
|
89
|
+
setActiveConversationId: this.options.setActiveConversationId,
|
|
90
|
+
orderedConversationIds: this.options.orderedConversationIds,
|
|
91
|
+
conversationDirectoryId: this.options.conversationDirectoryId,
|
|
92
|
+
resolveActiveDirectoryId: this.options.resolveActiveDirectoryId,
|
|
93
|
+
enterProjectPane: this.options.enterProjectPane,
|
|
94
|
+
activateConversation: this.options.activateConversation,
|
|
95
|
+
markDirty: this.options.markDirty,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async addDirectoryByPath(rawPath: string): Promise<void> {
|
|
100
|
+
await addDirectoryByPathFn({
|
|
101
|
+
rawPath,
|
|
102
|
+
resolveWorkspacePathForMux: this.options.resolveWorkspacePathForMux,
|
|
103
|
+
upsertDirectory: async (path) => {
|
|
104
|
+
return await this.options.controlPlaneService.upsertDirectory({
|
|
105
|
+
directoryId: this.options.createDirectoryId(),
|
|
106
|
+
path,
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
setDirectory: this.options.setDirectory,
|
|
110
|
+
directoryIdOf: this.options.directoryIdOf,
|
|
111
|
+
setActiveDirectoryId: (directoryId) => {
|
|
112
|
+
this.options.setActiveDirectoryId(directoryId);
|
|
113
|
+
},
|
|
114
|
+
syncGitStateWithDirectories: this.options.syncGitStateWithDirectories,
|
|
115
|
+
noteGitActivity: this.options.noteGitActivity,
|
|
116
|
+
hydratePersistedConversationsForDirectory:
|
|
117
|
+
this.options.hydratePersistedConversationsForDirectory,
|
|
118
|
+
findConversationIdByDirectory: this.options.findConversationIdByDirectory,
|
|
119
|
+
activateConversation: this.options.activateConversation,
|
|
120
|
+
enterProjectPane: this.options.enterProjectPane,
|
|
121
|
+
markDirty: this.options.markDirty,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async closeDirectory(directoryId: string): Promise<void> {
|
|
126
|
+
await closeDirectoryFn({
|
|
127
|
+
directoryId,
|
|
128
|
+
directoriesHas: this.options.directoriesHas,
|
|
129
|
+
orderedConversationIds: this.options.orderedConversationIds,
|
|
130
|
+
conversationDirectoryId: this.options.conversationDirectoryId,
|
|
131
|
+
conversationLive: this.options.conversationLive,
|
|
132
|
+
closePtySession: async (sessionId) => {
|
|
133
|
+
await this.options.controlPlaneService.closePtySession(sessionId);
|
|
134
|
+
},
|
|
135
|
+
archiveConversationRecord: async (sessionId) => {
|
|
136
|
+
await this.options.controlPlaneService.archiveConversation(sessionId);
|
|
137
|
+
},
|
|
138
|
+
unsubscribeConversationEvents: this.options.unsubscribeConversationEvents,
|
|
139
|
+
removeConversationState: this.options.removeConversationState,
|
|
140
|
+
activeConversationId: this.options.activeConversationId(),
|
|
141
|
+
setActiveConversationId: this.options.setActiveConversationId,
|
|
142
|
+
archiveDirectory: async (targetDirectoryId) => {
|
|
143
|
+
await this.options.controlPlaneService.archiveDirectory(targetDirectoryId);
|
|
144
|
+
},
|
|
145
|
+
deleteDirectory: this.options.deleteDirectory,
|
|
146
|
+
deleteDirectoryGitState: this.options.deleteDirectoryGitState,
|
|
147
|
+
projectPaneSnapshotDirectoryId: this.options.projectPaneSnapshotDirectoryId(),
|
|
148
|
+
clearProjectPaneSnapshot: this.options.clearProjectPaneSnapshot,
|
|
149
|
+
directoriesSize: this.options.directoriesSize,
|
|
150
|
+
addDirectoryByPath: async (path) => {
|
|
151
|
+
await this.addDirectoryByPath(path);
|
|
152
|
+
},
|
|
153
|
+
invocationDirectory: this.options.invocationDirectory,
|
|
154
|
+
activeDirectoryId: this.options.activeDirectoryId(),
|
|
155
|
+
setActiveDirectoryId: this.options.setActiveDirectoryId,
|
|
156
|
+
firstDirectoryId: this.options.firstDirectoryId,
|
|
157
|
+
noteGitActivity: this.options.noteGitActivity,
|
|
158
|
+
resolveActiveDirectoryId: this.options.resolveActiveDirectoryId,
|
|
159
|
+
activateConversation: this.options.activateConversation,
|
|
160
|
+
enterProjectPane: this.options.enterProjectPane,
|
|
161
|
+
markDirty: this.options.markDirty,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|