@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,705 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
InMemoryNimRuntime,
|
|
4
|
+
type NimModelRef,
|
|
5
|
+
type NimEventEnvelope,
|
|
6
|
+
type NimProviderDriver,
|
|
7
|
+
type NimUiEvent,
|
|
8
|
+
} from '../../packages/nim-core/src/index.ts';
|
|
9
|
+
import {
|
|
10
|
+
projectEventToUiEvents,
|
|
11
|
+
type NimUiMode,
|
|
12
|
+
} from '../../packages/nim-ui-core/src/projection.ts';
|
|
13
|
+
import { type RuntimeNimToolBridge } from './runtime-nim-tool-bridge.ts';
|
|
14
|
+
|
|
15
|
+
type NimSessionStatus = 'thinking' | 'tool-calling' | 'responding' | 'idle';
|
|
16
|
+
|
|
17
|
+
export interface RuntimeNimViewModel {
|
|
18
|
+
readonly sessionId: string | null;
|
|
19
|
+
readonly status: NimSessionStatus;
|
|
20
|
+
readonly uiMode: 'debug' | 'user';
|
|
21
|
+
readonly composerText: string;
|
|
22
|
+
readonly queuedCount: number;
|
|
23
|
+
readonly activeRunId: string | null;
|
|
24
|
+
readonly transcriptLines: readonly string[];
|
|
25
|
+
readonly assistantDraftText: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RuntimeNimSessionOptions {
|
|
29
|
+
readonly tenantId: string;
|
|
30
|
+
readonly userId: string;
|
|
31
|
+
readonly markDirty: () => void;
|
|
32
|
+
readonly toolBridge?: RuntimeNimToolBridge;
|
|
33
|
+
readonly model?: NimModelRef;
|
|
34
|
+
readonly providerDriver?: NimProviderDriver;
|
|
35
|
+
readonly runtime?: InMemoryNimRuntime;
|
|
36
|
+
readonly retryWithMockOnFailedTurn?: boolean;
|
|
37
|
+
readonly responseChunkDelayMs?: number;
|
|
38
|
+
readonly maxTranscriptLines?: number;
|
|
39
|
+
readonly sleep?: (delayMs: number) => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_MODEL: NimModelRef = 'mock/echo-v1';
|
|
43
|
+
const DEFAULT_UI_MODE: NimUiMode = 'debug';
|
|
44
|
+
const DEFAULT_RESPONSE_CHUNK_DELAY_MS = 10;
|
|
45
|
+
const DEFAULT_MAX_TRANSCRIPT_LINES = 200;
|
|
46
|
+
|
|
47
|
+
function sleep(delayMs: number): Promise<void> {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
setTimeout(resolve, delayMs);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isPrintableCharacter(char: string): boolean {
|
|
54
|
+
return char.length === 1 && char >= ' ' && char !== '\u007f';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toErrorMessage(error: unknown): string {
|
|
58
|
+
return error instanceof Error ? error.message : String(error);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function providerIdFromModel(model: NimModelRef): string {
|
|
62
|
+
const slash = model.indexOf('/');
|
|
63
|
+
if (slash <= 0) {
|
|
64
|
+
return 'mock';
|
|
65
|
+
}
|
|
66
|
+
return model.slice(0, slash);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseRequestedToolInvocation(
|
|
70
|
+
input: string,
|
|
71
|
+
): { readonly toolName: string; readonly argumentsText: string } | null {
|
|
72
|
+
const match = /(?:^|\s)use-tool(?:\s+([A-Za-z0-9._:-]+))?(?:\s+(.+))?/u.exec(input);
|
|
73
|
+
if (match === null) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const toolName = match[1];
|
|
77
|
+
if (typeof toolName !== 'string' || toolName.length === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
toolName,
|
|
82
|
+
argumentsText: typeof match[2] === 'string' ? match[2].trim() : '',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toUiModeLabel(mode: NimUiMode): 'debug' | 'user' {
|
|
87
|
+
return mode === 'debug' ? 'debug' : 'user';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseCompactTranscriptLine(text: string): {
|
|
91
|
+
readonly base: string;
|
|
92
|
+
readonly count: number;
|
|
93
|
+
} {
|
|
94
|
+
const match = /^(.*) \(x(\d+)\)$/u.exec(text);
|
|
95
|
+
if (match === null) {
|
|
96
|
+
return {
|
|
97
|
+
base: text,
|
|
98
|
+
count: 1,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const count = Number.parseInt(match[2] ?? '1', 10);
|
|
102
|
+
if (!Number.isInteger(count) || count < 2) {
|
|
103
|
+
return {
|
|
104
|
+
base: text,
|
|
105
|
+
count: 1,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
base: match[1] ?? text,
|
|
110
|
+
count,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createMockProviderDriver(input: {
|
|
115
|
+
readonly providerId: string;
|
|
116
|
+
readonly responseChunkDelayMs: number;
|
|
117
|
+
readonly sleep: (delayMs: number) => Promise<void>;
|
|
118
|
+
readonly invokeTool?: (toolName: string, argumentsText: string) => Promise<unknown>;
|
|
119
|
+
}): NimProviderDriver {
|
|
120
|
+
return {
|
|
121
|
+
providerId: input.providerId,
|
|
122
|
+
async *runTurn(turnInput) {
|
|
123
|
+
yield { type: 'provider.thinking.started' };
|
|
124
|
+
await input.sleep(input.responseChunkDelayMs);
|
|
125
|
+
yield { type: 'provider.thinking.completed' };
|
|
126
|
+
|
|
127
|
+
const requestedTool = parseRequestedToolInvocation(turnInput.input);
|
|
128
|
+
if (requestedTool !== null) {
|
|
129
|
+
const toolCallId = randomUUID();
|
|
130
|
+
const supportedTool = turnInput.tools.some((tool) => tool.name === requestedTool.toolName);
|
|
131
|
+
if (!supportedTool) {
|
|
132
|
+
yield {
|
|
133
|
+
type: 'tool.call.failed',
|
|
134
|
+
toolCallId,
|
|
135
|
+
toolName: requestedTool.toolName,
|
|
136
|
+
error: 'tool unavailable',
|
|
137
|
+
};
|
|
138
|
+
} else {
|
|
139
|
+
yield {
|
|
140
|
+
type: 'tool.call.started',
|
|
141
|
+
toolCallId,
|
|
142
|
+
toolName: requestedTool.toolName,
|
|
143
|
+
};
|
|
144
|
+
if (requestedTool.argumentsText.length > 0) {
|
|
145
|
+
yield {
|
|
146
|
+
type: 'tool.call.arguments.delta',
|
|
147
|
+
toolCallId,
|
|
148
|
+
delta: requestedTool.argumentsText,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const output =
|
|
153
|
+
input.invokeTool === undefined
|
|
154
|
+
? {
|
|
155
|
+
notice: 'nim tool bridge unavailable',
|
|
156
|
+
}
|
|
157
|
+
: await input.invokeTool(requestedTool.toolName, requestedTool.argumentsText);
|
|
158
|
+
yield {
|
|
159
|
+
type: 'tool.call.completed',
|
|
160
|
+
toolCallId,
|
|
161
|
+
toolName: requestedTool.toolName,
|
|
162
|
+
};
|
|
163
|
+
yield {
|
|
164
|
+
type: 'tool.result.emitted',
|
|
165
|
+
toolCallId,
|
|
166
|
+
toolName: requestedTool.toolName,
|
|
167
|
+
output,
|
|
168
|
+
};
|
|
169
|
+
} catch (error: unknown) {
|
|
170
|
+
yield {
|
|
171
|
+
type: 'tool.call.failed',
|
|
172
|
+
toolCallId,
|
|
173
|
+
toolName: requestedTool.toolName,
|
|
174
|
+
error: toErrorMessage(error),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const response = `nim mock: ${turnInput.input}`;
|
|
181
|
+
const tokens = response.split(/\s+/u);
|
|
182
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
183
|
+
const token = tokens[index];
|
|
184
|
+
if (token === undefined) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const prefix = index === 0 ? '' : ' ';
|
|
188
|
+
yield {
|
|
189
|
+
type: 'assistant.output.delta',
|
|
190
|
+
text: `${prefix}${token}`,
|
|
191
|
+
};
|
|
192
|
+
await input.sleep(input.responseChunkDelayMs);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
yield { type: 'assistant.output.completed' };
|
|
196
|
+
yield {
|
|
197
|
+
type: 'provider.turn.finished',
|
|
198
|
+
finishReason: 'stop',
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export class RuntimeNimSession {
|
|
205
|
+
private readonly runtime: InMemoryNimRuntime;
|
|
206
|
+
private readonly model: NimModelRef;
|
|
207
|
+
private readonly providerId: string;
|
|
208
|
+
private readonly providerDriver: NimProviderDriver | undefined;
|
|
209
|
+
private readonly retryWithMockOnFailedTurn: boolean;
|
|
210
|
+
private readonly maxTranscriptLines: number;
|
|
211
|
+
private readonly sleep: (delayMs: number) => Promise<void>;
|
|
212
|
+
private readonly responseChunkDelayMs: number;
|
|
213
|
+
|
|
214
|
+
private started = false;
|
|
215
|
+
private disposed = false;
|
|
216
|
+
private sessionId: string | null = null;
|
|
217
|
+
private status: NimSessionStatus = 'idle';
|
|
218
|
+
private uiMode: NimUiMode = DEFAULT_UI_MODE;
|
|
219
|
+
private composerText = '';
|
|
220
|
+
private assistantDraftText = '';
|
|
221
|
+
private transcriptLines: string[] = [];
|
|
222
|
+
private queuedInputs: string[] = [];
|
|
223
|
+
private activeRunId: string | null = null;
|
|
224
|
+
private runSequence = 0;
|
|
225
|
+
private mockFallbackDriverInstalled = false;
|
|
226
|
+
private inputLane: Promise<void> = Promise.resolve();
|
|
227
|
+
private uiIterator: AsyncIterator<NimEventEnvelope> | null = null;
|
|
228
|
+
private uiPump: Promise<void> | null = null;
|
|
229
|
+
|
|
230
|
+
constructor(private readonly options: RuntimeNimSessionOptions) {
|
|
231
|
+
this.runtime = options.runtime ?? new InMemoryNimRuntime();
|
|
232
|
+
this.model = options.model ?? DEFAULT_MODEL;
|
|
233
|
+
this.providerId = providerIdFromModel(this.model);
|
|
234
|
+
this.providerDriver = options.providerDriver;
|
|
235
|
+
this.retryWithMockOnFailedTurn =
|
|
236
|
+
options.retryWithMockOnFailedTurn ?? this.providerDriver !== undefined;
|
|
237
|
+
this.maxTranscriptLines = options.maxTranscriptLines ?? DEFAULT_MAX_TRANSCRIPT_LINES;
|
|
238
|
+
this.sleep = options.sleep ?? sleep;
|
|
239
|
+
this.responseChunkDelayMs = Math.max(
|
|
240
|
+
0,
|
|
241
|
+
options.responseChunkDelayMs ?? DEFAULT_RESPONSE_CHUNK_DELAY_MS,
|
|
242
|
+
);
|
|
243
|
+
this.runtime.registerProvider({
|
|
244
|
+
id: this.providerId,
|
|
245
|
+
displayName: 'Mock',
|
|
246
|
+
models: [this.model],
|
|
247
|
+
});
|
|
248
|
+
this.options.toolBridge?.registerWithRuntime(this.runtime);
|
|
249
|
+
this.runtime.registerProviderDriver(
|
|
250
|
+
this.providerDriver ??
|
|
251
|
+
createMockProviderDriver({
|
|
252
|
+
providerId: this.providerId,
|
|
253
|
+
responseChunkDelayMs: this.responseChunkDelayMs,
|
|
254
|
+
sleep: this.sleep,
|
|
255
|
+
invokeTool: async (toolName, argumentsText) => {
|
|
256
|
+
if (this.options.toolBridge === undefined) {
|
|
257
|
+
return {
|
|
258
|
+
notice: 'nim tool bridge unavailable',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return await this.options.toolBridge.invoke({
|
|
262
|
+
toolName,
|
|
263
|
+
argumentsText,
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async start(): Promise<void> {
|
|
271
|
+
if (this.started || this.disposed) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.started = true;
|
|
275
|
+
const session = await this.runtime.startSession({
|
|
276
|
+
tenantId: this.options.tenantId,
|
|
277
|
+
userId: this.options.userId,
|
|
278
|
+
model: this.model,
|
|
279
|
+
});
|
|
280
|
+
this.sessionId = session.sessionId;
|
|
281
|
+
this.startUiPump(session.sessionId);
|
|
282
|
+
this.options.markDirty();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async dispose(): Promise<void> {
|
|
286
|
+
this.disposed = true;
|
|
287
|
+
const iterator = this.uiIterator;
|
|
288
|
+
this.uiIterator = null;
|
|
289
|
+
if (iterator !== null) {
|
|
290
|
+
try {
|
|
291
|
+
void iterator.return?.();
|
|
292
|
+
} catch {
|
|
293
|
+
// Best-effort cleanup only.
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
this.uiPump = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
snapshot(): RuntimeNimViewModel {
|
|
300
|
+
return {
|
|
301
|
+
sessionId: this.sessionId,
|
|
302
|
+
status: this.status,
|
|
303
|
+
uiMode: toUiModeLabel(this.uiMode),
|
|
304
|
+
composerText: this.composerText,
|
|
305
|
+
queuedCount: this.queuedInputs.length,
|
|
306
|
+
activeRunId: this.activeRunId,
|
|
307
|
+
transcriptLines: this.transcriptLines,
|
|
308
|
+
assistantDraftText: this.assistantDraftText,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
handleInputChunk(text: string): void {
|
|
313
|
+
if (text.length === 0 || this.disposed) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.enqueueInput(async () => {
|
|
317
|
+
await this.consumeInputText(text);
|
|
318
|
+
this.options.markDirty();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
handleEscape(): void {
|
|
323
|
+
if (this.disposed) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.enqueueInput(async () => {
|
|
327
|
+
await this.requestAbort({
|
|
328
|
+
emitIdleNotice: false,
|
|
329
|
+
});
|
|
330
|
+
this.options.markDirty();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private enqueueInput(task: () => Promise<void>): void {
|
|
335
|
+
this.inputLane = this.inputLane
|
|
336
|
+
.then(async () => {
|
|
337
|
+
await task();
|
|
338
|
+
})
|
|
339
|
+
.catch((error: unknown) => {
|
|
340
|
+
this.pushSystemLine(`[error] ${toErrorMessage(error)}`);
|
|
341
|
+
this.options.markDirty();
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private startUiPump(sessionId: string): void {
|
|
346
|
+
const stream = this.runtime.streamEvents({
|
|
347
|
+
tenantId: this.options.tenantId,
|
|
348
|
+
sessionId,
|
|
349
|
+
fidelity: 'semantic',
|
|
350
|
+
});
|
|
351
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
352
|
+
this.uiIterator = iterator;
|
|
353
|
+
this.uiPump = (async () => {
|
|
354
|
+
try {
|
|
355
|
+
while (!this.disposed) {
|
|
356
|
+
const next = await iterator.next();
|
|
357
|
+
if (next.done) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
this.applyEventEnvelope(next.value);
|
|
361
|
+
this.options.markDirty();
|
|
362
|
+
}
|
|
363
|
+
} catch (error: unknown) {
|
|
364
|
+
if (this.disposed) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
this.pushSystemLine(`[error] ${toErrorMessage(error)}`);
|
|
368
|
+
this.options.markDirty();
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private applyEventEnvelope(event: NimEventEnvelope): void {
|
|
374
|
+
const projected = projectEventToUiEvents(event, this.uiMode);
|
|
375
|
+
for (const uiEvent of projected) {
|
|
376
|
+
this.applyUiEvent(uiEvent);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private applyUiEvent(event: NimUiEvent): void {
|
|
381
|
+
if (event.type === 'assistant.state') {
|
|
382
|
+
this.status = event.state;
|
|
383
|
+
if (event.state === 'idle') {
|
|
384
|
+
this.assistantDraftText = '';
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (event.type === 'assistant.text.delta') {
|
|
389
|
+
this.assistantDraftText += event.text;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (event.type === 'assistant.text.message') {
|
|
393
|
+
this.assistantDraftText = '';
|
|
394
|
+
this.pushAssistantLine(event.text);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (event.type === 'tool.activity') {
|
|
398
|
+
this.pushSystemLine(`[tool:${event.phase}] ${event.toolName}`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.pushSystemLine(`[notice] ${event.text}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private async consumeInputText(chunk: string): Promise<void> {
|
|
405
|
+
if (this.sessionId === null) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
let skipLf = false;
|
|
409
|
+
for (const char of chunk) {
|
|
410
|
+
if (skipLf && char === '\n') {
|
|
411
|
+
skipLf = false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
skipLf = false;
|
|
415
|
+
if (char === '\r') {
|
|
416
|
+
await this.submitComposer();
|
|
417
|
+
skipLf = true;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (char === '\n') {
|
|
421
|
+
await this.submitComposer();
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (char === '\t') {
|
|
425
|
+
await this.queueComposer();
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (char === '\u007f' || char === '\b') {
|
|
429
|
+
this.composerText = this.composerText.slice(0, -1);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (!isPrintableCharacter(char)) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
this.composerText += char;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private async submitComposer(): Promise<void> {
|
|
440
|
+
const text = this.composerText.trim();
|
|
441
|
+
this.composerText = '';
|
|
442
|
+
if (text.length === 0) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (text.startsWith('/')) {
|
|
446
|
+
await this.runCommand(text);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (this.activeRunId === null) {
|
|
450
|
+
await this.startTurn(text);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
this.pushUserLine(text);
|
|
454
|
+
const result = await this.runtime.steerTurn({
|
|
455
|
+
sessionId: this.requireSessionId(),
|
|
456
|
+
runId: this.activeRunId,
|
|
457
|
+
text,
|
|
458
|
+
});
|
|
459
|
+
if (!result.accepted) {
|
|
460
|
+
this.queuedInputs.push(text);
|
|
461
|
+
this.pushSystemLine(`[notice] steer rejected (${result.reason ?? 'unknown'}), queued`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private async runCommand(commandText: string): Promise<void> {
|
|
466
|
+
const trimmed = commandText.trim();
|
|
467
|
+
if (trimmed === '/help') {
|
|
468
|
+
this.pushSystemLine('[help] /help /mode <debug|user> /state /clear /abort use-tool <tool>');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (trimmed === '/state') {
|
|
472
|
+
this.pushSystemLine(
|
|
473
|
+
`[state] status:${this.status} mode:${toUiModeLabel(this.uiMode)} queued:${String(this.queuedInputs.length)} active:${this.activeRunId === null ? 'none' : 'yes'}`,
|
|
474
|
+
);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (trimmed === '/clear') {
|
|
478
|
+
this.transcriptLines = [];
|
|
479
|
+
this.assistantDraftText = '';
|
|
480
|
+
this.pushSystemLine('[notice] transcript cleared');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (trimmed === '/abort') {
|
|
484
|
+
await this.requestAbort({
|
|
485
|
+
emitIdleNotice: true,
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (trimmed.startsWith('/mode ')) {
|
|
490
|
+
const rawMode = trimmed.slice('/mode '.length).trim();
|
|
491
|
+
const resolvedMode =
|
|
492
|
+
rawMode === 'debug'
|
|
493
|
+
? 'debug'
|
|
494
|
+
: rawMode === 'user' || rawMode === 'seamless'
|
|
495
|
+
? 'seamless'
|
|
496
|
+
: null;
|
|
497
|
+
if (resolvedMode === null) {
|
|
498
|
+
this.pushSystemLine(`[error] invalid mode: ${rawMode}`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (this.uiMode === resolvedMode) {
|
|
502
|
+
this.pushSystemLine(`[notice] ui mode already ${toUiModeLabel(this.uiMode)}`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.uiMode = resolvedMode;
|
|
506
|
+
this.pushSystemLine(`[notice] ui mode set to ${toUiModeLabel(this.uiMode)}`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.pushSystemLine(`[error] unknown command: ${trimmed}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private async requestAbort(input: { readonly emitIdleNotice: boolean }): Promise<void> {
|
|
513
|
+
if (this.activeRunId === null) {
|
|
514
|
+
if (input.emitIdleNotice) {
|
|
515
|
+
this.pushSystemLine('[notice] no active run');
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
await this.runtime.abortTurn({
|
|
520
|
+
runId: this.activeRunId,
|
|
521
|
+
reason: 'manual',
|
|
522
|
+
});
|
|
523
|
+
this.pushSystemLine('[notice] abort requested');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private async queueComposer(): Promise<void> {
|
|
527
|
+
const text = this.composerText.trim();
|
|
528
|
+
this.composerText = '';
|
|
529
|
+
if (text.length === 0) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
this.queuedInputs.push(text);
|
|
533
|
+
this.pushSystemLine(`[queued] ${text}`);
|
|
534
|
+
await this.drainQueue();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private async drainQueue(): Promise<void> {
|
|
538
|
+
if (this.activeRunId !== null) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const next = this.queuedInputs.shift();
|
|
542
|
+
if (next === undefined) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
await this.startTurn(next);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private async startTurn(
|
|
549
|
+
text: string,
|
|
550
|
+
options?: {
|
|
551
|
+
readonly pushUserLine?: boolean;
|
|
552
|
+
readonly allowMockRetry?: boolean;
|
|
553
|
+
},
|
|
554
|
+
): Promise<void> {
|
|
555
|
+
const turn = await this.runtime.sendTurn({
|
|
556
|
+
sessionId: this.requireSessionId(),
|
|
557
|
+
input: text,
|
|
558
|
+
idempotencyKey: `nim-${String(this.runSequence + 1)}-${randomUUID()}`,
|
|
559
|
+
});
|
|
560
|
+
this.runSequence += 1;
|
|
561
|
+
this.activeRunId = turn.runId;
|
|
562
|
+
if (options?.pushUserLine !== false) {
|
|
563
|
+
this.pushUserLine(text);
|
|
564
|
+
}
|
|
565
|
+
void turn.done
|
|
566
|
+
.then((result) => {
|
|
567
|
+
this.enqueueInput(async () => {
|
|
568
|
+
if (this.activeRunId === turn.runId) {
|
|
569
|
+
this.activeRunId = null;
|
|
570
|
+
}
|
|
571
|
+
if (result.terminalState === 'failed') {
|
|
572
|
+
const failureMessage = await this.resolveRunFailureMessage(turn.runId);
|
|
573
|
+
if ((options?.allowMockRetry ?? true) && this.shouldRetryFailedTurnWithMock()) {
|
|
574
|
+
if (failureMessage !== null) {
|
|
575
|
+
this.pushSystemLine(`[error] ${failureMessage}`);
|
|
576
|
+
}
|
|
577
|
+
this.pushSystemLine('[notice] provider failed; retrying with local fallback');
|
|
578
|
+
this.installMockFallbackDriver();
|
|
579
|
+
await this.startTurn(text, {
|
|
580
|
+
pushUserLine: false,
|
|
581
|
+
allowMockRetry: false,
|
|
582
|
+
});
|
|
583
|
+
this.options.markDirty();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (failureMessage !== null) {
|
|
587
|
+
this.pushSystemLine(`[error] ${failureMessage}`);
|
|
588
|
+
} else {
|
|
589
|
+
this.pushSystemLine(`[turn:failed] ${turn.runId}`);
|
|
590
|
+
}
|
|
591
|
+
} else if (result.terminalState !== 'completed') {
|
|
592
|
+
this.pushSystemLine(`[turn:${result.terminalState}] ${turn.runId}`);
|
|
593
|
+
}
|
|
594
|
+
await this.drainQueue();
|
|
595
|
+
this.options.markDirty();
|
|
596
|
+
});
|
|
597
|
+
})
|
|
598
|
+
.catch((error: unknown) => {
|
|
599
|
+
this.enqueueInput(async () => {
|
|
600
|
+
if (this.activeRunId === turn.runId) {
|
|
601
|
+
this.activeRunId = null;
|
|
602
|
+
}
|
|
603
|
+
this.pushSystemLine(`[error] ${toErrorMessage(error)}`);
|
|
604
|
+
await this.drainQueue();
|
|
605
|
+
this.options.markDirty();
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private shouldRetryFailedTurnWithMock(): boolean {
|
|
611
|
+
return this.retryWithMockOnFailedTurn && !this.mockFallbackDriverInstalled;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private installMockFallbackDriver(): void {
|
|
615
|
+
if (this.mockFallbackDriverInstalled) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
this.runtime.registerProviderDriver(
|
|
619
|
+
createMockProviderDriver({
|
|
620
|
+
providerId: this.providerId,
|
|
621
|
+
responseChunkDelayMs: this.responseChunkDelayMs,
|
|
622
|
+
sleep: this.sleep,
|
|
623
|
+
invokeTool: async (toolName, argumentsText) => {
|
|
624
|
+
if (this.options.toolBridge === undefined) {
|
|
625
|
+
return {
|
|
626
|
+
notice: 'nim tool bridge unavailable',
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return await this.options.toolBridge.invoke({
|
|
630
|
+
toolName,
|
|
631
|
+
argumentsText,
|
|
632
|
+
});
|
|
633
|
+
},
|
|
634
|
+
}),
|
|
635
|
+
);
|
|
636
|
+
this.mockFallbackDriverInstalled = true;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private async resolveRunFailureMessage(runId: string): Promise<string | null> {
|
|
640
|
+
if (this.sessionId === null) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
const replay = await this.runtime.replayEvents({
|
|
645
|
+
tenantId: this.options.tenantId,
|
|
646
|
+
sessionId: this.sessionId,
|
|
647
|
+
runId,
|
|
648
|
+
fidelity: 'semantic',
|
|
649
|
+
});
|
|
650
|
+
for (let index = replay.events.length - 1; index >= 0; index -= 1) {
|
|
651
|
+
const event = replay.events[index];
|
|
652
|
+
if (event?.type !== 'turn.failed') {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const message = event.data?.['message'];
|
|
656
|
+
if (typeof message !== 'string') {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const trimmed = message.trim();
|
|
660
|
+
if (trimmed.length > 0) {
|
|
661
|
+
return trimmed;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private requireSessionId(): string {
|
|
671
|
+
if (this.sessionId === null) {
|
|
672
|
+
throw new Error('nim session not started');
|
|
673
|
+
}
|
|
674
|
+
return this.sessionId;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private pushUserLine(text: string): void {
|
|
678
|
+
this.pushTranscriptLine(`you> ${text}`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private pushAssistantLine(text: string): void {
|
|
682
|
+
this.pushTranscriptLine(`nim> ${text}`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private pushSystemLine(text: string): void {
|
|
686
|
+
this.pushTranscriptLine(text);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private pushTranscriptLine(text: string): void {
|
|
690
|
+
const lastLine = this.transcriptLines[this.transcriptLines.length - 1];
|
|
691
|
+
if (typeof lastLine === 'string') {
|
|
692
|
+
const parsed = parseCompactTranscriptLine(lastLine);
|
|
693
|
+
if (parsed.base === text) {
|
|
694
|
+
this.transcriptLines[this.transcriptLines.length - 1] =
|
|
695
|
+
`${text} (x${String(parsed.count + 1)})`;
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
this.transcriptLines.push(text);
|
|
700
|
+
const overflow = this.transcriptLines.length - this.maxTranscriptLines;
|
|
701
|
+
if (overflow > 0) {
|
|
702
|
+
this.transcriptLines.splice(0, overflow);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|