@jmoyers/harness 0.1.11 → 0.1.20
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/README.md +31 -39
- package/package.json +31 -11
- package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
- package/packages/harness-ai/src/stream-text.ts +13 -91
- package/packages/harness-ui/src/frame-primitives.ts +158 -0
- package/packages/harness-ui/src/index.ts +18 -0
- package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
- package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
- package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
- package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
- package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
- package/packages/harness-ui/src/interaction/input.ts +420 -0
- package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
- package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
- package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
- package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
- package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
- package/packages/harness-ui/src/kit.ts +476 -0
- package/packages/harness-ui/src/layout.ts +238 -0
- package/packages/harness-ui/src/modal-manager.ts +222 -0
- package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
- package/packages/harness-ui/src/surface.ts +252 -0
- package/packages/harness-ui/src/text-layout.ts +210 -0
- package/packages/nim-core/src/contracts.ts +239 -0
- package/packages/nim-core/src/event-store.ts +299 -0
- package/packages/nim-core/src/events.ts +53 -0
- package/packages/nim-core/src/index.ts +9 -0
- package/packages/nim-core/src/provider-router.ts +129 -0
- package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
- package/packages/nim-core/src/runtime-factory.ts +49 -0
- package/packages/nim-core/src/runtime.ts +1797 -0
- package/packages/nim-core/src/session-store.ts +516 -0
- package/packages/nim-core/src/telemetry.ts +48 -0
- package/packages/nim-test-tui/src/index.ts +150 -0
- package/packages/nim-ui-core/src/index.ts +1 -0
- package/packages/nim-ui-core/src/projection.ts +87 -0
- package/scripts/codex-live-mux-runtime.ts +2 -3872
- package/scripts/control-plane-daemon.ts +11 -0
- package/scripts/harness-bin.js +5 -0
- package/scripts/harness-commands.ts +300 -0
- package/scripts/harness-runtime.ts +82 -0
- package/scripts/harness.ts +33 -3019
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -0
- package/src/cli/gateway/runtime.ts +1872 -0
- package/src/cli/parsing/flags.ts +23 -0
- package/src/cli/parsing/session.ts +42 -0
- package/src/cli/runtime/context.ts +193 -0
- package/src/cli/runtime-app/application.ts +392 -0
- package/src/cli/runtime-infra/gateway-control.ts +729 -0
- package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
- package/src/cli/workflows/runtime.ts +965 -0
- package/src/clients/tui/left-rail-interactions.ts +519 -0
- package/src/clients/tui/main-pane-interactions.ts +509 -0
- package/src/clients/tui/modal-input-routing.ts +71 -0
- package/src/clients/tui/render-snapshot-adapter.ts +88 -0
- package/src/clients/web/synced-selectors.ts +132 -0
- package/src/codex/live-session.ts +82 -29
- package/src/config/config-core.ts +348 -8
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/control-plane/agent-realtime-api.ts +82 -427
- package/src/control-plane/session-summary.ts +10 -81
- package/src/control-plane/status/reducer-base.ts +12 -12
- package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
- package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
- package/src/control-plane/stream-client.ts +12 -2
- package/src/control-plane/stream-command-parser.ts +83 -143
- package/src/control-plane/stream-protocol.ts +53 -37
- package/src/control-plane/stream-server-command.ts +376 -69
- package/src/control-plane/stream-server-session-runtime.ts +3 -2
- package/src/control-plane/stream-server.ts +864 -70
- package/src/control-plane/stream-session-runtime-types.ts +41 -0
- package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
- package/src/core/state/observed-stream-cursor.ts +43 -0
- package/src/core/state/synced-observed-state.ts +273 -0
- package/src/core/store/harness-synced-store.ts +81 -0
- package/src/diff/budget.ts +136 -0
- package/src/diff/build.ts +289 -0
- package/src/diff/chunker.ts +146 -0
- package/src/diff/git-invoke.ts +315 -0
- package/src/diff/git-parse.ts +472 -0
- package/src/diff/hash.ts +70 -0
- package/src/diff/index.ts +24 -0
- package/src/diff/normalize.ts +134 -0
- package/src/diff/types.ts +178 -0
- package/src/diff-ui/args.ts +346 -0
- package/src/diff-ui/commands.ts +123 -0
- package/src/diff-ui/finder.ts +94 -0
- package/src/diff-ui/highlight.ts +127 -0
- package/src/diff-ui/index.ts +2 -0
- package/src/diff-ui/model.ts +141 -0
- package/src/diff-ui/pager.ts +412 -0
- package/src/diff-ui/render.ts +337 -0
- package/src/diff-ui/runtime.ts +379 -0
- package/src/diff-ui/state.ts +224 -0
- package/src/diff-ui/types.ts +236 -0
- package/src/domain/workspace.ts +68 -5
- package/src/mux/control-plane-op-queue.ts +93 -7
- package/src/mux/conversation-rail.ts +28 -71
- package/src/mux/dual-pane-core.ts +13 -13
- package/src/mux/harness-core-ui.ts +313 -42
- package/src/mux/input-shortcuts.ts +13 -131
- package/src/mux/keybinding-catalog.ts +340 -0
- package/src/mux/keybinding-registry.ts +103 -0
- package/src/mux/live-mux/command-menu-open-in.ts +280 -0
- package/src/mux/live-mux/command-menu.ts +167 -4
- package/src/mux/live-mux/conversation-state.ts +13 -0
- package/src/mux/live-mux/directory-resolution.ts +1 -1
- package/src/mux/live-mux/git-snapshot.ts +33 -2
- package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
- package/src/mux/live-mux/home-pane-drop.ts +1 -1
- package/src/mux/live-mux/home-pane-pointer.ts +10 -0
- package/src/mux/live-mux/input-forwarding.ts +59 -2
- package/src/mux/live-mux/left-nav-activation.ts +124 -7
- package/src/mux/live-mux/left-nav.ts +35 -0
- package/src/mux/live-mux/link-click.ts +292 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
- package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
- package/src/mux/live-mux/modal-input-reducers.ts +77 -12
- package/src/mux/live-mux/modal-overlays.ts +168 -34
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
- package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
- package/src/mux/live-mux/pointer-routing.ts +5 -2
- package/src/mux/live-mux/project-pane-pointer.ts +8 -0
- package/src/mux/live-mux/rail-layout.ts +33 -30
- package/src/mux/live-mux/release-notes.ts +383 -0
- package/src/mux/live-mux/render-trace-analysis.ts +52 -7
- package/src/mux/live-mux/repository-folding.ts +3 -0
- package/src/mux/live-mux/selection.ts +0 -4
- package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
- package/src/mux/project-pane-github-review.ts +271 -0
- package/src/mux/render-frame.ts +4 -0
- package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
- package/src/mux/task-composer.ts +21 -14
- package/src/mux/task-focused-pane.ts +118 -117
- package/src/mux/task-screen-keybindings.ts +10 -101
- package/src/mux/workspace-rail-model.ts +270 -104
- package/src/mux/workspace-rail.ts +45 -22
- package/src/pty/session-broker.ts +1 -1
- package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
- package/src/services/control-plane.ts +50 -32
- package/src/services/conversation-lifecycle.ts +118 -87
- package/src/services/conversation-startup-hydration.ts +20 -12
- package/src/services/directory-hydration.ts +21 -16
- package/src/services/event-persistence.ts +7 -0
- package/src/services/left-rail-pointer-handler.ts +329 -0
- package/src/services/mux-ui-state-persistence.ts +5 -1
- package/src/services/recording.ts +34 -26
- package/src/services/runtime-command-menu-agent-tools.ts +1 -1
- package/src/services/runtime-control-actions.ts +79 -61
- package/src/services/runtime-control-plane-ops.ts +122 -83
- package/src/services/runtime-conversation-actions.ts +40 -26
- package/src/services/runtime-conversation-activation.ts +73 -46
- package/src/services/runtime-conversation-starter.ts +53 -45
- package/src/services/runtime-conversation-title-edit.ts +91 -80
- package/src/services/runtime-envelope-handler.ts +107 -105
- package/src/services/runtime-git-state.ts +42 -29
- package/src/services/runtime-layout-resize.ts +3 -1
- package/src/services/runtime-left-rail-render.ts +99 -63
- package/src/services/runtime-nim-cli-session.ts +438 -0
- package/src/services/runtime-nim-session.ts +705 -0
- package/src/services/runtime-nim-tool-bridge.ts +141 -0
- package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
- package/src/services/runtime-process-wiring.ts +29 -36
- package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
- package/src/services/runtime-render-flush.ts +63 -70
- package/src/services/runtime-render-lifecycle.ts +65 -64
- package/src/services/runtime-render-orchestrator.ts +55 -45
- package/src/services/runtime-render-pipeline.ts +106 -103
- package/src/services/runtime-render-state.ts +62 -49
- package/src/services/runtime-repository-actions.ts +97 -72
- package/src/services/runtime-right-pane-render.ts +80 -53
- package/src/services/runtime-shutdown.ts +38 -35
- package/src/services/runtime-stream-subscriptions.ts +35 -27
- package/src/services/runtime-task-composer-persistence.ts +71 -59
- package/src/services/runtime-task-composer-snapshot.ts +14 -0
- package/src/services/runtime-task-editor-actions.ts +46 -29
- package/src/services/runtime-task-pane-actions.ts +220 -134
- package/src/services/runtime-task-pane-shortcuts.ts +323 -123
- package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
- package/src/services/runtime-workspace-observed-events.ts +33 -184
- package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
- package/src/services/session-diagnostics-store.ts +217 -0
- package/src/services/startup-background-resume.ts +26 -21
- package/src/services/startup-orchestrator.ts +16 -13
- package/src/services/startup-paint-tracker.ts +29 -21
- package/src/services/startup-persisted-conversation-queue.ts +19 -13
- package/src/services/startup-settled-gate.ts +25 -15
- package/src/services/startup-shutdown.ts +18 -22
- package/src/services/startup-state-hydration.ts +44 -34
- package/src/services/startup-visibility.ts +12 -4
- package/src/services/task-pane-selection-actions.ts +89 -72
- package/src/services/task-planning-hydration.ts +24 -18
- package/src/services/task-planning-observed-events.ts +50 -52
- package/src/services/workspace-observed-events.ts +66 -63
- package/src/storage/storage-lifecycle-core.ts +438 -0
- package/src/store/control-plane-store-normalize.ts +33 -242
- package/src/store/control-plane-store-types.ts +1 -35
- package/src/store/control-plane-store.ts +360 -56
- package/src/store/event-store.ts +366 -8
- package/src/terminal/snapshot-oracle.ts +207 -94
- package/src/ui/mux-theme.ts +112 -8
- package/src/ui/panes/home-gridfire.ts +40 -31
- package/src/ui/panes/home.ts +10 -2
- package/src/ui/panes/nim.ts +315 -0
- package/src/mux/live-mux/actions-task.ts +0 -115
- package/src/mux/live-mux/left-rail-actions.ts +0 -118
- package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
- package/src/mux/live-mux/left-rail-pointer.ts +0 -74
- package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
- package/src/services/runtime-directory-actions.ts +0 -164
- package/src/services/runtime-input-pipeline.ts +0 -50
- package/src/services/runtime-input-router.ts +0 -195
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -137
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -279
- package/src/services/runtime-task-pane.ts +0 -62
- package/src/services/runtime-workspace-actions.ts +0 -158
- package/src/ui/conversation-input-forwarder.ts +0 -114
- package/src/ui/conversation-selection-input.ts +0 -103
- package/src/ui/global-shortcut-input.ts +0 -89
- package/src/ui/input.ts +0 -269
- package/src/ui/kit.ts +0 -509
- package/src/ui/left-nav-input.ts +0 -80
- package/src/ui/left-rail-pointer-input.ts +0 -148
- package/src/ui/modals/manager.ts +0 -218
- package/src/ui/repository-fold-input.ts +0 -91
- package/src/ui/surface.ts +0 -224
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const MIN_LEFT_PANE_COLS = 28;
|
|
2
|
+
const MIN_RIGHT_PANE_COLS = 20;
|
|
3
|
+
const DEFAULT_LEFT_PANE_WIDTH_PERCENT = 30;
|
|
4
|
+
const MIN_PANE_WIDTH_PERCENT = 1;
|
|
5
|
+
const MAX_PANE_WIDTH_PERCENT = 99;
|
|
6
|
+
const DEFAULT_BASE_LAYER_Z_INDEX = 0;
|
|
7
|
+
const DEFAULT_OVERLAY_LAYER_Z_INDEX = 100;
|
|
8
|
+
|
|
9
|
+
export interface UiLayoutRect {
|
|
10
|
+
readonly col: number;
|
|
11
|
+
readonly row: number;
|
|
12
|
+
readonly cols: number;
|
|
13
|
+
readonly rows: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type UiLayoutAnchor = 'viewport' | 'left-pane' | 'right-pane' | 'status';
|
|
17
|
+
|
|
18
|
+
export interface UiLayoutOverlay {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly col: number;
|
|
21
|
+
readonly row: number;
|
|
22
|
+
readonly cols: number;
|
|
23
|
+
readonly rows: number;
|
|
24
|
+
readonly anchor?: UiLayoutAnchor;
|
|
25
|
+
readonly zIndex?: number;
|
|
26
|
+
readonly clipToViewport?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ComputeUiLayoutOptions {
|
|
30
|
+
readonly leftCols?: number;
|
|
31
|
+
readonly paneWidthPercent?: number;
|
|
32
|
+
readonly statusRows?: number;
|
|
33
|
+
readonly overlays?: readonly UiLayoutOverlay[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UiLayoutLayer {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly kind: 'left-pane' | 'separator' | 'right-pane' | 'status' | 'overlay';
|
|
39
|
+
readonly zIndex: number;
|
|
40
|
+
readonly rect: UiLayoutRect;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UiDualPaneLayout {
|
|
44
|
+
readonly cols: number;
|
|
45
|
+
readonly rows: number;
|
|
46
|
+
readonly paneRows: number;
|
|
47
|
+
readonly statusRow: number;
|
|
48
|
+
readonly leftCols: number;
|
|
49
|
+
readonly rightCols: number;
|
|
50
|
+
readonly separatorCol: number;
|
|
51
|
+
readonly rightStartCol: number;
|
|
52
|
+
readonly viewport: UiLayoutRect;
|
|
53
|
+
readonly leftPane: UiLayoutRect;
|
|
54
|
+
readonly separator: UiLayoutRect;
|
|
55
|
+
readonly rightPane: UiLayoutRect;
|
|
56
|
+
readonly status: UiLayoutRect;
|
|
57
|
+
readonly layers: readonly UiLayoutLayer[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clamp(value: number, min: number, max: number): number {
|
|
61
|
+
if (value < min) {
|
|
62
|
+
return min;
|
|
63
|
+
}
|
|
64
|
+
if (value > max) {
|
|
65
|
+
return max;
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeInt(value: number, minimum: number): number {
|
|
71
|
+
if (!Number.isFinite(value)) {
|
|
72
|
+
return minimum;
|
|
73
|
+
}
|
|
74
|
+
return Math.max(minimum, Math.floor(value));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizePaneWidthPercent(value: number): number {
|
|
78
|
+
if (!Number.isFinite(value)) {
|
|
79
|
+
return DEFAULT_LEFT_PANE_WIDTH_PERCENT;
|
|
80
|
+
}
|
|
81
|
+
return clamp(value, MIN_PANE_WIDTH_PERCENT, MAX_PANE_WIDTH_PERCENT);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveLeftPaneCols(
|
|
85
|
+
normalizedCols: number,
|
|
86
|
+
requestedLeftCols: number | undefined,
|
|
87
|
+
paneWidthPercent: number | undefined,
|
|
88
|
+
): number {
|
|
89
|
+
const availablePaneCols = normalizedCols - 1;
|
|
90
|
+
const percent = normalizePaneWidthPercent(paneWidthPercent ?? DEFAULT_LEFT_PANE_WIDTH_PERCENT);
|
|
91
|
+
const defaultLeftCols = Math.round((availablePaneCols * percent) / 100);
|
|
92
|
+
const requested =
|
|
93
|
+
requestedLeftCols === undefined ? defaultLeftCols : Math.floor(requestedLeftCols);
|
|
94
|
+
|
|
95
|
+
let leftCols = clamp(requested, 1, availablePaneCols - 1);
|
|
96
|
+
if (normalizedCols >= MIN_LEFT_PANE_COLS + MIN_RIGHT_PANE_COLS + 1) {
|
|
97
|
+
leftCols = Math.max(MIN_LEFT_PANE_COLS, leftCols);
|
|
98
|
+
const maxLeft = availablePaneCols - MIN_RIGHT_PANE_COLS;
|
|
99
|
+
leftCols = Math.min(leftCols, maxLeft);
|
|
100
|
+
}
|
|
101
|
+
return leftCols;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function rect(col: number, row: number, cols: number, rows: number): UiLayoutRect {
|
|
105
|
+
return {
|
|
106
|
+
col,
|
|
107
|
+
row,
|
|
108
|
+
cols: Math.max(1, cols),
|
|
109
|
+
rows: Math.max(1, rows),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function anchorRect(
|
|
114
|
+
layout: Omit<UiDualPaneLayout, 'layers'>,
|
|
115
|
+
anchor: UiLayoutAnchor,
|
|
116
|
+
): UiLayoutRect {
|
|
117
|
+
if (anchor === 'left-pane') {
|
|
118
|
+
return layout.leftPane;
|
|
119
|
+
}
|
|
120
|
+
if (anchor === 'right-pane') {
|
|
121
|
+
return layout.rightPane;
|
|
122
|
+
}
|
|
123
|
+
if (anchor === 'status') {
|
|
124
|
+
return layout.status;
|
|
125
|
+
}
|
|
126
|
+
return layout.viewport;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function intersectRect(left: UiLayoutRect, right: UiLayoutRect): UiLayoutRect | null {
|
|
130
|
+
const startCol = Math.max(left.col, right.col);
|
|
131
|
+
const startRow = Math.max(left.row, right.row);
|
|
132
|
+
const endCol = Math.min(left.col + left.cols - 1, right.col + right.cols - 1);
|
|
133
|
+
const endRow = Math.min(left.row + left.rows - 1, right.row + right.rows - 1);
|
|
134
|
+
if (endCol < startCol || endRow < startRow) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return rect(startCol, startRow, endCol - startCol + 1, endRow - startRow + 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function computeDualPaneLayoutWithLayers(
|
|
141
|
+
cols: number,
|
|
142
|
+
rows: number,
|
|
143
|
+
options: ComputeUiLayoutOptions = {},
|
|
144
|
+
): UiDualPaneLayout {
|
|
145
|
+
const normalizedCols = normalizeInt(cols, 3);
|
|
146
|
+
const normalizedRows = normalizeInt(rows, 2);
|
|
147
|
+
const requestedStatusRows = normalizeInt(options.statusRows ?? 1, 1);
|
|
148
|
+
const statusRows = Math.min(requestedStatusRows, normalizedRows - 1);
|
|
149
|
+
const paneRows = normalizedRows - statusRows;
|
|
150
|
+
const statusRow = paneRows + 1;
|
|
151
|
+
|
|
152
|
+
const availablePaneCols = normalizedCols - 1;
|
|
153
|
+
const leftCols = resolveLeftPaneCols(normalizedCols, options.leftCols, options.paneWidthPercent);
|
|
154
|
+
const rightCols = availablePaneCols - leftCols;
|
|
155
|
+
const separatorCol = leftCols + 1;
|
|
156
|
+
const rightStartCol = leftCols + 2;
|
|
157
|
+
|
|
158
|
+
const layoutBase = {
|
|
159
|
+
cols: normalizedCols,
|
|
160
|
+
rows: normalizedRows,
|
|
161
|
+
paneRows,
|
|
162
|
+
statusRow,
|
|
163
|
+
leftCols,
|
|
164
|
+
rightCols,
|
|
165
|
+
separatorCol,
|
|
166
|
+
rightStartCol,
|
|
167
|
+
viewport: rect(1, 1, normalizedCols, normalizedRows),
|
|
168
|
+
leftPane: rect(1, 1, leftCols, paneRows),
|
|
169
|
+
separator: rect(separatorCol, 1, 1, paneRows),
|
|
170
|
+
rightPane: rect(rightStartCol, 1, rightCols, paneRows),
|
|
171
|
+
status: rect(1, statusRow, normalizedCols, statusRows),
|
|
172
|
+
} satisfies Omit<UiDualPaneLayout, 'layers'>;
|
|
173
|
+
|
|
174
|
+
const layers: UiLayoutLayer[] = [
|
|
175
|
+
{
|
|
176
|
+
id: 'left-pane',
|
|
177
|
+
kind: 'left-pane',
|
|
178
|
+
zIndex: DEFAULT_BASE_LAYER_Z_INDEX,
|
|
179
|
+
rect: layoutBase.leftPane,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'separator',
|
|
183
|
+
kind: 'separator',
|
|
184
|
+
zIndex: DEFAULT_BASE_LAYER_Z_INDEX,
|
|
185
|
+
rect: layoutBase.separator,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'right-pane',
|
|
189
|
+
kind: 'right-pane',
|
|
190
|
+
zIndex: DEFAULT_BASE_LAYER_Z_INDEX,
|
|
191
|
+
rect: layoutBase.rightPane,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'status',
|
|
195
|
+
kind: 'status',
|
|
196
|
+
zIndex: DEFAULT_BASE_LAYER_Z_INDEX,
|
|
197
|
+
rect: layoutBase.status,
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
for (const overlay of options.overlays ?? []) {
|
|
202
|
+
if (overlay.id.trim().length === 0) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const overlayAnchor = anchorRect(layoutBase, overlay.anchor ?? 'viewport');
|
|
206
|
+
const absoluteRect = rect(
|
|
207
|
+
overlayAnchor.col + overlay.col - 1,
|
|
208
|
+
overlayAnchor.row + overlay.row - 1,
|
|
209
|
+
overlay.cols,
|
|
210
|
+
overlay.rows,
|
|
211
|
+
);
|
|
212
|
+
const resolvedRect =
|
|
213
|
+
overlay.clipToViewport === false
|
|
214
|
+
? absoluteRect
|
|
215
|
+
: intersectRect(absoluteRect, layoutBase.viewport);
|
|
216
|
+
if (resolvedRect === null) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
layers.push({
|
|
220
|
+
id: overlay.id,
|
|
221
|
+
kind: 'overlay',
|
|
222
|
+
zIndex: overlay.zIndex ?? DEFAULT_OVERLAY_LAYER_Z_INDEX,
|
|
223
|
+
rect: resolvedRect,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sortedLayers = layers.toSorted((left, right) => {
|
|
228
|
+
if (left.zIndex !== right.zIndex) {
|
|
229
|
+
return left.zIndex - right.zIndex;
|
|
230
|
+
}
|
|
231
|
+
return left.id.localeCompare(right.id);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
...layoutBase,
|
|
236
|
+
layers: sortedLayers,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { UiModalOverlay, UiModalTheme } from './kit.ts';
|
|
2
|
+
import type {
|
|
3
|
+
ApiKeyPromptState,
|
|
4
|
+
CommandMenuActionDescriptor,
|
|
5
|
+
CommandMenuState,
|
|
6
|
+
ConversationTitleEditState,
|
|
7
|
+
NewThreadPromptState,
|
|
8
|
+
RepositoryPromptState,
|
|
9
|
+
TaskEditorPromptState,
|
|
10
|
+
} from './interaction/input.ts';
|
|
11
|
+
|
|
12
|
+
type AddDirectoryPromptState = { value: string; error: string | null };
|
|
13
|
+
type ModalOverlay = UiModalOverlay;
|
|
14
|
+
type ModalTheme = Partial<UiModalTheme>;
|
|
15
|
+
|
|
16
|
+
interface ModalManagerOptions {
|
|
17
|
+
readonly theme: ModalTheme;
|
|
18
|
+
readonly resolveRepositoryName: (repositoryId: string) => string | null;
|
|
19
|
+
readonly getCommandMenu: () => CommandMenuState | null;
|
|
20
|
+
readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
|
|
21
|
+
readonly getNewThreadPrompt: () => NewThreadPromptState | null;
|
|
22
|
+
readonly getAddDirectoryPrompt: () => AddDirectoryPromptState | null;
|
|
23
|
+
readonly getApiKeyPrompt?: () => ApiKeyPromptState | null;
|
|
24
|
+
readonly getTaskEditorPrompt: () => TaskEditorPromptState | null;
|
|
25
|
+
readonly getRepositoryPrompt: () => RepositoryPromptState | null;
|
|
26
|
+
readonly getConversationTitleEdit: () => ConversationTitleEditState | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ModalDismissOnOutsideClickInput {
|
|
30
|
+
readonly input: Buffer;
|
|
31
|
+
readonly inputRemainder: string;
|
|
32
|
+
readonly dismiss: () => void;
|
|
33
|
+
readonly buildCurrentModalOverlay: () => ModalOverlay | null;
|
|
34
|
+
readonly onInsidePointerPress?: (col: number, row: number) => boolean;
|
|
35
|
+
readonly isOverlayHit: (overlay: ModalOverlay, col: number, row: number) => boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ModalDismissOnOutsideClickResult {
|
|
39
|
+
readonly handled: boolean;
|
|
40
|
+
readonly inputRemainder: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ModalManagerStrategies {
|
|
44
|
+
buildCommandMenuModalOverlay(
|
|
45
|
+
layoutCols: number,
|
|
46
|
+
viewportRows: number,
|
|
47
|
+
menu: CommandMenuState | null,
|
|
48
|
+
actions: readonly CommandMenuActionDescriptor[],
|
|
49
|
+
theme: ModalTheme,
|
|
50
|
+
): ModalOverlay | null;
|
|
51
|
+
buildNewThreadModalOverlay(
|
|
52
|
+
layoutCols: number,
|
|
53
|
+
viewportRows: number,
|
|
54
|
+
prompt: NewThreadPromptState | null,
|
|
55
|
+
theme: ModalTheme,
|
|
56
|
+
): ModalOverlay | null;
|
|
57
|
+
buildAddDirectoryModalOverlay(
|
|
58
|
+
layoutCols: number,
|
|
59
|
+
viewportRows: number,
|
|
60
|
+
prompt: AddDirectoryPromptState | null,
|
|
61
|
+
theme: ModalTheme,
|
|
62
|
+
): ModalOverlay | null;
|
|
63
|
+
buildTaskEditorModalOverlay(
|
|
64
|
+
layoutCols: number,
|
|
65
|
+
viewportRows: number,
|
|
66
|
+
prompt: TaskEditorPromptState | null,
|
|
67
|
+
resolveRepositoryName: (repositoryId: string) => string | null,
|
|
68
|
+
theme: ModalTheme,
|
|
69
|
+
): ModalOverlay | null;
|
|
70
|
+
buildApiKeyModalOverlay(
|
|
71
|
+
layoutCols: number,
|
|
72
|
+
viewportRows: number,
|
|
73
|
+
prompt: ApiKeyPromptState | null,
|
|
74
|
+
theme: ModalTheme,
|
|
75
|
+
): ModalOverlay | null;
|
|
76
|
+
buildRepositoryModalOverlay(
|
|
77
|
+
layoutCols: number,
|
|
78
|
+
viewportRows: number,
|
|
79
|
+
prompt: RepositoryPromptState | null,
|
|
80
|
+
theme: ModalTheme,
|
|
81
|
+
): ModalOverlay | null;
|
|
82
|
+
buildConversationTitleModalOverlay(
|
|
83
|
+
layoutCols: number,
|
|
84
|
+
viewportRows: number,
|
|
85
|
+
edit: ConversationTitleEditState | null,
|
|
86
|
+
theme: ModalTheme,
|
|
87
|
+
): ModalOverlay | null;
|
|
88
|
+
dismissModalOnOutsideClick(
|
|
89
|
+
input: ModalDismissOnOutsideClickInput,
|
|
90
|
+
): ModalDismissOnOutsideClickResult;
|
|
91
|
+
isOverlayHit(overlay: ModalOverlay, col: number, row: number): boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ModalDismissInput {
|
|
95
|
+
readonly input: Buffer;
|
|
96
|
+
readonly inputRemainder: string;
|
|
97
|
+
readonly layoutCols: number;
|
|
98
|
+
readonly viewportRows: number;
|
|
99
|
+
readonly dismiss: () => void;
|
|
100
|
+
readonly onInsidePointerPress?: (col: number, row: number) => boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface ModalDismissResult {
|
|
104
|
+
readonly handled: boolean;
|
|
105
|
+
readonly inputRemainder: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class ModalManager {
|
|
109
|
+
constructor(
|
|
110
|
+
private readonly options: ModalManagerOptions,
|
|
111
|
+
private readonly strategies: ModalManagerStrategies,
|
|
112
|
+
) {}
|
|
113
|
+
|
|
114
|
+
buildCommandMenuOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
|
|
115
|
+
return this.strategies.buildCommandMenuModalOverlay(
|
|
116
|
+
layoutCols,
|
|
117
|
+
viewportRows,
|
|
118
|
+
this.options.getCommandMenu(),
|
|
119
|
+
this.options.resolveCommandMenuActions(),
|
|
120
|
+
this.options.theme,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
buildNewThreadOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
|
|
125
|
+
return this.strategies.buildNewThreadModalOverlay(
|
|
126
|
+
layoutCols,
|
|
127
|
+
viewportRows,
|
|
128
|
+
this.options.getNewThreadPrompt(),
|
|
129
|
+
this.options.theme,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
buildAddDirectoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
|
|
134
|
+
return this.strategies.buildAddDirectoryModalOverlay(
|
|
135
|
+
layoutCols,
|
|
136
|
+
viewportRows,
|
|
137
|
+
this.options.getAddDirectoryPrompt(),
|
|
138
|
+
this.options.theme,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
buildTaskEditorOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
|
|
143
|
+
return this.strategies.buildTaskEditorModalOverlay(
|
|
144
|
+
layoutCols,
|
|
145
|
+
viewportRows,
|
|
146
|
+
this.options.getTaskEditorPrompt(),
|
|
147
|
+
this.options.resolveRepositoryName,
|
|
148
|
+
this.options.theme,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
buildApiKeyOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
|
|
153
|
+
return this.strategies.buildApiKeyModalOverlay(
|
|
154
|
+
layoutCols,
|
|
155
|
+
viewportRows,
|
|
156
|
+
this.options.getApiKeyPrompt?.() ?? null,
|
|
157
|
+
this.options.theme,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
buildRepositoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
|
|
162
|
+
return this.strategies.buildRepositoryModalOverlay(
|
|
163
|
+
layoutCols,
|
|
164
|
+
viewportRows,
|
|
165
|
+
this.options.getRepositoryPrompt(),
|
|
166
|
+
this.options.theme,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
buildConversationTitleOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
|
|
171
|
+
return this.strategies.buildConversationTitleModalOverlay(
|
|
172
|
+
layoutCols,
|
|
173
|
+
viewportRows,
|
|
174
|
+
this.options.getConversationTitleEdit(),
|
|
175
|
+
this.options.theme,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
buildCurrentOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
|
|
180
|
+
const commandMenuOverlay = this.buildCommandMenuOverlay(layoutCols, viewportRows);
|
|
181
|
+
if (commandMenuOverlay !== null) {
|
|
182
|
+
return commandMenuOverlay;
|
|
183
|
+
}
|
|
184
|
+
const newThreadOverlay = this.buildNewThreadOverlay(layoutCols, viewportRows);
|
|
185
|
+
if (newThreadOverlay !== null) {
|
|
186
|
+
return newThreadOverlay;
|
|
187
|
+
}
|
|
188
|
+
const addDirectoryOverlay = this.buildAddDirectoryOverlay(layoutCols, viewportRows);
|
|
189
|
+
if (addDirectoryOverlay !== null) {
|
|
190
|
+
return addDirectoryOverlay;
|
|
191
|
+
}
|
|
192
|
+
const taskEditorOverlay = this.buildTaskEditorOverlay(layoutCols, viewportRows);
|
|
193
|
+
if (taskEditorOverlay !== null) {
|
|
194
|
+
return taskEditorOverlay;
|
|
195
|
+
}
|
|
196
|
+
const apiKeyOverlay = this.buildApiKeyOverlay(layoutCols, viewportRows);
|
|
197
|
+
if (apiKeyOverlay !== null) {
|
|
198
|
+
return apiKeyOverlay;
|
|
199
|
+
}
|
|
200
|
+
const repositoryOverlay = this.buildRepositoryOverlay(layoutCols, viewportRows);
|
|
201
|
+
if (repositoryOverlay !== null) {
|
|
202
|
+
return repositoryOverlay;
|
|
203
|
+
}
|
|
204
|
+
return this.buildConversationTitleOverlay(layoutCols, viewportRows);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
dismissOnOutsideClick(input: ModalDismissInput): ModalDismissResult {
|
|
208
|
+
return this.strategies.dismissModalOnOutsideClick({
|
|
209
|
+
input: input.input,
|
|
210
|
+
inputRemainder: input.inputRemainder,
|
|
211
|
+
dismiss: input.dismiss,
|
|
212
|
+
buildCurrentModalOverlay: () =>
|
|
213
|
+
this.buildCurrentOverlay(input.layoutCols, input.viewportRows),
|
|
214
|
+
isOverlayHit: this.strategies.isOverlayHit,
|
|
215
|
+
...(input.onInsidePointerPress === undefined
|
|
216
|
+
? {}
|
|
217
|
+
: {
|
|
218
|
+
onInsidePointerPress: input.onInsidePointerPress,
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
cursorStyleEqual,
|
|
3
|
+
cursorStyleToDecscusr,
|
|
4
|
+
diffRenderedRows,
|
|
5
|
+
findAnsiIntegrityIssues,
|
|
6
|
+
type RenderCursorStyle,
|
|
7
|
+
} from './frame-primitives.ts';
|
|
4
8
|
|
|
5
|
-
export
|
|
6
|
-
readonly shape: 'block' | 'underline' | 'bar';
|
|
7
|
-
readonly blinking: boolean;
|
|
8
|
-
}
|
|
9
|
+
export type ScreenCursorStyle = RenderCursorStyle;
|
|
9
10
|
|
|
10
11
|
interface ScreenLayout {
|
|
11
12
|
readonly paneRows: number;
|
|
@@ -43,10 +44,33 @@ interface ScreenFlushResult {
|
|
|
43
44
|
readonly shouldShowCursor: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
interface
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
export interface ScreenWriter {
|
|
48
|
+
writeOutput(output: string): void;
|
|
49
|
+
writeError(output: string): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ProcessScreenWriter implements ScreenWriter {
|
|
53
|
+
constructor() {}
|
|
54
|
+
|
|
55
|
+
writeOutput(output: string): void {
|
|
56
|
+
process.stdout.write(output);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
writeError(output: string): void {
|
|
60
|
+
process.stderr.write(output);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ScreenAnsiValidator {
|
|
65
|
+
findIssues(rows: readonly string[]): readonly string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class DefaultScreenAnsiValidator implements ScreenAnsiValidator {
|
|
69
|
+
constructor() {}
|
|
70
|
+
|
|
71
|
+
findIssues(rows: readonly string[]): readonly string[] {
|
|
72
|
+
return findAnsiIntegrityIssues(rows);
|
|
73
|
+
}
|
|
50
74
|
}
|
|
51
75
|
|
|
52
76
|
const TERMINAL_SYNC_UPDATE_BEGIN = '\u001b[?2026h';
|
|
@@ -66,7 +90,17 @@ function mergeUniqueRows(left: readonly number[], right: readonly number[]): rea
|
|
|
66
90
|
for (const row of right) {
|
|
67
91
|
merged.add(row);
|
|
68
92
|
}
|
|
69
|
-
|
|
93
|
+
const output = [...merged];
|
|
94
|
+
for (let index = 1; index < output.length; index += 1) {
|
|
95
|
+
const value = output[index]!;
|
|
96
|
+
let insertIndex = index - 1;
|
|
97
|
+
while (insertIndex >= 0 && output[insertIndex]! > value) {
|
|
98
|
+
output[insertIndex + 1] = output[insertIndex]!;
|
|
99
|
+
insertIndex -= 1;
|
|
100
|
+
}
|
|
101
|
+
output[insertIndex + 1] = value;
|
|
102
|
+
}
|
|
103
|
+
return output;
|
|
70
104
|
}
|
|
71
105
|
|
|
72
106
|
export class Screen {
|
|
@@ -79,15 +113,10 @@ export class Screen {
|
|
|
79
113
|
private renderedBracketedPaste: boolean | null = null;
|
|
80
114
|
private ansiValidationReported = false;
|
|
81
115
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
}
|
|
116
|
+
constructor(
|
|
117
|
+
private readonly writer: ScreenWriter = new ProcessScreenWriter(),
|
|
118
|
+
private readonly ansiValidator: ScreenAnsiValidator = new DefaultScreenAnsiValidator(),
|
|
119
|
+
) {}
|
|
91
120
|
|
|
92
121
|
isDirty(): boolean {
|
|
93
122
|
return this.dirty;
|
|
@@ -116,10 +145,10 @@ export class Screen {
|
|
|
116
145
|
}
|
|
117
146
|
|
|
118
147
|
if (input.validateAnsi) {
|
|
119
|
-
const issues = this.
|
|
148
|
+
const issues = this.ansiValidator.findIssues(input.rows);
|
|
120
149
|
if (issues.length > 0 && !this.ansiValidationReported) {
|
|
121
150
|
this.ansiValidationReported = true;
|
|
122
|
-
this.
|
|
151
|
+
this.writer.writeError(`[mux] ansi-integrity-failed ${issues.join(' | ')}\n`);
|
|
123
152
|
}
|
|
124
153
|
}
|
|
125
154
|
|
|
@@ -163,7 +192,6 @@ export class Screen {
|
|
|
163
192
|
}
|
|
164
193
|
|
|
165
194
|
output += input.selectionOverlay;
|
|
166
|
-
|
|
167
195
|
shouldShowCursor =
|
|
168
196
|
input.rightFrame.viewport.followOutput &&
|
|
169
197
|
input.rightFrame.cursor.visible &&
|
|
@@ -194,13 +222,12 @@ export class Screen {
|
|
|
194
222
|
}
|
|
195
223
|
|
|
196
224
|
if (output.length > 0) {
|
|
197
|
-
this.
|
|
225
|
+
this.writer.writeOutput(`${TERMINAL_SYNC_UPDATE_BEGIN}${output}${TERMINAL_SYNC_UPDATE_END}`);
|
|
198
226
|
}
|
|
199
227
|
|
|
200
228
|
this.previousRows = diff.nextRows;
|
|
201
229
|
this.previousSelectionRows = input.selectionRows;
|
|
202
230
|
this.dirty = false;
|
|
203
|
-
|
|
204
231
|
return {
|
|
205
232
|
wroteOutput: output.length > 0,
|
|
206
233
|
changedRowCount: diff.changedRows.length,
|