@jmoyers/harness 0.1.10 → 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 -35
- 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/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
- 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 -3721
- package/scripts/control-plane-daemon.ts +24 -2
- 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 -3007
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -0
- package/src/cli/default-gateway-pointer.ts +193 -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 +361 -10
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/agent-realtime-api.ts +82 -427
- package/src/control-plane/prompt/thread-title-namer.ts +49 -23
- 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-background.ts +18 -2
- 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 +943 -80
- 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/conversations.ts +11 -7
- package/src/domain/workspace.ts +76 -4
- 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 +22 -112
- 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-parsing.ts +16 -0
- 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 +106 -8
- package/src/mux/live-mux/modal-overlays.ts +210 -31
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
- 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 +19 -82
- 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 +82 -30
- package/src/services/runtime-conversation-starter.ts +80 -48
- 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 -70
- 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 +396 -56
- package/src/store/event-store.ts +397 -3
- 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 -82
- 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 -189
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -119
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -278
- 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 -238
- 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/repository-fold-input.ts +0 -91
- package/src/ui/surface.ts +0 -224
package/README.md
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# Harness
|
|
2
2
|
|
|
3
|
-
Harness is
|
|
3
|
+
Harness is minimal agent orchestration in the terminal.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Programmable, agent-agnostic, threaded, ergonomic, and fast.
|
|
6
6
|
|
|
7
7
|
## What matters most
|
|
8
8
|
|
|
9
9
|
- Parallel threads across `codex`, `claude`, `cursor`, `terminal`, and `critique`.
|
|
10
|
-
- One command palette (`ctrl+p`
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
10
|
+
- One command palette (`ctrl+p`) to jump threads, run actions, and control workflow quickly.
|
|
11
|
+
- Toggle the bottom debug bar with `cmd+p` when you need runtime launch/auth context.
|
|
12
|
+
- Codex, Claude Code, and Cursor together in one workspace.
|
|
13
|
+
- Diff with Critique, with integrated terminals (`harness diff` + critique actions).
|
|
14
|
+
- Detached gateway sessions keep long-running work alive through reconnects.
|
|
15
|
+
- Storage lifecycle maintenance remains paused in interactive runtime paths; use `harness gateway gc` as a manual offline truncation/compaction escape hatch.
|
|
16
|
+
- Storage lifecycle policy updates from `harness.config.jsonc` still apply without restart.
|
|
17
|
+
- Command palette can open a GitHub thread entry in the left rail for the active project, then show full tracked-branch PR/review details in the main panel.
|
|
18
|
+
- Open the active project directly in local tools (`iTerm2`, `Ghostty`, `Zed`, `Cursor`, `VSCode`, `Warp`, `Finder`) or copy its path from the palette.
|
|
19
|
+
- Command-click links inside conversation terminal output: URLs open in browser, file-like paths open in your configured editor/open-in command.
|
|
15
20
|
|
|
16
21
|
## Demo
|
|
17
22
|
|
|
@@ -44,34 +49,25 @@ Use a named session when you want isolated state:
|
|
|
44
49
|
harness --session my-session
|
|
45
50
|
```
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
Named sessions automatically fall back to an available gateway port when the preferred port is already occupied. For deterministic restart/load diagnostics, you can still set an explicit non-default gateway port.
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
Standalone diff viewer (phase 1):
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
4. Open or create a PR from the same workspace.
|
|
55
|
-
|
|
56
|
-
## User details
|
|
57
|
-
|
|
58
|
-
- Thread-scoped command palette (`[+ thread]`) can launch/install supported agent CLIs per project.
|
|
59
|
-
- Critique review actions are available from the global palette and run in a terminal thread.
|
|
60
|
-
- `ctrl+g` opens the project’s critique thread (or creates one if needed).
|
|
61
|
-
- Theme selection is built in (`Set a Theme`) with OpenCode-compatible presets and live preview.
|
|
62
|
-
- PR actions use either `GITHUB_TOKEN` or an authenticated `gh` CLI session.
|
|
63
|
-
|
|
64
|
-
## Configuration
|
|
65
|
-
|
|
66
|
-
Runtime behavior is controlled by `harness.config.jsonc`.
|
|
67
|
-
|
|
68
|
-
Common customizations:
|
|
69
|
-
|
|
70
|
-
- Set install commands for `codex`, `claude`, `cursor`, and `critique`.
|
|
71
|
-
- Configure critique launch defaults.
|
|
72
|
-
- Customize keybindings.
|
|
73
|
-
- Choose a theme preset or custom OpenCode-compatible theme file.
|
|
74
|
-
|
|
75
|
-
## License
|
|
56
|
+
```bash
|
|
57
|
+
harness diff --help
|
|
58
|
+
```
|
|
76
59
|
|
|
77
|
-
|
|
60
|
+
## Architecture (VTE path)
|
|
61
|
+
|
|
62
|
+
```mermaid
|
|
63
|
+
flowchart LR
|
|
64
|
+
U[Keyboard + Command Palette] --> UI[Harness TUI]
|
|
65
|
+
UI --> CP[Control Plane Stream API]
|
|
66
|
+
CP --> TM[Thread Manager]
|
|
67
|
+
TM --> AG[Agent Threads<br/>Codex / Claude / Cursor]
|
|
68
|
+
TM --> PTY[Integrated PTY Terminals]
|
|
69
|
+
PTY --> VTE[VTE Parser + Screen Model]
|
|
70
|
+
VTE --> R[Terminal Renderer]
|
|
71
|
+
TM --> DF[harness diff + Critique]
|
|
72
|
+
DF --> R
|
|
73
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmoyers/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,11 +19,16 @@
|
|
|
19
19
|
"scripts/cursor-hook-relay.ts",
|
|
20
20
|
"scripts/harness-animate.ts",
|
|
21
21
|
"scripts/harness-bin.js",
|
|
22
|
+
"scripts/harness-commands.ts",
|
|
22
23
|
"scripts/harness-core.ts",
|
|
23
|
-
"scripts/harness-
|
|
24
|
+
"scripts/harness-runtime.ts",
|
|
24
25
|
"scripts/harness.ts",
|
|
26
|
+
"scripts/nim-tui-smoke.ts",
|
|
25
27
|
"packages/harness-ai/src",
|
|
26
|
-
"
|
|
28
|
+
"packages/harness-ui/src",
|
|
29
|
+
"packages/nim-core/src",
|
|
30
|
+
"packages/nim-ui-core/src",
|
|
31
|
+
"packages/nim-test-tui/src",
|
|
27
32
|
"scripts/require-bun.js",
|
|
28
33
|
"native/ptyd/Cargo.lock",
|
|
29
34
|
"native/ptyd/Cargo.toml",
|
|
@@ -35,6 +40,13 @@
|
|
|
35
40
|
"publishConfig": {
|
|
36
41
|
"access": "public"
|
|
37
42
|
},
|
|
43
|
+
"oclif": {
|
|
44
|
+
"bin": "harness",
|
|
45
|
+
"commands": {
|
|
46
|
+
"strategy": "explicit",
|
|
47
|
+
"target": "scripts/harness-commands.ts"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
38
50
|
"scripts": {
|
|
39
51
|
"migrate:bun": "bash ./scripts/migrate-to-bun.sh",
|
|
40
52
|
"harness": "bun scripts/harness.ts",
|
|
@@ -68,28 +80,36 @@
|
|
|
68
80
|
"perf:codex:startup:loop": "bun scripts/perf-codex-startup-loop.ts",
|
|
69
81
|
"loc": "bun scripts/loc-report.ts",
|
|
70
82
|
"loc:verify": "bun scripts/check-max-loc.ts --max-loc 2000",
|
|
71
|
-
"loc:verify:enforce": "bun scripts/check-max-loc.ts --max-loc 2000 --enforce",
|
|
83
|
+
"loc:verify:enforce": "bun scripts/check-max-loc.ts --max-loc 2000 --enforce --baseline harness.max-loc-baseline.json",
|
|
72
84
|
"deadcode": "bun scripts/check-dead-code.ts",
|
|
73
85
|
"coverage:check": "bun scripts/check-coverage.ts --lcov .harness/coverage-bun/lcov.info --config harness.coverage.jsonc",
|
|
74
86
|
"release": "bun scripts/release.ts",
|
|
75
|
-
"lint": "oxlint --deny-warnings --disable-unicorn-plugin -c .oxlintrc.json src test scripts packages/harness-ai/src",
|
|
76
|
-
"format": "oxfmt --config .oxfmtrc.json src test scripts packages/harness-ai/src",
|
|
77
|
-
"format:check": "oxfmt --check --config .oxfmtrc.json src test scripts packages/harness-ai/src",
|
|
87
|
+
"lint": "oxlint --deny-warnings --disable-unicorn-plugin -c .oxlintrc.json src test scripts packages/harness-ai/src packages/harness-ui/src packages/nim-core/src packages/nim-ui-core/src packages/nim-test-tui/src",
|
|
88
|
+
"format": "oxfmt --config .oxfmtrc.json src test scripts packages/harness-ai/src packages/harness-ui/src packages/nim-core/src packages/nim-ui-core/src packages/nim-test-tui/src",
|
|
89
|
+
"format:check": "oxfmt --check --config .oxfmtrc.json src test scripts packages/harness-ai/src packages/harness-ui/src packages/nim-core/src packages/nim-ui-core/src packages/nim-test-tui/src",
|
|
78
90
|
"typecheck": "tsc --noEmit",
|
|
79
|
-
"test": "bun run build:ptyd && bun test",
|
|
91
|
+
"test": "bun run build:ptyd && bun test --concurrent",
|
|
92
|
+
"test:serial": "bun run build:ptyd && bun test",
|
|
80
93
|
"test:integration:codex-status": "bun scripts/integration-codex-status-sequence.ts",
|
|
81
94
|
"test:integration:codex-status:long": "bun scripts/integration-codex-status-sequence.ts --timeout-ms 180000 --prompt \"Write three short poems titled Dawn, Voltage, and Orbit. Before each poem, perform one repository inspection action and include one factual line from that action. Use at least three total tool actions and do not edit any files.\"",
|
|
82
95
|
"test:integration:agent-prompts": "bun scripts/integration-agent-prompt-parity.ts",
|
|
83
96
|
"test:integration:agent-prompts:long": "bun scripts/integration-agent-prompt-parity.ts --timeout-ms 180000",
|
|
97
|
+
"test:integration:nim:haiku": "bun scripts/integration-nim-haiku.ts",
|
|
98
|
+
"nim:tui": "bun scripts/nim-tui-smoke.ts",
|
|
84
99
|
"smoke:harness-ai": "bun scripts/harness-ai-smoke.ts",
|
|
85
100
|
"smoke:harness-ai:parity": "bun scripts/harness-ai-parity-smoke.mts",
|
|
86
|
-
"test:coverage": "bun run build:ptyd && bun test --coverage --coverage-reporter=lcov --coverage-dir .harness/coverage-bun && bun run coverage:check",
|
|
101
|
+
"test:coverage": "bun run build:ptyd && bun test --concurrent --coverage --coverage-reporter=lcov --coverage-dir .harness/coverage-bun && bun run coverage:check",
|
|
102
|
+
"test:coverage:serial": "bun run build:ptyd && bun test --coverage --coverage-reporter=lcov --coverage-dir .harness/coverage-bun && bun run coverage:check",
|
|
87
103
|
"verify": "bun run format:check && bun run lint && bun run typecheck && bun run deadcode && bun run test:coverage",
|
|
88
|
-
"verify:
|
|
104
|
+
"verify:coverage-gate": "bun run format:check && bun run lint && bun run typecheck && bun run deadcode && bun run test:coverage",
|
|
105
|
+
"verify:strict": "bun run verify:coverage-gate && bun run loc:verify:enforce"
|
|
89
106
|
},
|
|
90
107
|
"dependencies": {
|
|
108
|
+
"@linear/sdk": "^75.0.0",
|
|
91
109
|
"@napi-rs/canvas": "^0.1.92",
|
|
92
|
-
"
|
|
110
|
+
"@oclif/core": "^4.8.0",
|
|
111
|
+
"gifenc": "^1.0.3",
|
|
112
|
+
"zustand": "^5.0.11"
|
|
93
113
|
},
|
|
94
114
|
"devDependencies": {
|
|
95
115
|
"@ai-sdk/anthropic": "^3.0.45",
|
|
@@ -308,79 +308,79 @@ function parseContentBlock(value: unknown): AnthropicContentBlock | null {
|
|
|
308
308
|
};
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
if (type
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
311
|
+
if (type !== 'web_fetch_tool_result') {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const toolUseId = asString(record['tool_use_id']);
|
|
316
|
+
const contentRecord = asRecord(record['content']);
|
|
317
|
+
if (toolUseId === null || contentRecord === null) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const contentType = asString(contentRecord['type']);
|
|
322
|
+
if (contentType === null) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (contentType === 'web_fetch_result') {
|
|
327
|
+
const innerContent = asRecord(contentRecord['content']);
|
|
328
|
+
const source = innerContent === null ? null : asRecord(innerContent['source']);
|
|
329
|
+
if (innerContent === null || source === null) {
|
|
315
330
|
return null;
|
|
316
331
|
}
|
|
317
332
|
|
|
318
|
-
const
|
|
319
|
-
|
|
333
|
+
const url = asString(contentRecord['url']);
|
|
334
|
+
const sourceType = asString(source['type']);
|
|
335
|
+
const sourceMediaType = asString(source['media_type']);
|
|
336
|
+
const sourceData = asString(source['data']);
|
|
337
|
+
const contentBlockType = asString(innerContent['type']);
|
|
338
|
+
if (
|
|
339
|
+
url === null ||
|
|
340
|
+
sourceType === null ||
|
|
341
|
+
sourceMediaType === null ||
|
|
342
|
+
sourceData === null ||
|
|
343
|
+
contentBlockType === null
|
|
344
|
+
) {
|
|
320
345
|
return null;
|
|
321
346
|
}
|
|
322
347
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const url = asString(contentRecord['url']);
|
|
331
|
-
const sourceType = asString(source['type']);
|
|
332
|
-
const sourceMediaType = asString(source['media_type']);
|
|
333
|
-
const sourceData = asString(source['data']);
|
|
334
|
-
const contentBlockType = asString(innerContent['type']);
|
|
335
|
-
if (
|
|
336
|
-
url === null ||
|
|
337
|
-
sourceType === null ||
|
|
338
|
-
sourceMediaType === null ||
|
|
339
|
-
sourceData === null ||
|
|
340
|
-
contentBlockType === null
|
|
341
|
-
) {
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const retrievedAt = asString(contentRecord['retrieved_at']);
|
|
346
|
-
const title = asString(innerContent['title']);
|
|
347
|
-
const citations = Array.isArray(innerContent['citations'])
|
|
348
|
-
? (innerContent['citations'] as unknown[])
|
|
349
|
-
: null;
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
type,
|
|
353
|
-
tool_use_id: toolUseId,
|
|
354
|
-
content: {
|
|
355
|
-
type: 'web_fetch_result',
|
|
356
|
-
url,
|
|
357
|
-
...(retrievedAt !== null ? { retrieved_at: retrievedAt } : {}),
|
|
358
|
-
content: {
|
|
359
|
-
type: contentBlockType,
|
|
360
|
-
...(title !== null ? { title } : {}),
|
|
361
|
-
source: {
|
|
362
|
-
type: sourceType,
|
|
363
|
-
media_type: sourceMediaType,
|
|
364
|
-
data: sourceData,
|
|
365
|
-
},
|
|
366
|
-
...(citations !== null ? { citations } : {}),
|
|
367
|
-
},
|
|
368
|
-
},
|
|
369
|
-
};
|
|
370
|
-
}
|
|
348
|
+
const retrievedAt = asString(contentRecord['retrieved_at']);
|
|
349
|
+
const title = asString(innerContent['title']);
|
|
350
|
+
const citations = Array.isArray(innerContent['citations'])
|
|
351
|
+
? (innerContent['citations'] as unknown[])
|
|
352
|
+
: null;
|
|
371
353
|
|
|
372
|
-
const errorCode = asString(contentRecord['error_code']);
|
|
373
354
|
return {
|
|
374
355
|
type,
|
|
375
356
|
tool_use_id: toolUseId,
|
|
376
357
|
content: {
|
|
377
|
-
type:
|
|
378
|
-
|
|
358
|
+
type: 'web_fetch_result',
|
|
359
|
+
url,
|
|
360
|
+
...(retrievedAt !== null ? { retrieved_at: retrievedAt } : {}),
|
|
361
|
+
content: {
|
|
362
|
+
type: contentBlockType,
|
|
363
|
+
...(title !== null ? { title } : {}),
|
|
364
|
+
source: {
|
|
365
|
+
type: sourceType,
|
|
366
|
+
media_type: sourceMediaType,
|
|
367
|
+
data: sourceData,
|
|
368
|
+
},
|
|
369
|
+
...(citations !== null ? { citations } : {}),
|
|
370
|
+
},
|
|
379
371
|
},
|
|
380
372
|
};
|
|
381
373
|
}
|
|
382
374
|
|
|
383
|
-
|
|
375
|
+
const errorCode = asString(contentRecord['error_code']);
|
|
376
|
+
return {
|
|
377
|
+
type,
|
|
378
|
+
tool_use_id: toolUseId,
|
|
379
|
+
content: {
|
|
380
|
+
type: contentType,
|
|
381
|
+
...(errorCode !== null ? { error_code: errorCode } : {}),
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
export function parseAnthropicStreamChunk(value: unknown): AnthropicStreamChunk | null {
|
|
@@ -550,18 +550,18 @@ export function parseAnthropicStreamChunk(value: unknown): AnthropicStreamChunk
|
|
|
550
550
|
};
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
-
if (type
|
|
554
|
-
|
|
555
|
-
if (errorRecord === null) {
|
|
556
|
-
return null;
|
|
557
|
-
}
|
|
558
|
-
return {
|
|
559
|
-
type,
|
|
560
|
-
error: errorRecord,
|
|
561
|
-
};
|
|
553
|
+
if (type !== 'error') {
|
|
554
|
+
return null;
|
|
562
555
|
}
|
|
563
556
|
|
|
564
|
-
|
|
557
|
+
const errorRecord = asRecord(record['error']);
|
|
558
|
+
if (errorRecord === null) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
type,
|
|
563
|
+
error: errorRecord,
|
|
564
|
+
};
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
export function mapAnthropicStopReason(reason: string | null | undefined): FinishReason {
|
|
@@ -37,31 +37,6 @@ import type {
|
|
|
37
37
|
TypedToolResult,
|
|
38
38
|
} from './types.ts';
|
|
39
39
|
|
|
40
|
-
interface Deferred<T> {
|
|
41
|
-
readonly promise: Promise<T>;
|
|
42
|
-
resolve(value: T): void;
|
|
43
|
-
reject(error: unknown): void;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function createDeferred<T>(): Deferred<T> {
|
|
47
|
-
let resolveFn: ((value: T) => void) | null = null;
|
|
48
|
-
let rejectFn: ((error: unknown) => void) | null = null;
|
|
49
|
-
const promise = new Promise<T>((resolve, reject) => {
|
|
50
|
-
resolveFn = resolve;
|
|
51
|
-
rejectFn = reject;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
promise,
|
|
56
|
-
resolve(value) {
|
|
57
|
-
resolveFn?.(value);
|
|
58
|
-
},
|
|
59
|
-
reject(error) {
|
|
60
|
-
rejectFn?.(error);
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
40
|
interface ContentBlockTextState {
|
|
66
41
|
readonly kind: 'text' | 'reasoning';
|
|
67
42
|
readonly id: string;
|
|
@@ -144,10 +119,6 @@ function normalizeMessages(
|
|
|
144
119
|
}
|
|
145
120
|
|
|
146
121
|
function toAnthropicMessageContent(message: ModelMessage): Array<Record<string, unknown>> {
|
|
147
|
-
if (message.role === 'system') {
|
|
148
|
-
return [{ type: 'text', text: message.content }];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
122
|
if (typeof message.content === 'string') {
|
|
152
123
|
return [{ type: 'text', text: message.content }];
|
|
153
124
|
}
|
|
@@ -319,27 +290,15 @@ interface StepRunResult<TOOLS extends ToolSet> {
|
|
|
319
290
|
readonly assistantText: string;
|
|
320
291
|
}
|
|
321
292
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
return value as Record<string, unknown>;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function mapWebSearchResult(result: AnthropicContentBlock):
|
|
293
|
+
function mapWebSearchResult(
|
|
294
|
+
result: Extract<AnthropicContentBlock, { type: 'web_search_tool_result' }>,
|
|
295
|
+
):
|
|
330
296
|
| {
|
|
331
297
|
readonly ok: true;
|
|
332
298
|
readonly output: unknown[];
|
|
333
299
|
readonly sources: Array<{ url: string; title?: string; pageAge?: string }>;
|
|
334
300
|
}
|
|
335
301
|
| { readonly ok: false; readonly error: unknown } {
|
|
336
|
-
if (result.type !== 'web_search_tool_result') {
|
|
337
|
-
return {
|
|
338
|
-
ok: false,
|
|
339
|
-
error: { message: 'invalid web search result payload' },
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
302
|
if (Array.isArray(result.content)) {
|
|
344
303
|
const mapped = result.content.map((entry) => ({
|
|
345
304
|
type: entry.type,
|
|
@@ -371,17 +330,10 @@ function mapWebSearchResult(result: AnthropicContentBlock):
|
|
|
371
330
|
}
|
|
372
331
|
|
|
373
332
|
function mapWebFetchResult(
|
|
374
|
-
result: AnthropicContentBlock,
|
|
333
|
+
result: Extract<AnthropicContentBlock, { type: 'web_fetch_tool_result' }>,
|
|
375
334
|
):
|
|
376
335
|
| { readonly ok: true; readonly output: unknown }
|
|
377
336
|
| { readonly ok: false; readonly error: unknown } {
|
|
378
|
-
if (result.type !== 'web_fetch_tool_result') {
|
|
379
|
-
return {
|
|
380
|
-
ok: false,
|
|
381
|
-
error: { message: 'invalid web fetch result payload' },
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
385
337
|
if ('url' in result.content && 'content' in result.content) {
|
|
386
338
|
const content = result.content.content;
|
|
387
339
|
return {
|
|
@@ -718,10 +670,7 @@ async function runSingleStep<TOOLS extends ToolSet>(
|
|
|
718
670
|
providerResults.push(error);
|
|
719
671
|
options.emit({ type: 'tool-error', ...error });
|
|
720
672
|
}
|
|
721
|
-
continue;
|
|
722
673
|
}
|
|
723
|
-
|
|
724
|
-
continue;
|
|
725
674
|
}
|
|
726
675
|
|
|
727
676
|
if (chunk.type === 'content_block_delta') {
|
|
@@ -798,8 +747,7 @@ async function runSingleStep<TOOLS extends ToolSet>(
|
|
|
798
747
|
}
|
|
799
748
|
|
|
800
749
|
if (chunk.type === 'error') {
|
|
801
|
-
const
|
|
802
|
-
const message = errorRecord?.['message'];
|
|
750
|
+
const message = chunk.error['message'];
|
|
803
751
|
options.emit({
|
|
804
752
|
type: 'error',
|
|
805
753
|
error:
|
|
@@ -809,9 +757,7 @@ async function runSingleStep<TOOLS extends ToolSet>(
|
|
|
809
757
|
continue;
|
|
810
758
|
}
|
|
811
759
|
|
|
812
|
-
if (chunk.type === 'message_stop')
|
|
813
|
-
break;
|
|
814
|
-
}
|
|
760
|
+
if (chunk.type === 'message_stop') break;
|
|
815
761
|
}
|
|
816
762
|
} finally {
|
|
817
763
|
reader.releaseLock();
|
|
@@ -1262,41 +1208,17 @@ export function streamText<TOOLS extends ToolSet>(
|
|
|
1262
1208
|
const uiMessageStream = createUIMessageStream(uiBranchA);
|
|
1263
1209
|
const uiResponseStream = createUIMessageStream(uiBranchB);
|
|
1264
1210
|
|
|
1265
|
-
const
|
|
1266
|
-
const toolCallsDeferred = createDeferred<TypedToolCall<TOOLS>[]>();
|
|
1267
|
-
const toolResultsDeferred =
|
|
1268
|
-
createDeferred<Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>>>();
|
|
1269
|
-
const finishReasonDeferred = createDeferred<FinishReason>();
|
|
1270
|
-
const usageDeferred = createDeferred<LanguageModelUsage>();
|
|
1271
|
-
const responseDeferred = createDeferred<LanguageModelResponseMetadata>();
|
|
1272
|
-
|
|
1273
|
-
collectResultFromStream(collectorBranch)
|
|
1274
|
-
.then((collected) => {
|
|
1275
|
-
textDeferred.resolve(collected.text);
|
|
1276
|
-
toolCallsDeferred.resolve(collected.toolCalls);
|
|
1277
|
-
toolResultsDeferred.resolve(collected.toolResults);
|
|
1278
|
-
finishReasonDeferred.resolve(collected.finishReason);
|
|
1279
|
-
usageDeferred.resolve(collected.usage);
|
|
1280
|
-
responseDeferred.resolve(collected.response);
|
|
1281
|
-
})
|
|
1282
|
-
.catch((error) => {
|
|
1283
|
-
textDeferred.reject(error);
|
|
1284
|
-
toolCallsDeferred.reject(error);
|
|
1285
|
-
toolResultsDeferred.reject(error);
|
|
1286
|
-
finishReasonDeferred.reject(error);
|
|
1287
|
-
usageDeferred.reject(error);
|
|
1288
|
-
responseDeferred.reject(error);
|
|
1289
|
-
});
|
|
1211
|
+
const collectedPromise = collectResultFromStream(collectorBranch);
|
|
1290
1212
|
|
|
1291
1213
|
return {
|
|
1292
1214
|
fullStream,
|
|
1293
1215
|
textStream,
|
|
1294
|
-
text:
|
|
1295
|
-
toolCalls:
|
|
1296
|
-
toolResults:
|
|
1297
|
-
finishReason:
|
|
1298
|
-
usage:
|
|
1299
|
-
response:
|
|
1216
|
+
text: collectedPromise.then((collected) => collected.text),
|
|
1217
|
+
toolCalls: collectedPromise.then((collected) => collected.toolCalls),
|
|
1218
|
+
toolResults: collectedPromise.then((collected) => collected.toolResults),
|
|
1219
|
+
finishReason: collectedPromise.then((collected) => collected.finishReason),
|
|
1220
|
+
usage: collectedPromise.then((collected) => collected.usage),
|
|
1221
|
+
response: collectedPromise.then((collected) => collected.response),
|
|
1300
1222
|
toUIMessageStream() {
|
|
1301
1223
|
return uiMessageStream;
|
|
1302
1224
|
},
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export interface RenderCursorStyle {
|
|
2
|
+
readonly shape: 'block' | 'underline' | 'bar';
|
|
3
|
+
readonly blinking: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface DiffRenderedRowsResult {
|
|
7
|
+
readonly output: string;
|
|
8
|
+
readonly nextRows: readonly string[];
|
|
9
|
+
readonly changedRows: readonly number[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function diffRenderedRows(
|
|
13
|
+
currentRows: readonly string[],
|
|
14
|
+
previousRows: readonly string[],
|
|
15
|
+
): DiffRenderedRowsResult {
|
|
16
|
+
const changedRows: number[] = [];
|
|
17
|
+
let output = '';
|
|
18
|
+
const rowCount = Math.max(currentRows.length, previousRows.length);
|
|
19
|
+
const nextRows: string[] = [];
|
|
20
|
+
for (let row = 0; row < rowCount; row += 1) {
|
|
21
|
+
const current = currentRows[row] ?? '';
|
|
22
|
+
const previous = previousRows[row] ?? '';
|
|
23
|
+
nextRows.push(current);
|
|
24
|
+
if (current === previous) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
changedRows.push(row);
|
|
28
|
+
output += `\u001b[${String(row + 1)};1H\u001b[2K${current}`;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
output,
|
|
32
|
+
nextRows,
|
|
33
|
+
changedRows,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function cursorStyleToDecscusr(style: RenderCursorStyle): string {
|
|
38
|
+
if (style.shape === 'block') {
|
|
39
|
+
return style.blinking ? '\u001b[1 q' : '\u001b[2 q';
|
|
40
|
+
}
|
|
41
|
+
if (style.shape === 'underline') {
|
|
42
|
+
return style.blinking ? '\u001b[3 q' : '\u001b[4 q';
|
|
43
|
+
}
|
|
44
|
+
return style.blinking ? '\u001b[5 q' : '\u001b[6 q';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function cursorStyleEqual(
|
|
48
|
+
left: RenderCursorStyle | null,
|
|
49
|
+
right: RenderCursorStyle,
|
|
50
|
+
): boolean {
|
|
51
|
+
if (left === null) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return left.shape === right.shape && left.blinking === right.blinking;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ScanResult =
|
|
58
|
+
| {
|
|
59
|
+
readonly valid: true;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
readonly valid: false;
|
|
63
|
+
readonly reason: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function scanAnsiText(text: string): ScanResult {
|
|
67
|
+
let index = 0;
|
|
68
|
+
while (index < text.length) {
|
|
69
|
+
const code = text.codePointAt(index)!;
|
|
70
|
+
const char = String.fromCodePoint(code);
|
|
71
|
+
const width = code > 0xffff ? 2 : 1;
|
|
72
|
+
if (char !== '\u001b') {
|
|
73
|
+
index += width;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const next = text[index + 1];
|
|
78
|
+
if (next === undefined) {
|
|
79
|
+
return {
|
|
80
|
+
valid: false,
|
|
81
|
+
reason: 'dangling ESC at end of row',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (next === '[') {
|
|
86
|
+
let csiIndex = index + 2;
|
|
87
|
+
let foundFinal = false;
|
|
88
|
+
while (csiIndex < text.length) {
|
|
89
|
+
const csiCode = text.codePointAt(csiIndex)!;
|
|
90
|
+
if (csiCode >= 0x40 && csiCode <= 0x7e) {
|
|
91
|
+
foundFinal = true;
|
|
92
|
+
csiIndex += 1;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (csiCode < 0x20 || csiCode > 0x3f) {
|
|
96
|
+
return {
|
|
97
|
+
valid: false,
|
|
98
|
+
reason: `invalid CSI byte 0x${csiCode.toString(16)}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
csiIndex += 1;
|
|
102
|
+
}
|
|
103
|
+
if (!foundFinal) {
|
|
104
|
+
return {
|
|
105
|
+
valid: false,
|
|
106
|
+
reason: 'unterminated CSI sequence',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
index = csiIndex;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (next === ']') {
|
|
114
|
+
let oscIndex = index + 2;
|
|
115
|
+
let terminated = false;
|
|
116
|
+
while (oscIndex < text.length) {
|
|
117
|
+
const oscCode = text.codePointAt(oscIndex)!;
|
|
118
|
+
if (oscCode === 0x07) {
|
|
119
|
+
terminated = true;
|
|
120
|
+
oscIndex += 1;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (oscCode === 0x1b && text[oscIndex + 1] === '\\') {
|
|
124
|
+
terminated = true;
|
|
125
|
+
oscIndex += 2;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
oscIndex += oscCode > 0xffff ? 2 : 1;
|
|
129
|
+
}
|
|
130
|
+
if (!terminated) {
|
|
131
|
+
return {
|
|
132
|
+
valid: false,
|
|
133
|
+
reason: 'unterminated OSC sequence',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
index = oscIndex;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
index += 2;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
valid: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function findAnsiIntegrityIssues(rows: readonly string[]): readonly string[] {
|
|
149
|
+
const issues: string[] = [];
|
|
150
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
151
|
+
const row = rows[rowIndex] ?? '';
|
|
152
|
+
const result = scanAnsiText(row);
|
|
153
|
+
if (!result.valid) {
|
|
154
|
+
issues.push(`row ${String(rowIndex + 1)}: ${result.reason}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return issues;
|
|
158
|
+
}
|