@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
|
@@ -1,191 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface
|
|
12
|
-
|
|
13
|
-
| {
|
|
14
|
-
kind: 'home';
|
|
15
|
-
}
|
|
16
|
-
| {
|
|
17
|
-
kind: 'project';
|
|
18
|
-
directoryId: string;
|
|
19
|
-
}
|
|
20
|
-
| {
|
|
21
|
-
kind: 'repository';
|
|
22
|
-
repositoryId: string;
|
|
23
|
-
}
|
|
24
|
-
| {
|
|
25
|
-
kind: 'conversation';
|
|
26
|
-
sessionId: string;
|
|
27
|
-
};
|
|
28
|
-
conversationTitleEdit: {
|
|
29
|
-
conversationId: string;
|
|
30
|
-
} | null;
|
|
31
|
-
projectPaneSnapshot: {
|
|
32
|
-
directoryId: string;
|
|
33
|
-
} | null;
|
|
34
|
-
projectPaneScrollTop: number;
|
|
35
|
-
activeDirectoryId: string | null;
|
|
36
|
-
selectLeftNavConversation(sessionId: string): void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface RuntimeWorkspaceObservedEventsOptions<TObservedEvent> {
|
|
40
|
-
readonly reducer: WorkspaceObservedReducer<TObservedEvent>;
|
|
41
|
-
readonly workspace: RuntimeWorkspaceStateLike;
|
|
1
|
+
import type { HarnessSyncedStore } from '../core/store/harness-synced-store.ts';
|
|
2
|
+
import {
|
|
3
|
+
enqueueRuntimeWorkspaceObservedReactions,
|
|
4
|
+
type RuntimeWorkspaceObservedEffectQueueOptions,
|
|
5
|
+
} from './runtime-workspace-observed-effect-queue.ts';
|
|
6
|
+
import {
|
|
7
|
+
planRuntimeWorkspaceObservedTransition,
|
|
8
|
+
type RuntimeWorkspaceObservedTransitionPolicyOptions,
|
|
9
|
+
} from './runtime-workspace-observed-transition-policy.ts';
|
|
10
|
+
|
|
11
|
+
export interface RuntimeWorkspaceObservedEventsOptions {
|
|
12
|
+
readonly store: HarnessSyncedStore;
|
|
42
13
|
readonly orderedConversationIds: () => readonly string[];
|
|
43
|
-
readonly
|
|
44
|
-
readonly
|
|
45
|
-
readonly getActiveConversationId: () => string | null;
|
|
46
|
-
readonly setActiveConversationId: (sessionId: string | null) => void;
|
|
47
|
-
readonly hasDirectory: (directoryId: string) => boolean;
|
|
48
|
-
readonly resolveActiveDirectoryId: () => string | null;
|
|
49
|
-
readonly unsubscribeConversationEvents: (sessionId: string) => Promise<void>;
|
|
50
|
-
readonly stopConversationTitleEdit: (persistPending: boolean) => void;
|
|
51
|
-
readonly enterProjectPane: (directoryId: string) => void;
|
|
52
|
-
readonly enterHomePane: () => void;
|
|
53
|
-
readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
|
|
54
|
-
readonly activateConversation: (sessionId: string) => Promise<void>;
|
|
14
|
+
readonly transitionPolicy: RuntimeWorkspaceObservedTransitionPolicyOptions;
|
|
15
|
+
readonly effectQueue: RuntimeWorkspaceObservedEffectQueueOptions;
|
|
55
16
|
readonly markDirty: () => void;
|
|
56
17
|
}
|
|
57
18
|
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const leftNavConversationIdBefore =
|
|
64
|
-
this.options.workspace.leftNavSelection.kind === 'conversation'
|
|
65
|
-
? this.options.workspace.leftNavSelection.sessionId
|
|
66
|
-
: null;
|
|
67
|
-
const previousConversationDirectoryById = new Map<string, string | null>();
|
|
68
|
-
for (const sessionId of this.options.orderedConversationIds()) {
|
|
69
|
-
previousConversationDirectoryById.set(
|
|
70
|
-
sessionId,
|
|
71
|
-
this.options.conversationDirectoryId(sessionId),
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const reduced = this.options.reducer.apply(observed);
|
|
76
|
-
if (!reduced.changed) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
for (const sessionId of reduced.removedConversationIds) {
|
|
81
|
-
void this.options.unsubscribeConversationEvents(sessionId);
|
|
82
|
-
if (this.options.workspace.conversationTitleEdit?.conversationId === sessionId) {
|
|
83
|
-
this.options.stopConversationTitleEdit(false);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const directoryId of reduced.removedDirectoryIds) {
|
|
88
|
-
if (this.options.workspace.projectPaneSnapshot?.directoryId === directoryId) {
|
|
89
|
-
this.options.workspace.projectPaneSnapshot = null;
|
|
90
|
-
this.options.workspace.projectPaneScrollTop = 0;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
this.options.workspace.activeDirectoryId !== null &&
|
|
96
|
-
!this.options.hasDirectory(this.options.workspace.activeDirectoryId)
|
|
97
|
-
) {
|
|
98
|
-
this.options.workspace.activeDirectoryId = this.options.resolveActiveDirectoryId();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const removedConversationIdSet = new Set(reduced.removedConversationIds);
|
|
102
|
-
const activateFallbackConversationInDirectory = (
|
|
103
|
-
preferredDirectoryId: string | null,
|
|
104
|
-
label: string,
|
|
105
|
-
): boolean => {
|
|
106
|
-
if (preferredDirectoryId === null) {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
const fallbackConversationId =
|
|
110
|
-
this.options
|
|
111
|
-
.orderedConversationIds()
|
|
112
|
-
.find(
|
|
113
|
-
(sessionId) => this.options.conversationDirectoryId(sessionId) === preferredDirectoryId,
|
|
114
|
-
) ?? null;
|
|
115
|
-
if (fallbackConversationId === null) {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
this.options.queueControlPlaneOp(async () => {
|
|
119
|
-
await this.options.activateConversation(fallbackConversationId);
|
|
120
|
-
}, label);
|
|
121
|
-
return true;
|
|
122
|
-
};
|
|
123
|
-
const fallbackToDirectoryOrHome = (): void => {
|
|
124
|
-
const fallbackDirectoryId = this.options.resolveActiveDirectoryId();
|
|
125
|
-
if (fallbackDirectoryId !== null) {
|
|
126
|
-
this.options.enterProjectPane(fallbackDirectoryId);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
this.options.enterHomePane();
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
activeConversationIdBefore !== null &&
|
|
134
|
-
removedConversationIdSet.has(activeConversationIdBefore)
|
|
135
|
-
) {
|
|
136
|
-
this.options.setActiveConversationId(null);
|
|
137
|
-
const preferredDirectoryId =
|
|
138
|
-
previousConversationDirectoryById.get(activeConversationIdBefore) ?? null;
|
|
139
|
-
if (
|
|
140
|
-
!activateFallbackConversationInDirectory(
|
|
141
|
-
preferredDirectoryId,
|
|
142
|
-
'observed-active-conversation-removed',
|
|
143
|
-
)
|
|
144
|
-
) {
|
|
145
|
-
fallbackToDirectoryOrHome();
|
|
146
|
-
}
|
|
147
|
-
this.options.markDirty();
|
|
19
|
+
export function subscribeRuntimeWorkspaceObservedEvents(
|
|
20
|
+
options: RuntimeWorkspaceObservedEventsOptions,
|
|
21
|
+
): () => void {
|
|
22
|
+
return options.store.subscribe((state, previousState) => {
|
|
23
|
+
if (state.synced === previousState.synced) {
|
|
148
24
|
return;
|
|
149
25
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
!activateFallbackConversationInDirectory(
|
|
165
|
-
preferredDirectoryId,
|
|
166
|
-
'observed-selected-conversation-removed',
|
|
167
|
-
)
|
|
168
|
-
) {
|
|
169
|
-
fallbackToDirectoryOrHome();
|
|
170
|
-
}
|
|
171
|
-
this.options.markDirty();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
this.options.workspace.leftNavSelection.kind === 'project' &&
|
|
177
|
-
!this.options.hasDirectory(this.options.workspace.leftNavSelection.directoryId)
|
|
178
|
-
) {
|
|
179
|
-
const fallbackDirectoryId = this.options.resolveActiveDirectoryId();
|
|
180
|
-
if (fallbackDirectoryId !== null) {
|
|
181
|
-
this.options.enterProjectPane(fallbackDirectoryId);
|
|
182
|
-
} else {
|
|
183
|
-
this.options.enterHomePane();
|
|
184
|
-
}
|
|
185
|
-
this.options.markDirty();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
this.options.markDirty();
|
|
190
|
-
}
|
|
26
|
+
const planned = planRuntimeWorkspaceObservedTransition({
|
|
27
|
+
transition: {
|
|
28
|
+
previous: previousState.synced,
|
|
29
|
+
current: state.synced,
|
|
30
|
+
orderedConversationIds: options.orderedConversationIds(),
|
|
31
|
+
},
|
|
32
|
+
options: options.transitionPolicy,
|
|
33
|
+
});
|
|
34
|
+
enqueueRuntimeWorkspaceObservedReactions({
|
|
35
|
+
reactions: planned.reactions,
|
|
36
|
+
options: options.effectQueue,
|
|
37
|
+
});
|
|
38
|
+
options.markDirty();
|
|
39
|
+
});
|
|
191
40
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { HarnessSyncedState } from '../core/state/synced-observed-state.ts';
|
|
2
|
+
|
|
3
|
+
interface RuntimeWorkspaceStateLike {
|
|
4
|
+
leftNavSelection:
|
|
5
|
+
| {
|
|
6
|
+
kind: 'home';
|
|
7
|
+
}
|
|
8
|
+
| {
|
|
9
|
+
kind: 'nim';
|
|
10
|
+
}
|
|
11
|
+
| {
|
|
12
|
+
kind: 'tasks';
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
kind: 'project';
|
|
16
|
+
directoryId: string;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
kind: 'github';
|
|
20
|
+
directoryId: string;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
kind: 'repository';
|
|
24
|
+
repositoryId: string;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
kind: 'conversation';
|
|
28
|
+
sessionId: string;
|
|
29
|
+
};
|
|
30
|
+
conversationTitleEdit: {
|
|
31
|
+
conversationId: string;
|
|
32
|
+
} | null;
|
|
33
|
+
projectPaneSnapshot: {
|
|
34
|
+
directoryId: string;
|
|
35
|
+
} | null;
|
|
36
|
+
projectPaneScrollTop: number;
|
|
37
|
+
activeDirectoryId: string | null;
|
|
38
|
+
selectLeftNavConversation(sessionId: string): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RuntimeWorkspaceObservedTransition {
|
|
42
|
+
readonly previous: HarnessSyncedState;
|
|
43
|
+
readonly current: HarnessSyncedState;
|
|
44
|
+
readonly orderedConversationIds: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RuntimeWorkspaceObservedQueuedReaction {
|
|
48
|
+
readonly kind: 'unsubscribe-conversation' | 'activate-conversation';
|
|
49
|
+
readonly sessionId: string;
|
|
50
|
+
readonly label: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RuntimeWorkspaceObservedTransitionPolicyResult {
|
|
54
|
+
readonly reactions: readonly RuntimeWorkspaceObservedQueuedReaction[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RuntimeWorkspaceObservedTransitionPolicyOptions {
|
|
58
|
+
readonly workspace: RuntimeWorkspaceStateLike;
|
|
59
|
+
readonly getActiveConversationId: () => string | null;
|
|
60
|
+
readonly setActiveConversationId: (sessionId: string | null) => void;
|
|
61
|
+
readonly resolveActiveDirectoryId: () => string | null;
|
|
62
|
+
readonly stopConversationTitleEdit: (persistPending: boolean) => void;
|
|
63
|
+
readonly enterProjectPane: (directoryId: string) => void;
|
|
64
|
+
readonly enterHomePane: () => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function conversationDirectoryIdOf(state: HarnessSyncedState, sessionId: string): string | null {
|
|
68
|
+
return state.conversationsById[sessionId]?.directoryId ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasConversation(state: HarnessSyncedState, sessionId: string): boolean {
|
|
72
|
+
return state.conversationsById[sessionId] !== undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasDirectory(state: HarnessSyncedState, directoryId: string): boolean {
|
|
76
|
+
return state.directoriesById[directoryId] !== undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removedIds(
|
|
80
|
+
previous: Readonly<Record<string, unknown>>,
|
|
81
|
+
current: Readonly<Record<string, unknown>>,
|
|
82
|
+
): readonly string[] {
|
|
83
|
+
const removed: string[] = [];
|
|
84
|
+
for (const id of Object.keys(previous)) {
|
|
85
|
+
if (current[id] !== undefined) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
removed.push(id);
|
|
89
|
+
}
|
|
90
|
+
return removed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function planRuntimeWorkspaceObservedTransition(input: {
|
|
94
|
+
readonly transition: RuntimeWorkspaceObservedTransition;
|
|
95
|
+
readonly options: RuntimeWorkspaceObservedTransitionPolicyOptions;
|
|
96
|
+
}): RuntimeWorkspaceObservedTransitionPolicyResult {
|
|
97
|
+
const reactions: RuntimeWorkspaceObservedQueuedReaction[] = [];
|
|
98
|
+
const removedConversationIds = removedIds(
|
|
99
|
+
input.transition.previous.conversationsById,
|
|
100
|
+
input.transition.current.conversationsById,
|
|
101
|
+
);
|
|
102
|
+
const removedDirectoryIds = removedIds(
|
|
103
|
+
input.transition.previous.directoriesById,
|
|
104
|
+
input.transition.current.directoriesById,
|
|
105
|
+
);
|
|
106
|
+
const activeConversationIdBefore = input.options.getActiveConversationId();
|
|
107
|
+
const leftNavConversationIdBefore =
|
|
108
|
+
input.options.workspace.leftNavSelection.kind === 'conversation'
|
|
109
|
+
? input.options.workspace.leftNavSelection.sessionId
|
|
110
|
+
: null;
|
|
111
|
+
|
|
112
|
+
for (const sessionId of removedConversationIds) {
|
|
113
|
+
reactions.push({
|
|
114
|
+
kind: 'unsubscribe-conversation',
|
|
115
|
+
sessionId,
|
|
116
|
+
label: `observed-unsubscribe-conversation:${sessionId}`,
|
|
117
|
+
});
|
|
118
|
+
if (input.options.workspace.conversationTitleEdit?.conversationId === sessionId) {
|
|
119
|
+
input.options.stopConversationTitleEdit(false);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const directoryId of removedDirectoryIds) {
|
|
124
|
+
if (input.options.workspace.projectPaneSnapshot?.directoryId === directoryId) {
|
|
125
|
+
input.options.workspace.projectPaneSnapshot = null;
|
|
126
|
+
input.options.workspace.projectPaneScrollTop = 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
input.options.workspace.activeDirectoryId !== null &&
|
|
132
|
+
!hasDirectory(input.transition.current, input.options.workspace.activeDirectoryId)
|
|
133
|
+
) {
|
|
134
|
+
input.options.workspace.activeDirectoryId = input.options.resolveActiveDirectoryId();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const removedConversationIdSet = new Set(removedConversationIds);
|
|
138
|
+
const activateFallbackConversationInDirectory = (
|
|
139
|
+
preferredDirectoryId: string | null,
|
|
140
|
+
label: string,
|
|
141
|
+
): boolean => {
|
|
142
|
+
if (preferredDirectoryId === null) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const fallbackConversationId =
|
|
146
|
+
input.transition.orderedConversationIds.find(
|
|
147
|
+
(sessionId) =>
|
|
148
|
+
conversationDirectoryIdOf(input.transition.current, sessionId) === preferredDirectoryId,
|
|
149
|
+
) ?? null;
|
|
150
|
+
if (fallbackConversationId === null) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
reactions.push({
|
|
154
|
+
kind: 'activate-conversation',
|
|
155
|
+
sessionId: fallbackConversationId,
|
|
156
|
+
label,
|
|
157
|
+
});
|
|
158
|
+
return true;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const fallbackToDirectoryOrHome = (): void => {
|
|
162
|
+
const fallbackDirectoryId = input.options.resolveActiveDirectoryId();
|
|
163
|
+
if (fallbackDirectoryId !== null) {
|
|
164
|
+
input.options.enterProjectPane(fallbackDirectoryId);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
input.options.enterHomePane();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
activeConversationIdBefore !== null &&
|
|
172
|
+
removedConversationIdSet.has(activeConversationIdBefore)
|
|
173
|
+
) {
|
|
174
|
+
input.options.setActiveConversationId(null);
|
|
175
|
+
const preferredDirectoryId = conversationDirectoryIdOf(
|
|
176
|
+
input.transition.previous,
|
|
177
|
+
activeConversationIdBefore,
|
|
178
|
+
);
|
|
179
|
+
if (
|
|
180
|
+
!activateFallbackConversationInDirectory(
|
|
181
|
+
preferredDirectoryId,
|
|
182
|
+
'observed-active-conversation-removed',
|
|
183
|
+
)
|
|
184
|
+
) {
|
|
185
|
+
fallbackToDirectoryOrHome();
|
|
186
|
+
}
|
|
187
|
+
return { reactions };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
leftNavConversationIdBefore !== null &&
|
|
192
|
+
removedConversationIdSet.has(leftNavConversationIdBefore)
|
|
193
|
+
) {
|
|
194
|
+
const currentActiveId = input.options.getActiveConversationId();
|
|
195
|
+
if (currentActiveId !== null && hasConversation(input.transition.current, currentActiveId)) {
|
|
196
|
+
input.options.workspace.selectLeftNavConversation(currentActiveId);
|
|
197
|
+
return { reactions };
|
|
198
|
+
}
|
|
199
|
+
const preferredDirectoryId = conversationDirectoryIdOf(
|
|
200
|
+
input.transition.previous,
|
|
201
|
+
leftNavConversationIdBefore,
|
|
202
|
+
);
|
|
203
|
+
if (
|
|
204
|
+
!activateFallbackConversationInDirectory(
|
|
205
|
+
preferredDirectoryId,
|
|
206
|
+
'observed-selected-conversation-removed',
|
|
207
|
+
)
|
|
208
|
+
) {
|
|
209
|
+
fallbackToDirectoryOrHome();
|
|
210
|
+
}
|
|
211
|
+
return { reactions };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
input.options.workspace.leftNavSelection.kind === 'project' &&
|
|
216
|
+
!hasDirectory(input.transition.current, input.options.workspace.leftNavSelection.directoryId)
|
|
217
|
+
) {
|
|
218
|
+
const fallbackDirectoryId = input.options.resolveActiveDirectoryId();
|
|
219
|
+
if (fallbackDirectoryId !== null) {
|
|
220
|
+
input.options.enterProjectPane(fallbackDirectoryId);
|
|
221
|
+
} else {
|
|
222
|
+
input.options.enterHomePane();
|
|
223
|
+
}
|
|
224
|
+
return { reactions };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { reactions };
|
|
228
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, rmSync, writeSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
renderTraceChunkPreview,
|
|
5
|
+
type RenderTraceControlIssue,
|
|
6
|
+
} from '../mux/live-mux/render-trace-analysis.ts';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_ENTRIES_PER_CONVERSATION = 512;
|
|
9
|
+
export const SESSION_DIAGNOSTICS_FILE_EXTENSION = '.jsonl';
|
|
10
|
+
|
|
11
|
+
export interface SessionStatusSnapshot {
|
|
12
|
+
readonly status: string;
|
|
13
|
+
readonly attentionReason: string | null;
|
|
14
|
+
readonly live: boolean;
|
|
15
|
+
readonly phase: string | null;
|
|
16
|
+
readonly detailText: string | null;
|
|
17
|
+
readonly lastKnownWork: string | null;
|
|
18
|
+
readonly lastKnownWorkAt: string | null;
|
|
19
|
+
readonly telemetrySource: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UnsupportedControlSequencesEntry {
|
|
23
|
+
readonly kind: 'unsupported-control-sequences';
|
|
24
|
+
readonly observedAt: string;
|
|
25
|
+
readonly source: string;
|
|
26
|
+
readonly cursor: number;
|
|
27
|
+
readonly chunkPreview: string;
|
|
28
|
+
readonly issues: ReadonlyArray<{
|
|
29
|
+
readonly kind: RenderTraceControlIssue['kind'];
|
|
30
|
+
readonly offset: number;
|
|
31
|
+
readonly sequence: string;
|
|
32
|
+
readonly sequencePreview: string;
|
|
33
|
+
readonly finalByte?: string;
|
|
34
|
+
readonly rawParams?: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SessionStatusTransitionEntry {
|
|
39
|
+
readonly kind: 'status-transition';
|
|
40
|
+
readonly observedAt: string;
|
|
41
|
+
readonly source: string;
|
|
42
|
+
readonly from: SessionStatusSnapshot;
|
|
43
|
+
readonly to: SessionStatusSnapshot;
|
|
44
|
+
readonly metadata?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type SessionDiagnosticsEntry =
|
|
48
|
+
| UnsupportedControlSequencesEntry
|
|
49
|
+
| SessionStatusTransitionEntry;
|
|
50
|
+
|
|
51
|
+
interface SessionDiagnosticsStoreOptions {
|
|
52
|
+
readonly maxEntriesPerConversation?: number;
|
|
53
|
+
readonly diagnosticsDirectory?: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class SessionDiagnosticsStore {
|
|
57
|
+
private readonly maxEntriesPerConversation: number;
|
|
58
|
+
private readonly diagnosticsDirectory: string | null;
|
|
59
|
+
private readonly entriesByConversationId = new Map<string, SessionDiagnosticsEntry[]>();
|
|
60
|
+
private readonly fileDescriptorByConversationId = new Map<string, number>();
|
|
61
|
+
|
|
62
|
+
constructor(options: SessionDiagnosticsStoreOptions = {}) {
|
|
63
|
+
const configuredMax = options.maxEntriesPerConversation;
|
|
64
|
+
this.maxEntriesPerConversation =
|
|
65
|
+
typeof configuredMax === 'number' && Number.isFinite(configuredMax)
|
|
66
|
+
? Math.max(1, Math.floor(configuredMax))
|
|
67
|
+
: DEFAULT_MAX_ENTRIES_PER_CONVERSATION;
|
|
68
|
+
this.diagnosticsDirectory =
|
|
69
|
+
typeof options.diagnosticsDirectory === 'string' && options.diagnosticsDirectory.length > 0
|
|
70
|
+
? resolve(options.diagnosticsDirectory)
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
close(): void {
|
|
75
|
+
for (const conversationId of this.fileDescriptorByConversationId.keys()) {
|
|
76
|
+
this.closeConversationFile(conversationId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
recordUnsupportedControlSequences(input: {
|
|
81
|
+
readonly conversationId: string;
|
|
82
|
+
readonly observedAt: string;
|
|
83
|
+
readonly source: string;
|
|
84
|
+
readonly cursor: number;
|
|
85
|
+
readonly chunkPreview: string;
|
|
86
|
+
readonly issues: readonly RenderTraceControlIssue[];
|
|
87
|
+
}): void {
|
|
88
|
+
if (input.issues.length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const normalizedIssues = input.issues.map((issue) => ({
|
|
92
|
+
kind: issue.kind,
|
|
93
|
+
offset: issue.offset,
|
|
94
|
+
sequence: issue.sequence,
|
|
95
|
+
sequencePreview: renderTraceChunkPreview(issue.sequence, 160),
|
|
96
|
+
...(issue.finalByte === undefined ? {} : { finalByte: issue.finalByte }),
|
|
97
|
+
...(issue.rawParams === undefined ? {} : { rawParams: issue.rawParams }),
|
|
98
|
+
}));
|
|
99
|
+
this.recordConversationEntry(input.conversationId, {
|
|
100
|
+
kind: 'unsupported-control-sequences',
|
|
101
|
+
observedAt: input.observedAt,
|
|
102
|
+
source: input.source,
|
|
103
|
+
cursor: input.cursor,
|
|
104
|
+
chunkPreview: input.chunkPreview,
|
|
105
|
+
issues: normalizedIssues,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
recordStatusTransition(input: {
|
|
110
|
+
readonly conversationId: string;
|
|
111
|
+
readonly observedAt: string;
|
|
112
|
+
readonly source: string;
|
|
113
|
+
readonly from: SessionStatusSnapshot;
|
|
114
|
+
readonly to: SessionStatusSnapshot;
|
|
115
|
+
readonly metadata?: Record<string, unknown>;
|
|
116
|
+
}): void {
|
|
117
|
+
this.recordConversationEntry(input.conversationId, {
|
|
118
|
+
kind: 'status-transition',
|
|
119
|
+
observedAt: input.observedAt,
|
|
120
|
+
source: input.source,
|
|
121
|
+
from: input.from,
|
|
122
|
+
to: input.to,
|
|
123
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
listConversationEntries(conversationId: string): readonly SessionDiagnosticsEntry[] {
|
|
128
|
+
const entries = this.entriesByConversationId.get(conversationId);
|
|
129
|
+
return entries === undefined ? [] : [...entries];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
clearConversation(conversationId: string): void {
|
|
133
|
+
this.entriesByConversationId.delete(conversationId);
|
|
134
|
+
this.closeConversationFile(conversationId);
|
|
135
|
+
const filePath = this.resolveConversationDiagnosticsPath(conversationId);
|
|
136
|
+
if (filePath === null) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
rmSync(filePath, { force: true });
|
|
141
|
+
} catch {
|
|
142
|
+
// Best-effort cleanup only.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private recordConversationEntry(conversationId: string, entry: SessionDiagnosticsEntry): void {
|
|
147
|
+
const entries = this.entriesByConversationId.get(conversationId) ?? [];
|
|
148
|
+
entries.push(entry);
|
|
149
|
+
if (entries.length > this.maxEntriesPerConversation) {
|
|
150
|
+
entries.splice(0, entries.length - this.maxEntriesPerConversation);
|
|
151
|
+
}
|
|
152
|
+
this.entriesByConversationId.set(conversationId, entries);
|
|
153
|
+
this.writeConversationEntry(conversationId, entry);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private writeConversationEntry(conversationId: string, entry: SessionDiagnosticsEntry): void {
|
|
157
|
+
const filePath = this.resolveConversationDiagnosticsPath(conversationId);
|
|
158
|
+
if (filePath === null) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const fd = this.ensureConversationFile(conversationId, filePath);
|
|
162
|
+
if (fd === null) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const record = {
|
|
166
|
+
conversationId,
|
|
167
|
+
...entry,
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
writeSync(fd, `${JSON.stringify(record)}\n`);
|
|
171
|
+
} catch {
|
|
172
|
+
this.closeConversationFile(conversationId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private ensureConversationFile(conversationId: string, filePath: string): number | null {
|
|
177
|
+
const existing = this.fileDescriptorByConversationId.get(conversationId);
|
|
178
|
+
if (existing !== undefined) {
|
|
179
|
+
return existing;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
183
|
+
const fd = openSync(filePath, 'a');
|
|
184
|
+
this.fileDescriptorByConversationId.set(conversationId, fd);
|
|
185
|
+
return fd;
|
|
186
|
+
} catch {
|
|
187
|
+
this.closeConversationFile(conversationId);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private closeConversationFile(conversationId: string): void {
|
|
193
|
+
const fd = this.fileDescriptorByConversationId.get(conversationId);
|
|
194
|
+
if (fd === undefined) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
closeSync(fd);
|
|
199
|
+
} catch {
|
|
200
|
+
// Best-effort close only.
|
|
201
|
+
}
|
|
202
|
+
this.fileDescriptorByConversationId.delete(conversationId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private resolveConversationDiagnosticsPath(conversationId: string): string | null {
|
|
206
|
+
if (this.diagnosticsDirectory === null) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const fileToken = sanitizeFileToken(conversationId);
|
|
210
|
+
return resolve(this.diagnosticsDirectory, `${fileToken}${SESSION_DIAGNOSTICS_FILE_EXTENSION}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sanitizeFileToken(value: string): string {
|
|
215
|
+
const normalized = value.trim().replace(/[^A-Za-z0-9._-]+/gu, '-');
|
|
216
|
+
return normalized.length === 0 ? 'conversation' : normalized;
|
|
217
|
+
}
|