@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,41 @@
|
|
|
1
|
+
import type { CodexLiveEvent } from '../codex/live-session.ts';
|
|
2
|
+
import type { PtyExit } from '../pty/pty_host.ts';
|
|
3
|
+
import type { TerminalBufferTail, TerminalSnapshotFrame } from '../terminal/snapshot-oracle.ts';
|
|
4
|
+
|
|
5
|
+
export interface SessionDataEvent {
|
|
6
|
+
cursor: number;
|
|
7
|
+
chunk: Buffer;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SessionAttachHandlers {
|
|
11
|
+
onData: (event: SessionDataEvent) => void;
|
|
12
|
+
onExit: (exit: PtyExit) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LiveSessionLike {
|
|
16
|
+
attach(handlers: SessionAttachHandlers, sinceCursor?: number): string;
|
|
17
|
+
detach(attachmentId: string): void;
|
|
18
|
+
latestCursorValue(): number;
|
|
19
|
+
processId(): number | null;
|
|
20
|
+
write(data: string | Uint8Array): void;
|
|
21
|
+
resize(cols: number, rows: number): void;
|
|
22
|
+
snapshot(): TerminalSnapshotFrame;
|
|
23
|
+
bufferTail?(tailLines?: number): TerminalBufferTail;
|
|
24
|
+
close(): void;
|
|
25
|
+
onEvent(listener: (event: CodexLiveEvent) => void): () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StartSessionRuntimeInput {
|
|
29
|
+
readonly sessionId: string;
|
|
30
|
+
readonly args: readonly string[];
|
|
31
|
+
readonly initialCols: number;
|
|
32
|
+
readonly initialRows: number;
|
|
33
|
+
readonly env?: Record<string, string>;
|
|
34
|
+
readonly cwd?: string;
|
|
35
|
+
readonly tenantId?: string;
|
|
36
|
+
readonly userId?: string;
|
|
37
|
+
readonly workspaceId?: string;
|
|
38
|
+
readonly worktreeId?: string;
|
|
39
|
+
readonly terminalForegroundHex?: string;
|
|
40
|
+
readonly terminalBackgroundHex?: string;
|
|
41
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
isStreamSessionRuntimeStatus,
|
|
3
|
+
parseStreamSessionStatusModel,
|
|
4
|
+
type StreamSessionController,
|
|
5
|
+
type StreamSessionRuntimeStatus,
|
|
6
|
+
type StreamSessionStatusModel,
|
|
7
|
+
} from '../../control-plane/stream-protocol.ts';
|
|
8
|
+
|
|
9
|
+
export interface ControlPlaneDirectoryRecord {
|
|
6
10
|
readonly directoryId: string;
|
|
7
11
|
readonly tenantId: string;
|
|
8
12
|
readonly userId: string;
|
|
@@ -12,7 +16,7 @@ interface ControlPlaneDirectoryRecord {
|
|
|
12
16
|
readonly archivedAt: string | null;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
interface ControlPlaneConversationRecord {
|
|
19
|
+
export interface ControlPlaneConversationRecord {
|
|
16
20
|
readonly conversationId: string;
|
|
17
21
|
readonly directoryId: string;
|
|
18
22
|
readonly tenantId: string;
|
|
@@ -21,12 +25,12 @@ interface ControlPlaneConversationRecord {
|
|
|
21
25
|
readonly title: string;
|
|
22
26
|
readonly agentType: string;
|
|
23
27
|
readonly adapterState: Record<string, unknown>;
|
|
24
|
-
readonly runtimeStatus:
|
|
28
|
+
readonly runtimeStatus: StreamSessionRuntimeStatus;
|
|
25
29
|
readonly runtimeStatusModel: StreamSessionStatusModel | null;
|
|
26
30
|
readonly runtimeLive: boolean;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
interface ControlPlaneRepositoryRecord {
|
|
33
|
+
export interface ControlPlaneRepositoryRecord {
|
|
30
34
|
readonly repositoryId: string;
|
|
31
35
|
readonly tenantId: string;
|
|
32
36
|
readonly userId: string;
|
|
@@ -39,14 +43,14 @@ interface ControlPlaneRepositoryRecord {
|
|
|
39
43
|
readonly archivedAt: string | null;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
interface ControlPlaneGitSummaryRecord {
|
|
46
|
+
export interface ControlPlaneGitSummaryRecord {
|
|
43
47
|
readonly branch: string;
|
|
44
48
|
readonly changedFiles: number;
|
|
45
49
|
readonly additions: number;
|
|
46
50
|
readonly deletions: number;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
interface ControlPlaneGitRepositorySnapshotRecord {
|
|
53
|
+
export interface ControlPlaneGitRepositorySnapshotRecord {
|
|
50
54
|
readonly normalizedRemoteUrl: string | null;
|
|
51
55
|
readonly commitCount: number | null;
|
|
52
56
|
readonly lastCommitAt: string | null;
|
|
@@ -55,7 +59,7 @@ interface ControlPlaneGitRepositorySnapshotRecord {
|
|
|
55
59
|
readonly defaultBranch: string | null;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
interface ControlPlaneDirectoryGitStatusRecord {
|
|
62
|
+
export interface ControlPlaneDirectoryGitStatusRecord {
|
|
59
63
|
readonly directoryId: string;
|
|
60
64
|
readonly summary: ControlPlaneGitSummaryRecord;
|
|
61
65
|
readonly repositorySnapshot: ControlPlaneGitRepositorySnapshotRecord;
|
|
@@ -64,10 +68,10 @@ interface ControlPlaneDirectoryGitStatusRecord {
|
|
|
64
68
|
readonly observedAt: string;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
type TaskStatus = 'draft' | 'ready' | 'in-progress' | 'completed';
|
|
68
|
-
type TaskScopeKind = 'global' | 'repository' | 'project';
|
|
71
|
+
export type TaskStatus = 'draft' | 'ready' | 'in-progress' | 'completed';
|
|
72
|
+
export type TaskScopeKind = 'global' | 'repository' | 'project';
|
|
69
73
|
|
|
70
|
-
interface ControlPlaneTaskRecord {
|
|
74
|
+
export interface ControlPlaneTaskRecord {
|
|
71
75
|
readonly taskId: string;
|
|
72
76
|
readonly tenantId: string;
|
|
73
77
|
readonly userId: string;
|
|
@@ -76,7 +80,7 @@ interface ControlPlaneTaskRecord {
|
|
|
76
80
|
readonly scopeKind: TaskScopeKind;
|
|
77
81
|
readonly projectId: string | null;
|
|
78
82
|
readonly title: string;
|
|
79
|
-
readonly
|
|
83
|
+
readonly body: string;
|
|
80
84
|
readonly status: TaskStatus;
|
|
81
85
|
readonly orderIndex: number;
|
|
82
86
|
readonly claimedByControllerId: string | null;
|
|
@@ -123,78 +127,6 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
|
|
123
127
|
return value as Record<string, unknown>;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
function parseRuntimeStatusModel(value: unknown): StreamSessionStatusModel | null | undefined {
|
|
127
|
-
if (value === null) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
const model = asObjectRecord(value);
|
|
131
|
-
if (model === null) {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
const runtimeStatus = model['runtimeStatus'];
|
|
135
|
-
const phase = model['phase'];
|
|
136
|
-
const glyph = model['glyph'];
|
|
137
|
-
const badge = model['badge'];
|
|
138
|
-
const detailText = asRequiredString(model['detailText']);
|
|
139
|
-
const attentionReason = asNullableString(model['attentionReason']);
|
|
140
|
-
const lastKnownWork = asNullableString(model['lastKnownWork']);
|
|
141
|
-
const lastKnownWorkAt = asNullableString(model['lastKnownWorkAt']);
|
|
142
|
-
const phaseHint = model['phaseHint'];
|
|
143
|
-
const observedAt = asRequiredString(model['observedAt']);
|
|
144
|
-
if (
|
|
145
|
-
detailText === null ||
|
|
146
|
-
attentionReason === undefined ||
|
|
147
|
-
lastKnownWork === undefined ||
|
|
148
|
-
lastKnownWorkAt === undefined ||
|
|
149
|
-
observedAt === null
|
|
150
|
-
) {
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
if (
|
|
154
|
-
runtimeStatus !== 'running' &&
|
|
155
|
-
runtimeStatus !== 'needs-input' &&
|
|
156
|
-
runtimeStatus !== 'completed' &&
|
|
157
|
-
runtimeStatus !== 'exited'
|
|
158
|
-
) {
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
if (
|
|
162
|
-
phase !== 'needs-action' &&
|
|
163
|
-
phase !== 'starting' &&
|
|
164
|
-
phase !== 'working' &&
|
|
165
|
-
phase !== 'idle' &&
|
|
166
|
-
phase !== 'exited'
|
|
167
|
-
) {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
if (glyph !== '▲' && glyph !== '◔' && glyph !== '◆' && glyph !== '○' && glyph !== '■') {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
if (badge !== 'NEED' && badge !== 'RUN ' && badge !== 'DONE' && badge !== 'EXIT') {
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
|
-
if (
|
|
177
|
-
phaseHint !== null &&
|
|
178
|
-
phaseHint !== 'needs-action' &&
|
|
179
|
-
phaseHint !== 'working' &&
|
|
180
|
-
phaseHint !== 'idle'
|
|
181
|
-
) {
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
runtimeStatus,
|
|
186
|
-
phase,
|
|
187
|
-
glyph,
|
|
188
|
-
badge,
|
|
189
|
-
detailText,
|
|
190
|
-
attentionReason,
|
|
191
|
-
lastKnownWork,
|
|
192
|
-
lastKnownWorkAt,
|
|
193
|
-
phaseHint,
|
|
194
|
-
observedAt,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
130
|
export function parseDirectoryRecord(value: unknown): ControlPlaneDirectoryRecord | null {
|
|
199
131
|
const record = asRecord(value);
|
|
200
132
|
if (record === null) {
|
|
@@ -247,7 +179,7 @@ export function parseConversationRecord(value: unknown): ControlPlaneConversatio
|
|
|
247
179
|
const agentType = asRequiredString(record['agentType']);
|
|
248
180
|
const adapterState = asObjectRecord(record['adapterState']);
|
|
249
181
|
const runtimeStatus = record['runtimeStatus'];
|
|
250
|
-
const runtimeStatusModel =
|
|
182
|
+
const runtimeStatusModel = parseStreamSessionStatusModel(record['runtimeStatusModel']);
|
|
251
183
|
const runtimeLive = record['runtimeLive'];
|
|
252
184
|
|
|
253
185
|
if (
|
|
@@ -265,12 +197,7 @@ export function parseConversationRecord(value: unknown): ControlPlaneConversatio
|
|
|
265
197
|
return null;
|
|
266
198
|
}
|
|
267
199
|
|
|
268
|
-
if (
|
|
269
|
-
runtimeStatus !== 'running' &&
|
|
270
|
-
runtimeStatus !== 'needs-input' &&
|
|
271
|
-
runtimeStatus !== 'completed' &&
|
|
272
|
-
runtimeStatus !== 'exited'
|
|
273
|
-
) {
|
|
200
|
+
if (!isStreamSessionRuntimeStatus(runtimeStatus)) {
|
|
274
201
|
return null;
|
|
275
202
|
}
|
|
276
203
|
|
|
@@ -457,7 +384,7 @@ export function parseTaskRecord(value: unknown): ControlPlaneTaskRecord | null {
|
|
|
457
384
|
const repositoryId = asOptionalString(record['repositoryId']);
|
|
458
385
|
const projectId = asOptionalString(record['projectId']);
|
|
459
386
|
const title = asRequiredString(record['title']);
|
|
460
|
-
const
|
|
387
|
+
const body = asRequiredString(record['body'] ?? record['description']);
|
|
461
388
|
const status = parseTaskStatus(record['status']);
|
|
462
389
|
const scopeKind = parseTaskScopeKind(record['scopeKind'], repositoryId, projectId);
|
|
463
390
|
const orderIndex = record['orderIndex'];
|
|
@@ -479,7 +406,7 @@ export function parseTaskRecord(value: unknown): ControlPlaneTaskRecord | null {
|
|
|
479
406
|
projectId === undefined ||
|
|
480
407
|
scopeKind === null ||
|
|
481
408
|
title === null ||
|
|
482
|
-
|
|
409
|
+
body === null ||
|
|
483
410
|
status === null ||
|
|
484
411
|
typeof orderIndex !== 'number' ||
|
|
485
412
|
claimedByControllerId === undefined ||
|
|
@@ -503,7 +430,7 @@ export function parseTaskRecord(value: unknown): ControlPlaneTaskRecord | null {
|
|
|
503
430
|
scopeKind,
|
|
504
431
|
projectId,
|
|
505
432
|
title,
|
|
506
|
-
|
|
433
|
+
body,
|
|
507
434
|
status,
|
|
508
435
|
orderIndex,
|
|
509
436
|
claimedByControllerId,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface ObservedStreamCursorState {
|
|
2
|
+
readonly lastCursorBySubscriptionId: ReadonlyMap<string, number>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface ObserveStreamCursorInput {
|
|
6
|
+
readonly subscriptionId: string;
|
|
7
|
+
readonly cursor: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ObserveStreamCursorResult {
|
|
11
|
+
readonly accepted: boolean;
|
|
12
|
+
readonly previousCursor: number | null;
|
|
13
|
+
readonly state: ObservedStreamCursorState;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createObservedStreamCursorState(): ObservedStreamCursorState {
|
|
17
|
+
return {
|
|
18
|
+
lastCursorBySubscriptionId: new Map<string, number>(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function observeStreamCursor(
|
|
23
|
+
state: ObservedStreamCursorState,
|
|
24
|
+
input: ObserveStreamCursorInput,
|
|
25
|
+
): ObserveStreamCursorResult {
|
|
26
|
+
const previousCursor = state.lastCursorBySubscriptionId.get(input.subscriptionId) ?? null;
|
|
27
|
+
if (previousCursor !== null && input.cursor <= previousCursor) {
|
|
28
|
+
return {
|
|
29
|
+
accepted: false,
|
|
30
|
+
previousCursor,
|
|
31
|
+
state,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const next = new Map(state.lastCursorBySubscriptionId);
|
|
35
|
+
next.set(input.subscriptionId, input.cursor);
|
|
36
|
+
return {
|
|
37
|
+
accepted: true,
|
|
38
|
+
previousCursor,
|
|
39
|
+
state: {
|
|
40
|
+
lastCursorBySubscriptionId: next,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type { StreamObservedEvent } from '../../control-plane/stream-protocol.ts';
|
|
2
|
+
import {
|
|
3
|
+
parseConversationRecord,
|
|
4
|
+
parseDirectoryRecord,
|
|
5
|
+
parseRepositoryRecord,
|
|
6
|
+
parseTaskRecord,
|
|
7
|
+
type ControlPlaneConversationRecord,
|
|
8
|
+
type ControlPlaneDirectoryRecord,
|
|
9
|
+
type ControlPlaneRepositoryRecord,
|
|
10
|
+
type ControlPlaneTaskRecord,
|
|
11
|
+
} from '../contracts/records.ts';
|
|
12
|
+
|
|
13
|
+
export interface HarnessSyncedState {
|
|
14
|
+
readonly directoriesById: Readonly<Record<string, ControlPlaneDirectoryRecord>>;
|
|
15
|
+
readonly conversationsById: Readonly<Record<string, ControlPlaneConversationRecord>>;
|
|
16
|
+
readonly repositoriesById: Readonly<Record<string, ControlPlaneRepositoryRecord>>;
|
|
17
|
+
readonly tasksById: Readonly<Record<string, ControlPlaneTaskRecord>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HarnessSyncedObservedReduction {
|
|
21
|
+
readonly state: HarnessSyncedState;
|
|
22
|
+
readonly changed: boolean;
|
|
23
|
+
readonly removedConversationIds: readonly string[];
|
|
24
|
+
readonly removedDirectoryIds: readonly string[];
|
|
25
|
+
readonly removedTaskIds: readonly string[];
|
|
26
|
+
readonly upsertedDirectoryIds: readonly string[];
|
|
27
|
+
readonly upsertedConversationIds: readonly string[];
|
|
28
|
+
readonly upsertedRepositoryIds: readonly string[];
|
|
29
|
+
readonly upsertedTaskIds: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const EMPTY_IDS: readonly string[] = [];
|
|
33
|
+
|
|
34
|
+
export function createHarnessSyncedState(): HarnessSyncedState {
|
|
35
|
+
return {
|
|
36
|
+
directoriesById: {},
|
|
37
|
+
conversationsById: {},
|
|
38
|
+
repositoriesById: {},
|
|
39
|
+
tasksById: {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unchanged(state: HarnessSyncedState): HarnessSyncedObservedReduction {
|
|
44
|
+
return {
|
|
45
|
+
state,
|
|
46
|
+
changed: false,
|
|
47
|
+
removedConversationIds: EMPTY_IDS,
|
|
48
|
+
removedDirectoryIds: EMPTY_IDS,
|
|
49
|
+
removedTaskIds: EMPTY_IDS,
|
|
50
|
+
upsertedDirectoryIds: EMPTY_IDS,
|
|
51
|
+
upsertedConversationIds: EMPTY_IDS,
|
|
52
|
+
upsertedRepositoryIds: EMPTY_IDS,
|
|
53
|
+
upsertedTaskIds: EMPTY_IDS,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function applyObservedEventToSyncedState(
|
|
58
|
+
state: HarnessSyncedState,
|
|
59
|
+
event: StreamObservedEvent,
|
|
60
|
+
): HarnessSyncedObservedReduction {
|
|
61
|
+
if (event.type === 'directory-upserted') {
|
|
62
|
+
const directory = parseDirectoryRecord(event.directory);
|
|
63
|
+
if (directory === null) {
|
|
64
|
+
return unchanged(state);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...unchanged(state),
|
|
68
|
+
state: {
|
|
69
|
+
...state,
|
|
70
|
+
directoriesById: {
|
|
71
|
+
...state.directoriesById,
|
|
72
|
+
[directory.directoryId]: directory,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
changed: true,
|
|
76
|
+
upsertedDirectoryIds: [directory.directoryId],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (event.type === 'directory-archived') {
|
|
81
|
+
const removedConversationIds: string[] = [];
|
|
82
|
+
const nextConversations = { ...state.conversationsById };
|
|
83
|
+
for (const [conversationId, conversation] of Object.entries(state.conversationsById)) {
|
|
84
|
+
if (conversation.directoryId !== event.directoryId) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
delete nextConversations[conversationId];
|
|
88
|
+
removedConversationIds.push(conversationId);
|
|
89
|
+
}
|
|
90
|
+
const directoryExisted = state.directoriesById[event.directoryId] !== undefined;
|
|
91
|
+
if (!directoryExisted && removedConversationIds.length === 0) {
|
|
92
|
+
return unchanged(state);
|
|
93
|
+
}
|
|
94
|
+
const nextDirectories = { ...state.directoriesById };
|
|
95
|
+
delete nextDirectories[event.directoryId];
|
|
96
|
+
return {
|
|
97
|
+
...unchanged(state),
|
|
98
|
+
state: {
|
|
99
|
+
...state,
|
|
100
|
+
directoriesById: nextDirectories,
|
|
101
|
+
conversationsById: nextConversations,
|
|
102
|
+
},
|
|
103
|
+
changed: true,
|
|
104
|
+
removedConversationIds,
|
|
105
|
+
removedDirectoryIds: directoryExisted ? [event.directoryId] : EMPTY_IDS,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (event.type === 'conversation-created' || event.type === 'conversation-updated') {
|
|
110
|
+
const conversation = parseConversationRecord(event.conversation);
|
|
111
|
+
if (conversation === null) {
|
|
112
|
+
return unchanged(state);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
...unchanged(state),
|
|
116
|
+
state: {
|
|
117
|
+
...state,
|
|
118
|
+
conversationsById: {
|
|
119
|
+
...state.conversationsById,
|
|
120
|
+
[conversation.conversationId]: conversation,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
changed: true,
|
|
124
|
+
upsertedConversationIds: [conversation.conversationId],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (event.type === 'conversation-archived' || event.type === 'conversation-deleted') {
|
|
129
|
+
if (state.conversationsById[event.conversationId] === undefined) {
|
|
130
|
+
return unchanged(state);
|
|
131
|
+
}
|
|
132
|
+
const nextConversations = { ...state.conversationsById };
|
|
133
|
+
delete nextConversations[event.conversationId];
|
|
134
|
+
return {
|
|
135
|
+
...unchanged(state),
|
|
136
|
+
state: {
|
|
137
|
+
...state,
|
|
138
|
+
conversationsById: nextConversations,
|
|
139
|
+
},
|
|
140
|
+
changed: true,
|
|
141
|
+
removedConversationIds: [event.conversationId],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (event.type === 'repository-upserted' || event.type === 'repository-updated') {
|
|
146
|
+
const repository = parseRepositoryRecord(event.repository);
|
|
147
|
+
if (repository === null) {
|
|
148
|
+
return unchanged(state);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
...unchanged(state),
|
|
152
|
+
state: {
|
|
153
|
+
...state,
|
|
154
|
+
repositoriesById: {
|
|
155
|
+
...state.repositoriesById,
|
|
156
|
+
[repository.repositoryId]: repository,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
changed: true,
|
|
160
|
+
upsertedRepositoryIds: [repository.repositoryId],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (event.type === 'repository-archived') {
|
|
165
|
+
const existing = state.repositoriesById[event.repositoryId];
|
|
166
|
+
if (existing === undefined) {
|
|
167
|
+
return unchanged(state);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
...unchanged(state),
|
|
171
|
+
state: {
|
|
172
|
+
...state,
|
|
173
|
+
repositoriesById: {
|
|
174
|
+
...state.repositoriesById,
|
|
175
|
+
[event.repositoryId]: {
|
|
176
|
+
...existing,
|
|
177
|
+
archivedAt: event.ts,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
changed: true,
|
|
182
|
+
upsertedRepositoryIds: [event.repositoryId],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (event.type === 'task-created' || event.type === 'task-updated') {
|
|
187
|
+
const task = parseTaskRecord(event.task);
|
|
188
|
+
if (task === null) {
|
|
189
|
+
return unchanged(state);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
...unchanged(state),
|
|
193
|
+
state: {
|
|
194
|
+
...state,
|
|
195
|
+
tasksById: {
|
|
196
|
+
...state.tasksById,
|
|
197
|
+
[task.taskId]: task,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
changed: true,
|
|
201
|
+
upsertedTaskIds: [task.taskId],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (event.type === 'task-deleted') {
|
|
206
|
+
if (state.tasksById[event.taskId] === undefined) {
|
|
207
|
+
return unchanged(state);
|
|
208
|
+
}
|
|
209
|
+
const nextTasks = { ...state.tasksById };
|
|
210
|
+
delete nextTasks[event.taskId];
|
|
211
|
+
return {
|
|
212
|
+
...unchanged(state),
|
|
213
|
+
state: {
|
|
214
|
+
...state,
|
|
215
|
+
tasksById: nextTasks,
|
|
216
|
+
},
|
|
217
|
+
changed: true,
|
|
218
|
+
removedTaskIds: [event.taskId],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (event.type === 'task-reordered') {
|
|
223
|
+
const nextTasks = { ...state.tasksById };
|
|
224
|
+
const upsertedTaskIds: string[] = [];
|
|
225
|
+
for (const value of event.tasks) {
|
|
226
|
+
const task = parseTaskRecord(value);
|
|
227
|
+
if (task === null) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
nextTasks[task.taskId] = task;
|
|
231
|
+
upsertedTaskIds.push(task.taskId);
|
|
232
|
+
}
|
|
233
|
+
if (upsertedTaskIds.length === 0) {
|
|
234
|
+
return unchanged(state);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
...unchanged(state),
|
|
238
|
+
state: {
|
|
239
|
+
...state,
|
|
240
|
+
tasksById: nextTasks,
|
|
241
|
+
},
|
|
242
|
+
changed: true,
|
|
243
|
+
upsertedTaskIds,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (event.type === 'session-status') {
|
|
248
|
+
const conversationId = event.conversationId ?? event.sessionId;
|
|
249
|
+
const existing = state.conversationsById[conversationId];
|
|
250
|
+
if (existing === undefined) {
|
|
251
|
+
return unchanged(state);
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
...unchanged(state),
|
|
255
|
+
state: {
|
|
256
|
+
...state,
|
|
257
|
+
conversationsById: {
|
|
258
|
+
...state.conversationsById,
|
|
259
|
+
[conversationId]: {
|
|
260
|
+
...existing,
|
|
261
|
+
runtimeStatus: event.status,
|
|
262
|
+
runtimeStatusModel: event.statusModel,
|
|
263
|
+
runtimeLive: event.live,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
changed: true,
|
|
268
|
+
upsertedConversationIds: [conversationId],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return unchanged(state);
|
|
273
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createStore, type StoreApi } from 'zustand/vanilla';
|
|
2
|
+
import type { StreamObservedEvent } from '../../control-plane/stream-protocol.ts';
|
|
3
|
+
import {
|
|
4
|
+
createObservedStreamCursorState,
|
|
5
|
+
observeStreamCursor,
|
|
6
|
+
type ObservedStreamCursorState,
|
|
7
|
+
} from '../state/observed-stream-cursor.ts';
|
|
8
|
+
import {
|
|
9
|
+
applyObservedEventToSyncedState,
|
|
10
|
+
createHarnessSyncedState,
|
|
11
|
+
type HarnessSyncedObservedReduction,
|
|
12
|
+
type HarnessSyncedState,
|
|
13
|
+
} from '../state/synced-observed-state.ts';
|
|
14
|
+
|
|
15
|
+
export interface HarnessSyncedStoreState {
|
|
16
|
+
readonly synced: HarnessSyncedState;
|
|
17
|
+
readonly observedStreamCursor: ObservedStreamCursorState;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HarnessSyncedStoreApplyInput {
|
|
21
|
+
readonly subscriptionId: string;
|
|
22
|
+
readonly cursor: number;
|
|
23
|
+
readonly event: StreamObservedEvent;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface HarnessSyncedStoreApplyResult extends HarnessSyncedObservedReduction {
|
|
27
|
+
readonly cursorAccepted: boolean;
|
|
28
|
+
readonly previousCursor: number | null;
|
|
29
|
+
readonly previousState: HarnessSyncedState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type HarnessSyncedStore = StoreApi<HarnessSyncedStoreState>;
|
|
33
|
+
|
|
34
|
+
export function createHarnessSyncedStore(
|
|
35
|
+
initial: Partial<HarnessSyncedStoreState> = {},
|
|
36
|
+
): HarnessSyncedStore {
|
|
37
|
+
return createStore<HarnessSyncedStoreState>(() => ({
|
|
38
|
+
synced: initial.synced ?? createHarnessSyncedState(),
|
|
39
|
+
observedStreamCursor: initial.observedStreamCursor ?? createObservedStreamCursorState(),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function applyObservedEventToHarnessSyncedStore(
|
|
44
|
+
store: HarnessSyncedStore,
|
|
45
|
+
input: HarnessSyncedStoreApplyInput,
|
|
46
|
+
): HarnessSyncedStoreApplyResult {
|
|
47
|
+
const state = store.getState();
|
|
48
|
+
const observedCursor = observeStreamCursor(state.observedStreamCursor, {
|
|
49
|
+
subscriptionId: input.subscriptionId,
|
|
50
|
+
cursor: input.cursor,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!observedCursor.accepted) {
|
|
54
|
+
return {
|
|
55
|
+
state: state.synced,
|
|
56
|
+
changed: false,
|
|
57
|
+
cursorAccepted: false,
|
|
58
|
+
previousCursor: observedCursor.previousCursor,
|
|
59
|
+
previousState: state.synced,
|
|
60
|
+
removedConversationIds: [],
|
|
61
|
+
removedDirectoryIds: [],
|
|
62
|
+
removedTaskIds: [],
|
|
63
|
+
upsertedDirectoryIds: [],
|
|
64
|
+
upsertedConversationIds: [],
|
|
65
|
+
upsertedRepositoryIds: [],
|
|
66
|
+
upsertedTaskIds: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const reduced = applyObservedEventToSyncedState(state.synced, input.event);
|
|
71
|
+
store.setState({
|
|
72
|
+
observedStreamCursor: observedCursor.state,
|
|
73
|
+
synced: reduced.state,
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
...reduced,
|
|
77
|
+
cursorAccepted: true,
|
|
78
|
+
previousCursor: observedCursor.previousCursor,
|
|
79
|
+
previousState: state.synced,
|
|
80
|
+
};
|
|
81
|
+
}
|