@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,187 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, truncateSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { DISABLE_MUX_INPUT_MODES } from '../terminal-input-modes.ts';
|
|
4
|
+
import { resolveWorkspacePath } from '../workspace-path.ts';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_STARTUP_TERMINAL_MIN_COLS = 40;
|
|
7
|
+
const DEFAULT_STARTUP_TERMINAL_MIN_ROWS = 10;
|
|
8
|
+
const DEFAULT_STARTUP_TERMINAL_PROBE_TIMEOUT_MS = 250;
|
|
9
|
+
const DEFAULT_STARTUP_TERMINAL_PROBE_INTERVAL_MS = 10;
|
|
10
|
+
|
|
11
|
+
interface FocusEventExtraction {
|
|
12
|
+
readonly sanitized: Buffer;
|
|
13
|
+
readonly focusInCount: number;
|
|
14
|
+
readonly focusOutCount: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface StartupTerminalProbeOptions {
|
|
18
|
+
readonly terminalSizeReader?: () => { cols: number; rows: number };
|
|
19
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
20
|
+
readonly now?: () => number;
|
|
21
|
+
readonly timeoutMs?: number;
|
|
22
|
+
readonly intervalMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatErrorMessage(error: unknown): string {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
return error.stack ?? error.message;
|
|
28
|
+
}
|
|
29
|
+
return String(error);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function restoreTerminalState(
|
|
33
|
+
newline: boolean,
|
|
34
|
+
restoreInputModes: (() => void) | null = null,
|
|
35
|
+
disableInputModes = DISABLE_MUX_INPUT_MODES,
|
|
36
|
+
): void {
|
|
37
|
+
try {
|
|
38
|
+
if (restoreInputModes === null) {
|
|
39
|
+
process.stdout.write(disableInputModes);
|
|
40
|
+
} else {
|
|
41
|
+
restoreInputModes();
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(`\u001b[?25h\u001b[0m${newline ? '\n' : ''}`);
|
|
44
|
+
} catch {
|
|
45
|
+
// Best-effort restore only.
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (process.stdin.isTTY) {
|
|
49
|
+
try {
|
|
50
|
+
process.stdin.setRawMode(false);
|
|
51
|
+
} catch {
|
|
52
|
+
// Best-effort restore only.
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
process.stdin.pause();
|
|
56
|
+
} catch {
|
|
57
|
+
// Best-effort restore only.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function extractFocusEvents(chunk: Buffer): FocusEventExtraction {
|
|
63
|
+
const text = chunk.toString('utf8');
|
|
64
|
+
const focusInCount = text.split('\u001b[I').length - 1;
|
|
65
|
+
const focusOutCount = text.split('\u001b[O').length - 1;
|
|
66
|
+
|
|
67
|
+
if (focusInCount === 0 && focusOutCount === 0) {
|
|
68
|
+
return {
|
|
69
|
+
sanitized: chunk,
|
|
70
|
+
focusInCount: 0,
|
|
71
|
+
focusOutCount: 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sanitizedText = text.replaceAll('\u001b[I', '').replaceAll('\u001b[O', '');
|
|
76
|
+
return {
|
|
77
|
+
sanitized: Buffer.from(sanitizedText, 'utf8'),
|
|
78
|
+
focusInCount,
|
|
79
|
+
focusOutCount,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function prepareArtifactPath(path: string, overwriteOnStart: boolean): string {
|
|
84
|
+
const resolvedPath = resolve(path);
|
|
85
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
86
|
+
if (overwriteOnStart) {
|
|
87
|
+
try {
|
|
88
|
+
truncateSync(resolvedPath, 0);
|
|
89
|
+
} catch (error: unknown) {
|
|
90
|
+
const code = (error as { code?: unknown }).code;
|
|
91
|
+
if (code !== 'ENOENT') {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
appendFileSync(resolvedPath, '', 'utf8');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return resolvedPath;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function sanitizeProcessEnv(env: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
|
101
|
+
const sanitized: Record<string, string> = {};
|
|
102
|
+
for (const [key, value] of Object.entries(env)) {
|
|
103
|
+
if (typeof value === 'string') {
|
|
104
|
+
sanitized[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return sanitized;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function terminalSize(): { cols: number; rows: number } {
|
|
111
|
+
const cols = process.stdout.columns;
|
|
112
|
+
const rows = process.stdout.rows;
|
|
113
|
+
if (typeof cols === 'number' && cols > 0 && typeof rows === 'number' && rows > 0) {
|
|
114
|
+
return { cols, rows };
|
|
115
|
+
}
|
|
116
|
+
return { cols: 120, rows: 40 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function startupTerminalSizeLooksPlausible(size: { cols: number; rows: number }): boolean {
|
|
120
|
+
return (
|
|
121
|
+
size.cols >= DEFAULT_STARTUP_TERMINAL_MIN_COLS && size.rows >= DEFAULT_STARTUP_TERMINAL_MIN_ROWS
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function readStartupTerminalSize(
|
|
126
|
+
options: StartupTerminalProbeOptions = {},
|
|
127
|
+
): Promise<{ cols: number; rows: number }> {
|
|
128
|
+
const terminalSizeReader = options.terminalSizeReader ?? terminalSize;
|
|
129
|
+
const sleep =
|
|
130
|
+
options.sleep ??
|
|
131
|
+
(async (ms: number): Promise<void> => {
|
|
132
|
+
await new Promise((resolveTimer) => {
|
|
133
|
+
setTimeout(resolveTimer, ms);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
const now = options.now ?? Date.now;
|
|
137
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_STARTUP_TERMINAL_PROBE_TIMEOUT_MS;
|
|
138
|
+
const intervalMs = options.intervalMs ?? DEFAULT_STARTUP_TERMINAL_PROBE_INTERVAL_MS;
|
|
139
|
+
|
|
140
|
+
let best = terminalSizeReader();
|
|
141
|
+
const startedAtMs = now();
|
|
142
|
+
while (!startupTerminalSizeLooksPlausible(best) && now() - startedAtMs < timeoutMs) {
|
|
143
|
+
await sleep(intervalMs);
|
|
144
|
+
const next = terminalSizeReader();
|
|
145
|
+
if (next.cols * next.rows > best.cols * best.rows) {
|
|
146
|
+
best = next;
|
|
147
|
+
}
|
|
148
|
+
if (startupTerminalSizeLooksPlausible(next)) {
|
|
149
|
+
return next;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return startupTerminalSizeLooksPlausible(best) ? best : { cols: 120, rows: 40 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function parsePositiveInt(value: string | undefined, fallback: number): number {
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
return fallback;
|
|
158
|
+
}
|
|
159
|
+
const parsed = Number.parseInt(value, 10);
|
|
160
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
161
|
+
return fallback;
|
|
162
|
+
}
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function parseBooleanEnv(value: string | undefined, fallback: boolean): boolean {
|
|
167
|
+
if (value === undefined) {
|
|
168
|
+
return fallback;
|
|
169
|
+
}
|
|
170
|
+
const normalized = value.trim().toLowerCase();
|
|
171
|
+
if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return fallback;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function resolveWorkspacePathForMux(
|
|
181
|
+
invocationDirectory: string,
|
|
182
|
+
value: string,
|
|
183
|
+
home = process.env.HOME,
|
|
184
|
+
): string {
|
|
185
|
+
const resolvedHome = typeof home === 'string' && home.length > 0 ? home : null;
|
|
186
|
+
return resolveWorkspacePath(invocationDirectory, value, resolvedHome);
|
|
187
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const STATUS_TIMELINE_STATE_FILE_NAME = 'active-status-timeline.json';
|
|
4
|
+
export const STATUS_TIMELINE_STATE_VERSION = 1;
|
|
5
|
+
export const STATUS_TIMELINE_MODE = 'live-mux-status-timeline';
|
|
6
|
+
export const DEFAULT_STATUS_TIMELINE_ROOT_PATH = '.harness/status-timelines';
|
|
7
|
+
export const STATUS_TIMELINE_FILE_NAME = 'status-timeline.log';
|
|
8
|
+
|
|
9
|
+
export interface ActiveStatusTimelineState {
|
|
10
|
+
version: typeof STATUS_TIMELINE_STATE_VERSION;
|
|
11
|
+
mode: typeof STATUS_TIMELINE_MODE;
|
|
12
|
+
outputPath: string;
|
|
13
|
+
sessionName: string | null;
|
|
14
|
+
startedAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveStatusTimelineStatePath(
|
|
18
|
+
invocationDirectory: string,
|
|
19
|
+
sessionName: string | null,
|
|
20
|
+
): string {
|
|
21
|
+
if (sessionName === null) {
|
|
22
|
+
return resolve(invocationDirectory, '.harness', STATUS_TIMELINE_STATE_FILE_NAME);
|
|
23
|
+
}
|
|
24
|
+
return resolve(
|
|
25
|
+
invocationDirectory,
|
|
26
|
+
'.harness',
|
|
27
|
+
'sessions',
|
|
28
|
+
sessionName,
|
|
29
|
+
STATUS_TIMELINE_STATE_FILE_NAME,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveDefaultStatusTimelineOutputPath(
|
|
34
|
+
invocationDirectory: string,
|
|
35
|
+
sessionName: string | null,
|
|
36
|
+
): string {
|
|
37
|
+
if (sessionName === null) {
|
|
38
|
+
return resolve(
|
|
39
|
+
invocationDirectory,
|
|
40
|
+
DEFAULT_STATUS_TIMELINE_ROOT_PATH,
|
|
41
|
+
STATUS_TIMELINE_FILE_NAME,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return resolve(
|
|
45
|
+
invocationDirectory,
|
|
46
|
+
DEFAULT_STATUS_TIMELINE_ROOT_PATH,
|
|
47
|
+
sessionName,
|
|
48
|
+
STATUS_TIMELINE_FILE_NAME,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseActiveStatusTimelineState(raw: unknown): ActiveStatusTimelineState | null {
|
|
53
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const candidate = raw as Record<string, unknown>;
|
|
57
|
+
if (candidate['version'] !== STATUS_TIMELINE_STATE_VERSION) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (candidate['mode'] !== STATUS_TIMELINE_MODE) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const outputPath = candidate['outputPath'];
|
|
64
|
+
const sessionName = candidate['sessionName'];
|
|
65
|
+
const startedAt = candidate['startedAt'];
|
|
66
|
+
if (typeof outputPath !== 'string' || outputPath.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (sessionName !== null && typeof sessionName !== 'string') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (typeof startedAt !== 'string' || startedAt.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
version: STATUS_TIMELINE_STATE_VERSION,
|
|
77
|
+
mode: STATUS_TIMELINE_MODE,
|
|
78
|
+
outputPath,
|
|
79
|
+
sessionName,
|
|
80
|
+
startedAt,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
insertTaskComposerText,
|
|
3
|
+
taskComposerBackspace,
|
|
4
|
+
taskComposerDeleteForward,
|
|
5
|
+
taskComposerDeleteToLineEnd,
|
|
6
|
+
taskComposerDeleteToLineStart,
|
|
7
|
+
taskComposerDeleteWordLeft,
|
|
8
|
+
taskComposerMoveLeft,
|
|
9
|
+
taskComposerMoveLineEnd,
|
|
10
|
+
taskComposerMoveLineStart,
|
|
11
|
+
taskComposerMoveRight,
|
|
12
|
+
taskComposerMoveVertical,
|
|
13
|
+
taskComposerMoveWordLeft,
|
|
14
|
+
taskComposerMoveWordRight,
|
|
15
|
+
type TaskComposerBuffer,
|
|
16
|
+
} from '../task-composer.ts';
|
|
17
|
+
import { detectTaskScreenKeybindingAction } from '../task-screen-keybindings.ts';
|
|
18
|
+
|
|
19
|
+
type TaskPaneActionShortcut =
|
|
20
|
+
| 'task.ready'
|
|
21
|
+
| 'task.draft'
|
|
22
|
+
| 'task.complete'
|
|
23
|
+
| 'task.reorder-up'
|
|
24
|
+
| 'task.reorder-down';
|
|
25
|
+
|
|
26
|
+
interface TaskEditorTargetDraft {
|
|
27
|
+
kind: 'draft';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TaskEditorTargetTask {
|
|
31
|
+
kind: 'task';
|
|
32
|
+
taskId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface HandleTaskPaneShortcutInputOptions {
|
|
36
|
+
input: Buffer;
|
|
37
|
+
mainPaneMode: 'conversation' | 'project' | 'home';
|
|
38
|
+
taskScreenKeybindings: Parameters<typeof detectTaskScreenKeybindingAction>[1];
|
|
39
|
+
taskEditorTarget: TaskEditorTargetDraft | TaskEditorTargetTask;
|
|
40
|
+
homeEditorBuffer: () => TaskComposerBuffer;
|
|
41
|
+
updateHomeEditorBuffer: (next: TaskComposerBuffer) => void;
|
|
42
|
+
moveTaskEditorFocusUp: () => void;
|
|
43
|
+
focusDraftComposer: () => void;
|
|
44
|
+
submitDraftTaskFromComposer: () => void;
|
|
45
|
+
runTaskPaneAction: (action: TaskPaneActionShortcut) => void;
|
|
46
|
+
selectRepositoryByDirection: (direction: 1 | -1) => void;
|
|
47
|
+
getTaskRepositoryDropdownOpen: () => boolean;
|
|
48
|
+
setTaskRepositoryDropdownOpen: (open: boolean) => void;
|
|
49
|
+
markDirty: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function handleTaskPaneShortcutInput(options: HandleTaskPaneShortcutInputOptions): boolean {
|
|
53
|
+
const {
|
|
54
|
+
input,
|
|
55
|
+
mainPaneMode,
|
|
56
|
+
taskScreenKeybindings,
|
|
57
|
+
taskEditorTarget,
|
|
58
|
+
homeEditorBuffer,
|
|
59
|
+
updateHomeEditorBuffer,
|
|
60
|
+
moveTaskEditorFocusUp,
|
|
61
|
+
focusDraftComposer,
|
|
62
|
+
submitDraftTaskFromComposer,
|
|
63
|
+
runTaskPaneAction,
|
|
64
|
+
selectRepositoryByDirection,
|
|
65
|
+
getTaskRepositoryDropdownOpen,
|
|
66
|
+
setTaskRepositoryDropdownOpen,
|
|
67
|
+
markDirty,
|
|
68
|
+
} = options;
|
|
69
|
+
if (mainPaneMode !== 'home') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const action = detectTaskScreenKeybindingAction(input, taskScreenKeybindings);
|
|
73
|
+
if (action !== null) {
|
|
74
|
+
if (action === 'mux.home.repo.dropdown.toggle') {
|
|
75
|
+
setTaskRepositoryDropdownOpen(!getTaskRepositoryDropdownOpen());
|
|
76
|
+
markDirty();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (action === 'mux.home.repo.next') {
|
|
80
|
+
setTaskRepositoryDropdownOpen(true);
|
|
81
|
+
selectRepositoryByDirection(1);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (action === 'mux.home.repo.previous') {
|
|
85
|
+
setTaskRepositoryDropdownOpen(true);
|
|
86
|
+
selectRepositoryByDirection(-1);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (action === 'mux.home.task.status.ready') {
|
|
90
|
+
runTaskPaneAction('task.ready');
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (action === 'mux.home.task.status.draft') {
|
|
94
|
+
runTaskPaneAction('task.draft');
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (action === 'mux.home.task.status.complete') {
|
|
98
|
+
runTaskPaneAction('task.complete');
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (action === 'mux.home.task.reorder.up') {
|
|
102
|
+
runTaskPaneAction('task.reorder-up');
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (action === 'mux.home.task.reorder.down') {
|
|
106
|
+
runTaskPaneAction('task.reorder-down');
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (action === 'mux.home.task.newline') {
|
|
110
|
+
updateHomeEditorBuffer(insertTaskComposerText(homeEditorBuffer(), '\n'));
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (action === 'mux.home.task.submit') {
|
|
114
|
+
if (taskEditorTarget.kind === 'draft') {
|
|
115
|
+
submitDraftTaskFromComposer();
|
|
116
|
+
} else {
|
|
117
|
+
focusDraftComposer();
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (action === 'mux.home.editor.cursor.left') {
|
|
122
|
+
updateHomeEditorBuffer(taskComposerMoveLeft(homeEditorBuffer()));
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (action === 'mux.home.editor.cursor.right') {
|
|
126
|
+
updateHomeEditorBuffer(taskComposerMoveRight(homeEditorBuffer()));
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (action === 'mux.home.editor.cursor.up') {
|
|
130
|
+
const vertical = taskComposerMoveVertical(homeEditorBuffer(), -1);
|
|
131
|
+
if (vertical.hitBoundary) {
|
|
132
|
+
moveTaskEditorFocusUp();
|
|
133
|
+
} else {
|
|
134
|
+
updateHomeEditorBuffer(vertical.next);
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (action === 'mux.home.editor.cursor.down') {
|
|
139
|
+
if (taskEditorTarget.kind === 'task') {
|
|
140
|
+
const vertical = taskComposerMoveVertical(homeEditorBuffer(), 1);
|
|
141
|
+
if (vertical.hitBoundary) {
|
|
142
|
+
focusDraftComposer();
|
|
143
|
+
} else {
|
|
144
|
+
updateHomeEditorBuffer(vertical.next);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
updateHomeEditorBuffer(taskComposerMoveVertical(homeEditorBuffer(), 1).next);
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (action === 'mux.home.editor.line.start') {
|
|
152
|
+
updateHomeEditorBuffer(taskComposerMoveLineStart(homeEditorBuffer()));
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (action === 'mux.home.editor.line.end') {
|
|
156
|
+
updateHomeEditorBuffer(taskComposerMoveLineEnd(homeEditorBuffer()));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (action === 'mux.home.editor.word.left') {
|
|
160
|
+
updateHomeEditorBuffer(taskComposerMoveWordLeft(homeEditorBuffer()));
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (action === 'mux.home.editor.word.right') {
|
|
164
|
+
updateHomeEditorBuffer(taskComposerMoveWordRight(homeEditorBuffer()));
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (action === 'mux.home.editor.delete.backward') {
|
|
168
|
+
updateHomeEditorBuffer(taskComposerBackspace(homeEditorBuffer()));
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (action === 'mux.home.editor.delete.forward') {
|
|
172
|
+
updateHomeEditorBuffer(taskComposerDeleteForward(homeEditorBuffer()));
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
if (action === 'mux.home.editor.delete.word.backward') {
|
|
176
|
+
updateHomeEditorBuffer(taskComposerDeleteWordLeft(homeEditorBuffer()));
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
if (action === 'mux.home.editor.delete.line.start') {
|
|
180
|
+
updateHomeEditorBuffer(taskComposerDeleteToLineStart(homeEditorBuffer()));
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (action === 'mux.home.editor.delete.line.end') {
|
|
184
|
+
updateHomeEditorBuffer(taskComposerDeleteToLineEnd(homeEditorBuffer()));
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (input.includes(0x1b)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let next = homeEditorBuffer();
|
|
194
|
+
let changed = false;
|
|
195
|
+
for (const byte of input) {
|
|
196
|
+
if (byte >= 32 && byte <= 126) {
|
|
197
|
+
next = insertTaskComposerText(next, String.fromCharCode(byte));
|
|
198
|
+
changed = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!changed) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
updateHomeEditorBuffer(next);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { extractOscColorReplies } from './palette-parsing.ts';
|
|
2
|
+
|
|
3
|
+
export async function probeTerminalPalette(timeoutMs = 80): Promise<{
|
|
4
|
+
foregroundHex?: string;
|
|
5
|
+
backgroundHex?: string;
|
|
6
|
+
indexedHexByCode?: Record<number, string>;
|
|
7
|
+
}> {
|
|
8
|
+
return await new Promise((resolve) => {
|
|
9
|
+
let finished = false;
|
|
10
|
+
let buffer = '';
|
|
11
|
+
let foregroundHex: string | undefined;
|
|
12
|
+
let backgroundHex: string | undefined;
|
|
13
|
+
const indexedHexByCode: Record<number, string> = {};
|
|
14
|
+
|
|
15
|
+
const finish = (): void => {
|
|
16
|
+
if (finished) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
finished = true;
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
process.stdin.off('data', onData);
|
|
22
|
+
resolve({
|
|
23
|
+
...(foregroundHex !== undefined
|
|
24
|
+
? {
|
|
25
|
+
foregroundHex,
|
|
26
|
+
}
|
|
27
|
+
: {}),
|
|
28
|
+
...(backgroundHex !== undefined
|
|
29
|
+
? {
|
|
30
|
+
backgroundHex,
|
|
31
|
+
}
|
|
32
|
+
: {}),
|
|
33
|
+
...(Object.keys(indexedHexByCode).length > 0
|
|
34
|
+
? {
|
|
35
|
+
indexedHexByCode,
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const onData = (chunk: Buffer): void => {
|
|
42
|
+
buffer += chunk.toString('utf8');
|
|
43
|
+
const extracted = extractOscColorReplies(buffer);
|
|
44
|
+
buffer = extracted.remainder;
|
|
45
|
+
|
|
46
|
+
if (extracted.foregroundHex !== undefined) {
|
|
47
|
+
foregroundHex = extracted.foregroundHex;
|
|
48
|
+
}
|
|
49
|
+
if (extracted.backgroundHex !== undefined) {
|
|
50
|
+
backgroundHex = extracted.backgroundHex;
|
|
51
|
+
}
|
|
52
|
+
for (const [key, value] of Object.entries(extracted.indexedHexByCode)) {
|
|
53
|
+
const index = Number.parseInt(key, 10);
|
|
54
|
+
if (Number.isInteger(index)) {
|
|
55
|
+
indexedHexByCode[index] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
foregroundHex !== undefined &&
|
|
61
|
+
backgroundHex !== undefined &&
|
|
62
|
+
Object.keys(indexedHexByCode).length >= 16
|
|
63
|
+
) {
|
|
64
|
+
finish();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
finish();
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
|
|
72
|
+
process.stdin.on('data', onData);
|
|
73
|
+
let probeSequence = '\u001b]10;?\u0007\u001b]11;?\u0007';
|
|
74
|
+
for (let idx = 0; idx < 16; idx += 1) {
|
|
75
|
+
probeSequence += `\u001b]4;${String(idx)};?\u0007`;
|
|
76
|
+
}
|
|
77
|
+
process.stdout.write(probeSequence);
|
|
78
|
+
});
|
|
79
|
+
}
|