@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,383 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { dirname, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { resolveHarnessWorkspaceDirectory } from '../../config/harness-paths.ts';
|
|
13
|
+
|
|
14
|
+
const RELEASE_NOTES_STATE_FILE_NAME = 'release-notes.json';
|
|
15
|
+
const DEFAULT_RELEASE_NOTES_URL = 'https://github.com/jmoyers/harness/releases';
|
|
16
|
+
const DEFAULT_RELEASE_NOTES_API_URL = 'https://api.github.com/repos/jmoyers/harness/releases';
|
|
17
|
+
const PACKAGE_JSON_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '../../../package.json');
|
|
18
|
+
|
|
19
|
+
const RELEASE_NOTES_STATE_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
interface ParsedSemver {
|
|
22
|
+
readonly major: number;
|
|
23
|
+
readonly minor: number;
|
|
24
|
+
readonly patch: number;
|
|
25
|
+
readonly prerelease: readonly (number | string)[] | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface NormalizedGitHubRelease {
|
|
29
|
+
readonly tag: string;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly url: string;
|
|
32
|
+
readonly body: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ReleaseNotesState {
|
|
36
|
+
readonly version: typeof RELEASE_NOTES_STATE_VERSION;
|
|
37
|
+
readonly neverShow: boolean;
|
|
38
|
+
readonly dismissedLatestTag: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ReleaseNotesPromptRelease {
|
|
42
|
+
readonly tag: string;
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly url: string;
|
|
45
|
+
readonly previewLines: readonly string[];
|
|
46
|
+
readonly previewTruncated: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ReleaseNotesPrompt {
|
|
50
|
+
readonly currentVersion: string;
|
|
51
|
+
readonly latestTag: string;
|
|
52
|
+
readonly releases: readonly ReleaseNotesPromptRelease[];
|
|
53
|
+
readonly releasesPageUrl: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ResolveReleaseNotesPromptOptions {
|
|
57
|
+
readonly currentVersion: string;
|
|
58
|
+
readonly releases: readonly NormalizedGitHubRelease[];
|
|
59
|
+
readonly previewLineCount: number;
|
|
60
|
+
readonly maxReleases: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface FetchReleaseNotesPromptOptions {
|
|
64
|
+
readonly currentVersion: string;
|
|
65
|
+
readonly previewLineCount: number;
|
|
66
|
+
readonly maxReleases: number;
|
|
67
|
+
readonly fetchImpl?: typeof fetch;
|
|
68
|
+
readonly apiUrl?: string;
|
|
69
|
+
readonly releasesPageUrl?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseSemverTag(value: string): ParsedSemver | null {
|
|
73
|
+
const trimmed = value.trim();
|
|
74
|
+
const normalized = trimmed.startsWith('v') ? trimmed.slice(1) : trimmed;
|
|
75
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/u.exec(normalized);
|
|
76
|
+
if (match === null) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const major = Number.parseInt(match[1]!, 10);
|
|
80
|
+
const minor = Number.parseInt(match[2]!, 10);
|
|
81
|
+
const patch = Number.parseInt(match[3]!, 10);
|
|
82
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const prereleaseRaw = match[4];
|
|
86
|
+
if (prereleaseRaw === undefined) {
|
|
87
|
+
return {
|
|
88
|
+
major,
|
|
89
|
+
minor,
|
|
90
|
+
patch,
|
|
91
|
+
prerelease: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const prerelease = prereleaseRaw
|
|
95
|
+
.split('.')
|
|
96
|
+
.map((part) => {
|
|
97
|
+
if (/^\d+$/u.test(part)) {
|
|
98
|
+
return Number.parseInt(part, 10);
|
|
99
|
+
}
|
|
100
|
+
return part;
|
|
101
|
+
})
|
|
102
|
+
.filter((part) => (typeof part === 'number' ? Number.isFinite(part) : part.length > 0));
|
|
103
|
+
return {
|
|
104
|
+
major,
|
|
105
|
+
minor,
|
|
106
|
+
patch,
|
|
107
|
+
prerelease: prerelease.length > 0 ? prerelease : null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function comparePrerelease(
|
|
112
|
+
left: readonly (number | string)[] | null,
|
|
113
|
+
right: readonly (number | string)[] | null,
|
|
114
|
+
): number {
|
|
115
|
+
if (left === null && right === null) {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (left === null) {
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
if (right === null) {
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
const maxLength = Math.max(left.length, right.length);
|
|
125
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
126
|
+
const leftEntry = left[index];
|
|
127
|
+
const rightEntry = right[index];
|
|
128
|
+
if (leftEntry === undefined) {
|
|
129
|
+
return -1;
|
|
130
|
+
}
|
|
131
|
+
if (rightEntry === undefined) {
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
if (leftEntry === rightEntry) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const leftNumber = typeof leftEntry === 'number';
|
|
138
|
+
const rightNumber = typeof rightEntry === 'number';
|
|
139
|
+
if (leftNumber && rightNumber) {
|
|
140
|
+
return leftEntry < rightEntry ? -1 : 1;
|
|
141
|
+
}
|
|
142
|
+
if (leftNumber && !rightNumber) {
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
145
|
+
if (!leftNumber && rightNumber) {
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
return String(leftEntry).localeCompare(String(rightEntry));
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function compareSemverTags(leftTag: string, rightTag: string): number {
|
|
154
|
+
const left = parseSemverTag(leftTag);
|
|
155
|
+
const right = parseSemverTag(rightTag);
|
|
156
|
+
if (left === null || right === null) {
|
|
157
|
+
return leftTag.localeCompare(rightTag);
|
|
158
|
+
}
|
|
159
|
+
if (left.major !== right.major) {
|
|
160
|
+
return left.major < right.major ? -1 : 1;
|
|
161
|
+
}
|
|
162
|
+
if (left.minor !== right.minor) {
|
|
163
|
+
return left.minor < right.minor ? -1 : 1;
|
|
164
|
+
}
|
|
165
|
+
if (left.patch !== right.patch) {
|
|
166
|
+
return left.patch < right.patch ? -1 : 1;
|
|
167
|
+
}
|
|
168
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function previewLinesForBody(
|
|
172
|
+
body: string,
|
|
173
|
+
maxLines: number,
|
|
174
|
+
): {
|
|
175
|
+
readonly lines: readonly string[];
|
|
176
|
+
readonly truncated: boolean;
|
|
177
|
+
} {
|
|
178
|
+
const safeMaxLines = Math.max(1, Math.floor(maxLines));
|
|
179
|
+
const normalizedLines = body
|
|
180
|
+
.replace(/\r\n?/gu, '\n')
|
|
181
|
+
.split('\n')
|
|
182
|
+
.map((line) => line.trim())
|
|
183
|
+
.filter((line) => line.length > 0);
|
|
184
|
+
return {
|
|
185
|
+
lines: normalizedLines.slice(0, safeMaxLines),
|
|
186
|
+
truncated: normalizedLines.length > safeMaxLines,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
191
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return value as Record<string, unknown>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseGitHubReleaseList(raw: unknown): readonly NormalizedGitHubRelease[] {
|
|
198
|
+
if (!Array.isArray(raw)) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const parsed: NormalizedGitHubRelease[] = [];
|
|
202
|
+
const seenTags = new Set<string>();
|
|
203
|
+
for (const item of raw) {
|
|
204
|
+
const record = asRecord(item);
|
|
205
|
+
if (record === null) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (record['draft'] === true || record['prerelease'] === true) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const tagName = record['tag_name'];
|
|
212
|
+
const htmlUrl = record['html_url'];
|
|
213
|
+
if (typeof tagName !== 'string' || typeof htmlUrl !== 'string') {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const tag = tagName.trim();
|
|
217
|
+
const url = htmlUrl.trim();
|
|
218
|
+
if (tag.length === 0 || url.length === 0 || seenTags.has(tag)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
seenTags.add(tag);
|
|
222
|
+
parsed.push({
|
|
223
|
+
tag,
|
|
224
|
+
name: typeof record['name'] === 'string' ? record['name'].trim() : '',
|
|
225
|
+
url,
|
|
226
|
+
body: typeof record['body'] === 'string' ? record['body'] : '',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
parsed.sort((left, right) => compareSemverTags(right.tag, left.tag));
|
|
230
|
+
return parsed;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function resolveReleaseNotesPrompt(
|
|
234
|
+
options: ResolveReleaseNotesPromptOptions,
|
|
235
|
+
): ReleaseNotesPrompt | null {
|
|
236
|
+
const sorted = [...options.releases].sort((left, right) =>
|
|
237
|
+
compareSemverTags(right.tag, left.tag),
|
|
238
|
+
);
|
|
239
|
+
if (sorted.length === 0) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const newer = sorted.filter(
|
|
243
|
+
(release) => compareSemverTags(release.tag, options.currentVersion) > 0,
|
|
244
|
+
);
|
|
245
|
+
if (newer.length === 0) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const safeMaxReleases = Math.max(1, Math.floor(options.maxReleases));
|
|
249
|
+
const releases = newer.slice(0, safeMaxReleases).map((release) => {
|
|
250
|
+
const preview = previewLinesForBody(release.body, options.previewLineCount);
|
|
251
|
+
return {
|
|
252
|
+
tag: release.tag,
|
|
253
|
+
name: release.name,
|
|
254
|
+
url: release.url,
|
|
255
|
+
previewLines: preview.lines,
|
|
256
|
+
previewTruncated: preview.truncated,
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
currentVersion: options.currentVersion,
|
|
261
|
+
latestTag: newer[0]!.tag,
|
|
262
|
+
releases,
|
|
263
|
+
releasesPageUrl: DEFAULT_RELEASE_NOTES_URL,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function fetchReleaseNotesPrompt(
|
|
268
|
+
options: FetchReleaseNotesPromptOptions,
|
|
269
|
+
): Promise<ReleaseNotesPrompt | null> {
|
|
270
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
271
|
+
const apiUrl = options.apiUrl ?? DEFAULT_RELEASE_NOTES_API_URL;
|
|
272
|
+
const releasesPageUrl = options.releasesPageUrl ?? DEFAULT_RELEASE_NOTES_URL;
|
|
273
|
+
try {
|
|
274
|
+
const response = await fetchImpl(`${apiUrl}?per_page=20`, {
|
|
275
|
+
headers: {
|
|
276
|
+
Accept: 'application/vnd.github+json',
|
|
277
|
+
'User-Agent': `harness/${options.currentVersion}`,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
if (!response.ok) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const raw = (await response.json()) as unknown;
|
|
284
|
+
const releases = parseGitHubReleaseList(raw);
|
|
285
|
+
const prompt = resolveReleaseNotesPrompt({
|
|
286
|
+
currentVersion: options.currentVersion,
|
|
287
|
+
releases,
|
|
288
|
+
previewLineCount: options.previewLineCount,
|
|
289
|
+
maxReleases: options.maxReleases,
|
|
290
|
+
});
|
|
291
|
+
if (prompt === null) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
...prompt,
|
|
296
|
+
releasesPageUrl,
|
|
297
|
+
};
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function defaultReleaseNotesState(): ReleaseNotesState {
|
|
304
|
+
return {
|
|
305
|
+
version: RELEASE_NOTES_STATE_VERSION,
|
|
306
|
+
neverShow: false,
|
|
307
|
+
dismissedLatestTag: null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function parseReleaseNotesState(raw: unknown): ReleaseNotesState | null {
|
|
312
|
+
const record = asRecord(raw);
|
|
313
|
+
if (record === null) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (record['version'] !== RELEASE_NOTES_STATE_VERSION) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
const neverShow = record['neverShow'];
|
|
320
|
+
const dismissedLatestTag = record['dismissedLatestTag'];
|
|
321
|
+
if (typeof neverShow !== 'boolean') {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (dismissedLatestTag !== null && typeof dismissedLatestTag !== 'string') {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
version: RELEASE_NOTES_STATE_VERSION,
|
|
329
|
+
neverShow,
|
|
330
|
+
dismissedLatestTag,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function resolveReleaseNotesStatePath(
|
|
335
|
+
invocationDirectory: string,
|
|
336
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
337
|
+
): string {
|
|
338
|
+
return resolve(
|
|
339
|
+
resolveHarnessWorkspaceDirectory(invocationDirectory, env),
|
|
340
|
+
RELEASE_NOTES_STATE_FILE_NAME,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function readReleaseNotesState(statePath: string): ReleaseNotesState {
|
|
345
|
+
if (!existsSync(statePath)) {
|
|
346
|
+
return defaultReleaseNotesState();
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const raw = JSON.parse(readFileSync(statePath, 'utf8')) as unknown;
|
|
350
|
+
return parseReleaseNotesState(raw) ?? defaultReleaseNotesState();
|
|
351
|
+
} catch {
|
|
352
|
+
return defaultReleaseNotesState();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function writeReleaseNotesState(statePath: string, state: ReleaseNotesState): void {
|
|
357
|
+
const tempPath = `${statePath}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
358
|
+
try {
|
|
359
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
360
|
+
writeFileSync(tempPath, `${JSON.stringify(state)}\n`, 'utf8');
|
|
361
|
+
renameSync(tempPath, statePath);
|
|
362
|
+
} catch (error: unknown) {
|
|
363
|
+
try {
|
|
364
|
+
unlinkSync(tempPath);
|
|
365
|
+
} catch {
|
|
366
|
+
// Best-effort cleanup only.
|
|
367
|
+
}
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function readInstalledHarnessVersion(packageJsonPath: string = PACKAGE_JSON_PATH): string {
|
|
373
|
+
try {
|
|
374
|
+
const raw = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as unknown;
|
|
375
|
+
const version = asRecord(raw)?.['version'];
|
|
376
|
+
if (typeof version === 'string' && version.trim().length > 0) {
|
|
377
|
+
return version.trim();
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
// Fall back to a safe value that still allows release comparisons.
|
|
381
|
+
}
|
|
382
|
+
return '0.0.0';
|
|
383
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const ESC = '\u001b';
|
|
2
2
|
|
|
3
3
|
const SUPPORTED_ESC_SINGLE = new Set(['7', '8', 'D', 'E', 'M', 'H', 'c']);
|
|
4
|
+
const SUPPORTED_C0 = new Set(['\b', '\t', '\n', '\r']);
|
|
4
5
|
const SUPPORTED_CSI_FINALS = new Set([
|
|
5
6
|
'm',
|
|
6
7
|
'A',
|
|
@@ -27,9 +28,13 @@ const SUPPORTED_PRIVATE_MODE_PARAMS = new Set([
|
|
|
27
28
|
6, 25, 1000, 1002, 1003, 1004, 1005, 1006, 1015, 2004, 1047, 1048, 1049,
|
|
28
29
|
]);
|
|
29
30
|
|
|
30
|
-
type RenderTraceControlIssueKind =
|
|
31
|
+
export type RenderTraceControlIssueKind =
|
|
32
|
+
| 'unsupported-c0'
|
|
33
|
+
| 'unsupported-esc'
|
|
34
|
+
| 'unsupported-csi'
|
|
35
|
+
| 'unsupported-dcs';
|
|
31
36
|
|
|
32
|
-
interface RenderTraceControlIssue {
|
|
37
|
+
export interface RenderTraceControlIssue {
|
|
33
38
|
readonly kind: RenderTraceControlIssueKind;
|
|
34
39
|
readonly offset: number;
|
|
35
40
|
readonly sequence: string;
|
|
@@ -37,6 +42,37 @@ interface RenderTraceControlIssue {
|
|
|
37
42
|
readonly rawParams?: string;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
function isUnsupportedControlCharacter(char: string): boolean {
|
|
46
|
+
if (char === ESC || SUPPORTED_C0.has(char)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const code = char.charCodeAt(0);
|
|
50
|
+
if (code < 0x20) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return code === 0x7f || (code >= 0x80 && code < 0xa0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function escapeCharForPreview(char: string): string {
|
|
57
|
+
if (char === '\r') {
|
|
58
|
+
return '\\r';
|
|
59
|
+
}
|
|
60
|
+
if (char === '\n') {
|
|
61
|
+
return '\\n';
|
|
62
|
+
}
|
|
63
|
+
if (char === '\t') {
|
|
64
|
+
return '\\t';
|
|
65
|
+
}
|
|
66
|
+
if (char === ESC) {
|
|
67
|
+
return '\\u001b';
|
|
68
|
+
}
|
|
69
|
+
const code = char.charCodeAt(0);
|
|
70
|
+
if (code < 0x20 || code === 0x7f || (code >= 0x80 && code < 0xa0)) {
|
|
71
|
+
return `\\u${code.toString(16).padStart(4, '0')}`;
|
|
72
|
+
}
|
|
73
|
+
return char;
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
function isLikelyCsiQueryPayload(payload: string): boolean {
|
|
41
77
|
if (/^(?:c|0c|>c|>0c)$/u.test(payload)) {
|
|
42
78
|
return true;
|
|
@@ -88,11 +124,10 @@ function csiSupported(rawParams: string, finalByte: string): boolean {
|
|
|
88
124
|
|
|
89
125
|
export function renderTraceChunkPreview(chunk: Buffer | string, maxChars = 200): string {
|
|
90
126
|
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.replaceAll(ESC, '\\u001b');
|
|
127
|
+
let replaced = '';
|
|
128
|
+
for (const char of text) {
|
|
129
|
+
replaced += escapeCharForPreview(char);
|
|
130
|
+
}
|
|
96
131
|
if (replaced.length <= maxChars) {
|
|
97
132
|
return replaced;
|
|
98
133
|
}
|
|
@@ -108,6 +143,16 @@ export function findRenderTraceControlIssues(
|
|
|
108
143
|
|
|
109
144
|
let index = 0;
|
|
110
145
|
while (index < text.length && issues.length < maxIssues) {
|
|
146
|
+
const current = text[index]!;
|
|
147
|
+
if (isUnsupportedControlCharacter(current)) {
|
|
148
|
+
issues.push({
|
|
149
|
+
kind: 'unsupported-c0',
|
|
150
|
+
offset: index,
|
|
151
|
+
sequence: current,
|
|
152
|
+
});
|
|
153
|
+
index += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
111
156
|
if (text[index] !== ESC) {
|
|
112
157
|
index += 1;
|
|
113
158
|
continue;
|
|
@@ -12,6 +12,9 @@ export function selectedRepositoryGroupIdForLeftNav(
|
|
|
12
12
|
if (leftNavSelection.kind === 'project') {
|
|
13
13
|
return repositoryGroupIdForDirectory(leftNavSelection.directoryId);
|
|
14
14
|
}
|
|
15
|
+
if (leftNavSelection.kind === 'github') {
|
|
16
|
+
return repositoryGroupIdForDirectory(leftNavSelection.directoryId);
|
|
17
|
+
}
|
|
15
18
|
if (leftNavSelection.kind === 'conversation') {
|
|
16
19
|
const conversation = conversations.get(leftNavSelection.sessionId);
|
|
17
20
|
if (conversation?.directoryId !== null && conversation?.directoryId !== undefined) {
|
|
@@ -89,10 +89,6 @@ export function hasAltModifier(code: number): boolean {
|
|
|
89
89
|
return (code & 0b0000_1000) !== 0;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export function hasShiftModifier(code: number): boolean {
|
|
93
|
-
return (code & 0b0000_0100) !== 0;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
92
|
export function isLeftButtonPress(code: number, final: 'M' | 'm'): boolean {
|
|
97
93
|
if (final !== 'M') {
|
|
98
94
|
return false;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { resolveHarnessWorkspaceDirectory } from '../../config/harness-paths.ts';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_SESSION_DIAGNOSTICS_ROOT_PATH = 'session-diagnostics';
|
|
5
|
+
|
|
6
|
+
export function resolveSessionDiagnosticsDirectory(
|
|
7
|
+
invocationDirectory: string,
|
|
8
|
+
sessionName: string | null,
|
|
9
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
10
|
+
): string {
|
|
11
|
+
const workspaceDirectory = resolveHarnessWorkspaceDirectory(invocationDirectory, env);
|
|
12
|
+
if (sessionName === null) {
|
|
13
|
+
return resolve(workspaceDirectory, DEFAULT_SESSION_DIAGNOSTICS_ROOT_PATH);
|
|
14
|
+
}
|
|
15
|
+
return resolve(
|
|
16
|
+
workspaceDirectory,
|
|
17
|
+
'sessions',
|
|
18
|
+
sessionName,
|
|
19
|
+
DEFAULT_SESSION_DIAGNOSTICS_ROOT_PATH,
|
|
20
|
+
);
|
|
21
|
+
}
|