@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
package/src/ui/kit.ts
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { measureDisplayWidth } from '../terminal/snapshot-oracle.ts';
|
|
2
|
+
import {
|
|
3
|
+
createUiSurface,
|
|
4
|
+
DEFAULT_UI_STYLE,
|
|
5
|
+
drawUiText,
|
|
6
|
+
fillUiRow,
|
|
7
|
+
renderUiSurfaceAnsiRows,
|
|
8
|
+
type UiColor,
|
|
9
|
+
type UiStyle,
|
|
10
|
+
type UiSurface,
|
|
11
|
+
} from './surface.ts';
|
|
12
|
+
|
|
13
|
+
interface UiRect {
|
|
14
|
+
readonly col: number;
|
|
15
|
+
readonly row: number;
|
|
16
|
+
readonly width: number;
|
|
17
|
+
readonly height: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type UiTextAlign = 'left' | 'center' | 'right';
|
|
21
|
+
|
|
22
|
+
interface UiBoxGlyphs {
|
|
23
|
+
readonly topLeft: string;
|
|
24
|
+
readonly topRight: string;
|
|
25
|
+
readonly bottomLeft: string;
|
|
26
|
+
readonly bottomRight: string;
|
|
27
|
+
readonly horizontal: string;
|
|
28
|
+
readonly vertical: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const SINGLE_LINE_UI_BOX_GLYPHS: UiBoxGlyphs = {
|
|
32
|
+
topLeft: '┌',
|
|
33
|
+
topRight: '┐',
|
|
34
|
+
bottomLeft: '└',
|
|
35
|
+
bottomRight: '┘',
|
|
36
|
+
horizontal: '─',
|
|
37
|
+
vertical: '│',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface UiModalTheme {
|
|
41
|
+
readonly frameStyle: UiStyle;
|
|
42
|
+
readonly titleStyle: UiStyle;
|
|
43
|
+
readonly bodyStyle: UiStyle;
|
|
44
|
+
readonly footerStyle: UiStyle;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface UiModalContent {
|
|
48
|
+
readonly title?: string;
|
|
49
|
+
readonly bodyLines?: readonly string[];
|
|
50
|
+
readonly footer?: string;
|
|
51
|
+
readonly paddingX?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type UiModalAnchor = 'center' | 'bottom';
|
|
55
|
+
|
|
56
|
+
interface UiModalOverlayOptions extends UiModalContent {
|
|
57
|
+
readonly viewportCols: number;
|
|
58
|
+
readonly viewportRows: number;
|
|
59
|
+
readonly width: number;
|
|
60
|
+
readonly height: number;
|
|
61
|
+
readonly anchor?: UiModalAnchor;
|
|
62
|
+
readonly marginRows?: number;
|
|
63
|
+
readonly theme?: Partial<UiModalTheme>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface UiModalOverlay {
|
|
67
|
+
readonly left: number;
|
|
68
|
+
readonly top: number;
|
|
69
|
+
readonly width: number;
|
|
70
|
+
readonly height: number;
|
|
71
|
+
readonly rows: readonly string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface UiButtonContent {
|
|
75
|
+
readonly label: string;
|
|
76
|
+
readonly prefixIcon?: string;
|
|
77
|
+
readonly suffixIcon?: string;
|
|
78
|
+
readonly paddingX?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface UiTrailingLabelRowOptions {
|
|
82
|
+
readonly col?: number;
|
|
83
|
+
readonly width?: number;
|
|
84
|
+
readonly gap?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const MODAL_FRAME_FG: UiColor = { kind: 'indexed', index: 252 };
|
|
88
|
+
const MODAL_TITLE_FG: UiColor = { kind: 'indexed', index: 231 };
|
|
89
|
+
const MODAL_BODY_FG: UiColor = { kind: 'indexed', index: 253 };
|
|
90
|
+
const MODAL_FOOTER_FG: UiColor = { kind: 'indexed', index: 247 };
|
|
91
|
+
const MODAL_BG: UiColor = { kind: 'indexed', index: 236 };
|
|
92
|
+
|
|
93
|
+
export const DEFAULT_UI_MODAL_THEME: UiModalTheme = {
|
|
94
|
+
frameStyle: {
|
|
95
|
+
fg: MODAL_FRAME_FG,
|
|
96
|
+
bg: MODAL_BG,
|
|
97
|
+
bold: true,
|
|
98
|
+
},
|
|
99
|
+
titleStyle: {
|
|
100
|
+
fg: MODAL_TITLE_FG,
|
|
101
|
+
bg: MODAL_BG,
|
|
102
|
+
bold: true,
|
|
103
|
+
},
|
|
104
|
+
bodyStyle: {
|
|
105
|
+
fg: MODAL_BODY_FG,
|
|
106
|
+
bg: MODAL_BG,
|
|
107
|
+
bold: false,
|
|
108
|
+
},
|
|
109
|
+
footerStyle: {
|
|
110
|
+
fg: MODAL_FOOTER_FG,
|
|
111
|
+
bg: MODAL_BG,
|
|
112
|
+
bold: false,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function clamp(value: number, min: number, max: number): number {
|
|
117
|
+
if (value < min) {
|
|
118
|
+
return min;
|
|
119
|
+
}
|
|
120
|
+
if (value > max) {
|
|
121
|
+
return max;
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeRect(surface: UiSurface, rect: UiRect): UiRect | null {
|
|
127
|
+
const colStart = Math.max(0, Math.floor(rect.col));
|
|
128
|
+
const rowStart = Math.max(0, Math.floor(rect.row));
|
|
129
|
+
const colEnd = Math.min(surface.cols, Math.ceil(rect.col + rect.width));
|
|
130
|
+
const rowEnd = Math.min(surface.rows, Math.ceil(rect.row + rect.height));
|
|
131
|
+
const width = colEnd - colStart;
|
|
132
|
+
const height = rowEnd - rowStart;
|
|
133
|
+
if (width <= 0 || height <= 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
col: colStart,
|
|
138
|
+
row: rowStart,
|
|
139
|
+
width,
|
|
140
|
+
height,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mergeModalTheme(theme: Partial<UiModalTheme> | undefined): UiModalTheme {
|
|
145
|
+
if (theme === undefined) {
|
|
146
|
+
return DEFAULT_UI_MODAL_THEME;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
frameStyle: theme.frameStyle ?? DEFAULT_UI_MODAL_THEME.frameStyle,
|
|
150
|
+
titleStyle: theme.titleStyle ?? DEFAULT_UI_MODAL_THEME.titleStyle,
|
|
151
|
+
bodyStyle: theme.bodyStyle ?? DEFAULT_UI_MODAL_THEME.bodyStyle,
|
|
152
|
+
footerStyle: theme.footerStyle ?? DEFAULT_UI_MODAL_THEME.footerStyle,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function truncateUiText(text: string, width: number): string {
|
|
157
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
158
|
+
if (safeWidth === 0 || text.length === 0) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const consumed: Array<{ glyph: string; width: number }> = [];
|
|
163
|
+
let consumedWidth = 0;
|
|
164
|
+
let truncated = false;
|
|
165
|
+
|
|
166
|
+
for (const glyph of text) {
|
|
167
|
+
const glyphWidth = Math.max(1, measureDisplayWidth(glyph));
|
|
168
|
+
if (consumedWidth + glyphWidth > safeWidth) {
|
|
169
|
+
truncated = true;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
consumed.push({ glyph, width: glyphWidth });
|
|
173
|
+
consumedWidth += glyphWidth;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!truncated) {
|
|
177
|
+
return consumed.map((entry) => entry.glyph).join('');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (safeWidth === 1) {
|
|
181
|
+
return '…';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
while (consumed.length > 0 && consumedWidth + 1 > safeWidth) {
|
|
185
|
+
const removed = consumed.pop()!;
|
|
186
|
+
consumedWidth -= removed.width;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return `${consumed.map((entry) => entry.glyph).join('')}…`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function formatUiButton(content: UiButtonContent): string {
|
|
193
|
+
const label = content.label.trim();
|
|
194
|
+
const prefixIcon = content.prefixIcon?.trim();
|
|
195
|
+
const suffixIcon = content.suffixIcon?.trim();
|
|
196
|
+
const paddingX = clamp(Math.floor(content.paddingX ?? 1), 0, 8);
|
|
197
|
+
|
|
198
|
+
const segments: string[] = [];
|
|
199
|
+
if (prefixIcon !== undefined && prefixIcon.length > 0) {
|
|
200
|
+
segments.push(prefixIcon);
|
|
201
|
+
}
|
|
202
|
+
segments.push(label.length > 0 ? label : 'button');
|
|
203
|
+
if (suffixIcon !== undefined && suffixIcon.length > 0) {
|
|
204
|
+
segments.push(suffixIcon);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const padded = `${' '.repeat(paddingX)}${segments.join(' ')}${' '.repeat(paddingX)}`;
|
|
208
|
+
return `[${padded}]`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function drawUiAlignedText(
|
|
212
|
+
surface: UiSurface,
|
|
213
|
+
col: number,
|
|
214
|
+
row: number,
|
|
215
|
+
width: number,
|
|
216
|
+
text: string,
|
|
217
|
+
style: UiStyle,
|
|
218
|
+
align: UiTextAlign = 'left',
|
|
219
|
+
): void {
|
|
220
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
221
|
+
if (safeWidth === 0) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const clipped = truncateUiText(text, safeWidth);
|
|
226
|
+
const clippedWidth = Math.max(0, measureDisplayWidth(clipped));
|
|
227
|
+
let offset = 0;
|
|
228
|
+
if (align === 'center') {
|
|
229
|
+
offset = Math.max(0, Math.floor((safeWidth - clippedWidth) / 2));
|
|
230
|
+
} else if (align === 'right') {
|
|
231
|
+
offset = Math.max(0, safeWidth - clippedWidth);
|
|
232
|
+
}
|
|
233
|
+
drawUiText(surface, col + offset, row, clipped, style);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function paintUiRow(
|
|
237
|
+
surface: UiSurface,
|
|
238
|
+
row: number,
|
|
239
|
+
text: string,
|
|
240
|
+
textStyle: UiStyle,
|
|
241
|
+
fillStyle: UiStyle = textStyle,
|
|
242
|
+
col = 0,
|
|
243
|
+
): void {
|
|
244
|
+
fillUiRow(surface, row, fillStyle);
|
|
245
|
+
drawUiText(surface, col, row, text, textStyle);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function paintUiRowWithTrailingLabel(
|
|
249
|
+
surface: UiSurface,
|
|
250
|
+
row: number,
|
|
251
|
+
leftText: string,
|
|
252
|
+
trailingLabel: string,
|
|
253
|
+
leftStyle: UiStyle,
|
|
254
|
+
trailingStyle: UiStyle,
|
|
255
|
+
fillStyle: UiStyle = leftStyle,
|
|
256
|
+
options: UiTrailingLabelRowOptions = {},
|
|
257
|
+
): void {
|
|
258
|
+
const col = Math.max(0, Math.floor(options.col ?? 0));
|
|
259
|
+
const width = Math.max(0, Math.floor(options.width ?? surface.cols - col));
|
|
260
|
+
const gap = clamp(Math.floor(options.gap ?? 1), 0, width);
|
|
261
|
+
fillUiRow(surface, row, fillStyle);
|
|
262
|
+
if (width === 0) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const clippedTrailing = truncateUiText(trailingLabel, width);
|
|
267
|
+
const trailingWidth = Math.max(0, measureDisplayWidth(clippedTrailing));
|
|
268
|
+
const reservedGap = trailingWidth > 0 ? gap : 0;
|
|
269
|
+
const leftWidth = Math.max(0, width - trailingWidth - reservedGap);
|
|
270
|
+
const clippedLeft = truncateUiText(leftText, leftWidth);
|
|
271
|
+
if (clippedLeft.length > 0) {
|
|
272
|
+
drawUiText(surface, col, row, clippedLeft, leftStyle);
|
|
273
|
+
}
|
|
274
|
+
if (clippedTrailing.length > 0) {
|
|
275
|
+
drawUiText(surface, col + width - trailingWidth, row, clippedTrailing, trailingStyle);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function fillUiRect(surface: UiSurface, rect: UiRect, style: UiStyle): void {
|
|
280
|
+
const normalized = normalizeRect(surface, rect);
|
|
281
|
+
if (normalized === null) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const blank = ' '.repeat(normalized.width);
|
|
285
|
+
for (let row = normalized.row; row < normalized.row + normalized.height; row += 1) {
|
|
286
|
+
drawUiText(surface, normalized.col, row, blank, style);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function strokeUiRect(
|
|
291
|
+
surface: UiSurface,
|
|
292
|
+
rect: UiRect,
|
|
293
|
+
style: UiStyle,
|
|
294
|
+
glyphs: UiBoxGlyphs = SINGLE_LINE_UI_BOX_GLYPHS,
|
|
295
|
+
): void {
|
|
296
|
+
const normalized = normalizeRect(surface, rect);
|
|
297
|
+
if (normalized === null) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (normalized.width === 1 && normalized.height === 1) {
|
|
302
|
+
drawUiText(surface, normalized.col, normalized.row, glyphs.topLeft, style);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (normalized.height === 1) {
|
|
307
|
+
drawUiText(
|
|
308
|
+
surface,
|
|
309
|
+
normalized.col,
|
|
310
|
+
normalized.row,
|
|
311
|
+
glyphs.horizontal.repeat(normalized.width),
|
|
312
|
+
style,
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (normalized.width === 1) {
|
|
318
|
+
for (let row = normalized.row; row < normalized.row + normalized.height; row += 1) {
|
|
319
|
+
drawUiText(surface, normalized.col, row, glyphs.vertical, style);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const horizontal = glyphs.horizontal.repeat(Math.max(0, normalized.width - 2));
|
|
325
|
+
drawUiText(
|
|
326
|
+
surface,
|
|
327
|
+
normalized.col,
|
|
328
|
+
normalized.row,
|
|
329
|
+
`${glyphs.topLeft}${horizontal}${glyphs.topRight}`,
|
|
330
|
+
style,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const bottomRow = normalized.row + normalized.height - 1;
|
|
334
|
+
drawUiText(
|
|
335
|
+
surface,
|
|
336
|
+
normalized.col,
|
|
337
|
+
bottomRow,
|
|
338
|
+
`${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`,
|
|
339
|
+
style,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
for (let row = normalized.row + 1; row < bottomRow; row += 1) {
|
|
343
|
+
drawUiText(surface, normalized.col, row, glyphs.vertical, style);
|
|
344
|
+
drawUiText(surface, normalized.col + normalized.width - 1, row, glyphs.vertical, style);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function layoutUiModalRect(
|
|
349
|
+
viewportCols: number,
|
|
350
|
+
viewportRows: number,
|
|
351
|
+
width: number,
|
|
352
|
+
height: number,
|
|
353
|
+
anchor: UiModalAnchor = 'center',
|
|
354
|
+
marginRows = 1,
|
|
355
|
+
): UiRect {
|
|
356
|
+
const safeViewportCols = Math.max(1, Math.floor(viewportCols));
|
|
357
|
+
const safeViewportRows = Math.max(1, Math.floor(viewportRows));
|
|
358
|
+
const safeWidth = clamp(Math.floor(width), 1, safeViewportCols);
|
|
359
|
+
const safeHeight = clamp(Math.floor(height), 1, safeViewportRows);
|
|
360
|
+
|
|
361
|
+
const left = Math.floor((safeViewportCols - safeWidth) / 2);
|
|
362
|
+
const top =
|
|
363
|
+
anchor === 'bottom'
|
|
364
|
+
? Math.max(0, safeViewportRows - safeHeight - Math.max(0, Math.floor(marginRows)))
|
|
365
|
+
: Math.floor((safeViewportRows - safeHeight) / 2);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
col: left,
|
|
369
|
+
row: top,
|
|
370
|
+
width: safeWidth,
|
|
371
|
+
height: safeHeight,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function drawUiModal(
|
|
376
|
+
surface: UiSurface,
|
|
377
|
+
rect: UiRect,
|
|
378
|
+
content: UiModalContent,
|
|
379
|
+
theme: Partial<UiModalTheme> | undefined = undefined,
|
|
380
|
+
): UiRect | null {
|
|
381
|
+
const normalized = normalizeRect(surface, rect);
|
|
382
|
+
if (normalized === null) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const resolvedTheme = mergeModalTheme(theme);
|
|
387
|
+
fillUiRect(surface, normalized, resolvedTheme.bodyStyle);
|
|
388
|
+
strokeUiRect(surface, normalized, resolvedTheme.frameStyle);
|
|
389
|
+
|
|
390
|
+
const inner: UiRect = {
|
|
391
|
+
col: normalized.col + 1,
|
|
392
|
+
row: normalized.row + 1,
|
|
393
|
+
width: Math.max(0, normalized.width - 2),
|
|
394
|
+
height: Math.max(0, normalized.height - 2),
|
|
395
|
+
};
|
|
396
|
+
const normalizedInner = normalizeRect(surface, inner);
|
|
397
|
+
if (normalizedInner === null) {
|
|
398
|
+
return normalized;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const paddingX = clamp(
|
|
402
|
+
Math.floor(content.paddingX ?? 1),
|
|
403
|
+
0,
|
|
404
|
+
Math.floor(normalizedInner.width / 2),
|
|
405
|
+
);
|
|
406
|
+
const textCol = normalizedInner.col + paddingX;
|
|
407
|
+
const textWidth = Math.max(0, normalizedInner.width - paddingX * 2);
|
|
408
|
+
if (textWidth === 0) {
|
|
409
|
+
return normalized;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let nextRow = normalizedInner.row;
|
|
413
|
+
const innerBottom = normalizedInner.row + normalizedInner.height - 1;
|
|
414
|
+
|
|
415
|
+
if (content.title !== undefined && content.title.length > 0 && nextRow <= innerBottom) {
|
|
416
|
+
drawUiAlignedText(
|
|
417
|
+
surface,
|
|
418
|
+
textCol,
|
|
419
|
+
nextRow,
|
|
420
|
+
textWidth,
|
|
421
|
+
content.title,
|
|
422
|
+
resolvedTheme.titleStyle,
|
|
423
|
+
'center',
|
|
424
|
+
);
|
|
425
|
+
nextRow += 1;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const footerRow =
|
|
429
|
+
content.footer !== undefined && content.footer.length > 0 && nextRow <= innerBottom
|
|
430
|
+
? innerBottom
|
|
431
|
+
: null;
|
|
432
|
+
const footerText = content.footer;
|
|
433
|
+
const bodyBottom = footerRow === null ? innerBottom : footerRow - 1;
|
|
434
|
+
|
|
435
|
+
const bodyLines = content.bodyLines ?? [];
|
|
436
|
+
for (const line of bodyLines) {
|
|
437
|
+
if (nextRow > bodyBottom) {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
drawUiAlignedText(surface, textCol, nextRow, textWidth, line, resolvedTheme.bodyStyle, 'left');
|
|
441
|
+
nextRow += 1;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (footerRow !== null && footerText !== undefined) {
|
|
445
|
+
drawUiAlignedText(
|
|
446
|
+
surface,
|
|
447
|
+
textCol,
|
|
448
|
+
footerRow,
|
|
449
|
+
textWidth,
|
|
450
|
+
footerText,
|
|
451
|
+
resolvedTheme.footerStyle,
|
|
452
|
+
'right',
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return normalized;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function buildUiModalOverlay(options: UiModalOverlayOptions): UiModalOverlay {
|
|
460
|
+
const rect = layoutUiModalRect(
|
|
461
|
+
options.viewportCols,
|
|
462
|
+
options.viewportRows,
|
|
463
|
+
options.width,
|
|
464
|
+
options.height,
|
|
465
|
+
options.anchor ?? 'center',
|
|
466
|
+
options.marginRows ?? 1,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const surface = createUiSurface(rect.width, rect.height, DEFAULT_UI_STYLE);
|
|
470
|
+
const content: UiModalContent = {
|
|
471
|
+
bodyLines: options.bodyLines ?? [],
|
|
472
|
+
...(options.title !== undefined ? { title: options.title } : {}),
|
|
473
|
+
...(options.footer !== undefined ? { footer: options.footer } : {}),
|
|
474
|
+
...(options.paddingX !== undefined ? { paddingX: options.paddingX } : {}),
|
|
475
|
+
};
|
|
476
|
+
drawUiModal(
|
|
477
|
+
surface,
|
|
478
|
+
{
|
|
479
|
+
col: 0,
|
|
480
|
+
row: 0,
|
|
481
|
+
width: rect.width,
|
|
482
|
+
height: rect.height,
|
|
483
|
+
},
|
|
484
|
+
content,
|
|
485
|
+
options.theme,
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
left: rect.col,
|
|
490
|
+
top: rect.row,
|
|
491
|
+
width: rect.width,
|
|
492
|
+
height: rect.height,
|
|
493
|
+
rows: renderUiSurfaceAnsiRows(surface),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function isUiModalOverlayHit(overlay: UiModalOverlay, col: number, row: number): boolean {
|
|
498
|
+
if (col < 1 || row < 1) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const colZero = col - 1;
|
|
502
|
+
const rowZero = row - 1;
|
|
503
|
+
return (
|
|
504
|
+
colZero >= overlay.left &&
|
|
505
|
+
colZero < overlay.left + overlay.width &&
|
|
506
|
+
rowZero >= overlay.top &&
|
|
507
|
+
rowZero < overlay.top + overlay.height
|
|
508
|
+
);
|
|
509
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
activateLeftNavTarget as activateLeftNavTargetFrame,
|
|
3
|
+
cycleLeftNavSelection as cycleLeftNavSelectionFrame,
|
|
4
|
+
} from '../mux/live-mux/left-nav-activation.ts';
|
|
5
|
+
import {
|
|
6
|
+
visibleLeftNavTargets as visibleLeftNavTargetsFrame,
|
|
7
|
+
type LeftNavSelection,
|
|
8
|
+
} from '../mux/live-mux/left-nav.ts';
|
|
9
|
+
import type { buildWorkspaceRailViewRows } from '../mux/workspace-rail-model.ts';
|
|
10
|
+
|
|
11
|
+
interface LeftNavInputOptions {
|
|
12
|
+
readonly getLatestRailRows: () => ReturnType<typeof buildWorkspaceRailViewRows>;
|
|
13
|
+
readonly getCurrentSelection: () => LeftNavSelection;
|
|
14
|
+
readonly enterHomePane: () => void;
|
|
15
|
+
readonly firstDirectoryForRepositoryGroup: (repositoryGroupId: string) => string | null;
|
|
16
|
+
readonly enterProjectPane: (directoryId: string) => void;
|
|
17
|
+
readonly setMainPaneProjectMode: () => void;
|
|
18
|
+
readonly selectLeftNavRepository: (repositoryGroupId: string) => void;
|
|
19
|
+
readonly markDirty: () => void;
|
|
20
|
+
readonly directoriesHas: (directoryId: string) => boolean;
|
|
21
|
+
readonly conversationDirectoryId: (sessionId: string) => string | null;
|
|
22
|
+
readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
|
|
23
|
+
readonly activateConversation: (sessionId: string) => Promise<void>;
|
|
24
|
+
readonly conversationsHas: (sessionId: string) => boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface LeftNavInputDependencies {
|
|
28
|
+
readonly visibleLeftNavTargets?: typeof visibleLeftNavTargetsFrame;
|
|
29
|
+
readonly activateLeftNavTarget?: typeof activateLeftNavTargetFrame;
|
|
30
|
+
readonly cycleLeftNavSelection?: typeof cycleLeftNavSelectionFrame;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class LeftNavInput {
|
|
34
|
+
private readonly visibleLeftNavTargets: typeof visibleLeftNavTargetsFrame;
|
|
35
|
+
private readonly activateLeftNavTargetFrame: typeof activateLeftNavTargetFrame;
|
|
36
|
+
private readonly cycleLeftNavSelectionFrame: typeof cycleLeftNavSelectionFrame;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly options: LeftNavInputOptions,
|
|
40
|
+
dependencies: LeftNavInputDependencies = {},
|
|
41
|
+
) {
|
|
42
|
+
this.visibleLeftNavTargets = dependencies.visibleLeftNavTargets ?? visibleLeftNavTargetsFrame;
|
|
43
|
+
this.activateLeftNavTargetFrame =
|
|
44
|
+
dependencies.activateLeftNavTarget ?? activateLeftNavTargetFrame;
|
|
45
|
+
this.cycleLeftNavSelectionFrame =
|
|
46
|
+
dependencies.cycleLeftNavSelection ?? cycleLeftNavSelectionFrame;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
visibleTargets(): readonly LeftNavSelection[] {
|
|
50
|
+
return this.visibleLeftNavTargets(this.options.getLatestRailRows());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
activateTarget(target: LeftNavSelection, direction: 'next' | 'previous'): void {
|
|
54
|
+
this.activateLeftNavTargetFrame({
|
|
55
|
+
target,
|
|
56
|
+
direction,
|
|
57
|
+
enterHomePane: this.options.enterHomePane,
|
|
58
|
+
firstDirectoryForRepositoryGroup: this.options.firstDirectoryForRepositoryGroup,
|
|
59
|
+
enterProjectPane: this.options.enterProjectPane,
|
|
60
|
+
setMainPaneProjectMode: this.options.setMainPaneProjectMode,
|
|
61
|
+
selectLeftNavRepository: this.options.selectLeftNavRepository,
|
|
62
|
+
markDirty: this.options.markDirty,
|
|
63
|
+
directoriesHas: this.options.directoriesHas,
|
|
64
|
+
visibleTargetsForState: () => this.visibleTargets(),
|
|
65
|
+
conversationDirectoryId: this.options.conversationDirectoryId,
|
|
66
|
+
queueControlPlaneOp: this.options.queueControlPlaneOp,
|
|
67
|
+
activateConversation: this.options.activateConversation,
|
|
68
|
+
conversationsHas: this.options.conversationsHas,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
cycleSelection(direction: 'next' | 'previous'): boolean {
|
|
73
|
+
return this.cycleLeftNavSelectionFrame({
|
|
74
|
+
visibleTargets: this.visibleTargets(),
|
|
75
|
+
currentSelection: this.options.getCurrentSelection(),
|
|
76
|
+
direction,
|
|
77
|
+
activateTarget: (target, nextDirection) => this.activateTarget(target, nextDirection),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|