@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { NimToolDefinition, NimToolPolicy } from '../../packages/nim-core/src/index.ts';
|
|
2
|
+
|
|
3
|
+
const RUNTIME_NIM_READ_TOOLS: readonly NimToolDefinition[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'directory.list',
|
|
6
|
+
description: 'List directories known to the current workspace.',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'repository.list',
|
|
10
|
+
description: 'List repositories known to the current workspace.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'task.list',
|
|
14
|
+
description: 'List tasks known to the current workspace.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'session.list',
|
|
18
|
+
description: 'List active and historical sessions in the current workspace.',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const RUNTIME_NIM_READ_POLICY: NimToolPolicy = {
|
|
23
|
+
hash: 'nim-control-plane-read-v1',
|
|
24
|
+
allow: RUNTIME_NIM_READ_TOOLS.map((tool) => tool.name),
|
|
25
|
+
deny: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface RuntimeNimToolBridgeOptions {
|
|
29
|
+
readonly listDirectories: () => Promise<readonly unknown[]>;
|
|
30
|
+
readonly listRepositories: () => Promise<readonly unknown[]>;
|
|
31
|
+
readonly listTasks: (limit: number) => Promise<readonly unknown[]>;
|
|
32
|
+
readonly listSessions: () => Promise<readonly unknown[]>;
|
|
33
|
+
readonly taskListLimit?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RuntimeNimToolBridgeInvokeInput {
|
|
37
|
+
readonly toolName: string;
|
|
38
|
+
readonly argumentsText?: string;
|
|
39
|
+
readonly argumentsValue?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RuntimeNimToolRuntime {
|
|
43
|
+
registerTools(tools: readonly NimToolDefinition[]): void;
|
|
44
|
+
setToolPolicy(policy: NimToolPolicy): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class RuntimeNimToolBridge {
|
|
48
|
+
private readonly taskListLimit: number;
|
|
49
|
+
|
|
50
|
+
constructor(private readonly options: RuntimeNimToolBridgeOptions) {
|
|
51
|
+
this.taskListLimit = Math.max(1, options.taskListLimit ?? 100);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registerWithRuntime(runtime: RuntimeNimToolRuntime): void {
|
|
55
|
+
runtime.registerTools(RUNTIME_NIM_READ_TOOLS);
|
|
56
|
+
runtime.setToolPolicy(RUNTIME_NIM_READ_POLICY);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async invoke(input: RuntimeNimToolBridgeInvokeInput): Promise<unknown> {
|
|
60
|
+
if (input.toolName === 'directory.list') {
|
|
61
|
+
const directories = await this.options.listDirectories();
|
|
62
|
+
return {
|
|
63
|
+
count: directories.length,
|
|
64
|
+
directories,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (input.toolName === 'repository.list') {
|
|
68
|
+
const repositories = await this.options.listRepositories();
|
|
69
|
+
return {
|
|
70
|
+
count: repositories.length,
|
|
71
|
+
repositories,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (input.toolName === 'task.list') {
|
|
75
|
+
const limitInput: {
|
|
76
|
+
readonly argumentsText: string | undefined;
|
|
77
|
+
readonly argumentsValue: unknown;
|
|
78
|
+
readonly fallback: number;
|
|
79
|
+
} = {
|
|
80
|
+
argumentsText: input.argumentsText,
|
|
81
|
+
argumentsValue: input.argumentsValue,
|
|
82
|
+
fallback: this.taskListLimit,
|
|
83
|
+
};
|
|
84
|
+
const limit = resolveTaskListLimit(limitInput);
|
|
85
|
+
const tasks = await this.options.listTasks(limit);
|
|
86
|
+
return {
|
|
87
|
+
count: tasks.length,
|
|
88
|
+
limit,
|
|
89
|
+
tasks,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (input.toolName === 'session.list') {
|
|
93
|
+
const sessions = await this.options.listSessions();
|
|
94
|
+
return {
|
|
95
|
+
count: sessions.length,
|
|
96
|
+
sessions,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`unsupported nim tool: ${input.toolName}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolvePositiveLimit(argumentsText: string, fallback: number): number {
|
|
104
|
+
const trimmed = argumentsText.trim();
|
|
105
|
+
if (trimmed.length === 0) {
|
|
106
|
+
return fallback;
|
|
107
|
+
}
|
|
108
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
109
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
110
|
+
throw new Error(`invalid task.list limit: ${trimmed}`);
|
|
111
|
+
}
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveTaskListLimit(input: {
|
|
116
|
+
readonly argumentsText: string | undefined;
|
|
117
|
+
readonly argumentsValue: unknown;
|
|
118
|
+
readonly fallback: number;
|
|
119
|
+
}): number {
|
|
120
|
+
const value = input.argumentsValue;
|
|
121
|
+
if (typeof value === 'number') {
|
|
122
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
123
|
+
throw new Error(`invalid task.list limit: ${String(value)}`);
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value === 'object' && value !== null && 'limit' in value) {
|
|
128
|
+
const limit = (value as { readonly limit?: unknown }).limit;
|
|
129
|
+
if (typeof limit === 'number') {
|
|
130
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
131
|
+
throw new Error(`invalid task.list limit: ${String(limit)}`);
|
|
132
|
+
}
|
|
133
|
+
return limit;
|
|
134
|
+
}
|
|
135
|
+
if (typeof limit === 'string') {
|
|
136
|
+
return resolvePositiveLimit(limit, input.fallback);
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`invalid task.list limit: ${String(limit)}`);
|
|
139
|
+
}
|
|
140
|
+
return resolvePositiveLimit(input.argumentsText ?? '', input.fallback);
|
|
141
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { StreamObservedEvent } from '../control-plane/stream-protocol.ts';
|
|
2
|
+
import {
|
|
3
|
+
applyObservedEventToHarnessSyncedStore,
|
|
4
|
+
type HarnessSyncedStore,
|
|
5
|
+
type HarnessSyncedStoreApplyResult,
|
|
6
|
+
} from '../core/store/harness-synced-store.ts';
|
|
7
|
+
|
|
8
|
+
export interface RuntimeObservedEventProjectionInput {
|
|
9
|
+
readonly subscriptionId: string;
|
|
10
|
+
readonly cursor: number;
|
|
11
|
+
readonly event: StreamObservedEvent;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RuntimeObservedEventProjectionResult {
|
|
15
|
+
readonly cursorAccepted: boolean;
|
|
16
|
+
readonly previousCursor: number | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RuntimeObservedEventProjectionPipelineOptions {
|
|
20
|
+
readonly syncedStore: HarnessSyncedStore;
|
|
21
|
+
readonly applyWorkspaceProjection: (reduction: HarnessSyncedStoreApplyResult) => void;
|
|
22
|
+
readonly applyTaskPlanningProjection: (reduction: HarnessSyncedStoreApplyResult) => void;
|
|
23
|
+
readonly applyDirectoryGitProjection: (event: StreamObservedEvent) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyRuntimeObservedEventProjection(
|
|
27
|
+
input: RuntimeObservedEventProjectionInput,
|
|
28
|
+
options: RuntimeObservedEventProjectionPipelineOptions,
|
|
29
|
+
): RuntimeObservedEventProjectionResult {
|
|
30
|
+
const reduced = applyObservedEventToHarnessSyncedStore(options.syncedStore, input);
|
|
31
|
+
if (!reduced.cursorAccepted) {
|
|
32
|
+
return {
|
|
33
|
+
cursorAccepted: false,
|
|
34
|
+
previousCursor: reduced.previousCursor,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
options.applyWorkspaceProjection(reduced);
|
|
38
|
+
// Directory git status updates remain an explicit non-synced projection boundary.
|
|
39
|
+
options.applyDirectoryGitProjection(input.event);
|
|
40
|
+
options.applyTaskPlanningProjection(reduced);
|
|
41
|
+
return {
|
|
42
|
+
cursorAccepted: true,
|
|
43
|
+
previousCursor: reduced.previousCursor,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -10,7 +10,7 @@ interface RuntimeProcessTarget {
|
|
|
10
10
|
off(event: 'unhandledRejection', listener: (reason: unknown) => void): RuntimeProcessTarget;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
interface RuntimeProcessWiringOptions {
|
|
13
|
+
export interface RuntimeProcessWiringOptions {
|
|
14
14
|
readonly onInput: (chunk: Buffer) => void;
|
|
15
15
|
readonly onResize: () => void;
|
|
16
16
|
readonly requestStop: () => void;
|
|
@@ -18,52 +18,45 @@ interface RuntimeProcessWiringOptions {
|
|
|
18
18
|
readonly target?: RuntimeProcessTarget;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
constructor(private readonly options: RuntimeProcessWiringOptions) {
|
|
25
|
-
this.target = options.target ?? process;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
attach(): void {
|
|
29
|
-
this.target.stdin.on('data', this.onInputSafe);
|
|
30
|
-
this.target.stdout.on('resize', this.onResizeSafe);
|
|
31
|
-
this.target.on('SIGINT', this.options.requestStop);
|
|
32
|
-
this.target.on('SIGTERM', this.options.requestStop);
|
|
33
|
-
this.target.once('uncaughtException', this.onUncaughtException);
|
|
34
|
-
this.target.once('unhandledRejection', this.onUnhandledRejection);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
detach(): void {
|
|
38
|
-
this.target.stdin.off('data', this.onInputSafe);
|
|
39
|
-
this.target.stdout.off('resize', this.onResizeSafe);
|
|
40
|
-
this.target.off('SIGINT', this.options.requestStop);
|
|
41
|
-
this.target.off('SIGTERM', this.options.requestStop);
|
|
42
|
-
this.target.off('uncaughtException', this.onUncaughtException);
|
|
43
|
-
this.target.off('unhandledRejection', this.onUnhandledRejection);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private readonly onInputSafe = (chunk: Buffer): void => {
|
|
21
|
+
export function attachRuntimeProcessWiring(options: RuntimeProcessWiringOptions): () => void {
|
|
22
|
+
const target = options.target ?? process;
|
|
23
|
+
const onInputSafe = (chunk: Buffer): void => {
|
|
47
24
|
try {
|
|
48
|
-
|
|
25
|
+
options.onInput(chunk);
|
|
49
26
|
} catch (error: unknown) {
|
|
50
|
-
|
|
27
|
+
options.handleRuntimeFatal('stdin-data', error);
|
|
51
28
|
}
|
|
52
29
|
};
|
|
53
30
|
|
|
54
|
-
|
|
31
|
+
const onResizeSafe = (): void => {
|
|
55
32
|
try {
|
|
56
|
-
|
|
33
|
+
options.onResize();
|
|
57
34
|
} catch (error: unknown) {
|
|
58
|
-
|
|
35
|
+
options.handleRuntimeFatal('stdout-resize', error);
|
|
59
36
|
}
|
|
60
37
|
};
|
|
61
38
|
|
|
62
|
-
|
|
63
|
-
|
|
39
|
+
const onUncaughtException = (error: Error): void => {
|
|
40
|
+
options.handleRuntimeFatal('uncaught-exception', error);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const onUnhandledRejection = (reason: unknown): void => {
|
|
44
|
+
options.handleRuntimeFatal('unhandled-rejection', reason);
|
|
64
45
|
};
|
|
65
46
|
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
target.stdin.on('data', onInputSafe);
|
|
48
|
+
target.stdout.on('resize', onResizeSafe);
|
|
49
|
+
target.on('SIGINT', options.requestStop);
|
|
50
|
+
target.on('SIGTERM', options.requestStop);
|
|
51
|
+
target.once('uncaughtException', onUncaughtException);
|
|
52
|
+
target.once('unhandledRejection', onUnhandledRejection);
|
|
53
|
+
|
|
54
|
+
return (): void => {
|
|
55
|
+
target.stdin.off('data', onInputSafe);
|
|
56
|
+
target.stdout.off('resize', onResizeSafe);
|
|
57
|
+
target.off('SIGINT', options.requestStop);
|
|
58
|
+
target.off('SIGTERM', options.requestStop);
|
|
59
|
+
target.off('uncaughtException', onUncaughtException);
|
|
60
|
+
target.off('unhandledRejection', onUnhandledRejection);
|
|
68
61
|
};
|
|
69
62
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ProjectPaneGitHubReviewSummary } from '../mux/project-pane-github-review.ts';
|
|
2
|
+
|
|
3
|
+
interface QueueLatestControlPlaneOp {
|
|
4
|
+
(
|
|
5
|
+
key: string,
|
|
6
|
+
task: (options: { readonly signal: AbortSignal }) => Promise<void>,
|
|
7
|
+
label: string,
|
|
8
|
+
): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RuntimeProjectPaneGitHubReviewCacheOptions {
|
|
12
|
+
readonly ttlMs: number;
|
|
13
|
+
readonly refreshIntervalMs: number;
|
|
14
|
+
readonly queueLatestControlPlaneOp: QueueLatestControlPlaneOp;
|
|
15
|
+
readonly loadReview: (
|
|
16
|
+
directoryId: string,
|
|
17
|
+
options: RuntimeProjectPaneGitHubReviewCacheRequestOptions,
|
|
18
|
+
) => Promise<ProjectPaneGitHubReviewSummary>;
|
|
19
|
+
readonly onUpdate: (directoryId: string, review: ProjectPaneGitHubReviewSummary) => void;
|
|
20
|
+
readonly formatErrorMessage: (error: unknown) => string;
|
|
21
|
+
readonly nowMs?: () => number;
|
|
22
|
+
readonly setInterval?: (callback: () => void, ms: number) => NodeJS.Timeout;
|
|
23
|
+
readonly clearInterval?: (timer: NodeJS.Timeout) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RuntimeProjectPaneGitHubReviewCacheRequestOptions {
|
|
27
|
+
readonly forceRefresh?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CacheEntry {
|
|
31
|
+
review: ProjectPaneGitHubReviewSummary | null;
|
|
32
|
+
fetchedAtMs: number | null;
|
|
33
|
+
inFlight: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadingState(
|
|
37
|
+
previous: ProjectPaneGitHubReviewSummary | null,
|
|
38
|
+
): ProjectPaneGitHubReviewSummary {
|
|
39
|
+
return {
|
|
40
|
+
status: 'loading',
|
|
41
|
+
branchName: previous?.branchName ?? null,
|
|
42
|
+
branchSource: previous?.branchSource ?? null,
|
|
43
|
+
pr: previous?.pr ?? null,
|
|
44
|
+
openThreads: previous?.openThreads ?? [],
|
|
45
|
+
resolvedThreads: previous?.resolvedThreads ?? [],
|
|
46
|
+
errorMessage: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function errorState(
|
|
51
|
+
previous: ProjectPaneGitHubReviewSummary | null,
|
|
52
|
+
message: string,
|
|
53
|
+
): ProjectPaneGitHubReviewSummary {
|
|
54
|
+
return {
|
|
55
|
+
status: 'error',
|
|
56
|
+
branchName: previous?.branchName ?? null,
|
|
57
|
+
branchSource: previous?.branchSource ?? null,
|
|
58
|
+
pr: previous?.pr ?? null,
|
|
59
|
+
openThreads: previous?.openThreads ?? [],
|
|
60
|
+
resolvedThreads: previous?.resolvedThreads ?? [],
|
|
61
|
+
errorMessage: message,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class RuntimeProjectPaneGitHubReviewCacheEngine {
|
|
66
|
+
private readonly entries = new Map<string, CacheEntry>();
|
|
67
|
+
private readonly nowMs: () => number;
|
|
68
|
+
private readonly setIntervalFn: (callback: () => void, ms: number) => NodeJS.Timeout;
|
|
69
|
+
private readonly clearIntervalFn: (timer: NodeJS.Timeout) => void;
|
|
70
|
+
private refreshTimer: NodeJS.Timeout | null = null;
|
|
71
|
+
|
|
72
|
+
constructor(private readonly options: RuntimeProjectPaneGitHubReviewCacheOptions) {
|
|
73
|
+
this.nowMs = options.nowMs ?? Date.now;
|
|
74
|
+
this.setIntervalFn = options.setInterval ?? setInterval;
|
|
75
|
+
this.clearIntervalFn = options.clearInterval ?? clearInterval;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
request(
|
|
79
|
+
directoryId: string,
|
|
80
|
+
requestOptions: RuntimeProjectPaneGitHubReviewCacheRequestOptions = {},
|
|
81
|
+
): void {
|
|
82
|
+
const entry = this.entries.get(directoryId) ?? {
|
|
83
|
+
review: null,
|
|
84
|
+
fetchedAtMs: null,
|
|
85
|
+
inFlight: false,
|
|
86
|
+
};
|
|
87
|
+
this.entries.set(directoryId, entry);
|
|
88
|
+
const forceRefresh = requestOptions.forceRefresh === true;
|
|
89
|
+
if (entry.inFlight) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!forceRefresh && this.isFresh(entry)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const previous = entry.review;
|
|
96
|
+
entry.inFlight = true;
|
|
97
|
+
const nextLoading = loadingState(previous);
|
|
98
|
+
entry.review = nextLoading;
|
|
99
|
+
this.options.onUpdate(directoryId, nextLoading);
|
|
100
|
+
|
|
101
|
+
this.options.queueLatestControlPlaneOp(
|
|
102
|
+
`project-pane-github-review:${directoryId}`,
|
|
103
|
+
async ({ signal }) => {
|
|
104
|
+
if (signal.aborted) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const loaded = await this.options.loadReview(directoryId, {
|
|
109
|
+
forceRefresh,
|
|
110
|
+
});
|
|
111
|
+
if (signal.aborted) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
entry.review = loaded;
|
|
115
|
+
entry.fetchedAtMs = this.nowMs();
|
|
116
|
+
this.options.onUpdate(directoryId, loaded);
|
|
117
|
+
} catch (error: unknown) {
|
|
118
|
+
if (signal.aborted) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const message = this.options.formatErrorMessage(error);
|
|
122
|
+
const nextError = errorState(previous, message);
|
|
123
|
+
entry.review = nextError;
|
|
124
|
+
this.options.onUpdate(directoryId, nextError);
|
|
125
|
+
} finally {
|
|
126
|
+
entry.inFlight = false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
'project-pane-github-review',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
startAutoRefresh(resolveDirectoryId: () => string | null): void {
|
|
134
|
+
this.stopAutoRefresh();
|
|
135
|
+
if (this.options.refreshIntervalMs <= 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.refreshTimer = this.setIntervalFn(() => {
|
|
139
|
+
const directoryId = resolveDirectoryId();
|
|
140
|
+
if (directoryId === null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.request(directoryId);
|
|
144
|
+
}, this.options.refreshIntervalMs);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
stopAutoRefresh(): void {
|
|
148
|
+
if (this.refreshTimer === null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.clearIntervalFn(this.refreshTimer);
|
|
152
|
+
this.refreshTimer = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private isFresh(entry: CacheEntry): boolean {
|
|
156
|
+
if (entry.review?.status !== 'ready') {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (entry.fetchedAtMs === null) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return this.nowMs() - entry.fetchedAtMs < this.options.ttlMs;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
interface RuntimeRenderFlushResult {
|
|
1
|
+
export interface RuntimeRenderFlushResult {
|
|
2
2
|
readonly changedRowCount: number;
|
|
3
3
|
readonly wroteOutput: boolean;
|
|
4
4
|
readonly shouldShowCursor: boolean;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
interface RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout> {
|
|
7
|
+
export interface RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout> {
|
|
8
8
|
readonly layout: TLayout;
|
|
9
9
|
readonly projectPaneActive: boolean;
|
|
10
10
|
readonly homePaneActive: boolean;
|
|
@@ -16,7 +16,7 @@ interface RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout> {
|
|
|
16
16
|
readonly rightRows: readonly string[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
interface RuntimeRenderFlushOptions<
|
|
19
|
+
export interface RuntimeRenderFlushOptions<
|
|
20
20
|
TConversation,
|
|
21
21
|
TFrame,
|
|
22
22
|
TSelection,
|
|
@@ -66,81 +66,74 @@ interface RuntimeRenderFlushOptions<
|
|
|
66
66
|
readonly recordRenderSample: (durationMs: number, changedRowCount: number) => void;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export
|
|
69
|
+
export function flushRuntimeRender<
|
|
70
70
|
TConversation,
|
|
71
71
|
TFrame,
|
|
72
72
|
TSelection,
|
|
73
73
|
TLayout,
|
|
74
74
|
TModalOverlay,
|
|
75
75
|
TStatusRow,
|
|
76
|
-
>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
76
|
+
>(
|
|
77
|
+
options: RuntimeRenderFlushOptions<
|
|
78
|
+
TConversation,
|
|
79
|
+
TFrame,
|
|
80
|
+
TSelection,
|
|
81
|
+
TLayout,
|
|
82
|
+
TModalOverlay,
|
|
83
|
+
TStatusRow
|
|
84
|
+
>,
|
|
85
|
+
input: RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout>,
|
|
86
|
+
): void {
|
|
87
|
+
const renderStartedAtNs = options.perfNowNs();
|
|
88
|
+
const baseStatusFooter =
|
|
89
|
+
!input.projectPaneActive && !input.homePaneActive && input.activeConversation !== null
|
|
90
|
+
? options.statusFooterForConversation(input.activeConversation)
|
|
91
|
+
: '';
|
|
92
|
+
const statusNotice = options.currentStatusNotice();
|
|
93
|
+
const statusFooter =
|
|
94
|
+
statusNotice === null || statusNotice.length === 0
|
|
95
|
+
? baseStatusFooter
|
|
96
|
+
: `${baseStatusFooter.length > 0 ? `${baseStatusFooter} ` : ''}${statusNotice}`;
|
|
97
|
+
const statusRow = options.currentStatusRow();
|
|
98
|
+
options.onStatusLineComposed?.({
|
|
99
|
+
activeConversation: input.activeConversation,
|
|
100
|
+
statusFooter,
|
|
101
|
+
statusRow,
|
|
102
|
+
projectPaneActive: input.projectPaneActive,
|
|
103
|
+
homePaneActive: input.homePaneActive,
|
|
104
|
+
});
|
|
105
|
+
const rows = options.buildRenderRows(
|
|
106
|
+
input.layout,
|
|
107
|
+
input.railAnsiRows,
|
|
108
|
+
input.rightRows,
|
|
109
|
+
statusRow,
|
|
110
|
+
statusFooter,
|
|
111
|
+
);
|
|
112
|
+
const modalOverlay = options.buildModalOverlay();
|
|
113
|
+
if (modalOverlay !== null) {
|
|
114
|
+
options.applyModalOverlay(rows, modalOverlay);
|
|
115
|
+
}
|
|
116
|
+
const selectionOverlay =
|
|
117
|
+
input.rightFrame === null
|
|
118
|
+
? ''
|
|
119
|
+
: options.renderSelectionOverlay(input.layout, input.rightFrame, input.renderSelection);
|
|
120
|
+
const flushResult = options.flush({
|
|
121
|
+
layout: input.layout,
|
|
122
|
+
rows,
|
|
123
|
+
rightFrame: input.rightFrame,
|
|
124
|
+
selectionRows: input.selectionRows,
|
|
125
|
+
selectionOverlay,
|
|
126
|
+
});
|
|
127
|
+
const changedRowCount = flushResult.changedRowCount;
|
|
128
|
+
if (flushResult.wroteOutput) {
|
|
129
|
+
options.onFlushOutput({
|
|
101
130
|
activeConversation: input.activeConversation,
|
|
102
|
-
statusFooter,
|
|
103
|
-
statusRow,
|
|
104
|
-
projectPaneActive: input.projectPaneActive,
|
|
105
|
-
homePaneActive: input.homePaneActive,
|
|
106
|
-
});
|
|
107
|
-
const rows = this.options.buildRenderRows(
|
|
108
|
-
input.layout,
|
|
109
|
-
input.railAnsiRows,
|
|
110
|
-
input.rightRows,
|
|
111
|
-
statusRow,
|
|
112
|
-
statusFooter,
|
|
113
|
-
);
|
|
114
|
-
const modalOverlay = this.options.buildModalOverlay();
|
|
115
|
-
if (modalOverlay !== null) {
|
|
116
|
-
this.options.applyModalOverlay(rows, modalOverlay);
|
|
117
|
-
}
|
|
118
|
-
const selectionOverlay =
|
|
119
|
-
input.rightFrame === null
|
|
120
|
-
? ''
|
|
121
|
-
: this.options.renderSelectionOverlay(
|
|
122
|
-
input.layout,
|
|
123
|
-
input.rightFrame,
|
|
124
|
-
input.renderSelection,
|
|
125
|
-
);
|
|
126
|
-
const flushResult = this.options.flush({
|
|
127
|
-
layout: input.layout,
|
|
128
|
-
rows,
|
|
129
131
|
rightFrame: input.rightFrame,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
rows,
|
|
133
|
+
flushResult,
|
|
134
|
+
changedRowCount,
|
|
132
135
|
});
|
|
133
|
-
const changedRowCount = flushResult.changedRowCount;
|
|
134
|
-
if (flushResult.wroteOutput) {
|
|
135
|
-
this.options.onFlushOutput({
|
|
136
|
-
activeConversation: input.activeConversation,
|
|
137
|
-
rightFrame: input.rightFrame,
|
|
138
|
-
rows,
|
|
139
|
-
flushResult,
|
|
140
|
-
changedRowCount,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
const renderDurationMs = Number(this.options.perfNowNs() - renderStartedAtNs) / 1e6;
|
|
144
|
-
this.options.recordRenderSample(renderDurationMs, changedRowCount);
|
|
145
136
|
}
|
|
137
|
+
const renderDurationMs = Number(options.perfNowNs() - renderStartedAtNs) / 1e6;
|
|
138
|
+
options.recordRenderSample(renderDurationMs, changedRowCount);
|
|
146
139
|
}
|