@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,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reduceRepositoryFoldChordInput,
|
|
3
|
+
repositoryTreeArrowAction,
|
|
4
|
+
} from '../mux/live-mux/repository-folding.ts';
|
|
5
|
+
import type { LeftNavSelection } from '../mux/live-mux/left-nav.ts';
|
|
6
|
+
|
|
7
|
+
interface RepositoryFoldInputOptions {
|
|
8
|
+
readonly getLeftNavSelection: () => LeftNavSelection;
|
|
9
|
+
readonly getRepositoryToggleChordPrefixAtMs: () => number | null;
|
|
10
|
+
readonly setRepositoryToggleChordPrefixAtMs: (value: number | null) => void;
|
|
11
|
+
readonly conversations: ReadonlyMap<string, { directoryId: string | null }>;
|
|
12
|
+
readonly repositoryGroupIdForDirectory: (directoryId: string) => string;
|
|
13
|
+
readonly collapseRepositoryGroup: (repositoryGroupId: string) => void;
|
|
14
|
+
readonly expandRepositoryGroup: (repositoryGroupId: string) => void;
|
|
15
|
+
readonly collapseAllRepositoryGroups: () => void;
|
|
16
|
+
readonly expandAllRepositoryGroups: () => void;
|
|
17
|
+
readonly selectLeftNavRepository: (repositoryGroupId: string) => void;
|
|
18
|
+
readonly markDirty: () => void;
|
|
19
|
+
readonly chordTimeoutMs: number;
|
|
20
|
+
readonly collapseAllChordPrefix: Buffer;
|
|
21
|
+
readonly nowMs: () => number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class RepositoryFoldInput {
|
|
25
|
+
constructor(private readonly options: RepositoryFoldInputOptions) {}
|
|
26
|
+
|
|
27
|
+
private selectedRepositoryGroupId(): string | null {
|
|
28
|
+
const leftNavSelection = this.options.getLeftNavSelection();
|
|
29
|
+
if (leftNavSelection.kind === 'repository') {
|
|
30
|
+
return leftNavSelection.repositoryId;
|
|
31
|
+
}
|
|
32
|
+
if (leftNavSelection.kind === 'project') {
|
|
33
|
+
return this.options.repositoryGroupIdForDirectory(leftNavSelection.directoryId);
|
|
34
|
+
}
|
|
35
|
+
if (leftNavSelection.kind === 'conversation') {
|
|
36
|
+
const conversation = this.options.conversations.get(leftNavSelection.sessionId);
|
|
37
|
+
if (conversation?.directoryId !== null && conversation?.directoryId !== undefined) {
|
|
38
|
+
return this.options.repositoryGroupIdForDirectory(conversation.directoryId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
handleRepositoryTreeArrow(input: Buffer): boolean {
|
|
45
|
+
const repositoryId = this.selectedRepositoryGroupId();
|
|
46
|
+
const action = repositoryTreeArrowAction(
|
|
47
|
+
input,
|
|
48
|
+
this.options.getLeftNavSelection(),
|
|
49
|
+
repositoryId,
|
|
50
|
+
);
|
|
51
|
+
if (repositoryId === null || action === null) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (action === 'expand') {
|
|
55
|
+
this.options.expandRepositoryGroup(repositoryId);
|
|
56
|
+
this.options.selectLeftNavRepository(repositoryId);
|
|
57
|
+
this.options.markDirty();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (action === 'collapse') {
|
|
61
|
+
this.options.collapseRepositoryGroup(repositoryId);
|
|
62
|
+
this.options.selectLeftNavRepository(repositoryId);
|
|
63
|
+
this.options.markDirty();
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
handleRepositoryFoldChords(input: Buffer): boolean {
|
|
70
|
+
const reduced = reduceRepositoryFoldChordInput({
|
|
71
|
+
input,
|
|
72
|
+
leftNavSelection: this.options.getLeftNavSelection(),
|
|
73
|
+
nowMs: this.options.nowMs(),
|
|
74
|
+
prefixAtMs: this.options.getRepositoryToggleChordPrefixAtMs(),
|
|
75
|
+
chordTimeoutMs: this.options.chordTimeoutMs,
|
|
76
|
+
collapseAllChordPrefix: this.options.collapseAllChordPrefix,
|
|
77
|
+
});
|
|
78
|
+
this.options.setRepositoryToggleChordPrefixAtMs(reduced.nextPrefixAtMs);
|
|
79
|
+
if (reduced.action === 'expand-all') {
|
|
80
|
+
this.options.expandAllRepositoryGroups();
|
|
81
|
+
this.options.markDirty();
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (reduced.action === 'collapse-all') {
|
|
85
|
+
this.options.collapseAllRepositoryGroups();
|
|
86
|
+
this.options.markDirty();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return reduced.consumed;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/ui/screen.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { diffRenderedRows } from '../mux/dual-pane-core.ts';
|
|
2
|
+
import { cursorStyleEqual, cursorStyleToDecscusr } from '../mux/render-frame.ts';
|
|
3
|
+
import { findAnsiIntegrityIssues } from '../mux/ansi-integrity.ts';
|
|
4
|
+
|
|
5
|
+
export interface ScreenCursorStyle {
|
|
6
|
+
readonly shape: 'block' | 'underline' | 'bar';
|
|
7
|
+
readonly blinking: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ScreenLayout {
|
|
11
|
+
readonly paneRows: number;
|
|
12
|
+
readonly rightCols: number;
|
|
13
|
+
readonly rightStartCol: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ScreenRenderFrame {
|
|
17
|
+
readonly modes: {
|
|
18
|
+
readonly bracketedPaste: boolean;
|
|
19
|
+
};
|
|
20
|
+
readonly cursor: {
|
|
21
|
+
readonly style: ScreenCursorStyle;
|
|
22
|
+
readonly visible: boolean;
|
|
23
|
+
readonly row: number;
|
|
24
|
+
readonly col: number;
|
|
25
|
+
};
|
|
26
|
+
readonly viewport: {
|
|
27
|
+
readonly followOutput: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ScreenFlushInput {
|
|
32
|
+
readonly layout: ScreenLayout;
|
|
33
|
+
readonly rows: readonly string[];
|
|
34
|
+
readonly rightFrame: ScreenRenderFrame | null;
|
|
35
|
+
readonly selectionRows: readonly number[];
|
|
36
|
+
readonly selectionOverlay: string;
|
|
37
|
+
readonly validateAnsi: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ScreenFlushResult {
|
|
41
|
+
readonly wroteOutput: boolean;
|
|
42
|
+
readonly changedRowCount: number;
|
|
43
|
+
readonly shouldShowCursor: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ScreenDependencies {
|
|
47
|
+
readonly writeOutput: (output: string) => void;
|
|
48
|
+
readonly writeError: (output: string) => void;
|
|
49
|
+
readonly findAnsiIssues: (rows: readonly string[]) => readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TERMINAL_SYNC_UPDATE_BEGIN = '\u001b[?2026h';
|
|
53
|
+
const TERMINAL_SYNC_UPDATE_END = '\u001b[?2026l';
|
|
54
|
+
|
|
55
|
+
function mergeUniqueRows(left: readonly number[], right: readonly number[]): readonly number[] {
|
|
56
|
+
if (left.length === 0) {
|
|
57
|
+
return right;
|
|
58
|
+
}
|
|
59
|
+
if (right.length === 0) {
|
|
60
|
+
return left;
|
|
61
|
+
}
|
|
62
|
+
const merged = new Set<number>();
|
|
63
|
+
for (const row of left) {
|
|
64
|
+
merged.add(row);
|
|
65
|
+
}
|
|
66
|
+
for (const row of right) {
|
|
67
|
+
merged.add(row);
|
|
68
|
+
}
|
|
69
|
+
return [...merged].sort((a, b) => a - b);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class Screen {
|
|
73
|
+
private dirty = true;
|
|
74
|
+
private previousRows: readonly string[] = [];
|
|
75
|
+
private previousSelectionRows: readonly number[] = [];
|
|
76
|
+
private forceFullClear = true;
|
|
77
|
+
private renderedCursorVisible: boolean | null = null;
|
|
78
|
+
private renderedCursorStyle: ScreenCursorStyle | null = null;
|
|
79
|
+
private renderedBracketedPaste: boolean | null = null;
|
|
80
|
+
private ansiValidationReported = false;
|
|
81
|
+
|
|
82
|
+
private readonly deps: ScreenDependencies;
|
|
83
|
+
|
|
84
|
+
constructor(deps?: Partial<ScreenDependencies>) {
|
|
85
|
+
this.deps = {
|
|
86
|
+
writeOutput: deps?.writeOutput ?? ((output) => process.stdout.write(output)),
|
|
87
|
+
writeError: deps?.writeError ?? ((output) => process.stderr.write(output)),
|
|
88
|
+
findAnsiIssues: deps?.findAnsiIssues ?? findAnsiIntegrityIssues,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isDirty(): boolean {
|
|
93
|
+
return this.dirty;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
markDirty(): void {
|
|
97
|
+
this.dirty = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clearDirty(): void {
|
|
101
|
+
this.dirty = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
resetFrameCache(): void {
|
|
105
|
+
this.previousRows = [];
|
|
106
|
+
this.forceFullClear = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
flush(input: ScreenFlushInput): ScreenFlushResult {
|
|
110
|
+
if (!this.dirty) {
|
|
111
|
+
return {
|
|
112
|
+
wroteOutput: false,
|
|
113
|
+
changedRowCount: 0,
|
|
114
|
+
shouldShowCursor: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (input.validateAnsi) {
|
|
119
|
+
const issues = this.deps.findAnsiIssues(input.rows);
|
|
120
|
+
if (issues.length > 0 && !this.ansiValidationReported) {
|
|
121
|
+
this.ansiValidationReported = true;
|
|
122
|
+
this.deps.writeError(`[mux] ansi-integrity-failed ${issues.join(' | ')}\n`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const diff = this.forceFullClear
|
|
127
|
+
? diffRenderedRows(input.rows, [])
|
|
128
|
+
: diffRenderedRows(input.rows, this.previousRows);
|
|
129
|
+
const overlayResetRows = mergeUniqueRows(this.previousSelectionRows, input.selectionRows);
|
|
130
|
+
|
|
131
|
+
let output = '';
|
|
132
|
+
if (this.forceFullClear) {
|
|
133
|
+
output += '\u001b[?25l\u001b[H\u001b[2J';
|
|
134
|
+
this.forceFullClear = false;
|
|
135
|
+
this.renderedCursorVisible = false;
|
|
136
|
+
this.renderedCursorStyle = null;
|
|
137
|
+
this.renderedBracketedPaste = null;
|
|
138
|
+
}
|
|
139
|
+
output += diff.output;
|
|
140
|
+
|
|
141
|
+
if (overlayResetRows.length > 0) {
|
|
142
|
+
const changedRows = new Set<number>(diff.changedRows);
|
|
143
|
+
for (const row of overlayResetRows) {
|
|
144
|
+
if (row < 0 || row >= input.layout.paneRows || changedRows.has(row)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const rowContent = input.rows[row] ?? '';
|
|
148
|
+
output += `\u001b[${String(row + 1)};1H\u001b[2K${rowContent}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let shouldShowCursor = false;
|
|
153
|
+
if (input.rightFrame !== null) {
|
|
154
|
+
const shouldEnableBracketedPaste = input.rightFrame.modes.bracketedPaste;
|
|
155
|
+
if (this.renderedBracketedPaste !== shouldEnableBracketedPaste) {
|
|
156
|
+
output += shouldEnableBracketedPaste ? '\u001b[?2004h' : '\u001b[?2004l';
|
|
157
|
+
this.renderedBracketedPaste = shouldEnableBracketedPaste;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!cursorStyleEqual(this.renderedCursorStyle, input.rightFrame.cursor.style)) {
|
|
161
|
+
output += cursorStyleToDecscusr(input.rightFrame.cursor.style);
|
|
162
|
+
this.renderedCursorStyle = input.rightFrame.cursor.style;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
output += input.selectionOverlay;
|
|
166
|
+
|
|
167
|
+
shouldShowCursor =
|
|
168
|
+
input.rightFrame.viewport.followOutput &&
|
|
169
|
+
input.rightFrame.cursor.visible &&
|
|
170
|
+
input.rightFrame.cursor.row >= 0 &&
|
|
171
|
+
input.rightFrame.cursor.row < input.layout.paneRows &&
|
|
172
|
+
input.rightFrame.cursor.col >= 0 &&
|
|
173
|
+
input.rightFrame.cursor.col < input.layout.rightCols;
|
|
174
|
+
|
|
175
|
+
if (shouldShowCursor) {
|
|
176
|
+
if (this.renderedCursorVisible !== true) {
|
|
177
|
+
output += '\u001b[?25h';
|
|
178
|
+
this.renderedCursorVisible = true;
|
|
179
|
+
}
|
|
180
|
+
output += `\u001b[${String(input.rightFrame.cursor.row + 1)};${String(input.layout.rightStartCol + input.rightFrame.cursor.col)}H`;
|
|
181
|
+
} else if (this.renderedCursorVisible !== false) {
|
|
182
|
+
output += '\u001b[?25l';
|
|
183
|
+
this.renderedCursorVisible = false;
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
if (this.renderedBracketedPaste !== false) {
|
|
187
|
+
output += '\u001b[?2004l';
|
|
188
|
+
this.renderedBracketedPaste = false;
|
|
189
|
+
}
|
|
190
|
+
if (this.renderedCursorVisible !== false) {
|
|
191
|
+
output += '\u001b[?25l';
|
|
192
|
+
this.renderedCursorVisible = false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (output.length > 0) {
|
|
197
|
+
this.deps.writeOutput(`${TERMINAL_SYNC_UPDATE_BEGIN}${output}${TERMINAL_SYNC_UPDATE_END}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.previousRows = diff.nextRows;
|
|
201
|
+
this.previousSelectionRows = input.selectionRows;
|
|
202
|
+
this.dirty = false;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
wroteOutput: output.length > 0,
|
|
206
|
+
changedRowCount: diff.changedRows.length,
|
|
207
|
+
shouldShowCursor,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { measureDisplayWidth } from '../terminal/snapshot-oracle.ts';
|
|
2
|
+
|
|
3
|
+
export type UiColor =
|
|
4
|
+
| { kind: 'default' }
|
|
5
|
+
| { kind: 'indexed'; index: number }
|
|
6
|
+
| { kind: 'rgb'; r: number; g: number; b: number };
|
|
7
|
+
|
|
8
|
+
export interface UiStyle {
|
|
9
|
+
readonly fg: UiColor;
|
|
10
|
+
readonly bg: UiColor;
|
|
11
|
+
readonly bold: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UiCell {
|
|
15
|
+
glyph: string;
|
|
16
|
+
continued: boolean;
|
|
17
|
+
style: UiStyle;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UiSurface {
|
|
21
|
+
readonly cols: number;
|
|
22
|
+
readonly rows: number;
|
|
23
|
+
readonly baseStyle: UiStyle;
|
|
24
|
+
readonly cells: UiCell[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_COLOR: UiColor = {
|
|
28
|
+
kind: 'default',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_UI_STYLE: UiStyle = {
|
|
32
|
+
fg: DEFAULT_COLOR,
|
|
33
|
+
bg: DEFAULT_COLOR,
|
|
34
|
+
bold: false,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function cloneColor(color: UiColor): UiColor {
|
|
38
|
+
if (color.kind === 'default') {
|
|
39
|
+
return DEFAULT_COLOR;
|
|
40
|
+
}
|
|
41
|
+
if (color.kind === 'indexed') {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'indexed',
|
|
44
|
+
index: color.index,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
kind: 'rgb',
|
|
49
|
+
r: color.r,
|
|
50
|
+
g: color.g,
|
|
51
|
+
b: color.b,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cloneStyle(style: UiStyle): UiStyle {
|
|
56
|
+
return {
|
|
57
|
+
fg: cloneColor(style.fg),
|
|
58
|
+
bg: cloneColor(style.bg),
|
|
59
|
+
bold: style.bold,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function styleEqual(left: UiStyle, right: UiStyle): boolean {
|
|
64
|
+
if (left.bold !== right.bold) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (left.fg.kind !== right.fg.kind || left.bg.kind !== right.bg.kind) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (left.fg.kind === 'indexed') {
|
|
72
|
+
if (left.fg.index !== (right.fg as Extract<UiColor, { kind: 'indexed' }>).index) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
} else if (left.fg.kind === 'rgb') {
|
|
76
|
+
const typedRight = right.fg as Extract<UiColor, { kind: 'rgb' }>;
|
|
77
|
+
if (left.fg.r !== typedRight.r || left.fg.g !== typedRight.g || left.fg.b !== typedRight.b) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (left.bg.kind === 'indexed') {
|
|
83
|
+
if (left.bg.index !== (right.bg as Extract<UiColor, { kind: 'indexed' }>).index) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
} else if (left.bg.kind === 'rgb') {
|
|
87
|
+
const typedRight = right.bg as Extract<UiColor, { kind: 'rgb' }>;
|
|
88
|
+
if (left.bg.r !== typedRight.r || left.bg.g !== typedRight.g || left.bg.b !== typedRight.b) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createCell(style: UiStyle): UiCell {
|
|
97
|
+
return {
|
|
98
|
+
glyph: ' ',
|
|
99
|
+
continued: false,
|
|
100
|
+
style: cloneStyle(style),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cellOffset(surface: UiSurface, col: number, row: number): number {
|
|
105
|
+
return row * surface.cols + col;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function colorSgrCodes(color: UiColor, target: 'fg' | 'bg'): readonly string[] {
|
|
109
|
+
const prefix = target === 'fg' ? '38' : '48';
|
|
110
|
+
if (color.kind === 'default') {
|
|
111
|
+
return [target === 'fg' ? '39' : '49'];
|
|
112
|
+
}
|
|
113
|
+
if (color.kind === 'indexed') {
|
|
114
|
+
return [prefix, '5', String(color.index)];
|
|
115
|
+
}
|
|
116
|
+
return [prefix, '2', String(color.r), String(color.g), String(color.b)];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function styleToSgr(style: UiStyle): string {
|
|
120
|
+
const codes: string[] = ['0'];
|
|
121
|
+
if (style.bold) {
|
|
122
|
+
codes.push('1');
|
|
123
|
+
}
|
|
124
|
+
codes.push(...colorSgrCodes(style.fg, 'fg'));
|
|
125
|
+
codes.push(...colorSgrCodes(style.bg, 'bg'));
|
|
126
|
+
return `\u001b[${codes.join(';')}m`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createUiSurface(
|
|
130
|
+
cols: number,
|
|
131
|
+
rows: number,
|
|
132
|
+
baseStyle: UiStyle = DEFAULT_UI_STYLE,
|
|
133
|
+
): UiSurface {
|
|
134
|
+
const safeCols = Math.max(1, cols);
|
|
135
|
+
const safeRows = Math.max(1, rows);
|
|
136
|
+
const typedBaseStyle = cloneStyle(baseStyle);
|
|
137
|
+
return {
|
|
138
|
+
cols: safeCols,
|
|
139
|
+
rows: safeRows,
|
|
140
|
+
baseStyle: typedBaseStyle,
|
|
141
|
+
cells: Array.from({ length: safeCols * safeRows }, () => createCell(typedBaseStyle)),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function fillUiRow(surface: UiSurface, row: number, style: UiStyle): void {
|
|
146
|
+
if (row < 0 || row >= surface.rows) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const typedStyle = cloneStyle(style);
|
|
150
|
+
for (let col = 0; col < surface.cols; col += 1) {
|
|
151
|
+
const cell = surface.cells[cellOffset(surface, col, row)]!;
|
|
152
|
+
cell.glyph = ' ';
|
|
153
|
+
cell.continued = false;
|
|
154
|
+
cell.style = typedStyle;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function drawUiText(
|
|
159
|
+
surface: UiSurface,
|
|
160
|
+
colStart: number,
|
|
161
|
+
row: number,
|
|
162
|
+
text: string,
|
|
163
|
+
style: UiStyle = surface.baseStyle,
|
|
164
|
+
): void {
|
|
165
|
+
if (row < 0 || row >= surface.rows || colStart >= surface.cols) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
let col = Math.max(0, colStart);
|
|
169
|
+
const typedStyle = cloneStyle(style);
|
|
170
|
+
|
|
171
|
+
for (const glyph of text) {
|
|
172
|
+
const width = Math.max(0, measureDisplayWidth(glyph));
|
|
173
|
+
if (width === 0) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (col >= surface.cols) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (width === 1) {
|
|
181
|
+
const cell = surface.cells[cellOffset(surface, col, row)]!;
|
|
182
|
+
cell.glyph = glyph;
|
|
183
|
+
cell.continued = false;
|
|
184
|
+
cell.style = typedStyle;
|
|
185
|
+
col += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (col + width > surface.cols) {
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const first = surface.cells[cellOffset(surface, col, row)]!;
|
|
194
|
+
first.glyph = glyph;
|
|
195
|
+
first.continued = false;
|
|
196
|
+
first.style = typedStyle;
|
|
197
|
+
for (let offset = 1; offset < width && col + offset < surface.cols; offset += 1) {
|
|
198
|
+
const cell = surface.cells[cellOffset(surface, col + offset, row)]!;
|
|
199
|
+
cell.glyph = '';
|
|
200
|
+
cell.continued = true;
|
|
201
|
+
cell.style = typedStyle;
|
|
202
|
+
}
|
|
203
|
+
col += width;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function renderUiSurfaceAnsiRows(surface: UiSurface): readonly string[] {
|
|
208
|
+
const rows: string[] = [];
|
|
209
|
+
for (let row = 0; row < surface.rows; row += 1) {
|
|
210
|
+
let output = '';
|
|
211
|
+
let lastStyle: UiStyle | null = null;
|
|
212
|
+
for (let col = 0; col < surface.cols; col += 1) {
|
|
213
|
+
const cell = surface.cells[cellOffset(surface, col, row)]!;
|
|
214
|
+
if (lastStyle === null || !styleEqual(lastStyle, cell.style)) {
|
|
215
|
+
output += styleToSgr(cell.style);
|
|
216
|
+
lastStyle = cell.style;
|
|
217
|
+
}
|
|
218
|
+
output += cell.continued ? '' : cell.glyph.length > 0 ? cell.glyph : ' ';
|
|
219
|
+
}
|
|
220
|
+
output += '\u001b[0m';
|
|
221
|
+
rows.push(output);
|
|
222
|
+
}
|
|
223
|
+
return rows;
|
|
224
|
+
}
|