@murphai/murph 0.1.1
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/CHANGELOG.md +2009 -0
- package/LICENSE +674 -0
- package/README.md +97 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/assistant/automation/run-loop.d.ts +21 -0
- package/dist/assistant/automation/run-loop.d.ts.map +1 -0
- package/dist/assistant/automation/run-loop.js +31 -0
- package/dist/assistant/automation/run-loop.js.map +1 -0
- package/dist/assistant/automation.d.ts +10 -0
- package/dist/assistant/automation.d.ts.map +1 -0
- package/dist/assistant/automation.js +5 -0
- package/dist/assistant/automation.js.map +1 -0
- package/dist/assistant/cron.d.ts +19 -0
- package/dist/assistant/cron.d.ts.map +1 -0
- package/dist/assistant/cron.js +59 -0
- package/dist/assistant/cron.js.map +1 -0
- package/dist/assistant/doctor-security.d.ts +15 -0
- package/dist/assistant/doctor-security.d.ts.map +1 -0
- package/dist/assistant/doctor-security.js +172 -0
- package/dist/assistant/doctor-security.js.map +1 -0
- package/dist/assistant/doctor.d.ts +5 -0
- package/dist/assistant/doctor.d.ts.map +1 -0
- package/dist/assistant/doctor.js +527 -0
- package/dist/assistant/doctor.js.map +1 -0
- package/dist/assistant/outbox.d.ts +19 -0
- package/dist/assistant/outbox.d.ts.map +1 -0
- package/dist/assistant/outbox.js +28 -0
- package/dist/assistant/outbox.js.map +1 -0
- package/dist/assistant/provider-catalog.d.ts +61 -0
- package/dist/assistant/provider-catalog.d.ts.map +1 -0
- package/dist/assistant/provider-catalog.js +205 -0
- package/dist/assistant/provider-catalog.js.map +1 -0
- package/dist/assistant/service.d.ts +85 -0
- package/dist/assistant/service.d.ts.map +1 -0
- package/dist/assistant/service.js +26 -0
- package/dist/assistant/service.js.map +1 -0
- package/dist/assistant/status.d.ts +9 -0
- package/dist/assistant/status.d.ts.map +1 -0
- package/dist/assistant/status.js +16 -0
- package/dist/assistant/status.js.map +1 -0
- package/dist/assistant/stop.d.ts +20 -0
- package/dist/assistant/stop.d.ts.map +1 -0
- package/dist/assistant/stop.js +142 -0
- package/dist/assistant/stop.js.map +1 -0
- package/dist/assistant/store.d.ts +6 -0
- package/dist/assistant/store.d.ts.map +1 -0
- package/dist/assistant/store.js +21 -0
- package/dist/assistant/store.js.map +1 -0
- package/dist/assistant/ui/ink.d.ts +247 -0
- package/dist/assistant/ui/ink.d.ts.map +1 -0
- package/dist/assistant/ui/ink.js +2417 -0
- package/dist/assistant/ui/ink.js.map +1 -0
- package/dist/assistant/ui/theme.d.ts +64 -0
- package/dist/assistant/ui/theme.d.ts.map +1 -0
- package/dist/assistant/ui/theme.js +180 -0
- package/dist/assistant/ui/theme.js.map +1 -0
- package/dist/assistant/ui/view-model.d.ts +89 -0
- package/dist/assistant/ui/view-model.d.ts.map +1 -0
- package/dist/assistant/ui/view-model.js +298 -0
- package/dist/assistant/ui/view-model.js.map +1 -0
- package/dist/assistant-chat-ink.d.ts +2 -0
- package/dist/assistant-chat-ink.d.ts.map +1 -0
- package/dist/assistant-chat-ink.js +2 -0
- package/dist/assistant-chat-ink.js.map +1 -0
- package/dist/assistant-daemon-client.d.ts +81 -0
- package/dist/assistant-daemon-client.d.ts.map +1 -0
- package/dist/assistant-daemon-client.js +473 -0
- package/dist/assistant-daemon-client.js.map +1 -0
- package/dist/assistant-runtime.d.ts +25 -0
- package/dist/assistant-runtime.d.ts.map +1 -0
- package/dist/assistant-runtime.js +17 -0
- package/dist/assistant-runtime.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +7 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli-entry.d.ts +10 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +127 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/commands/assistant.d.ts +5 -0
- package/dist/commands/assistant.d.ts.map +1 -0
- package/dist/commands/assistant.js +1663 -0
- package/dist/commands/assistant.js.map +1 -0
- package/dist/commands/audit-command-helpers.d.ts +15 -0
- package/dist/commands/audit-command-helpers.d.ts.map +1 -0
- package/dist/commands/audit-command-helpers.js +24 -0
- package/dist/commands/audit-command-helpers.js.map +1 -0
- package/dist/commands/audit.d.ts +4 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +107 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/device.d.ts +4 -0
- package/dist/commands/device.d.ts.map +1 -0
- package/dist/commands/device.js +177 -0
- package/dist/commands/device.js.map +1 -0
- package/dist/commands/document.d.ts +4 -0
- package/dist/commands/document.d.ts.map +1 -0
- package/dist/commands/document.js +117 -0
- package/dist/commands/document.js.map +1 -0
- package/dist/commands/event.d.ts +4 -0
- package/dist/commands/event.d.ts.map +1 -0
- package/dist/commands/event.js +136 -0
- package/dist/commands/event.js.map +1 -0
- package/dist/commands/experiment.d.ts +4 -0
- package/dist/commands/experiment.d.ts.map +1 -0
- package/dist/commands/experiment.js +140 -0
- package/dist/commands/experiment.js.map +1 -0
- package/dist/commands/export-intake-read-helpers.d.ts +150 -0
- package/dist/commands/export-intake-read-helpers.d.ts.map +1 -0
- package/dist/commands/export-intake-read-helpers.js +328 -0
- package/dist/commands/export-intake-read-helpers.js.map +1 -0
- package/dist/commands/export.d.ts +4 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +179 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/food.d.ts +4 -0
- package/dist/commands/food.d.ts.map +1 -0
- package/dist/commands/food.js +190 -0
- package/dist/commands/food.js.map +1 -0
- package/dist/commands/health-command-factory.d.ts +230 -0
- package/dist/commands/health-command-factory.d.ts.map +1 -0
- package/dist/commands/health-command-factory.js +551 -0
- package/dist/commands/health-command-factory.js.map +1 -0
- package/dist/commands/health-entity-command-registry.d.ts +27 -0
- package/dist/commands/health-entity-command-registry.d.ts.map +1 -0
- package/dist/commands/health-entity-command-registry.js +84 -0
- package/dist/commands/health-entity-command-registry.js.map +1 -0
- package/dist/commands/inbox.d.ts +5 -0
- package/dist/commands/inbox.d.ts.map +1 -0
- package/dist/commands/inbox.js +841 -0
- package/dist/commands/inbox.js.map +1 -0
- package/dist/commands/intake.d.ts +4 -0
- package/dist/commands/intake.d.ts.map +1 -0
- package/dist/commands/intake.js +175 -0
- package/dist/commands/intake.js.map +1 -0
- package/dist/commands/intervention.d.ts +4 -0
- package/dist/commands/intervention.d.ts.map +1 -0
- package/dist/commands/intervention.js +122 -0
- package/dist/commands/intervention.js.map +1 -0
- package/dist/commands/journal.d.ts +12 -0
- package/dist/commands/journal.d.ts.map +1 -0
- package/dist/commands/journal.js +186 -0
- package/dist/commands/journal.js.map +1 -0
- package/dist/commands/meal.d.ts +4 -0
- package/dist/commands/meal.d.ts.map +1 -0
- package/dist/commands/meal.js +123 -0
- package/dist/commands/meal.js.map +1 -0
- package/dist/commands/profile.d.ts +4 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +62 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/protocol.d.ts +4 -0
- package/dist/commands/protocol.d.ts.map +1 -0
- package/dist/commands/protocol.js +79 -0
- package/dist/commands/protocol.js.map +1 -0
- package/dist/commands/provider.d.ts +4 -0
- package/dist/commands/provider.d.ts.map +1 -0
- package/dist/commands/provider.js +115 -0
- package/dist/commands/provider.js.map +1 -0
- package/dist/commands/read.d.ts +4 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +55 -0
- package/dist/commands/read.js.map +1 -0
- package/dist/commands/recipe.d.ts +4 -0
- package/dist/commands/recipe.d.ts.map +1 -0
- package/dist/commands/recipe.js +116 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/record-mutation-command-helpers.d.ts +196 -0
- package/dist/commands/record-mutation-command-helpers.d.ts.map +1 -0
- package/dist/commands/record-mutation-command-helpers.js +150 -0
- package/dist/commands/record-mutation-command-helpers.js.map +1 -0
- package/dist/commands/research.d.ts +3 -0
- package/dist/commands/research.d.ts.map +1 -0
- package/dist/commands/research.js +104 -0
- package/dist/commands/research.js.map +1 -0
- package/dist/commands/sample-batch-command-helpers.d.ts +24 -0
- package/dist/commands/sample-batch-command-helpers.d.ts.map +1 -0
- package/dist/commands/sample-batch-command-helpers.js +99 -0
- package/dist/commands/sample-batch-command-helpers.js.map +1 -0
- package/dist/commands/sample-import-command-helpers.d.ts +24 -0
- package/dist/commands/sample-import-command-helpers.d.ts.map +1 -0
- package/dist/commands/sample-import-command-helpers.js +49 -0
- package/dist/commands/sample-import-command-helpers.js.map +1 -0
- package/dist/commands/sample-query-command-helpers.d.ts +11 -0
- package/dist/commands/sample-query-command-helpers.d.ts.map +1 -0
- package/dist/commands/sample-query-command-helpers.js +26 -0
- package/dist/commands/sample-query-command-helpers.js.map +1 -0
- package/dist/commands/samples.d.ts +4 -0
- package/dist/commands/samples.d.ts.map +1 -0
- package/dist/commands/samples.js +261 -0
- package/dist/commands/samples.js.map +1 -0
- package/dist/commands/search.d.ts +4 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +295 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/supplement.d.ts +4 -0
- package/dist/commands/supplement.d.ts.map +1 -0
- package/dist/commands/supplement.js +338 -0
- package/dist/commands/supplement.js.map +1 -0
- package/dist/commands/vault.d.ts +4 -0
- package/dist/commands/vault.d.ts.map +1 -0
- package/dist/commands/vault.js +164 -0
- package/dist/commands/vault.js.map +1 -0
- package/dist/commands/workout.d.ts +4 -0
- package/dist/commands/workout.d.ts.map +1 -0
- package/dist/commands/workout.js +284 -0
- package/dist/commands/workout.js.map +1 -0
- package/dist/incur.generated.d.ts +2164 -0
- package/dist/incur.generated.d.ts.map +1 -0
- package/dist/incur.generated.js +2 -0
- package/dist/incur.generated.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/research-cli-contracts.d.ts +22 -0
- package/dist/research-cli-contracts.d.ts.map +1 -0
- package/dist/research-cli-contracts.js +18 -0
- package/dist/research-cli-contracts.js.map +1 -0
- package/dist/research-runtime.d.ts +79 -0
- package/dist/research-runtime.d.ts.map +1 -0
- package/dist/research-runtime.js +351 -0
- package/dist/research-runtime.js.map +1 -0
- package/dist/run-terminal-logging.d.ts +12 -0
- package/dist/run-terminal-logging.d.ts.map +1 -0
- package/dist/run-terminal-logging.js +323 -0
- package/dist/run-terminal-logging.js.map +1 -0
- package/dist/setup-agentmail.d.ts +30 -0
- package/dist/setup-agentmail.d.ts.map +1 -0
- package/dist/setup-agentmail.js +136 -0
- package/dist/setup-agentmail.js.map +1 -0
- package/dist/setup-assistant-account.d.ts +29 -0
- package/dist/setup-assistant-account.d.ts.map +1 -0
- package/dist/setup-assistant-account.js +443 -0
- package/dist/setup-assistant-account.js.map +1 -0
- package/dist/setup-assistant.d.ts +34 -0
- package/dist/setup-assistant.d.ts.map +1 -0
- package/dist/setup-assistant.js +355 -0
- package/dist/setup-assistant.js.map +1 -0
- package/dist/setup-cli.d.ts +72 -0
- package/dist/setup-cli.d.ts.map +1 -0
- package/dist/setup-cli.js +387 -0
- package/dist/setup-cli.js.map +1 -0
- package/dist/setup-services/channels.d.ts +19 -0
- package/dist/setup-services/channels.d.ts.map +1 -0
- package/dist/setup-services/channels.js +721 -0
- package/dist/setup-services/channels.js.map +1 -0
- package/dist/setup-services/process.d.ts +18 -0
- package/dist/setup-services/process.d.ts.map +1 -0
- package/dist/setup-services/process.js +98 -0
- package/dist/setup-services/process.js.map +1 -0
- package/dist/setup-services/scheduled-updates.d.ts +9 -0
- package/dist/setup-services/scheduled-updates.d.ts.map +1 -0
- package/dist/setup-services/scheduled-updates.js +64 -0
- package/dist/setup-services/scheduled-updates.js.map +1 -0
- package/dist/setup-services/shell.d.ts +18 -0
- package/dist/setup-services/shell.d.ts.map +1 -0
- package/dist/setup-services/shell.js +447 -0
- package/dist/setup-services/shell.js.map +1 -0
- package/dist/setup-services/steps.d.ts +39 -0
- package/dist/setup-services/steps.d.ts.map +1 -0
- package/dist/setup-services/steps.js +86 -0
- package/dist/setup-services/steps.js.map +1 -0
- package/dist/setup-services/toolchain.d.ts +46 -0
- package/dist/setup-services/toolchain.d.ts.map +1 -0
- package/dist/setup-services/toolchain.js +232 -0
- package/dist/setup-services/toolchain.js.map +1 -0
- package/dist/setup-services.d.ts +44 -0
- package/dist/setup-services.d.ts.map +1 -0
- package/dist/setup-services.js +739 -0
- package/dist/setup-services.js.map +1 -0
- package/dist/setup-wizard.d.ts +101 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +1458 -0
- package/dist/setup-wizard.js.map +1 -0
- package/dist/usecases/intervention.d.ts +63 -0
- package/dist/usecases/intervention.d.ts.map +1 -0
- package/dist/usecases/intervention.js +205 -0
- package/dist/usecases/intervention.js.map +1 -0
- package/dist/usecases/text-duration.d.ts +4 -0
- package/dist/usecases/text-duration.d.ts.map +1 -0
- package/dist/usecases/text-duration.js +63 -0
- package/dist/usecases/text-duration.js.map +1 -0
- package/dist/usecases/workout-format.d.ts +139 -0
- package/dist/usecases/workout-format.d.ts.map +1 -0
- package/dist/usecases/workout-format.js +445 -0
- package/dist/usecases/workout-format.js.map +1 -0
- package/dist/usecases/workout.d.ts +94 -0
- package/dist/usecases/workout.d.ts.map +1 -0
- package/dist/usecases/workout.js +411 -0
- package/dist/usecases/workout.js.map +1 -0
- package/dist/vault-cli-command-manifest.d.ts +562 -0
- package/dist/vault-cli-command-manifest.d.ts.map +1 -0
- package/dist/vault-cli-command-manifest.js +759 -0
- package/dist/vault-cli-command-manifest.js.map +1 -0
- package/dist/vault-cli.d.ts +6 -0
- package/dist/vault-cli.d.ts.map +1 -0
- package/dist/vault-cli.js +38 -0
- package/dist/vault-cli.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,2417 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import tty from 'node:tty';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { Box, Static, Text, render, useApp, useInput, useStdout, } from 'ink';
|
|
7
|
+
import { assistantChatResultSchema, } from '@murphai/assistant-core/assistant-cli-contracts';
|
|
8
|
+
import { discoverAssistantProviderModels, resolveAssistantCatalogReasoningOptions, resolveAssistantModelCatalog, } from '../provider-catalog.js';
|
|
9
|
+
import { resolveCodexDisplayOptions } from '@murphai/assistant-core/assistant-codex';
|
|
10
|
+
import { buildAssistantProviderDefaultsPatch, resolveAssistantOperatorDefaults, resolveAssistantProviderDefaults, saveAssistantOperatorDefaultsPatch, } from '@murphai/assistant-core/operator-config';
|
|
11
|
+
import { openAssistantConversation, sendAssistantMessage, updateAssistantSessionOptions, } from '../service.js';
|
|
12
|
+
import { extractRecoveredAssistantSession, isAssistantProviderConnectionLostError, isAssistantProviderInterruptedError, } from '@murphai/assistant-core/assistant-provider';
|
|
13
|
+
import { appendAssistantTranscriptEntries, isAssistantSessionNotFoundError, listAssistantTranscriptEntries, redactAssistantDisplayPath, } from '../store.js';
|
|
14
|
+
import { normalizeNullableString } from '@murphai/assistant-core/assistant-runtime';
|
|
15
|
+
import { redactAssistantSessionForDisplay } from '@murphai/assistant-core/assistant-runtime';
|
|
16
|
+
import { CHAT_COMPOSER_HINT, CHAT_SLASH_COMMANDS, CHAT_STARTER_SUGGESTIONS, applyProviderProgressEventToEntries, finalizePendingInkChatTraces, findAssistantModelOptionIndex, findAssistantReasoningOptionIndex, formatSessionBinding, applyInkChatTraceUpdates, getMatchingSlashCommands, resolveChatMetadataBadges, resolveChatSubmitAction, shouldShowChatComposerGuidance, shouldClearComposerForSubmitAction, seedChatEntries, } from './view-model.js';
|
|
17
|
+
import { LIGHT_ASSISTANT_INK_THEME, captureAssistantInkThemeBaseline, resolveAssistantInkThemeForOpenChat, } from './theme.js';
|
|
18
|
+
const AssistantInkThemeContext = React.createContext(LIGHT_ASSISTANT_INK_THEME);
|
|
19
|
+
const StaticTranscript = Static;
|
|
20
|
+
const COMPOSER_WORD_SEPARATORS = "`~!@#$%^&*()-=+[{]}\\\\|;:'\\\",.<>/?";
|
|
21
|
+
const MODIFIED_RETURN_SEQUENCE = /^\u001b?\[27;(\d+);13~$/u;
|
|
22
|
+
const RAW_ARROW_SEQUENCE = /^\u001b?(?:\[(?:(\d+;)?(\d+))?([ABCD])|O([ABCD]))$/u;
|
|
23
|
+
const QUEUED_FOLLOW_UP_SHORTCUT_HINT = '⌥ + ↑ edit last queued message';
|
|
24
|
+
const MAX_QUEUED_FOLLOW_UP_PREVIEW_LENGTH = 88;
|
|
25
|
+
const ASSISTANT_INK_THEME_REFRESH_INTERVAL_MS = 2_000;
|
|
26
|
+
function useAssistantInkTheme() {
|
|
27
|
+
return React.useContext(AssistantInkThemeContext);
|
|
28
|
+
}
|
|
29
|
+
const BUSY_INDICATOR_CHARACTER = '•';
|
|
30
|
+
const ASSISTANT_CHAT_VIEW_PADDING_X = 1;
|
|
31
|
+
const ASSISTANT_PLAIN_TEXT_WRAP_SLACK = 1;
|
|
32
|
+
const ASSISTANT_INK_TTY_PATH = process.platform === 'win32' ? 'CONIN$' : '/dev/tty';
|
|
33
|
+
export function resolveChromePanelBoxProps(props) {
|
|
34
|
+
const boxProps = {
|
|
35
|
+
flexDirection: 'column',
|
|
36
|
+
marginBottom: props.marginBottom ?? 1,
|
|
37
|
+
paddingX: props.paddingX ??
|
|
38
|
+
(typeof props.backgroundColor === 'string' && props.backgroundColor.length > 0
|
|
39
|
+
? 1
|
|
40
|
+
: 0),
|
|
41
|
+
paddingY: props.paddingY ?? 0,
|
|
42
|
+
width: props.width ?? '100%',
|
|
43
|
+
};
|
|
44
|
+
if (typeof props.backgroundColor === 'string' && props.backgroundColor.length > 0) {
|
|
45
|
+
boxProps.backgroundColor = props.backgroundColor;
|
|
46
|
+
}
|
|
47
|
+
return boxProps;
|
|
48
|
+
}
|
|
49
|
+
export function supportsAssistantInkRawMode(stdin) {
|
|
50
|
+
return Boolean(stdin?.isTTY && typeof stdin.setRawMode === 'function');
|
|
51
|
+
}
|
|
52
|
+
export function resolveAssistantInkInputAdapter(input = {}) {
|
|
53
|
+
const stdin = input.stdin ?? process.stdin;
|
|
54
|
+
if (supportsAssistantInkRawMode(stdin)) {
|
|
55
|
+
return {
|
|
56
|
+
close: () => { },
|
|
57
|
+
source: 'stdin',
|
|
58
|
+
stdin,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const ttyPath = input.ttyPath ?? ASSISTANT_INK_TTY_PATH;
|
|
62
|
+
const openTtyFd = input.openTtyFd ??
|
|
63
|
+
((pathToOpen, flags) => fs.openSync(pathToOpen, flags));
|
|
64
|
+
const createTtyReadStream = input.createTtyReadStream ??
|
|
65
|
+
((fd) => new tty.ReadStream(fd));
|
|
66
|
+
let fd = null;
|
|
67
|
+
let closed = false;
|
|
68
|
+
const closeFallbackFd = () => {
|
|
69
|
+
if (fd === null || closed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
closed = true;
|
|
73
|
+
try {
|
|
74
|
+
fs.closeSync(fd);
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
fd = openTtyFd(ttyPath, 'r');
|
|
80
|
+
const ttyInput = createTtyReadStream(fd);
|
|
81
|
+
if (!supportsAssistantInkRawMode(ttyInput)) {
|
|
82
|
+
ttyInput.destroy?.();
|
|
83
|
+
closeFallbackFd();
|
|
84
|
+
return {
|
|
85
|
+
close: () => { },
|
|
86
|
+
source: 'unsupported',
|
|
87
|
+
stdin: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
close: () => {
|
|
92
|
+
ttyInput.destroy?.();
|
|
93
|
+
closeFallbackFd();
|
|
94
|
+
},
|
|
95
|
+
source: 'tty',
|
|
96
|
+
stdin: ttyInput,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
closeFallbackFd();
|
|
101
|
+
return {
|
|
102
|
+
close: () => { },
|
|
103
|
+
source: 'unsupported',
|
|
104
|
+
stdin: null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const ChromePanel = React.memo(function ChromePanel(props) {
|
|
109
|
+
const createElement = React.createElement;
|
|
110
|
+
return createElement(Box, resolveChromePanelBoxProps(props), props.children);
|
|
111
|
+
});
|
|
112
|
+
const BusySpinner = React.memo(function BusySpinner(input) {
|
|
113
|
+
const createElement = React.createElement;
|
|
114
|
+
const theme = useAssistantInkTheme();
|
|
115
|
+
return createElement(Text, {
|
|
116
|
+
color: input.color ?? theme.accentColor,
|
|
117
|
+
}, BUSY_INDICATOR_CHARACTER);
|
|
118
|
+
});
|
|
119
|
+
export function resolveMessageRoleLabel(kind) {
|
|
120
|
+
if (kind === 'error') {
|
|
121
|
+
return 'error';
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const MessageRoleLabel = React.memo(function MessageRoleLabel(input) {
|
|
126
|
+
const createElement = React.createElement;
|
|
127
|
+
const theme = useAssistantInkTheme();
|
|
128
|
+
const label = resolveMessageRoleLabel(input.kind);
|
|
129
|
+
if (!label) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return createElement(Box, {
|
|
133
|
+
marginBottom: 1,
|
|
134
|
+
}, createElement(Text, {
|
|
135
|
+
bold: true,
|
|
136
|
+
color: theme.errorColor,
|
|
137
|
+
}, label));
|
|
138
|
+
});
|
|
139
|
+
export function renderWrappedTextBlock(input) {
|
|
140
|
+
const createElement = React.createElement;
|
|
141
|
+
return createElement(Box, {
|
|
142
|
+
flexDirection: 'column',
|
|
143
|
+
width: '100%',
|
|
144
|
+
}, createElement(Text, {
|
|
145
|
+
color: input.color,
|
|
146
|
+
dimColor: input.dimColor,
|
|
147
|
+
wrap: 'wrap',
|
|
148
|
+
}, input.children));
|
|
149
|
+
}
|
|
150
|
+
export function wrapAssistantPlainText(input, columns) {
|
|
151
|
+
return input
|
|
152
|
+
.replaceAll('\r\n', '\n')
|
|
153
|
+
.split('\n')
|
|
154
|
+
.map((line) => wrapAssistantPlainTextLine(line, columns))
|
|
155
|
+
.join('\n');
|
|
156
|
+
}
|
|
157
|
+
function wrapAssistantPlainTextLine(input, columns) {
|
|
158
|
+
if (input.length === 0 || columns <= 0) {
|
|
159
|
+
return input;
|
|
160
|
+
}
|
|
161
|
+
const leadingWhitespace = input.match(/^\s*/u)?.[0] ?? '';
|
|
162
|
+
const content = input.slice(leadingWhitespace.length);
|
|
163
|
+
if (content.length === 0) {
|
|
164
|
+
return input;
|
|
165
|
+
}
|
|
166
|
+
const tokens = content.match(/\S+|\s+/gu) ?? [content];
|
|
167
|
+
const lines = [];
|
|
168
|
+
let currentLine = leadingWhitespace;
|
|
169
|
+
let currentWidth = leadingWhitespace.length;
|
|
170
|
+
let pendingWhitespace = '';
|
|
171
|
+
for (const token of tokens) {
|
|
172
|
+
if (/^\s+$/u.test(token)) {
|
|
173
|
+
pendingWhitespace += token;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const spacer = currentWidth > leadingWhitespace.length
|
|
177
|
+
? pendingWhitespace.length > 0
|
|
178
|
+
? pendingWhitespace
|
|
179
|
+
: ' '
|
|
180
|
+
: '';
|
|
181
|
+
const candidateWidth = currentWidth + spacer.length + token.length;
|
|
182
|
+
if (currentWidth > leadingWhitespace.length && candidateWidth > columns) {
|
|
183
|
+
lines.push(currentLine);
|
|
184
|
+
currentLine = `${leadingWhitespace}${token}`;
|
|
185
|
+
currentWidth = leadingWhitespace.length + token.length;
|
|
186
|
+
pendingWhitespace = '';
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (spacer.length > 0) {
|
|
190
|
+
currentLine += spacer;
|
|
191
|
+
currentWidth += spacer.length;
|
|
192
|
+
}
|
|
193
|
+
currentLine += token;
|
|
194
|
+
currentWidth += token.length;
|
|
195
|
+
pendingWhitespace = '';
|
|
196
|
+
}
|
|
197
|
+
lines.push(currentLine);
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
function resolveAssistantTerminalColumns(columns) {
|
|
201
|
+
return typeof columns === 'number' && Number.isFinite(columns)
|
|
202
|
+
? Math.max(1, Math.floor(columns))
|
|
203
|
+
: 80;
|
|
204
|
+
}
|
|
205
|
+
export function resolveAssistantChatViewportWidth(columns) {
|
|
206
|
+
return Math.max(1, resolveAssistantTerminalColumns(columns) - ASSISTANT_CHAT_VIEW_PADDING_X * 2);
|
|
207
|
+
}
|
|
208
|
+
export function resolveAssistantPlainTextWrapColumns(columns) {
|
|
209
|
+
return Math.max(1, resolveAssistantChatViewportWidth(columns) - ASSISTANT_PLAIN_TEXT_WRAP_SLACK);
|
|
210
|
+
}
|
|
211
|
+
const WrappedTextBlock = React.memo(function WrappedTextBlock(input) {
|
|
212
|
+
return renderWrappedTextBlock(input);
|
|
213
|
+
});
|
|
214
|
+
export function renderWrappedPlainTextBlock(input) {
|
|
215
|
+
const createElement = React.createElement;
|
|
216
|
+
const lines = wrapAssistantPlainText(input.text, input.columns).split('\n');
|
|
217
|
+
return createElement(Box, {
|
|
218
|
+
flexDirection: 'column',
|
|
219
|
+
width: '100%',
|
|
220
|
+
}, ...lines.map((line, index) => createElement(Text, {
|
|
221
|
+
color: input.color,
|
|
222
|
+
dimColor: input.dimColor,
|
|
223
|
+
key: `wrapped-plain-text:${index}`,
|
|
224
|
+
}, line.length > 0 ? line : ' ')));
|
|
225
|
+
}
|
|
226
|
+
export function resolveAssistantTurnErrorPresentation(input) {
|
|
227
|
+
const errorText = input.error instanceof Error ? input.error.message : String(input.error);
|
|
228
|
+
const connectionLost = isAssistantProviderConnectionLostError(input.error);
|
|
229
|
+
const missingSession = isAssistantSessionNotFoundError(input.error);
|
|
230
|
+
const queuedFollowUpSuffix = input.restoredQueuedPromptCount > 0
|
|
231
|
+
? ' Queued follow-ups are back in the composer.'
|
|
232
|
+
: '';
|
|
233
|
+
return {
|
|
234
|
+
entry: {
|
|
235
|
+
kind: 'error',
|
|
236
|
+
text: errorText,
|
|
237
|
+
},
|
|
238
|
+
persistTranscriptError: !missingSession,
|
|
239
|
+
status: connectionLost
|
|
240
|
+
? {
|
|
241
|
+
kind: 'error',
|
|
242
|
+
text: `The assistant lost its provider connection. Restore connectivity, then keep chatting to resume.${queuedFollowUpSuffix}`,
|
|
243
|
+
}
|
|
244
|
+
: missingSession
|
|
245
|
+
? {
|
|
246
|
+
kind: 'error',
|
|
247
|
+
text: `The local assistant session record is missing. Check the current vault/default vault or start a new chat.${queuedFollowUpSuffix}`,
|
|
248
|
+
}
|
|
249
|
+
: {
|
|
250
|
+
kind: 'error',
|
|
251
|
+
text: `The assistant hit an error. Fix it or keep chatting.${queuedFollowUpSuffix}`,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const WrappedPlainTextBlock = React.memo(function WrappedPlainTextBlock(input) {
|
|
256
|
+
return renderWrappedPlainTextBlock(input);
|
|
257
|
+
});
|
|
258
|
+
const FooterBadge = React.memo(function FooterBadge(input) {
|
|
259
|
+
const createElement = React.createElement;
|
|
260
|
+
const theme = useAssistantInkTheme();
|
|
261
|
+
const backgroundColor = input.badge.key === 'model' ? theme.accentColor : theme.footerBadgeBackground;
|
|
262
|
+
const color = input.badge.key === 'model'
|
|
263
|
+
? theme.composerCursorTextColor
|
|
264
|
+
: input.badge.key === 'vault'
|
|
265
|
+
? theme.mutedColor
|
|
266
|
+
: theme.footerBadgeTextColor;
|
|
267
|
+
return createElement(Text, {
|
|
268
|
+
backgroundColor,
|
|
269
|
+
color,
|
|
270
|
+
}, formatFooterBadgeText(input.badge));
|
|
271
|
+
});
|
|
272
|
+
export function formatFooterBadgeText(badge) {
|
|
273
|
+
if (badge.key === 'model' || badge.key === 'reasoning') {
|
|
274
|
+
return ` ${badge.value} `;
|
|
275
|
+
}
|
|
276
|
+
return ` ${badge.label}: ${badge.value} `;
|
|
277
|
+
}
|
|
278
|
+
export function splitAssistantMarkdownLinks(input) {
|
|
279
|
+
const segments = [];
|
|
280
|
+
const markdownLinkPattern = /\[([^\]\n]+)\]\(([^)\s]+)\)/gu;
|
|
281
|
+
let lastIndex = 0;
|
|
282
|
+
for (const match of input.matchAll(markdownLinkPattern)) {
|
|
283
|
+
const matchedText = match[0];
|
|
284
|
+
const label = match[1];
|
|
285
|
+
const target = match[2];
|
|
286
|
+
const start = match.index ?? -1;
|
|
287
|
+
if (typeof matchedText !== 'string' ||
|
|
288
|
+
typeof label !== 'string' ||
|
|
289
|
+
typeof target !== 'string' ||
|
|
290
|
+
start < 0) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (start > lastIndex) {
|
|
294
|
+
segments.push({
|
|
295
|
+
kind: 'text',
|
|
296
|
+
text: input.slice(lastIndex, start),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
segments.push({
|
|
300
|
+
kind: 'link',
|
|
301
|
+
label,
|
|
302
|
+
target,
|
|
303
|
+
});
|
|
304
|
+
lastIndex = start + matchedText.length;
|
|
305
|
+
}
|
|
306
|
+
if (lastIndex < input.length) {
|
|
307
|
+
segments.push({
|
|
308
|
+
kind: 'text',
|
|
309
|
+
text: input.slice(lastIndex),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return segments.length > 0
|
|
313
|
+
? segments
|
|
314
|
+
: [
|
|
315
|
+
{
|
|
316
|
+
kind: 'text',
|
|
317
|
+
text: input,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
export function resolveAssistantHyperlinkTarget(target) {
|
|
322
|
+
if (/^(https?|mailto):/iu.test(target)) {
|
|
323
|
+
return target;
|
|
324
|
+
}
|
|
325
|
+
const fragmentIndex = target.indexOf('#');
|
|
326
|
+
const pathPart = fragmentIndex >= 0
|
|
327
|
+
? target.slice(0, fragmentIndex)
|
|
328
|
+
: target;
|
|
329
|
+
const fragment = fragmentIndex >= 0
|
|
330
|
+
? target.slice(fragmentIndex)
|
|
331
|
+
: '';
|
|
332
|
+
if (!path.isAbsolute(pathPart)) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return `${pathToFileURL(pathPart).href}${fragment}`;
|
|
336
|
+
}
|
|
337
|
+
export function formatAssistantTerminalHyperlink(label, target) {
|
|
338
|
+
return `\u001B]8;;${target}\u0007${label}\u001B]8;;\u0007`;
|
|
339
|
+
}
|
|
340
|
+
export function supportsAssistantTerminalHyperlinks(input = {}) {
|
|
341
|
+
const env = input.env ?? process.env;
|
|
342
|
+
const isTTY = input.isTTY ?? process.stderr.isTTY ?? false;
|
|
343
|
+
if (!isTTY || env.CI === 'true') {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
if (env.FORCE_HYPERLINK === '1') {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
return Boolean(env.KITTY_WINDOW_ID ||
|
|
350
|
+
env.ITERM_SESSION_ID ||
|
|
351
|
+
env.WT_SESSION ||
|
|
352
|
+
env.WEZTERM_PANE ||
|
|
353
|
+
env.VSCODE_INJECTION ||
|
|
354
|
+
env.TERM_PROGRAM === 'Apple_Terminal' ||
|
|
355
|
+
env.TERM_PROGRAM === 'WarpTerminal' ||
|
|
356
|
+
env.TERM_PROGRAM === 'vscode');
|
|
357
|
+
}
|
|
358
|
+
export function renderAssistantMessageText(input) {
|
|
359
|
+
const createElement = React.createElement;
|
|
360
|
+
const theme = useAssistantInkTheme();
|
|
361
|
+
const { stdout } = useStdout();
|
|
362
|
+
const enableHyperlinks = supportsAssistantTerminalHyperlinks();
|
|
363
|
+
const segments = splitAssistantMarkdownLinks(input.text);
|
|
364
|
+
const plainTextOnly = segments.every((segment) => segment.kind === 'text');
|
|
365
|
+
if (plainTextOnly) {
|
|
366
|
+
return createElement(WrappedTextBlock, {}, wrapAssistantPlainText(input.text, resolveAssistantPlainTextWrapColumns(stdout?.columns)));
|
|
367
|
+
}
|
|
368
|
+
return createElement(WrappedTextBlock, {}, ...segments.map((segment, index) => {
|
|
369
|
+
if (segment.kind === 'text') {
|
|
370
|
+
return segment.text;
|
|
371
|
+
}
|
|
372
|
+
const hyperlinkTarget = resolveAssistantHyperlinkTarget(segment.target);
|
|
373
|
+
const displayedLabel = hyperlinkTarget && enableHyperlinks
|
|
374
|
+
? formatAssistantTerminalHyperlink(segment.label, hyperlinkTarget)
|
|
375
|
+
: segment.label;
|
|
376
|
+
return createElement(Text, {
|
|
377
|
+
color: theme.accentColor,
|
|
378
|
+
key: `link:${index}:${segment.label}`,
|
|
379
|
+
underline: true,
|
|
380
|
+
}, displayedLabel);
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
function resolveComposerModifiedReturnAction(input, key) {
|
|
384
|
+
const match = MODIFIED_RETURN_SEQUENCE.exec(input);
|
|
385
|
+
if (!match) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const modifier = Math.max(0, Number.parseInt(match[1] ?? '1', 10) - 1);
|
|
389
|
+
const shift = key.shift || (modifier & 1) === 1;
|
|
390
|
+
if (!shift) {
|
|
391
|
+
return {
|
|
392
|
+
kind: 'submit',
|
|
393
|
+
mode: 'enter',
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
kind: 'edit',
|
|
398
|
+
input: '\n',
|
|
399
|
+
key: {
|
|
400
|
+
...key,
|
|
401
|
+
return: false,
|
|
402
|
+
shift: true,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
export function normalizeAssistantInkArrowKey(input, key) {
|
|
407
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
|
|
408
|
+
return key;
|
|
409
|
+
}
|
|
410
|
+
const match = RAW_ARROW_SEQUENCE.exec(input);
|
|
411
|
+
const direction = match?.[3] ?? match?.[4];
|
|
412
|
+
if (!direction) {
|
|
413
|
+
return key;
|
|
414
|
+
}
|
|
415
|
+
const modifier = Math.max(0, Number.parseInt(match?.[2] ?? '1', 10) - 1);
|
|
416
|
+
return {
|
|
417
|
+
...key,
|
|
418
|
+
ctrl: key.ctrl || (modifier & 4) === 4,
|
|
419
|
+
downArrow: direction === 'B',
|
|
420
|
+
leftArrow: direction === 'D',
|
|
421
|
+
meta: key.meta || (modifier & 2) === 2,
|
|
422
|
+
rightArrow: direction === 'C',
|
|
423
|
+
shift: key.shift || (modifier & 1) === 1,
|
|
424
|
+
upArrow: direction === 'A',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export function mergeComposerDraftWithQueuedPrompts(draft, queuedPrompts) {
|
|
428
|
+
return [draft, ...queuedPrompts]
|
|
429
|
+
.filter((value) => value.trim().length > 0)
|
|
430
|
+
.join('\n\n');
|
|
431
|
+
}
|
|
432
|
+
export function resolveComposerTerminalAction(input, key) {
|
|
433
|
+
const normalizedKey = normalizeAssistantInkArrowKey(input, key);
|
|
434
|
+
const modifiedReturnAction = resolveComposerModifiedReturnAction(input, normalizedKey);
|
|
435
|
+
if (modifiedReturnAction) {
|
|
436
|
+
return modifiedReturnAction;
|
|
437
|
+
}
|
|
438
|
+
if ((input === '\u007f' || input === '\b') &&
|
|
439
|
+
!normalizedKey.ctrl &&
|
|
440
|
+
!normalizedKey.meta &&
|
|
441
|
+
!normalizedKey.shift &&
|
|
442
|
+
!normalizedKey.super &&
|
|
443
|
+
!normalizedKey.hyper) {
|
|
444
|
+
return {
|
|
445
|
+
kind: 'edit',
|
|
446
|
+
input: '',
|
|
447
|
+
key: {
|
|
448
|
+
...normalizedKey,
|
|
449
|
+
backspace: true,
|
|
450
|
+
delete: false,
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (normalizedKey.meta && normalizedKey.upArrow) {
|
|
455
|
+
return {
|
|
456
|
+
kind: 'edit-last-queued',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (normalizedKey.tab && !normalizedKey.shift) {
|
|
460
|
+
return {
|
|
461
|
+
kind: 'submit',
|
|
462
|
+
mode: 'tab',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (normalizedKey.return) {
|
|
466
|
+
if (!normalizedKey.shift) {
|
|
467
|
+
return {
|
|
468
|
+
kind: 'submit',
|
|
469
|
+
mode: 'enter',
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
kind: 'edit',
|
|
474
|
+
input: '\n',
|
|
475
|
+
key: {
|
|
476
|
+
...normalizedKey,
|
|
477
|
+
return: false,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (normalizedKey.delete) {
|
|
482
|
+
// Many terminals report the primary delete/backspace key as `delete`.
|
|
483
|
+
// Preserve an actual forward-delete path via Ctrl+D inside the editor helpers.
|
|
484
|
+
return {
|
|
485
|
+
kind: 'edit',
|
|
486
|
+
input,
|
|
487
|
+
key: {
|
|
488
|
+
...normalizedKey,
|
|
489
|
+
backspace: true,
|
|
490
|
+
delete: false,
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
kind: 'edit',
|
|
496
|
+
input,
|
|
497
|
+
key: normalizedKey,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
export function formatQueuedFollowUpPreview(prompt) {
|
|
501
|
+
const normalized = prompt.trim().replace(/\s+/gu, ' ');
|
|
502
|
+
if (normalized.length <= MAX_QUEUED_FOLLOW_UP_PREVIEW_LENGTH) {
|
|
503
|
+
return normalized;
|
|
504
|
+
}
|
|
505
|
+
const truncated = normalized
|
|
506
|
+
.slice(0, MAX_QUEUED_FOLLOW_UP_PREVIEW_LENGTH - 1)
|
|
507
|
+
.trimEnd();
|
|
508
|
+
const boundary = truncated.lastIndexOf(' ');
|
|
509
|
+
const preview = boundary >= Math.floor(MAX_QUEUED_FOLLOW_UP_PREVIEW_LENGTH / 2)
|
|
510
|
+
? truncated.slice(0, boundary).trimEnd()
|
|
511
|
+
: truncated;
|
|
512
|
+
return `${preview}…`;
|
|
513
|
+
}
|
|
514
|
+
function ComposerInput(props) {
|
|
515
|
+
const createElement = React.createElement;
|
|
516
|
+
const theme = useAssistantInkTheme();
|
|
517
|
+
const [displayValue, setDisplayValue] = React.useState(props.value);
|
|
518
|
+
const [cursorOffset, setCursorOffset] = React.useState(props.value.length);
|
|
519
|
+
const valueRef = React.useRef(props.value);
|
|
520
|
+
const cursorOffsetRef = React.useRef(props.value.length);
|
|
521
|
+
const killBufferRef = React.useRef('');
|
|
522
|
+
const preferredColumnRef = React.useRef(null);
|
|
523
|
+
const lastPropValueRef = React.useRef(props.value);
|
|
524
|
+
// Keep a queue of locally emitted draft values so older controlled echoes
|
|
525
|
+
// cannot clobber a newer in-flight paste or mid-buffer edit.
|
|
526
|
+
const pendingControlledValuesRef = React.useRef([]);
|
|
527
|
+
const onChangeRef = React.useRef(props.onChange);
|
|
528
|
+
const onEditLastQueuedPromptRef = React.useRef(props.onEditLastQueuedPrompt);
|
|
529
|
+
const onSubmitRef = React.useRef(props.onSubmit);
|
|
530
|
+
const disabledRef = React.useRef(props.disabled);
|
|
531
|
+
onChangeRef.current = props.onChange;
|
|
532
|
+
onEditLastQueuedPromptRef.current = props.onEditLastQueuedPrompt;
|
|
533
|
+
onSubmitRef.current = props.onSubmit;
|
|
534
|
+
disabledRef.current = props.disabled;
|
|
535
|
+
React.useLayoutEffect(() => {
|
|
536
|
+
const syncResult = reconcileComposerControlledValue({
|
|
537
|
+
cursorOffset: cursorOffsetRef.current,
|
|
538
|
+
currentValue: valueRef.current,
|
|
539
|
+
nextControlledValue: props.value,
|
|
540
|
+
pendingValues: pendingControlledValuesRef.current,
|
|
541
|
+
previousControlledValue: lastPropValueRef.current,
|
|
542
|
+
});
|
|
543
|
+
lastPropValueRef.current = props.value;
|
|
544
|
+
pendingControlledValuesRef.current = syncResult.pendingValues;
|
|
545
|
+
valueRef.current = syncResult.nextValue;
|
|
546
|
+
setDisplayValue((previous) => previous === syncResult.nextValue ? previous : syncResult.nextValue);
|
|
547
|
+
if (syncResult.cursorOffset !== cursorOffsetRef.current) {
|
|
548
|
+
cursorOffsetRef.current = syncResult.cursorOffset;
|
|
549
|
+
setCursorOffset(syncResult.cursorOffset);
|
|
550
|
+
}
|
|
551
|
+
}, [props.value]);
|
|
552
|
+
const handleComposerInput = React.useCallback((input, key) => {
|
|
553
|
+
if (disabledRef.current) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if ((key.shift && key.tab) || (key.ctrl && input === 'c')) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const currentValue = valueRef.current;
|
|
560
|
+
const currentCursorOffset = cursorOffsetRef.current;
|
|
561
|
+
const action = resolveComposerTerminalAction(input, key);
|
|
562
|
+
if (action.kind === 'edit-last-queued') {
|
|
563
|
+
onEditLastQueuedPromptRef.current();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (action.kind === 'edit' &&
|
|
567
|
+
(action.key.upArrow || action.key.downArrow)) {
|
|
568
|
+
const verticalMovement = resolveComposerVerticalCursorMove({
|
|
569
|
+
cursorOffset: currentCursorOffset,
|
|
570
|
+
direction: action.key.upArrow ? 'up' : 'down',
|
|
571
|
+
preferredColumn: preferredColumnRef.current,
|
|
572
|
+
value: currentValue,
|
|
573
|
+
});
|
|
574
|
+
if (verticalMovement.cursorOffset !== currentCursorOffset) {
|
|
575
|
+
cursorOffsetRef.current = verticalMovement.cursorOffset;
|
|
576
|
+
preferredColumnRef.current = verticalMovement.preferredColumn;
|
|
577
|
+
setCursorOffset(verticalMovement.cursorOffset);
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (action.kind === 'submit') {
|
|
582
|
+
if (onSubmitRef.current(currentValue, action.mode) === 'clear') {
|
|
583
|
+
valueRef.current = '';
|
|
584
|
+
pendingControlledValuesRef.current = enqueuePendingComposerValue(pendingControlledValuesRef.current, '');
|
|
585
|
+
cursorOffsetRef.current = 0;
|
|
586
|
+
killBufferRef.current = '';
|
|
587
|
+
preferredColumnRef.current = null;
|
|
588
|
+
setDisplayValue('');
|
|
589
|
+
setCursorOffset(0);
|
|
590
|
+
onChangeRef.current('');
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (action.kind !== 'edit') {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const editingResult = applyComposerEditingInput({
|
|
598
|
+
cursorOffset: currentCursorOffset,
|
|
599
|
+
killBuffer: killBufferRef.current,
|
|
600
|
+
value: currentValue,
|
|
601
|
+
}, action.input, action.key);
|
|
602
|
+
if (!editingResult.handled) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
cursorOffsetRef.current = editingResult.cursorOffset;
|
|
606
|
+
killBufferRef.current = editingResult.killBuffer;
|
|
607
|
+
valueRef.current = editingResult.value;
|
|
608
|
+
preferredColumnRef.current = null;
|
|
609
|
+
if (editingResult.cursorOffset !== currentCursorOffset) {
|
|
610
|
+
setCursorOffset(editingResult.cursorOffset);
|
|
611
|
+
}
|
|
612
|
+
if (editingResult.value !== currentValue) {
|
|
613
|
+
pendingControlledValuesRef.current = enqueuePendingComposerValue(pendingControlledValuesRef.current, editingResult.value);
|
|
614
|
+
setDisplayValue(editingResult.value);
|
|
615
|
+
onChangeRef.current(editingResult.value);
|
|
616
|
+
}
|
|
617
|
+
}, []);
|
|
618
|
+
useInput(handleComposerInput, {
|
|
619
|
+
isActive: !props.disabled,
|
|
620
|
+
});
|
|
621
|
+
return createElement(Box, {
|
|
622
|
+
flexDirection: 'column',
|
|
623
|
+
flexGrow: 1,
|
|
624
|
+
flexShrink: 1,
|
|
625
|
+
}, renderComposerValue({
|
|
626
|
+
cursorOffset,
|
|
627
|
+
disabled: props.disabled,
|
|
628
|
+
placeholder: props.placeholder,
|
|
629
|
+
theme,
|
|
630
|
+
value: displayValue,
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
const ChatHeader = React.memo(function ChatHeader(props) {
|
|
634
|
+
const createElement = React.createElement;
|
|
635
|
+
const theme = useAssistantInkTheme();
|
|
636
|
+
const terminalColumns = process.stderr.columns ?? 80;
|
|
637
|
+
const terminalRows = process.stderr.rows ?? 24;
|
|
638
|
+
const compactHeader = terminalColumns < 72 || terminalRows < 18;
|
|
639
|
+
if (compactHeader) {
|
|
640
|
+
return createElement(ChromePanel, {
|
|
641
|
+
backgroundColor: theme.switcherBackground,
|
|
642
|
+
marginBottom: 1,
|
|
643
|
+
}, createElement(Text, {
|
|
644
|
+
wrap: 'wrap',
|
|
645
|
+
}, createElement(Text, { color: theme.accentColor }, '●'), ' ', createElement(Text, { bold: true }, 'Murph')), props.bindingSummary
|
|
646
|
+
? createElement(Text, {
|
|
647
|
+
color: theme.mutedColor,
|
|
648
|
+
wrap: 'wrap',
|
|
649
|
+
}, props.bindingSummary)
|
|
650
|
+
: null);
|
|
651
|
+
}
|
|
652
|
+
return createElement(Box, {
|
|
653
|
+
flexDirection: 'column',
|
|
654
|
+
marginBottom: 1,
|
|
655
|
+
width: '100%',
|
|
656
|
+
}, createElement(ChromePanel, {
|
|
657
|
+
backgroundColor: theme.switcherBackground,
|
|
658
|
+
marginBottom: 1,
|
|
659
|
+
}, createElement(Text, {
|
|
660
|
+
wrap: 'wrap',
|
|
661
|
+
}, createElement(Text, { color: theme.accentColor }, '●'), ' ', createElement(Text, { bold: true }, 'Murph'), ' ', createElement(Text, { color: theme.mutedColor }, 'interactive chat'))), createElement(ChromePanel, {
|
|
662
|
+
backgroundColor: theme.switcherBackground,
|
|
663
|
+
marginBottom: 0,
|
|
664
|
+
}, createElement(Text, {
|
|
665
|
+
color: theme.mutedColor,
|
|
666
|
+
wrap: 'wrap',
|
|
667
|
+
}, createElement(Text, { color: theme.accentColor }, '↳'), ` ${props.bindingSummary ?? 'local transcript-backed session'}`)));
|
|
668
|
+
});
|
|
669
|
+
const ChatEntryRow = React.memo(function ChatEntryRow(props) {
|
|
670
|
+
const createElement = React.createElement;
|
|
671
|
+
const theme = useAssistantInkTheme();
|
|
672
|
+
const { stdout } = useStdout();
|
|
673
|
+
const rowWidth = resolveAssistantChatViewportWidth(stdout?.columns);
|
|
674
|
+
if (props.entry.kind === 'assistant') {
|
|
675
|
+
return createElement(ChromePanel, {
|
|
676
|
+
marginBottom: 1,
|
|
677
|
+
width: rowWidth,
|
|
678
|
+
}, createElement(AssistantMessageText, { text: props.entry.text }));
|
|
679
|
+
}
|
|
680
|
+
if (props.entry.kind === 'error') {
|
|
681
|
+
return createElement(ChromePanel, {
|
|
682
|
+
backgroundColor: theme.switcherBackground,
|
|
683
|
+
marginBottom: 1,
|
|
684
|
+
width: rowWidth,
|
|
685
|
+
}, createElement(MessageRoleLabel, {
|
|
686
|
+
kind: 'error',
|
|
687
|
+
}), createElement(WrappedTextBlock, {
|
|
688
|
+
color: theme.errorColor,
|
|
689
|
+
}, props.entry.text));
|
|
690
|
+
}
|
|
691
|
+
if (props.entry.kind === 'trace') {
|
|
692
|
+
return createElement(Box, {
|
|
693
|
+
marginBottom: 1,
|
|
694
|
+
paddingLeft: 2,
|
|
695
|
+
width: rowWidth,
|
|
696
|
+
}, createElement(WrappedPlainTextBlock, {
|
|
697
|
+
columns: Math.max(1, rowWidth - 2),
|
|
698
|
+
dimColor: true,
|
|
699
|
+
text: `${props.entry.pending ? '· ' : ' '}${props.entry.text}`,
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
if (props.entry.kind === 'thinking' || props.entry.kind === 'status') {
|
|
703
|
+
return createElement(Box, {
|
|
704
|
+
marginBottom: 1,
|
|
705
|
+
width: rowWidth,
|
|
706
|
+
}, createElement(Box, {
|
|
707
|
+
flexDirection: 'row',
|
|
708
|
+
width: rowWidth,
|
|
709
|
+
}, createElement(Text, { dimColor: true }, props.entry.kind === 'thinking' ? '· ' : '↻ '), createElement(Box, {
|
|
710
|
+
flexDirection: 'column',
|
|
711
|
+
flexGrow: 1,
|
|
712
|
+
flexShrink: 1,
|
|
713
|
+
}, createElement(WrappedTextBlock, {
|
|
714
|
+
dimColor: true,
|
|
715
|
+
}, props.entry.text))));
|
|
716
|
+
}
|
|
717
|
+
return createElement(ChromePanel, {
|
|
718
|
+
backgroundColor: theme.composerBackground,
|
|
719
|
+
marginBottom: 1,
|
|
720
|
+
paddingY: 1,
|
|
721
|
+
width: rowWidth,
|
|
722
|
+
}, createElement(Box, {
|
|
723
|
+
flexDirection: 'row',
|
|
724
|
+
width: '100%',
|
|
725
|
+
}, createElement(Text, {
|
|
726
|
+
color: theme.composerTextColor,
|
|
727
|
+
}, '› '), createElement(Box, {
|
|
728
|
+
flexDirection: 'column',
|
|
729
|
+
flexGrow: 1,
|
|
730
|
+
flexShrink: 1,
|
|
731
|
+
}, createElement(Text, {
|
|
732
|
+
color: theme.composerTextColor,
|
|
733
|
+
wrap: 'wrap',
|
|
734
|
+
}, props.entry.text))));
|
|
735
|
+
});
|
|
736
|
+
const AssistantMessageText = React.memo(function AssistantMessageText(props) {
|
|
737
|
+
return renderAssistantMessageText(props);
|
|
738
|
+
});
|
|
739
|
+
export function renderChatTranscriptFeed(input) {
|
|
740
|
+
const createElement = React.createElement;
|
|
741
|
+
const { liveEntries, staticEntries } = partitionChatTranscriptEntries({
|
|
742
|
+
busy: input.busy,
|
|
743
|
+
entries: input.entries,
|
|
744
|
+
});
|
|
745
|
+
const staticRows = [
|
|
746
|
+
{
|
|
747
|
+
kind: 'header',
|
|
748
|
+
bindingSummary: input.bindingSummary,
|
|
749
|
+
sessionId: input.sessionId,
|
|
750
|
+
},
|
|
751
|
+
...staticEntries.map((entry) => ({
|
|
752
|
+
kind: 'entry',
|
|
753
|
+
entry,
|
|
754
|
+
})),
|
|
755
|
+
];
|
|
756
|
+
return createElement(React.Fragment, {}, createElement(StaticTranscript, {
|
|
757
|
+
items: staticRows,
|
|
758
|
+
children: renderStaticTranscriptRow,
|
|
759
|
+
}), createElement(Box, {
|
|
760
|
+
flexDirection: 'column',
|
|
761
|
+
width: '100%',
|
|
762
|
+
}, ...liveEntries.map((entry, index) => createElement(ChatEntryRow, {
|
|
763
|
+
key: `live-entry:${staticEntries.length + index}`,
|
|
764
|
+
entry,
|
|
765
|
+
}))));
|
|
766
|
+
}
|
|
767
|
+
export function partitionChatTranscriptEntries(input) {
|
|
768
|
+
if (input.entries.length === 0) {
|
|
769
|
+
return {
|
|
770
|
+
liveEntries: [],
|
|
771
|
+
staticEntries: [],
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
if (!input.busy) {
|
|
775
|
+
return {
|
|
776
|
+
liveEntries: [],
|
|
777
|
+
staticEntries: [...input.entries],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
let lastUserEntryIndex = -1;
|
|
781
|
+
for (let index = input.entries.length - 1; index >= 0; index -= 1) {
|
|
782
|
+
const entry = input.entries[index];
|
|
783
|
+
if (entry?.kind === 'user') {
|
|
784
|
+
lastUserEntryIndex = index;
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (lastUserEntryIndex < 0) {
|
|
789
|
+
return {
|
|
790
|
+
liveEntries: [...input.entries],
|
|
791
|
+
staticEntries: [],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
liveEntries: input.entries.slice(lastUserEntryIndex + 1),
|
|
796
|
+
staticEntries: input.entries.slice(0, lastUserEntryIndex + 1),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
export function shouldShowBusyStatus(input) {
|
|
800
|
+
if (!input.busy) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
let lastUserEntryIndex = -1;
|
|
804
|
+
for (let index = input.entries.length - 1; index >= 0; index -= 1) {
|
|
805
|
+
if (input.entries[index]?.kind === 'user') {
|
|
806
|
+
lastUserEntryIndex = index;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (lastUserEntryIndex < 0) {
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
for (let index = lastUserEntryIndex + 1; index < input.entries.length; index += 1) {
|
|
814
|
+
const entry = input.entries[index];
|
|
815
|
+
if (entry?.kind === 'assistant' && normalizeNullableString(entry.text)) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
if (entry?.kind === 'error' && normalizeNullableString(entry.text)) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
function renderStaticTranscriptRow(item, index) {
|
|
825
|
+
const createElement = React.createElement;
|
|
826
|
+
if (item.kind === 'header') {
|
|
827
|
+
return createElement(ChatHeader, {
|
|
828
|
+
key: `static-header:${item.sessionId}`,
|
|
829
|
+
bindingSummary: item.bindingSummary,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
return createElement(ChatEntryRow, {
|
|
833
|
+
key: `static-entry:${index}`,
|
|
834
|
+
entry: item.entry,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
const ChatTranscriptFeed = React.memo(function ChatTranscriptFeed(input) {
|
|
838
|
+
return renderChatTranscriptFeed(input);
|
|
839
|
+
});
|
|
840
|
+
const ChatStatus = React.memo(function ChatStatus(props) {
|
|
841
|
+
const createElement = React.createElement;
|
|
842
|
+
const theme = useAssistantInkTheme();
|
|
843
|
+
const { stdout } = useStdout();
|
|
844
|
+
const wrapColumns = resolveAssistantPlainTextWrapColumns(stdout?.columns);
|
|
845
|
+
if (props.busy) {
|
|
846
|
+
const busyColor = props.status?.kind === 'error'
|
|
847
|
+
? theme.errorColor
|
|
848
|
+
: props.status?.kind === 'success'
|
|
849
|
+
? theme.successColor
|
|
850
|
+
: theme.infoColor;
|
|
851
|
+
const busyDetail = normalizeNullableString(props.status?.text);
|
|
852
|
+
const busyLabel = 'Working';
|
|
853
|
+
return createElement(ChromePanel, {
|
|
854
|
+
marginBottom: 1,
|
|
855
|
+
}, createElement(Box, {
|
|
856
|
+
flexDirection: 'row',
|
|
857
|
+
width: '100%',
|
|
858
|
+
}, createElement(BusySpinner, {
|
|
859
|
+
color: busyColor,
|
|
860
|
+
}), createElement(Text, {}, ' '), createElement(Box, {
|
|
861
|
+
flexDirection: 'column',
|
|
862
|
+
flexGrow: 1,
|
|
863
|
+
flexShrink: 1,
|
|
864
|
+
}, createElement(Text, {
|
|
865
|
+
color: busyColor,
|
|
866
|
+
}, busyLabel), busyDetail
|
|
867
|
+
? createElement(WrappedPlainTextBlock, {
|
|
868
|
+
color: theme.mutedColor,
|
|
869
|
+
columns: wrapColumns,
|
|
870
|
+
text: busyDetail,
|
|
871
|
+
})
|
|
872
|
+
: null)));
|
|
873
|
+
}
|
|
874
|
+
if (!props.status) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
const statusColor = props.status.kind === 'error'
|
|
878
|
+
? theme.errorColor
|
|
879
|
+
: props.status.kind === 'success'
|
|
880
|
+
? theme.successColor
|
|
881
|
+
: theme.infoColor;
|
|
882
|
+
const statusIcon = props.status.kind === 'error'
|
|
883
|
+
? '!'
|
|
884
|
+
: props.status.kind === 'success'
|
|
885
|
+
? '✓'
|
|
886
|
+
: 'ℹ';
|
|
887
|
+
return createElement(ChromePanel, {
|
|
888
|
+
backgroundColor: theme.switcherBackground,
|
|
889
|
+
marginBottom: 1,
|
|
890
|
+
}, createElement(Box, {
|
|
891
|
+
flexDirection: 'row',
|
|
892
|
+
width: '100%',
|
|
893
|
+
}, createElement(Text, { color: statusColor }, `${statusIcon} `), createElement(Box, {
|
|
894
|
+
flexDirection: 'column',
|
|
895
|
+
flexGrow: 1,
|
|
896
|
+
flexShrink: 1,
|
|
897
|
+
}, createElement(WrappedPlainTextBlock, {
|
|
898
|
+
color: props.status.kind === 'info'
|
|
899
|
+
? theme.composerTextColor
|
|
900
|
+
: statusColor,
|
|
901
|
+
columns: Math.max(1, wrapColumns - 2),
|
|
902
|
+
text: props.status.text,
|
|
903
|
+
}))));
|
|
904
|
+
});
|
|
905
|
+
const ChatComposer = React.memo(function ChatComposer(props) {
|
|
906
|
+
const createElement = React.createElement;
|
|
907
|
+
const theme = useAssistantInkTheme();
|
|
908
|
+
const { stdout } = useStdout();
|
|
909
|
+
const slashSuggestions = props.modelSwitcherActive
|
|
910
|
+
? []
|
|
911
|
+
: getMatchingSlashCommands(props.value);
|
|
912
|
+
const showComposerGuidance = shouldShowChatComposerGuidance(props.entryCount);
|
|
913
|
+
const showStarterSuggestions = showComposerGuidance &&
|
|
914
|
+
!props.modelSwitcherActive &&
|
|
915
|
+
props.value.trim().length === 0;
|
|
916
|
+
const wrapColumns = resolveAssistantPlainTextWrapColumns(stdout?.columns);
|
|
917
|
+
return createElement(React.Fragment, {}, createElement(ChromePanel, {
|
|
918
|
+
backgroundColor: theme.composerBackground,
|
|
919
|
+
marginBottom: slashSuggestions.length > 0 ? 0 : 1,
|
|
920
|
+
paddingY: 1,
|
|
921
|
+
}, createElement(Box, {
|
|
922
|
+
flexDirection: 'row',
|
|
923
|
+
width: '100%',
|
|
924
|
+
}, createElement(Text, { color: theme.composerTextColor }, '› '), createElement(ComposerInput, {
|
|
925
|
+
disabled: props.modelSwitcherActive,
|
|
926
|
+
value: props.value,
|
|
927
|
+
placeholder: 'Type a message',
|
|
928
|
+
onChange: props.onChange,
|
|
929
|
+
onEditLastQueuedPrompt: props.onEditLastQueuedPrompt,
|
|
930
|
+
onSubmit: props.onSubmit,
|
|
931
|
+
})), showComposerGuidance
|
|
932
|
+
? createElement(Box, {
|
|
933
|
+
marginTop: 1,
|
|
934
|
+
}, createElement(Text, {
|
|
935
|
+
color: theme.mutedColor,
|
|
936
|
+
wrap: 'wrap',
|
|
937
|
+
}, CHAT_COMPOSER_HINT))
|
|
938
|
+
: null, showStarterSuggestions
|
|
939
|
+
? createElement(Box, {
|
|
940
|
+
marginTop: 1,
|
|
941
|
+
width: '100%',
|
|
942
|
+
}, createElement(Text, { color: theme.mutedColor }, 'try:'), createElement(WrappedPlainTextBlock, {
|
|
943
|
+
color: theme.accentColor,
|
|
944
|
+
columns: Math.max(1, wrapColumns - 2),
|
|
945
|
+
text: ` ${CHAT_STARTER_SUGGESTIONS.join(' · ')}`,
|
|
946
|
+
}))
|
|
947
|
+
: null), createElement(SlashCommandSuggestions, {
|
|
948
|
+
commands: slashSuggestions,
|
|
949
|
+
}));
|
|
950
|
+
});
|
|
951
|
+
const QueuedFollowUpStatus = React.memo(function QueuedFollowUpStatus(props) {
|
|
952
|
+
const createElement = React.createElement;
|
|
953
|
+
const theme = useAssistantInkTheme();
|
|
954
|
+
const { stdout } = useStdout();
|
|
955
|
+
if (props.queuedPromptCount === 0 || !props.latestPrompt) {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
const extraQueuedCount = props.queuedPromptCount - 1;
|
|
959
|
+
const wrapColumns = resolveAssistantPlainTextWrapColumns(stdout?.columns);
|
|
960
|
+
return createElement(Box, {
|
|
961
|
+
flexDirection: 'column',
|
|
962
|
+
marginBottom: 1,
|
|
963
|
+
width: '100%',
|
|
964
|
+
}, createElement(Text, {
|
|
965
|
+
color: theme.composerTextColor,
|
|
966
|
+
wrap: 'wrap',
|
|
967
|
+
}, '• Queued follow-up messages'), createElement(WrappedPlainTextBlock, {
|
|
968
|
+
color: theme.composerTextColor,
|
|
969
|
+
columns: wrapColumns,
|
|
970
|
+
text: ` ↳ ${formatQueuedFollowUpPreview(props.latestPrompt)}`,
|
|
971
|
+
}), extraQueuedCount > 0
|
|
972
|
+
? createElement(WrappedPlainTextBlock, {
|
|
973
|
+
color: theme.mutedColor,
|
|
974
|
+
columns: wrapColumns,
|
|
975
|
+
text: ` +${extraQueuedCount} more queued`,
|
|
976
|
+
})
|
|
977
|
+
: null, createElement(WrappedPlainTextBlock, {
|
|
978
|
+
color: theme.mutedColor,
|
|
979
|
+
columns: wrapColumns,
|
|
980
|
+
text: ` ${QUEUED_FOLLOW_UP_SHORTCUT_HINT}`,
|
|
981
|
+
}));
|
|
982
|
+
});
|
|
983
|
+
const ChatFooter = React.memo(function ChatFooter(props) {
|
|
984
|
+
const createElement = React.createElement;
|
|
985
|
+
return createElement(Box, {
|
|
986
|
+
flexDirection: 'column',
|
|
987
|
+
width: '100%',
|
|
988
|
+
}, createElement(Text, {
|
|
989
|
+
wrap: 'wrap',
|
|
990
|
+
}, ...props.badges.flatMap((badge, index) => [
|
|
991
|
+
index > 0 ? ' ' : '',
|
|
992
|
+
createElement(FooterBadge, {
|
|
993
|
+
badge,
|
|
994
|
+
key: `badge:${badge.key}`,
|
|
995
|
+
}),
|
|
996
|
+
])));
|
|
997
|
+
});
|
|
998
|
+
function enqueuePendingComposerValue(pendingValues, nextValue) {
|
|
999
|
+
return pendingValues[pendingValues.length - 1] === nextValue
|
|
1000
|
+
? [...pendingValues]
|
|
1001
|
+
: [...pendingValues, nextValue];
|
|
1002
|
+
}
|
|
1003
|
+
export function reconcileComposerControlledValue(input) {
|
|
1004
|
+
// Controlled updates that match a queued local value are only acknowledgements
|
|
1005
|
+
// from the parent state, so keep the newest local draft visible until the last
|
|
1006
|
+
// pending value is observed. Anything else is an external restore/reset and
|
|
1007
|
+
// should replace the live draft immediately.
|
|
1008
|
+
const nextControlledValue = input.nextControlledValue;
|
|
1009
|
+
const currentValue = input.currentValue;
|
|
1010
|
+
const clampedCursorOffset = clampComposerCursorOffset(input.cursorOffset, currentValue.length);
|
|
1011
|
+
if (nextControlledValue === input.previousControlledValue) {
|
|
1012
|
+
return {
|
|
1013
|
+
cursorOffset: clampedCursorOffset,
|
|
1014
|
+
nextValue: currentValue,
|
|
1015
|
+
pendingValues: [...input.pendingValues],
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
const matchedPendingIndex = input.pendingValues.indexOf(nextControlledValue);
|
|
1019
|
+
if (matchedPendingIndex >= 0) {
|
|
1020
|
+
const remainingPendingValues = input.pendingValues.slice(matchedPendingIndex + 1);
|
|
1021
|
+
const nextValue = remainingPendingValues.length === 0 ? nextControlledValue : currentValue;
|
|
1022
|
+
return {
|
|
1023
|
+
cursorOffset: clampComposerCursorOffset(clampedCursorOffset, nextValue.length),
|
|
1024
|
+
nextValue,
|
|
1025
|
+
pendingValues: remainingPendingValues,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
cursorOffset: nextControlledValue.length,
|
|
1030
|
+
nextValue: nextControlledValue,
|
|
1031
|
+
pendingValues: [],
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function clampComposerCursorOffset(offset, valueLength) {
|
|
1035
|
+
return Math.max(0, Math.min(offset, valueLength));
|
|
1036
|
+
}
|
|
1037
|
+
function isComposerWordSeparator(character) {
|
|
1038
|
+
return COMPOSER_WORD_SEPARATORS.includes(character);
|
|
1039
|
+
}
|
|
1040
|
+
function isComposerWhitespace(character) {
|
|
1041
|
+
return /\s/u.test(character);
|
|
1042
|
+
}
|
|
1043
|
+
function moveComposerCursorLeft(state) {
|
|
1044
|
+
return {
|
|
1045
|
+
...state,
|
|
1046
|
+
cursorOffset: clampComposerCursorOffset(state.cursorOffset - 1, state.value.length),
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function moveComposerCursorRight(state) {
|
|
1050
|
+
return {
|
|
1051
|
+
...state,
|
|
1052
|
+
cursorOffset: clampComposerCursorOffset(state.cursorOffset + 1, state.value.length),
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function moveComposerCursorToStart(state) {
|
|
1056
|
+
return {
|
|
1057
|
+
...state,
|
|
1058
|
+
cursorOffset: 0,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
function moveComposerCursorToEnd(state) {
|
|
1062
|
+
return {
|
|
1063
|
+
...state,
|
|
1064
|
+
cursorOffset: state.value.length,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function resolveComposerLineRanges(value) {
|
|
1068
|
+
const ranges = [];
|
|
1069
|
+
let lineStart = 0;
|
|
1070
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1071
|
+
if (value[index] !== '\n') {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
ranges.push({
|
|
1075
|
+
end: index,
|
|
1076
|
+
start: lineStart,
|
|
1077
|
+
});
|
|
1078
|
+
lineStart = index + 1;
|
|
1079
|
+
}
|
|
1080
|
+
ranges.push({
|
|
1081
|
+
end: value.length,
|
|
1082
|
+
start: lineStart,
|
|
1083
|
+
});
|
|
1084
|
+
return ranges;
|
|
1085
|
+
}
|
|
1086
|
+
function resolveComposerCursorLocation(value, cursorOffset) {
|
|
1087
|
+
const clampedCursorOffset = clampComposerCursorOffset(cursorOffset, value.length);
|
|
1088
|
+
let lineIndex = 0;
|
|
1089
|
+
let lineStart = 0;
|
|
1090
|
+
for (let index = 0; index < clampedCursorOffset; index += 1) {
|
|
1091
|
+
if (value[index] !== '\n') {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
lineIndex += 1;
|
|
1095
|
+
lineStart = index + 1;
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
column: clampedCursorOffset - lineStart,
|
|
1099
|
+
lineIndex,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
export function resolveComposerVerticalCursorMove(input) {
|
|
1103
|
+
const clampedCursorOffset = clampComposerCursorOffset(input.cursorOffset, input.value.length);
|
|
1104
|
+
const lineRanges = resolveComposerLineRanges(input.value);
|
|
1105
|
+
const currentLocation = resolveComposerCursorLocation(input.value, clampedCursorOffset);
|
|
1106
|
+
const targetLineIndex = input.direction === 'up'
|
|
1107
|
+
? currentLocation.lineIndex - 1
|
|
1108
|
+
: currentLocation.lineIndex + 1;
|
|
1109
|
+
if (targetLineIndex < 0 || targetLineIndex >= lineRanges.length) {
|
|
1110
|
+
return {
|
|
1111
|
+
cursorOffset: clampedCursorOffset,
|
|
1112
|
+
preferredColumn: input.preferredColumn,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
const desiredColumn = input.preferredColumn ?? currentLocation.column;
|
|
1116
|
+
const targetLine = lineRanges[targetLineIndex];
|
|
1117
|
+
if (!targetLine) {
|
|
1118
|
+
return {
|
|
1119
|
+
cursorOffset: clampedCursorOffset,
|
|
1120
|
+
preferredColumn: input.preferredColumn,
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
cursorOffset: targetLine.start + Math.min(desiredColumn, targetLine.end - targetLine.start),
|
|
1125
|
+
preferredColumn: desiredColumn,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function findComposerPreviousWordStart(value, cursorOffset) {
|
|
1129
|
+
let index = clampComposerCursorOffset(cursorOffset, value.length);
|
|
1130
|
+
while (index > 0) {
|
|
1131
|
+
const previousCharacter = value.slice(index - 1, index);
|
|
1132
|
+
if (!isComposerWhitespace(previousCharacter)) {
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
index -= 1;
|
|
1136
|
+
}
|
|
1137
|
+
if (index === 0) {
|
|
1138
|
+
return 0;
|
|
1139
|
+
}
|
|
1140
|
+
const previousCharacter = value.slice(index - 1, index);
|
|
1141
|
+
const separator = isComposerWordSeparator(previousCharacter);
|
|
1142
|
+
while (index > 0) {
|
|
1143
|
+
const character = value.slice(index - 1, index);
|
|
1144
|
+
if (isComposerWhitespace(character) ||
|
|
1145
|
+
isComposerWordSeparator(character) !== separator) {
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
index -= 1;
|
|
1149
|
+
}
|
|
1150
|
+
return index;
|
|
1151
|
+
}
|
|
1152
|
+
function findComposerNextWordEnd(value, cursorOffset) {
|
|
1153
|
+
let index = clampComposerCursorOffset(cursorOffset, value.length);
|
|
1154
|
+
while (index < value.length) {
|
|
1155
|
+
const character = value.slice(index, index + 1);
|
|
1156
|
+
if (!isComposerWhitespace(character)) {
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
index += 1;
|
|
1160
|
+
}
|
|
1161
|
+
if (index >= value.length) {
|
|
1162
|
+
return value.length;
|
|
1163
|
+
}
|
|
1164
|
+
const separator = isComposerWordSeparator(value.slice(index, index + 1));
|
|
1165
|
+
while (index < value.length) {
|
|
1166
|
+
const character = value.slice(index, index + 1);
|
|
1167
|
+
if (isComposerWhitespace(character) ||
|
|
1168
|
+
isComposerWordSeparator(character) !== separator) {
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
index += 1;
|
|
1172
|
+
}
|
|
1173
|
+
return index;
|
|
1174
|
+
}
|
|
1175
|
+
function moveComposerCursorToPreviousWord(state) {
|
|
1176
|
+
return {
|
|
1177
|
+
...state,
|
|
1178
|
+
cursorOffset: findComposerPreviousWordStart(state.value, state.cursorOffset),
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function moveComposerCursorToNextWord(state) {
|
|
1182
|
+
return {
|
|
1183
|
+
...state,
|
|
1184
|
+
cursorOffset: findComposerNextWordEnd(state.value, state.cursorOffset),
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function replaceComposerRange(state, range, replacement) {
|
|
1188
|
+
const nextValue = state.value.slice(0, range.start) + replacement + state.value.slice(range.end);
|
|
1189
|
+
return {
|
|
1190
|
+
...state,
|
|
1191
|
+
cursorOffset: range.start + replacement.length,
|
|
1192
|
+
value: nextValue,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function killComposerRange(state, range) {
|
|
1196
|
+
if (range.end <= range.start) {
|
|
1197
|
+
return state;
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
...replaceComposerRange(state, range, ''),
|
|
1201
|
+
killBuffer: state.value.slice(range.start, range.end),
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function deleteComposerBackward(state) {
|
|
1205
|
+
if (state.cursorOffset <= 0) {
|
|
1206
|
+
return state;
|
|
1207
|
+
}
|
|
1208
|
+
return replaceComposerRange(state, {
|
|
1209
|
+
end: state.cursorOffset,
|
|
1210
|
+
start: state.cursorOffset - 1,
|
|
1211
|
+
}, '');
|
|
1212
|
+
}
|
|
1213
|
+
function deleteComposerForward(state) {
|
|
1214
|
+
if (state.cursorOffset >= state.value.length) {
|
|
1215
|
+
return state;
|
|
1216
|
+
}
|
|
1217
|
+
return replaceComposerRange(state, {
|
|
1218
|
+
end: state.cursorOffset + 1,
|
|
1219
|
+
start: state.cursorOffset,
|
|
1220
|
+
}, '');
|
|
1221
|
+
}
|
|
1222
|
+
function deleteComposerBackwardWord(state) {
|
|
1223
|
+
return killComposerRange(state, {
|
|
1224
|
+
end: state.cursorOffset,
|
|
1225
|
+
start: findComposerPreviousWordStart(state.value, state.cursorOffset),
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
function deleteComposerForwardWord(state) {
|
|
1229
|
+
return killComposerRange(state, {
|
|
1230
|
+
end: findComposerNextWordEnd(state.value, state.cursorOffset),
|
|
1231
|
+
start: state.cursorOffset,
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
function killComposerToStart(state) {
|
|
1235
|
+
return killComposerRange(state, {
|
|
1236
|
+
end: state.cursorOffset,
|
|
1237
|
+
start: 0,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function killComposerToEnd(state) {
|
|
1241
|
+
return killComposerRange(state, {
|
|
1242
|
+
end: state.value.length,
|
|
1243
|
+
start: state.cursorOffset,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
function yankComposerKillBuffer(state) {
|
|
1247
|
+
if (state.killBuffer.length === 0) {
|
|
1248
|
+
return state;
|
|
1249
|
+
}
|
|
1250
|
+
return replaceComposerRange(state, {
|
|
1251
|
+
end: state.cursorOffset,
|
|
1252
|
+
start: state.cursorOffset,
|
|
1253
|
+
}, state.killBuffer);
|
|
1254
|
+
}
|
|
1255
|
+
function finalizeComposerEditingResult(next) {
|
|
1256
|
+
return {
|
|
1257
|
+
...next,
|
|
1258
|
+
handled: true,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
export function applyComposerEditingInput(state, input, key) {
|
|
1262
|
+
const currentState = {
|
|
1263
|
+
...state,
|
|
1264
|
+
cursorOffset: clampComposerCursorOffset(state.cursorOffset, state.value.length),
|
|
1265
|
+
};
|
|
1266
|
+
if (key.home || (key.super && key.leftArrow)) {
|
|
1267
|
+
return finalizeComposerEditingResult(moveComposerCursorToStart(currentState));
|
|
1268
|
+
}
|
|
1269
|
+
if (key.end || (key.super && key.rightArrow)) {
|
|
1270
|
+
return finalizeComposerEditingResult(moveComposerCursorToEnd(currentState));
|
|
1271
|
+
}
|
|
1272
|
+
if (key.leftArrow) {
|
|
1273
|
+
return finalizeComposerEditingResult(key.meta || key.ctrl
|
|
1274
|
+
? moveComposerCursorToPreviousWord(currentState)
|
|
1275
|
+
: moveComposerCursorLeft(currentState));
|
|
1276
|
+
}
|
|
1277
|
+
if (key.rightArrow) {
|
|
1278
|
+
return finalizeComposerEditingResult(key.meta || key.ctrl
|
|
1279
|
+
? moveComposerCursorToNextWord(currentState)
|
|
1280
|
+
: moveComposerCursorRight(currentState));
|
|
1281
|
+
}
|
|
1282
|
+
if (key.backspace) {
|
|
1283
|
+
return finalizeComposerEditingResult(key.super
|
|
1284
|
+
? killComposerToStart(currentState)
|
|
1285
|
+
: key.meta
|
|
1286
|
+
? deleteComposerBackwardWord(currentState)
|
|
1287
|
+
: deleteComposerBackward(currentState));
|
|
1288
|
+
}
|
|
1289
|
+
if (key.delete) {
|
|
1290
|
+
return finalizeComposerEditingResult(key.super
|
|
1291
|
+
? killComposerToEnd(currentState)
|
|
1292
|
+
: key.meta
|
|
1293
|
+
? deleteComposerForwardWord(currentState)
|
|
1294
|
+
: deleteComposerForward(currentState));
|
|
1295
|
+
}
|
|
1296
|
+
if (key.ctrl) {
|
|
1297
|
+
switch (input) {
|
|
1298
|
+
case 'a':
|
|
1299
|
+
return finalizeComposerEditingResult(moveComposerCursorToStart(currentState));
|
|
1300
|
+
case 'b':
|
|
1301
|
+
return finalizeComposerEditingResult(moveComposerCursorLeft(currentState));
|
|
1302
|
+
case 'd':
|
|
1303
|
+
return finalizeComposerEditingResult(deleteComposerForward(currentState));
|
|
1304
|
+
case 'e':
|
|
1305
|
+
return finalizeComposerEditingResult(moveComposerCursorToEnd(currentState));
|
|
1306
|
+
case 'f':
|
|
1307
|
+
return finalizeComposerEditingResult(moveComposerCursorRight(currentState));
|
|
1308
|
+
case 'h':
|
|
1309
|
+
return finalizeComposerEditingResult(deleteComposerBackward(currentState));
|
|
1310
|
+
case 'k':
|
|
1311
|
+
return finalizeComposerEditingResult(killComposerToEnd(currentState));
|
|
1312
|
+
case 'u':
|
|
1313
|
+
return finalizeComposerEditingResult(killComposerToStart(currentState));
|
|
1314
|
+
case 'w':
|
|
1315
|
+
return finalizeComposerEditingResult(deleteComposerBackwardWord(currentState));
|
|
1316
|
+
case 'y':
|
|
1317
|
+
return finalizeComposerEditingResult(yankComposerKillBuffer(currentState));
|
|
1318
|
+
default:
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (key.meta) {
|
|
1323
|
+
switch (input) {
|
|
1324
|
+
case 'b':
|
|
1325
|
+
return finalizeComposerEditingResult(moveComposerCursorToPreviousWord(currentState));
|
|
1326
|
+
case 'd':
|
|
1327
|
+
return finalizeComposerEditingResult(deleteComposerForwardWord(currentState));
|
|
1328
|
+
case 'f':
|
|
1329
|
+
return finalizeComposerEditingResult(moveComposerCursorToNextWord(currentState));
|
|
1330
|
+
default:
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (input.length === 0) {
|
|
1335
|
+
return {
|
|
1336
|
+
...currentState,
|
|
1337
|
+
handled: false,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const insertionText = normalizeComposerInsertedText(input);
|
|
1341
|
+
if (insertionText.length === 0) {
|
|
1342
|
+
return {
|
|
1343
|
+
...currentState,
|
|
1344
|
+
handled: false,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
return finalizeComposerEditingResult(replaceComposerRange(currentState, {
|
|
1348
|
+
end: currentState.cursorOffset,
|
|
1349
|
+
start: currentState.cursorOffset,
|
|
1350
|
+
}, insertionText));
|
|
1351
|
+
}
|
|
1352
|
+
export function normalizeComposerInsertedText(input) {
|
|
1353
|
+
return input.replace(/\r\n?/gu, '\n');
|
|
1354
|
+
}
|
|
1355
|
+
function resolveComposerCursorDisplay(input) {
|
|
1356
|
+
const cursorOffset = clampComposerCursorOffset(input.cursorOffset, input.value.length);
|
|
1357
|
+
const beforeCursor = input.value.slice(0, cursorOffset);
|
|
1358
|
+
const rawCursorCharacter = input.value.slice(cursorOffset, cursorOffset + 1);
|
|
1359
|
+
const afterCursor = cursorOffset < input.value.length
|
|
1360
|
+
? input.value.slice(cursorOffset + 1)
|
|
1361
|
+
: '';
|
|
1362
|
+
if (rawCursorCharacter === '\n') {
|
|
1363
|
+
return {
|
|
1364
|
+
afterCursor: `\n${input.value.slice(cursorOffset + 1)}`,
|
|
1365
|
+
beforeCursor,
|
|
1366
|
+
cursorCharacter: ' ',
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
if (rawCursorCharacter.length === 0) {
|
|
1370
|
+
return {
|
|
1371
|
+
afterCursor: '',
|
|
1372
|
+
beforeCursor,
|
|
1373
|
+
cursorCharacter: ' ',
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
afterCursor,
|
|
1378
|
+
beforeCursor,
|
|
1379
|
+
cursorCharacter: rawCursorCharacter,
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
export function renderComposerValue(input) {
|
|
1383
|
+
const createElement = React.createElement;
|
|
1384
|
+
if (input.value.length === 0) {
|
|
1385
|
+
if (input.disabled) {
|
|
1386
|
+
return createElement(Text, {
|
|
1387
|
+
color: input.theme.composerPlaceholderColor,
|
|
1388
|
+
wrap: 'wrap',
|
|
1389
|
+
}, input.placeholder);
|
|
1390
|
+
}
|
|
1391
|
+
const cursorCharacter = input.placeholder.slice(0, 1) || ' ';
|
|
1392
|
+
const remainder = input.placeholder.slice(1);
|
|
1393
|
+
return createElement(Text, {
|
|
1394
|
+
color: input.theme.composerPlaceholderColor,
|
|
1395
|
+
wrap: 'wrap',
|
|
1396
|
+
}, createElement(Text, {
|
|
1397
|
+
backgroundColor: input.theme.composerCursorBackground,
|
|
1398
|
+
color: input.theme.composerCursorTextColor,
|
|
1399
|
+
}, cursorCharacter), remainder);
|
|
1400
|
+
}
|
|
1401
|
+
const cursorDisplay = resolveComposerCursorDisplay({
|
|
1402
|
+
cursorOffset: input.cursorOffset,
|
|
1403
|
+
value: input.value,
|
|
1404
|
+
});
|
|
1405
|
+
if (input.disabled) {
|
|
1406
|
+
return createElement(Text, {
|
|
1407
|
+
color: input.theme.composerTextColor,
|
|
1408
|
+
wrap: 'wrap',
|
|
1409
|
+
}, input.value);
|
|
1410
|
+
}
|
|
1411
|
+
return createElement(Text, {
|
|
1412
|
+
color: input.theme.composerTextColor,
|
|
1413
|
+
wrap: 'wrap',
|
|
1414
|
+
}, cursorDisplay.beforeCursor, createElement(Text, {
|
|
1415
|
+
backgroundColor: input.theme.composerCursorBackground,
|
|
1416
|
+
color: input.theme.composerCursorTextColor,
|
|
1417
|
+
}, cursorDisplay.cursorCharacter), cursorDisplay.afterCursor);
|
|
1418
|
+
}
|
|
1419
|
+
function ModelSwitcher(props) {
|
|
1420
|
+
const createElement = React.createElement;
|
|
1421
|
+
const theme = useAssistantInkTheme();
|
|
1422
|
+
const onCancelRef = React.useRef(props.onCancel);
|
|
1423
|
+
const onConfirmRef = React.useRef(props.onConfirm);
|
|
1424
|
+
const onMoveRef = React.useRef(props.onMove);
|
|
1425
|
+
onCancelRef.current = props.onCancel;
|
|
1426
|
+
onConfirmRef.current = props.onConfirm;
|
|
1427
|
+
onMoveRef.current = props.onMove;
|
|
1428
|
+
const handleModelSwitcherInput = React.useCallback((input, key) => {
|
|
1429
|
+
const normalizedKey = normalizeAssistantInkArrowKey(input, key);
|
|
1430
|
+
if (normalizedKey.escape) {
|
|
1431
|
+
onCancelRef.current();
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (normalizedKey.upArrow || input === 'k') {
|
|
1435
|
+
onMoveRef.current(-1);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (normalizedKey.downArrow || input === 'j') {
|
|
1439
|
+
onMoveRef.current(1);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
if (normalizedKey.return) {
|
|
1443
|
+
onConfirmRef.current();
|
|
1444
|
+
}
|
|
1445
|
+
}, []);
|
|
1446
|
+
useInput(handleModelSwitcherInput);
|
|
1447
|
+
const selectedModelLabel = props.modelOptions[props.modelIndex]?.value ??
|
|
1448
|
+
props.currentModel ??
|
|
1449
|
+
'the current model';
|
|
1450
|
+
const canChooseReasoning = props.reasoningOptions.length > 0;
|
|
1451
|
+
const title = props.mode === 'model'
|
|
1452
|
+
? 'Choose a model'
|
|
1453
|
+
: `Choose reasoning for ${selectedModelLabel}`;
|
|
1454
|
+
const subtitle = props.mode === 'model'
|
|
1455
|
+
? canChooseReasoning
|
|
1456
|
+
? 'Step 1 of 2. Enter continues to reasoning depth.'
|
|
1457
|
+
: 'Enter confirms the active model.'
|
|
1458
|
+
: 'Step 2 of 2. Enter confirms the active reasoning depth.';
|
|
1459
|
+
const helpText = props.mode === 'model'
|
|
1460
|
+
? canChooseReasoning
|
|
1461
|
+
? '↑/↓ move · Enter next · Esc close'
|
|
1462
|
+
: '↑/↓ move · Enter confirm · Esc close'
|
|
1463
|
+
: '↑/↓ move · Enter confirm · Esc back';
|
|
1464
|
+
const options = props.mode === 'model'
|
|
1465
|
+
? props.modelOptions.map((option, index) => renderSwitcherRow({
|
|
1466
|
+
current: normalizeNullableString(option.value) ===
|
|
1467
|
+
normalizeNullableString(props.currentModel),
|
|
1468
|
+
description: option.description,
|
|
1469
|
+
index,
|
|
1470
|
+
label: option.value,
|
|
1471
|
+
selected: index === props.modelIndex,
|
|
1472
|
+
theme,
|
|
1473
|
+
}))
|
|
1474
|
+
: props.reasoningOptions.map((option, index) => renderSwitcherRow({
|
|
1475
|
+
current: isCurrentReasoningOption(option.value, props.currentReasoningEffort),
|
|
1476
|
+
description: option.description,
|
|
1477
|
+
index,
|
|
1478
|
+
label: option.value === 'medium'
|
|
1479
|
+
? `${option.label} (default)`
|
|
1480
|
+
: option.label,
|
|
1481
|
+
selected: index === props.reasoningIndex,
|
|
1482
|
+
theme,
|
|
1483
|
+
}));
|
|
1484
|
+
return createElement(ChromePanel, {
|
|
1485
|
+
backgroundColor: theme.switcherBackground,
|
|
1486
|
+
marginBottom: 1,
|
|
1487
|
+
}, createElement(Box, {
|
|
1488
|
+
flexDirection: 'column',
|
|
1489
|
+
}, createElement(Text, {
|
|
1490
|
+
bold: true,
|
|
1491
|
+
color: theme.switcherTextColor,
|
|
1492
|
+
}, title), createElement(Text, {
|
|
1493
|
+
color: theme.switcherMutedColor,
|
|
1494
|
+
}, subtitle), createElement(Box, {
|
|
1495
|
+
height: 1,
|
|
1496
|
+
}), ...options, createElement(Box, {
|
|
1497
|
+
height: 1,
|
|
1498
|
+
}), createElement(Text, {
|
|
1499
|
+
color: theme.switcherMutedColor,
|
|
1500
|
+
}, helpText)));
|
|
1501
|
+
}
|
|
1502
|
+
function SlashCommandSuggestions(input) {
|
|
1503
|
+
const theme = useAssistantInkTheme();
|
|
1504
|
+
const { stdout } = useStdout();
|
|
1505
|
+
if (input.commands.length === 0) {
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
const createElement = React.createElement;
|
|
1509
|
+
const wrapColumns = resolveAssistantPlainTextWrapColumns(stdout?.columns);
|
|
1510
|
+
return createElement(ChromePanel, {
|
|
1511
|
+
backgroundColor: theme.switcherBackground,
|
|
1512
|
+
marginBottom: 1,
|
|
1513
|
+
}, createElement(Text, {
|
|
1514
|
+
bold: true,
|
|
1515
|
+
color: theme.mutedColor,
|
|
1516
|
+
}, 'commands'), ...input.commands.map((command) => createElement(Box, {
|
|
1517
|
+
flexDirection: 'row',
|
|
1518
|
+
key: command.command,
|
|
1519
|
+
width: '100%',
|
|
1520
|
+
}, createElement(Text, { color: theme.accentColor }, command.command), createElement(Box, {
|
|
1521
|
+
flexDirection: 'column',
|
|
1522
|
+
flexGrow: 1,
|
|
1523
|
+
flexShrink: 1,
|
|
1524
|
+
}, createElement(WrappedPlainTextBlock, {
|
|
1525
|
+
color: theme.mutedColor,
|
|
1526
|
+
columns: Math.max(1, wrapColumns - command.command.length - 2),
|
|
1527
|
+
text: ` ${command.description}`,
|
|
1528
|
+
})))));
|
|
1529
|
+
}
|
|
1530
|
+
function renderSwitcherRow(input) {
|
|
1531
|
+
const createElement = React.createElement;
|
|
1532
|
+
const textColor = input.selected
|
|
1533
|
+
? input.theme.switcherSelectionTextColor
|
|
1534
|
+
: input.theme.switcherTextColor;
|
|
1535
|
+
const descriptionColor = input.selected
|
|
1536
|
+
? input.theme.switcherSelectionTextColor
|
|
1537
|
+
: input.theme.switcherMutedColor;
|
|
1538
|
+
return createElement(Box, {
|
|
1539
|
+
backgroundColor: input.selected
|
|
1540
|
+
? input.theme.switcherSelectionBackground
|
|
1541
|
+
: undefined,
|
|
1542
|
+
key: `${input.label}:${input.index}`,
|
|
1543
|
+
flexDirection: 'column',
|
|
1544
|
+
marginBottom: 1,
|
|
1545
|
+
paddingX: 1,
|
|
1546
|
+
width: '100%',
|
|
1547
|
+
}, createElement(Text, {
|
|
1548
|
+
color: textColor,
|
|
1549
|
+
}, createElement(Text, { color: textColor }, input.selected ? '●' : '○'), ` ${input.index + 1}. ${input.label}`, input.current
|
|
1550
|
+
? createElement(Text, {
|
|
1551
|
+
color: input.selected ? textColor : input.theme.accentColor,
|
|
1552
|
+
}, ' · current')
|
|
1553
|
+
: null), createElement(Text, {
|
|
1554
|
+
color: descriptionColor,
|
|
1555
|
+
wrap: 'wrap',
|
|
1556
|
+
}, input.description));
|
|
1557
|
+
}
|
|
1558
|
+
function namespaceTurnTraceUpdates(updates, turnTracePrefix) {
|
|
1559
|
+
return updates.map((update) => ({
|
|
1560
|
+
...update,
|
|
1561
|
+
streamKey: update.streamKey
|
|
1562
|
+
? `${turnTracePrefix}:${update.streamKey}`
|
|
1563
|
+
: update.streamKey,
|
|
1564
|
+
}));
|
|
1565
|
+
}
|
|
1566
|
+
function namespaceProviderProgressEvent(event, turnTracePrefix) {
|
|
1567
|
+
return {
|
|
1568
|
+
...event,
|
|
1569
|
+
id: event.id ? `${turnTracePrefix}:${event.id}` : `${turnTracePrefix}:trace`,
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function isCurrentReasoningOption(option, currentReasoningEffort) {
|
|
1573
|
+
const normalizedCurrent = normalizeNullableString(currentReasoningEffort) ?? 'medium';
|
|
1574
|
+
return normalizeNullableString(option) === normalizedCurrent;
|
|
1575
|
+
}
|
|
1576
|
+
function wrapPickerIndex(index, count) {
|
|
1577
|
+
if (count <= 0) {
|
|
1578
|
+
return 0;
|
|
1579
|
+
}
|
|
1580
|
+
return ((index % count) + count) % count;
|
|
1581
|
+
}
|
|
1582
|
+
function assistantModelDiscoveryResultsEqual(left, right) {
|
|
1583
|
+
if (left === right) {
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
if (!left || !right) {
|
|
1587
|
+
return false;
|
|
1588
|
+
}
|
|
1589
|
+
return (left.status === right.status &&
|
|
1590
|
+
(normalizeNullableString(left.message) ?? null) ===
|
|
1591
|
+
(normalizeNullableString(right.message) ?? null) &&
|
|
1592
|
+
left.models.length === right.models.length &&
|
|
1593
|
+
left.models.every((model, index) => model.id === right.models[index]?.id));
|
|
1594
|
+
}
|
|
1595
|
+
const EMPTY_ASSISTANT_PROMPT_QUEUE_STATE = {
|
|
1596
|
+
prompts: [],
|
|
1597
|
+
};
|
|
1598
|
+
const IDLE_ASSISTANT_TURN_STATE = {
|
|
1599
|
+
pauseRequested: false,
|
|
1600
|
+
phase: 'idle',
|
|
1601
|
+
};
|
|
1602
|
+
export function reduceAssistantPromptQueueState(state, action) {
|
|
1603
|
+
switch (action.kind) {
|
|
1604
|
+
case 'clear':
|
|
1605
|
+
return EMPTY_ASSISTANT_PROMPT_QUEUE_STATE;
|
|
1606
|
+
case 'dequeue':
|
|
1607
|
+
return state.prompts.length > 0
|
|
1608
|
+
? {
|
|
1609
|
+
prompts: state.prompts.slice(1),
|
|
1610
|
+
}
|
|
1611
|
+
: state;
|
|
1612
|
+
case 'enqueue':
|
|
1613
|
+
return {
|
|
1614
|
+
prompts: [...state.prompts, action.prompt],
|
|
1615
|
+
};
|
|
1616
|
+
case 'pop-last':
|
|
1617
|
+
return state.prompts.length > 0
|
|
1618
|
+
? {
|
|
1619
|
+
prompts: state.prompts.slice(0, -1),
|
|
1620
|
+
}
|
|
1621
|
+
: state;
|
|
1622
|
+
default:
|
|
1623
|
+
return state;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
export function reduceAssistantTurnState(state, action) {
|
|
1627
|
+
switch (action.kind) {
|
|
1628
|
+
case 'finish':
|
|
1629
|
+
return IDLE_ASSISTANT_TURN_STATE;
|
|
1630
|
+
case 'request-pause':
|
|
1631
|
+
return state.phase === 'running'
|
|
1632
|
+
? {
|
|
1633
|
+
...state,
|
|
1634
|
+
pauseRequested: true,
|
|
1635
|
+
}
|
|
1636
|
+
: state;
|
|
1637
|
+
case 'start':
|
|
1638
|
+
return {
|
|
1639
|
+
pauseRequested: false,
|
|
1640
|
+
phase: 'running',
|
|
1641
|
+
};
|
|
1642
|
+
default:
|
|
1643
|
+
return state;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function createAssistantTurnTracePrefix() {
|
|
1647
|
+
return `turn:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
1648
|
+
}
|
|
1649
|
+
export function resolveAssistantQueuedPromptDisposition(input) {
|
|
1650
|
+
if (input.turnOutcome === 'failed' ||
|
|
1651
|
+
input.turnOutcome === 'interrupted' ||
|
|
1652
|
+
(input.pauseRequested && input.turnOutcome === 'completed')) {
|
|
1653
|
+
return {
|
|
1654
|
+
kind: 'restore-composer',
|
|
1655
|
+
restoredQueuedPromptCount: input.queuedPrompts.length,
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (input.turnOutcome === 'completed' && input.queuedPrompts.length > 0) {
|
|
1659
|
+
return {
|
|
1660
|
+
kind: 'replay-next',
|
|
1661
|
+
nextQueuedPrompt: input.queuedPrompts[0] ?? '',
|
|
1662
|
+
remainingQueuedPrompts: input.queuedPrompts.slice(1),
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
kind: 'idle',
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
function normalizeAssistantTurnSelection(input) {
|
|
1670
|
+
return {
|
|
1671
|
+
activeModel: normalizeNullableString(input.activeModel),
|
|
1672
|
+
activeReasoningEffort: normalizeNullableString(input.activeReasoningEffort),
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function resolveAssistantSessionTurnSelection(session) {
|
|
1676
|
+
return normalizeAssistantTurnSelection({
|
|
1677
|
+
activeModel: session.providerOptions.model,
|
|
1678
|
+
activeReasoningEffort: session.providerOptions.reasoningEffort,
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
export function resolveAssistantSelectionAfterSessionSync(input) {
|
|
1682
|
+
const currentSelection = normalizeAssistantTurnSelection(input.currentSelection);
|
|
1683
|
+
const previousSessionSelection = resolveAssistantSessionTurnSelection(input.previousSession);
|
|
1684
|
+
const nextSessionSelection = resolveAssistantSessionTurnSelection(input.nextSession);
|
|
1685
|
+
const effectiveSelectionChanged = input.previousSession.provider !== input.nextSession.provider ||
|
|
1686
|
+
previousSessionSelection.activeModel !== nextSessionSelection.activeModel ||
|
|
1687
|
+
previousSessionSelection.activeReasoningEffort !==
|
|
1688
|
+
nextSessionSelection.activeReasoningEffort;
|
|
1689
|
+
return effectiveSelectionChanged ? nextSessionSelection : currentSelection;
|
|
1690
|
+
}
|
|
1691
|
+
export async function runAssistantPromptTurn(input) {
|
|
1692
|
+
let streamedAssistantEntryKey = null;
|
|
1693
|
+
const handleTraceEvent = (event) => {
|
|
1694
|
+
const namespacedUpdates = namespaceTurnTraceUpdates(event.updates, input.turnTracePrefix);
|
|
1695
|
+
if (namespacedUpdates.length === 0) {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
for (const update of namespacedUpdates) {
|
|
1699
|
+
if (update.kind === 'assistant' && update.streamKey) {
|
|
1700
|
+
streamedAssistantEntryKey = streamedAssistantEntryKey ?? update.streamKey;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
input.setEntries((previous) => applyInkChatTraceUpdates(previous, namespacedUpdates));
|
|
1704
|
+
const latestStatusUpdate = [...namespacedUpdates]
|
|
1705
|
+
.reverse()
|
|
1706
|
+
.find((update) => update.kind === 'error' || update.kind === 'status');
|
|
1707
|
+
if (latestStatusUpdate) {
|
|
1708
|
+
input.setStatus({
|
|
1709
|
+
kind: latestStatusUpdate.kind === 'error' ? 'error' : 'info',
|
|
1710
|
+
text: latestStatusUpdate.text,
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
try {
|
|
1715
|
+
const result = await sendAssistantMessage({
|
|
1716
|
+
...input.input,
|
|
1717
|
+
abortSignal: input.input.abortSignal,
|
|
1718
|
+
conversation: {
|
|
1719
|
+
...(input.input.conversation ?? {}),
|
|
1720
|
+
sessionId: input.session.sessionId,
|
|
1721
|
+
},
|
|
1722
|
+
model: input.activeModel,
|
|
1723
|
+
onProviderEvent: (event) => {
|
|
1724
|
+
input.setEntries((previous) => applyProviderProgressEventToEntries({
|
|
1725
|
+
entries: previous,
|
|
1726
|
+
event: namespaceProviderProgressEvent(event, input.turnTracePrefix),
|
|
1727
|
+
}));
|
|
1728
|
+
},
|
|
1729
|
+
onTraceEvent: handleTraceEvent,
|
|
1730
|
+
prompt: input.prompt,
|
|
1731
|
+
reasoningEffort: input.activeReasoningEffort,
|
|
1732
|
+
showThinkingTraces: true,
|
|
1733
|
+
});
|
|
1734
|
+
return {
|
|
1735
|
+
delivery: result.delivery,
|
|
1736
|
+
deliveryError: result.deliveryError,
|
|
1737
|
+
kind: 'completed',
|
|
1738
|
+
response: result.response,
|
|
1739
|
+
session: result.session,
|
|
1740
|
+
streamedAssistantEntryKey,
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
catch (error) {
|
|
1744
|
+
const recoveredSession = extractRecoveredAssistantSession(error);
|
|
1745
|
+
if (isAssistantProviderInterruptedError(error)) {
|
|
1746
|
+
return {
|
|
1747
|
+
kind: 'interrupted',
|
|
1748
|
+
recoveredSession,
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
return {
|
|
1752
|
+
error,
|
|
1753
|
+
kind: 'failed',
|
|
1754
|
+
recoveredSession,
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function useAssistantChatController(input) {
|
|
1759
|
+
const { exit } = useApp();
|
|
1760
|
+
const [session, setSession] = React.useState(input.resolvedSession);
|
|
1761
|
+
const [entries, setEntries] = React.useState(seedChatEntries(input.transcriptEntries));
|
|
1762
|
+
const entriesRef = React.useRef(entries);
|
|
1763
|
+
const [status, setStatus] = React.useState(null);
|
|
1764
|
+
const [composerValue, setComposerValue] = React.useState('');
|
|
1765
|
+
const initialActiveModel = normalizeNullableString(input.input.model) ??
|
|
1766
|
+
normalizeNullableString(input.selectedProviderDefaults?.model) ??
|
|
1767
|
+
normalizeNullableString(input.resolvedSession.providerOptions.model) ??
|
|
1768
|
+
normalizeNullableString(input.codexDisplay.model);
|
|
1769
|
+
const initialActiveReasoningEffort = normalizeNullableString(input.input.reasoningEffort) ??
|
|
1770
|
+
normalizeNullableString(input.selectedProviderDefaults?.reasoningEffort) ??
|
|
1771
|
+
normalizeNullableString(input.resolvedSession.providerOptions.reasoningEffort) ??
|
|
1772
|
+
normalizeNullableString(input.codexDisplay.reasoningEffort);
|
|
1773
|
+
const [activeModel, setActiveModel] = React.useState(initialActiveModel);
|
|
1774
|
+
const [activeReasoningEffort, setActiveReasoningEffort] = React.useState(initialActiveReasoningEffort);
|
|
1775
|
+
const [modelDiscovery, setModelDiscovery] = React.useState(null);
|
|
1776
|
+
const [modelSwitcherState, setModelSwitcherState] = React.useState(null);
|
|
1777
|
+
const [promptQueueState, setPromptQueueState] = React.useState(EMPTY_ASSISTANT_PROMPT_QUEUE_STATE);
|
|
1778
|
+
const [turnState, setTurnState] = React.useState(IDLE_ASSISTANT_TURN_STATE);
|
|
1779
|
+
const latestSessionRef = React.useRef(input.resolvedSession);
|
|
1780
|
+
const latestTurnsRef = React.useRef(0);
|
|
1781
|
+
const initialPromptRef = React.useRef(normalizeNullableString(input.input.initialPrompt));
|
|
1782
|
+
const bootstrappedRef = React.useRef(false);
|
|
1783
|
+
const promptQueueStateRef = React.useRef(EMPTY_ASSISTANT_PROMPT_QUEUE_STATE);
|
|
1784
|
+
const turnStateRef = React.useRef(IDLE_ASSISTANT_TURN_STATE);
|
|
1785
|
+
const activeTurnAbortControllerRef = React.useRef(null);
|
|
1786
|
+
const activeSelectionRef = React.useRef({
|
|
1787
|
+
activeModel: initialActiveModel,
|
|
1788
|
+
activeReasoningEffort: initialActiveReasoningEffort,
|
|
1789
|
+
});
|
|
1790
|
+
const modelCatalog = resolveAssistantModelCatalog({
|
|
1791
|
+
provider: session.provider,
|
|
1792
|
+
baseUrl: session.providerOptions.baseUrl,
|
|
1793
|
+
currentModel: activeModel,
|
|
1794
|
+
currentReasoningEffort: activeReasoningEffort,
|
|
1795
|
+
discovery: modelDiscovery,
|
|
1796
|
+
headers: session.providerOptions.headers ?? null,
|
|
1797
|
+
apiKeyEnv: session.providerOptions.apiKeyEnv,
|
|
1798
|
+
oss: session.providerOptions.oss,
|
|
1799
|
+
providerName: session.providerOptions.providerName,
|
|
1800
|
+
});
|
|
1801
|
+
const updatePromptQueue = React.useCallback((action) => {
|
|
1802
|
+
const nextState = reduceAssistantPromptQueueState(promptQueueStateRef.current, action);
|
|
1803
|
+
promptQueueStateRef.current = nextState;
|
|
1804
|
+
setPromptQueueState(nextState);
|
|
1805
|
+
return nextState;
|
|
1806
|
+
}, []);
|
|
1807
|
+
const updateTurnState = React.useCallback((action) => {
|
|
1808
|
+
const nextState = reduceAssistantTurnState(turnStateRef.current, action);
|
|
1809
|
+
turnStateRef.current = nextState;
|
|
1810
|
+
setTurnState(nextState);
|
|
1811
|
+
return nextState;
|
|
1812
|
+
}, []);
|
|
1813
|
+
React.useEffect(() => {
|
|
1814
|
+
latestSessionRef.current = session;
|
|
1815
|
+
}, [session]);
|
|
1816
|
+
React.useEffect(() => {
|
|
1817
|
+
entriesRef.current = entries;
|
|
1818
|
+
}, [entries]);
|
|
1819
|
+
const setActiveSelection = React.useCallback((nextSelection) => {
|
|
1820
|
+
const normalizedSelection = normalizeAssistantTurnSelection(nextSelection);
|
|
1821
|
+
activeSelectionRef.current = normalizedSelection;
|
|
1822
|
+
setActiveModel(normalizedSelection.activeModel);
|
|
1823
|
+
setActiveReasoningEffort(normalizedSelection.activeReasoningEffort);
|
|
1824
|
+
}, []);
|
|
1825
|
+
const commitSession = React.useCallback((nextSession) => {
|
|
1826
|
+
const previousSession = latestSessionRef.current;
|
|
1827
|
+
latestSessionRef.current = nextSession;
|
|
1828
|
+
setSession(nextSession);
|
|
1829
|
+
const nextSelection = resolveAssistantSelectionAfterSessionSync({
|
|
1830
|
+
currentSelection: activeSelectionRef.current,
|
|
1831
|
+
previousSession,
|
|
1832
|
+
nextSession,
|
|
1833
|
+
});
|
|
1834
|
+
if (nextSelection.activeModel !== activeSelectionRef.current.activeModel ||
|
|
1835
|
+
nextSelection.activeReasoningEffort !==
|
|
1836
|
+
activeSelectionRef.current.activeReasoningEffort) {
|
|
1837
|
+
setActiveSelection(nextSelection);
|
|
1838
|
+
}
|
|
1839
|
+
}, [setActiveSelection]);
|
|
1840
|
+
React.useEffect(() => {
|
|
1841
|
+
let cancelled = false;
|
|
1842
|
+
const baseUrl = normalizeNullableString(session.providerOptions.baseUrl);
|
|
1843
|
+
if (!modelCatalog.capabilities.supportsModelDiscovery || !baseUrl) {
|
|
1844
|
+
setModelDiscovery((existing) => (existing === null ? existing : null));
|
|
1845
|
+
return () => {
|
|
1846
|
+
cancelled = true;
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
void (async () => {
|
|
1850
|
+
const nextDiscovery = await discoverAssistantProviderModels({
|
|
1851
|
+
provider: session.provider,
|
|
1852
|
+
baseUrl,
|
|
1853
|
+
apiKeyEnv: session.providerOptions.apiKeyEnv,
|
|
1854
|
+
headers: session.providerOptions.headers ?? null,
|
|
1855
|
+
providerName: session.providerOptions.providerName,
|
|
1856
|
+
});
|
|
1857
|
+
if (cancelled) {
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
setModelDiscovery((existing) => assistantModelDiscoveryResultsEqual(existing, nextDiscovery)
|
|
1861
|
+
? existing
|
|
1862
|
+
: nextDiscovery);
|
|
1863
|
+
})();
|
|
1864
|
+
return () => {
|
|
1865
|
+
cancelled = true;
|
|
1866
|
+
};
|
|
1867
|
+
}, [
|
|
1868
|
+
modelCatalog.capabilities.supportsModelDiscovery,
|
|
1869
|
+
session.provider,
|
|
1870
|
+
session.providerOptions.apiKeyEnv,
|
|
1871
|
+
session.providerOptions.baseUrl,
|
|
1872
|
+
session.providerOptions.headers,
|
|
1873
|
+
session.providerOptions.providerName,
|
|
1874
|
+
]);
|
|
1875
|
+
const queuePrompt = (prompt) => {
|
|
1876
|
+
updatePromptQueue({
|
|
1877
|
+
kind: 'enqueue',
|
|
1878
|
+
prompt,
|
|
1879
|
+
});
|
|
1880
|
+
};
|
|
1881
|
+
const applyQueuedPromptDisposition = (disposition, queuedPrompts) => {
|
|
1882
|
+
if (disposition.kind === 'restore-composer') {
|
|
1883
|
+
updatePromptQueue({
|
|
1884
|
+
kind: 'clear',
|
|
1885
|
+
});
|
|
1886
|
+
if (queuedPrompts.length > 0) {
|
|
1887
|
+
setComposerValue((previous) => mergeComposerDraftWithQueuedPrompts(previous, queuedPrompts));
|
|
1888
|
+
}
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
if (disposition.kind === 'replay-next') {
|
|
1892
|
+
promptQueueStateRef.current = {
|
|
1893
|
+
prompts: disposition.remainingQueuedPrompts,
|
|
1894
|
+
};
|
|
1895
|
+
setPromptQueueState(promptQueueStateRef.current);
|
|
1896
|
+
return disposition.nextQueuedPrompt;
|
|
1897
|
+
}
|
|
1898
|
+
return null;
|
|
1899
|
+
};
|
|
1900
|
+
const editLastQueuedPrompt = () => {
|
|
1901
|
+
const lastQueuedPrompt = promptQueueStateRef.current.prompts.at(-1);
|
|
1902
|
+
if (!lastQueuedPrompt) {
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
updatePromptQueue({
|
|
1906
|
+
kind: 'pop-last',
|
|
1907
|
+
});
|
|
1908
|
+
setComposerValue((previous) => mergeComposerDraftWithQueuedPrompts(previous, [lastQueuedPrompt]));
|
|
1909
|
+
};
|
|
1910
|
+
const startPromptTurn = (prompt) => {
|
|
1911
|
+
setEntries((previous) => [
|
|
1912
|
+
...previous,
|
|
1913
|
+
{
|
|
1914
|
+
kind: 'user',
|
|
1915
|
+
text: prompt,
|
|
1916
|
+
},
|
|
1917
|
+
]);
|
|
1918
|
+
setStatus(null);
|
|
1919
|
+
updateTurnState({
|
|
1920
|
+
kind: 'start',
|
|
1921
|
+
});
|
|
1922
|
+
const abortController = new AbortController();
|
|
1923
|
+
const turnTracePrefix = createAssistantTurnTracePrefix();
|
|
1924
|
+
activeTurnAbortControllerRef.current = abortController;
|
|
1925
|
+
void (async () => {
|
|
1926
|
+
const activeSelection = activeSelectionRef.current;
|
|
1927
|
+
const outcome = await runAssistantPromptTurn({
|
|
1928
|
+
activeModel: activeSelection.activeModel,
|
|
1929
|
+
activeReasoningEffort: activeSelection.activeReasoningEffort,
|
|
1930
|
+
input: {
|
|
1931
|
+
...input.input,
|
|
1932
|
+
abortSignal: abortController.signal,
|
|
1933
|
+
},
|
|
1934
|
+
prompt,
|
|
1935
|
+
session: latestSessionRef.current,
|
|
1936
|
+
setEntries,
|
|
1937
|
+
setStatus,
|
|
1938
|
+
turnTracePrefix,
|
|
1939
|
+
});
|
|
1940
|
+
if ('session' in outcome &&
|
|
1941
|
+
outcome.session !== latestSessionRef.current) {
|
|
1942
|
+
commitSession(outcome.session);
|
|
1943
|
+
}
|
|
1944
|
+
if (outcome.kind === 'completed') {
|
|
1945
|
+
latestTurnsRef.current += 1;
|
|
1946
|
+
setEntries((previous) => outcome.streamedAssistantEntryKey
|
|
1947
|
+
? applyInkChatTraceUpdates(previous, [
|
|
1948
|
+
{
|
|
1949
|
+
kind: 'assistant',
|
|
1950
|
+
mode: 'replace',
|
|
1951
|
+
streamKey: outcome.streamedAssistantEntryKey,
|
|
1952
|
+
text: outcome.response,
|
|
1953
|
+
},
|
|
1954
|
+
])
|
|
1955
|
+
: [
|
|
1956
|
+
...previous,
|
|
1957
|
+
{
|
|
1958
|
+
kind: 'assistant',
|
|
1959
|
+
text: outcome.response,
|
|
1960
|
+
},
|
|
1961
|
+
]);
|
|
1962
|
+
setStatus(outcome.delivery
|
|
1963
|
+
? {
|
|
1964
|
+
kind: 'success',
|
|
1965
|
+
text: `Delivered over ${outcome.delivery.channel} to ${outcome.delivery.target}.`,
|
|
1966
|
+
}
|
|
1967
|
+
: outcome.deliveryError
|
|
1968
|
+
? {
|
|
1969
|
+
kind: 'error',
|
|
1970
|
+
text: `Response saved locally, but delivery failed: ${outcome.deliveryError.message}`,
|
|
1971
|
+
}
|
|
1972
|
+
: null);
|
|
1973
|
+
}
|
|
1974
|
+
if (outcome.kind === 'failed') {
|
|
1975
|
+
if (outcome.recoveredSession) {
|
|
1976
|
+
commitSession(outcome.recoveredSession);
|
|
1977
|
+
}
|
|
1978
|
+
const queuedPrompts = promptQueueStateRef.current.prompts;
|
|
1979
|
+
const queuedPromptDisposition = resolveAssistantQueuedPromptDisposition({
|
|
1980
|
+
pauseRequested: false,
|
|
1981
|
+
queuedPrompts,
|
|
1982
|
+
turnOutcome: 'failed',
|
|
1983
|
+
});
|
|
1984
|
+
applyQueuedPromptDisposition(queuedPromptDisposition, queuedPrompts);
|
|
1985
|
+
const errorPresentation = resolveAssistantTurnErrorPresentation({
|
|
1986
|
+
error: outcome.error,
|
|
1987
|
+
restoredQueuedPromptCount: queuedPromptDisposition.kind === 'restore-composer'
|
|
1988
|
+
? queuedPromptDisposition.restoredQueuedPromptCount
|
|
1989
|
+
: 0,
|
|
1990
|
+
});
|
|
1991
|
+
setEntries((previous) => [
|
|
1992
|
+
...previous,
|
|
1993
|
+
errorPresentation.entry,
|
|
1994
|
+
]);
|
|
1995
|
+
setStatus(errorPresentation.status);
|
|
1996
|
+
if (errorPresentation.persistTranscriptError) {
|
|
1997
|
+
void appendAssistantTranscriptEntries(input.input.vault, latestSessionRef.current.sessionId, [
|
|
1998
|
+
{
|
|
1999
|
+
kind: 'error',
|
|
2000
|
+
text: errorPresentation.entry.text,
|
|
2001
|
+
},
|
|
2002
|
+
]).catch(() => { });
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (outcome.kind === 'interrupted' && outcome.recoveredSession) {
|
|
2006
|
+
commitSession(outcome.recoveredSession);
|
|
2007
|
+
}
|
|
2008
|
+
activeTurnAbortControllerRef.current = null;
|
|
2009
|
+
setEntries((previous) => finalizePendingInkChatTraces(previous, turnTracePrefix));
|
|
2010
|
+
const pauseRequested = turnStateRef.current.pauseRequested;
|
|
2011
|
+
updateTurnState({
|
|
2012
|
+
kind: 'finish',
|
|
2013
|
+
});
|
|
2014
|
+
if (outcome.kind === 'interrupted') {
|
|
2015
|
+
const queuedPrompts = promptQueueStateRef.current.prompts;
|
|
2016
|
+
const queuedPromptDisposition = resolveAssistantQueuedPromptDisposition({
|
|
2017
|
+
pauseRequested,
|
|
2018
|
+
queuedPrompts,
|
|
2019
|
+
turnOutcome: 'interrupted',
|
|
2020
|
+
});
|
|
2021
|
+
applyQueuedPromptDisposition(queuedPromptDisposition, queuedPrompts);
|
|
2022
|
+
setStatus({
|
|
2023
|
+
kind: 'info',
|
|
2024
|
+
text: queuedPromptDisposition.kind === 'restore-composer' &&
|
|
2025
|
+
queuedPromptDisposition.restoredQueuedPromptCount > 0
|
|
2026
|
+
? 'Paused current turn. Queued follow-ups are back in the composer.'
|
|
2027
|
+
: 'Paused current turn.',
|
|
2028
|
+
});
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
const queuedPrompts = promptQueueStateRef.current.prompts;
|
|
2032
|
+
const queuedPromptDisposition = resolveAssistantQueuedPromptDisposition({
|
|
2033
|
+
pauseRequested,
|
|
2034
|
+
queuedPrompts,
|
|
2035
|
+
turnOutcome: outcome.kind,
|
|
2036
|
+
});
|
|
2037
|
+
if (queuedPromptDisposition.kind === 'restore-composer' &&
|
|
2038
|
+
outcome.kind === 'completed' &&
|
|
2039
|
+
pauseRequested) {
|
|
2040
|
+
applyQueuedPromptDisposition(queuedPromptDisposition, queuedPrompts);
|
|
2041
|
+
setStatus({
|
|
2042
|
+
kind: 'info',
|
|
2043
|
+
text: queuedPromptDisposition.restoredQueuedPromptCount > 0
|
|
2044
|
+
? 'Stopped after the current turn. Queued follow-ups are back in the composer.'
|
|
2045
|
+
: 'Stopped after the current turn.',
|
|
2046
|
+
});
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
if (outcome.kind === 'completed') {
|
|
2050
|
+
const nextQueuedPrompt = applyQueuedPromptDisposition(queuedPromptDisposition, queuedPrompts);
|
|
2051
|
+
if (nextQueuedPrompt) {
|
|
2052
|
+
queueMicrotask(() => {
|
|
2053
|
+
startPromptTurn(nextQueuedPrompt);
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
})();
|
|
2058
|
+
};
|
|
2059
|
+
const openModelSwitcher = () => {
|
|
2060
|
+
const reasoningOptions = resolveAssistantCatalogReasoningOptions(modelCatalog.models[findAssistantModelOptionIndex(activeModel, modelCatalog.modelOptions)]);
|
|
2061
|
+
setModelSwitcherState({
|
|
2062
|
+
models: modelCatalog.models,
|
|
2063
|
+
mode: 'model',
|
|
2064
|
+
modelIndex: findAssistantModelOptionIndex(activeModel, modelCatalog.modelOptions),
|
|
2065
|
+
reasoningIndex: findAssistantReasoningOptionIndex(activeReasoningEffort, reasoningOptions),
|
|
2066
|
+
modelOptions: modelCatalog.modelOptions,
|
|
2067
|
+
reasoningOptions,
|
|
2068
|
+
});
|
|
2069
|
+
};
|
|
2070
|
+
const moveModelSwitcherSelection = (delta) => {
|
|
2071
|
+
setModelSwitcherState((previous) => {
|
|
2072
|
+
if (!previous) {
|
|
2073
|
+
return previous;
|
|
2074
|
+
}
|
|
2075
|
+
if (previous.mode === 'model') {
|
|
2076
|
+
const modelIndex = wrapPickerIndex(previous.modelIndex + delta, previous.modelOptions.length);
|
|
2077
|
+
const reasoningOptions = resolveAssistantCatalogReasoningOptions(previous.models[modelIndex]);
|
|
2078
|
+
return {
|
|
2079
|
+
...previous,
|
|
2080
|
+
modelIndex,
|
|
2081
|
+
reasoningIndex: findAssistantReasoningOptionIndex(activeReasoningEffort, reasoningOptions),
|
|
2082
|
+
reasoningOptions,
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
return {
|
|
2086
|
+
...previous,
|
|
2087
|
+
reasoningIndex: wrapPickerIndex(previous.reasoningIndex + delta, previous.reasoningOptions.length),
|
|
2088
|
+
};
|
|
2089
|
+
});
|
|
2090
|
+
};
|
|
2091
|
+
const cancelModelSwitcher = () => {
|
|
2092
|
+
setModelSwitcherState((previous) => {
|
|
2093
|
+
if (!previous) {
|
|
2094
|
+
return previous;
|
|
2095
|
+
}
|
|
2096
|
+
if (previous.mode === 'reasoning') {
|
|
2097
|
+
return {
|
|
2098
|
+
...previous,
|
|
2099
|
+
mode: 'model',
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
return null;
|
|
2103
|
+
});
|
|
2104
|
+
};
|
|
2105
|
+
const applyModelSwitcherSelection = (selection) => {
|
|
2106
|
+
const nextModel = selection.modelOptions[selection.modelIndex]?.value ??
|
|
2107
|
+
activeModel ??
|
|
2108
|
+
null;
|
|
2109
|
+
const nextReasoningEffort = selection.reasoningOptions.length > 0
|
|
2110
|
+
? selection.reasoningOptions[selection.reasoningIndex]?.value ??
|
|
2111
|
+
activeReasoningEffort ??
|
|
2112
|
+
'medium'
|
|
2113
|
+
: null;
|
|
2114
|
+
const selectedLabel = [
|
|
2115
|
+
nextModel ?? 'the configured model',
|
|
2116
|
+
normalizeNullableString(nextReasoningEffort),
|
|
2117
|
+
]
|
|
2118
|
+
.filter((value) => Boolean(value))
|
|
2119
|
+
.join(' ');
|
|
2120
|
+
setActiveSelection({
|
|
2121
|
+
activeModel: nextModel,
|
|
2122
|
+
activeReasoningEffort: nextReasoningEffort,
|
|
2123
|
+
});
|
|
2124
|
+
setModelSwitcherState(null);
|
|
2125
|
+
setStatus({
|
|
2126
|
+
kind: 'info',
|
|
2127
|
+
text: `Using ${selectedLabel}.`,
|
|
2128
|
+
});
|
|
2129
|
+
void (async () => {
|
|
2130
|
+
try {
|
|
2131
|
+
const updatedSession = await updateAssistantSessionOptions({
|
|
2132
|
+
vault: input.input.vault,
|
|
2133
|
+
sessionId: latestSessionRef.current.sessionId,
|
|
2134
|
+
providerOptions: {
|
|
2135
|
+
model: nextModel,
|
|
2136
|
+
reasoningEffort: nextReasoningEffort,
|
|
2137
|
+
},
|
|
2138
|
+
});
|
|
2139
|
+
commitSession(updatedSession);
|
|
2140
|
+
await saveAssistantOperatorDefaultsPatch(buildAssistantProviderDefaultsPatch({
|
|
2141
|
+
defaults: input.defaults,
|
|
2142
|
+
provider: updatedSession.provider,
|
|
2143
|
+
providerConfig: {
|
|
2144
|
+
...updatedSession.providerOptions,
|
|
2145
|
+
model: nextModel,
|
|
2146
|
+
reasoningEffort: nextReasoningEffort,
|
|
2147
|
+
},
|
|
2148
|
+
}));
|
|
2149
|
+
}
|
|
2150
|
+
catch (error) {
|
|
2151
|
+
setStatus({
|
|
2152
|
+
kind: 'error',
|
|
2153
|
+
text: error instanceof Error && error.message.trim().length > 0
|
|
2154
|
+
? `Using ${selectedLabel} for now, but failed to save it for later chats: ${error.message}`
|
|
2155
|
+
: `Using ${selectedLabel} for now, but failed to save it for later chats.`,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
})();
|
|
2159
|
+
};
|
|
2160
|
+
const confirmModelSwitcher = () => {
|
|
2161
|
+
if (!modelSwitcherState) {
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
if (modelSwitcherState.mode === 'model' &&
|
|
2165
|
+
modelSwitcherState.reasoningOptions.length > 0) {
|
|
2166
|
+
setModelSwitcherState({
|
|
2167
|
+
...modelSwitcherState,
|
|
2168
|
+
mode: 'reasoning',
|
|
2169
|
+
});
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
applyModelSwitcherSelection(modelSwitcherState);
|
|
2173
|
+
};
|
|
2174
|
+
const requestPause = () => {
|
|
2175
|
+
if (turnStateRef.current.phase !== 'running' ||
|
|
2176
|
+
modelSwitcherState ||
|
|
2177
|
+
turnStateRef.current.pauseRequested ||
|
|
2178
|
+
!activeTurnAbortControllerRef.current) {
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
updateTurnState({
|
|
2182
|
+
kind: 'request-pause',
|
|
2183
|
+
});
|
|
2184
|
+
setStatus({
|
|
2185
|
+
kind: 'info',
|
|
2186
|
+
text: promptQueueStateRef.current.prompts.length > 0
|
|
2187
|
+
? 'Pausing current turn. Queued follow-ups will return to the composer.'
|
|
2188
|
+
: 'Pausing current turn...',
|
|
2189
|
+
});
|
|
2190
|
+
activeTurnAbortControllerRef.current.abort();
|
|
2191
|
+
};
|
|
2192
|
+
useInput((_input, key) => {
|
|
2193
|
+
if (!key.escape) {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
requestPause();
|
|
2197
|
+
}, {
|
|
2198
|
+
isActive: turnState.phase === 'running' && modelSwitcherState === null,
|
|
2199
|
+
});
|
|
2200
|
+
const submitPrompt = (rawValue, mode) => {
|
|
2201
|
+
const action = resolveChatSubmitAction(rawValue, {
|
|
2202
|
+
busy: turnState.phase === 'running',
|
|
2203
|
+
trigger: mode,
|
|
2204
|
+
});
|
|
2205
|
+
if (action.kind === 'ignore') {
|
|
2206
|
+
return 'keep';
|
|
2207
|
+
}
|
|
2208
|
+
if (action.kind === 'exit') {
|
|
2209
|
+
exit();
|
|
2210
|
+
return 'keep';
|
|
2211
|
+
}
|
|
2212
|
+
if (action.kind === 'session') {
|
|
2213
|
+
setStatus({
|
|
2214
|
+
kind: 'info',
|
|
2215
|
+
text: `session ${latestSessionRef.current.sessionId}`,
|
|
2216
|
+
});
|
|
2217
|
+
return 'keep';
|
|
2218
|
+
}
|
|
2219
|
+
if (action.kind === 'model') {
|
|
2220
|
+
setStatus(null);
|
|
2221
|
+
openModelSwitcher();
|
|
2222
|
+
return 'clear';
|
|
2223
|
+
}
|
|
2224
|
+
if (action.kind === 'queue') {
|
|
2225
|
+
queuePrompt(action.prompt);
|
|
2226
|
+
return 'clear';
|
|
2227
|
+
}
|
|
2228
|
+
startPromptTurn(action.prompt);
|
|
2229
|
+
return shouldClearComposerForSubmitAction(action) ? 'clear' : 'keep';
|
|
2230
|
+
};
|
|
2231
|
+
React.useEffect(() => {
|
|
2232
|
+
if (bootstrappedRef.current) {
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
bootstrappedRef.current = true;
|
|
2236
|
+
if (initialPromptRef.current) {
|
|
2237
|
+
submitPrompt(initialPromptRef.current, 'enter');
|
|
2238
|
+
}
|
|
2239
|
+
}, []);
|
|
2240
|
+
const bindingSummary = formatSessionBinding(session);
|
|
2241
|
+
const metadataBadges = resolveChatMetadataBadges({
|
|
2242
|
+
baseUrl: session.providerOptions.baseUrl,
|
|
2243
|
+
provider: session.provider,
|
|
2244
|
+
model: activeModel ?? session.providerOptions.model ?? input.codexDisplay.model,
|
|
2245
|
+
reasoningEffort: activeReasoningEffort ?? input.codexDisplay.reasoningEffort,
|
|
2246
|
+
}, input.redactedVault);
|
|
2247
|
+
return {
|
|
2248
|
+
activeModel,
|
|
2249
|
+
activeReasoningEffort,
|
|
2250
|
+
bindingSummary,
|
|
2251
|
+
busy: turnState.phase === 'running',
|
|
2252
|
+
cancelModelSwitcher,
|
|
2253
|
+
composerValue,
|
|
2254
|
+
confirmModelSwitcher,
|
|
2255
|
+
editLastQueuedPrompt,
|
|
2256
|
+
entries,
|
|
2257
|
+
lastQueuedPrompt: promptQueueState.prompts.at(-1) ?? null,
|
|
2258
|
+
latestSessionRef,
|
|
2259
|
+
latestTurnsRef,
|
|
2260
|
+
metadataBadges,
|
|
2261
|
+
modelSwitcherState,
|
|
2262
|
+
moveModelSwitcherSelection,
|
|
2263
|
+
queuedPromptCount: promptQueueState.prompts.length,
|
|
2264
|
+
session,
|
|
2265
|
+
setComposerValue,
|
|
2266
|
+
status,
|
|
2267
|
+
submitPrompt,
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
export async function runAssistantChatWithInk(input) {
|
|
2271
|
+
const startedAt = new Date().toISOString();
|
|
2272
|
+
const defaults = await resolveAssistantOperatorDefaults();
|
|
2273
|
+
const themeBaseline = captureAssistantInkThemeBaseline();
|
|
2274
|
+
const resolved = await openAssistantConversation(input);
|
|
2275
|
+
const selectedProviderDefaults = resolveAssistantProviderDefaults(defaults, resolved.session.provider);
|
|
2276
|
+
const transcriptEntries = await listAssistantTranscriptEntries(input.vault, resolved.session.sessionId);
|
|
2277
|
+
const redactedVault = redactAssistantDisplayPath(input.vault);
|
|
2278
|
+
const codexDisplay = await resolveCodexDisplayOptions({
|
|
2279
|
+
model: input.model ??
|
|
2280
|
+
selectedProviderDefaults?.model ??
|
|
2281
|
+
resolved.session.providerOptions.model,
|
|
2282
|
+
profile: input.profile ??
|
|
2283
|
+
selectedProviderDefaults?.profile ??
|
|
2284
|
+
resolved.session.providerOptions.profile,
|
|
2285
|
+
});
|
|
2286
|
+
const inkInput = resolveAssistantInkInputAdapter();
|
|
2287
|
+
if (!inkInput.stdin) {
|
|
2288
|
+
throw new Error('Murph chat requires interactive terminal input. process.stdin does not support raw mode, and Murph could not open the controlling terminal for Ink input.');
|
|
2289
|
+
}
|
|
2290
|
+
const inkStdin = inkInput.stdin;
|
|
2291
|
+
return await new Promise((resolve, reject) => {
|
|
2292
|
+
let settled = false;
|
|
2293
|
+
let instance = null;
|
|
2294
|
+
const resolveOnce = (result) => {
|
|
2295
|
+
if (settled) {
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
settled = true;
|
|
2299
|
+
inkInput.close();
|
|
2300
|
+
resolve(result);
|
|
2301
|
+
};
|
|
2302
|
+
const rejectOnce = (error) => {
|
|
2303
|
+
if (settled) {
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
settled = true;
|
|
2307
|
+
inkInput.close();
|
|
2308
|
+
reject(error);
|
|
2309
|
+
};
|
|
2310
|
+
const App = () => {
|
|
2311
|
+
const createElement = React.createElement;
|
|
2312
|
+
const [theme, setTheme] = React.useState(() => themeBaseline.theme);
|
|
2313
|
+
const controller = useAssistantChatController({
|
|
2314
|
+
codexDisplay,
|
|
2315
|
+
defaults,
|
|
2316
|
+
input,
|
|
2317
|
+
redactedVault,
|
|
2318
|
+
resolvedSession: resolved.session,
|
|
2319
|
+
selectedProviderDefaults,
|
|
2320
|
+
transcriptEntries,
|
|
2321
|
+
});
|
|
2322
|
+
React.useEffect(() => {
|
|
2323
|
+
if (process.platform !== 'darwin') {
|
|
2324
|
+
return undefined;
|
|
2325
|
+
}
|
|
2326
|
+
const refreshTimer = setInterval(() => {
|
|
2327
|
+
setTheme((currentTheme) => {
|
|
2328
|
+
const nextTheme = resolveAssistantInkThemeForOpenChat({
|
|
2329
|
+
currentMode: currentTheme.mode,
|
|
2330
|
+
initialAppleInterfaceStyle: themeBaseline.initialAppleInterfaceStyle,
|
|
2331
|
+
initialColorFgbg: themeBaseline.initialColorFgbg,
|
|
2332
|
+
});
|
|
2333
|
+
return nextTheme.mode === currentTheme.mode
|
|
2334
|
+
? currentTheme
|
|
2335
|
+
: nextTheme;
|
|
2336
|
+
});
|
|
2337
|
+
}, ASSISTANT_INK_THEME_REFRESH_INTERVAL_MS);
|
|
2338
|
+
return () => {
|
|
2339
|
+
clearInterval(refreshTimer);
|
|
2340
|
+
};
|
|
2341
|
+
}, []);
|
|
2342
|
+
React.useEffect(() => () => {
|
|
2343
|
+
resolveOnce(assistantChatResultSchema.parse({
|
|
2344
|
+
vault: redactedVault,
|
|
2345
|
+
startedAt,
|
|
2346
|
+
stoppedAt: new Date().toISOString(),
|
|
2347
|
+
turns: controller.latestTurnsRef.current,
|
|
2348
|
+
session: redactAssistantSessionForDisplay(controller.latestSessionRef.current),
|
|
2349
|
+
}));
|
|
2350
|
+
}, []);
|
|
2351
|
+
return createElement(AssistantInkThemeContext.Provider, {
|
|
2352
|
+
value: theme,
|
|
2353
|
+
}, createElement(Box, {
|
|
2354
|
+
flexDirection: 'column',
|
|
2355
|
+
paddingX: ASSISTANT_CHAT_VIEW_PADDING_X,
|
|
2356
|
+
paddingY: 1,
|
|
2357
|
+
width: '100%',
|
|
2358
|
+
}, createElement(ChatTranscriptFeed, {
|
|
2359
|
+
bindingSummary: controller.bindingSummary,
|
|
2360
|
+
busy: controller.busy,
|
|
2361
|
+
entries: controller.entries,
|
|
2362
|
+
sessionId: controller.session.sessionId,
|
|
2363
|
+
}), createElement(Box, {
|
|
2364
|
+
flexDirection: 'column',
|
|
2365
|
+
width: '100%',
|
|
2366
|
+
}, createElement(ChatStatus, {
|
|
2367
|
+
busy: shouldShowBusyStatus({
|
|
2368
|
+
busy: controller.busy,
|
|
2369
|
+
entries: controller.entries,
|
|
2370
|
+
}),
|
|
2371
|
+
status: controller.status,
|
|
2372
|
+
}), createElement(QueuedFollowUpStatus, {
|
|
2373
|
+
latestPrompt: controller.lastQueuedPrompt,
|
|
2374
|
+
queuedPromptCount: controller.queuedPromptCount,
|
|
2375
|
+
}), controller.modelSwitcherState
|
|
2376
|
+
? createElement(ModelSwitcher, {
|
|
2377
|
+
currentModel: controller.activeModel,
|
|
2378
|
+
currentReasoningEffort: controller.activeReasoningEffort,
|
|
2379
|
+
mode: controller.modelSwitcherState.mode,
|
|
2380
|
+
modelIndex: controller.modelSwitcherState.modelIndex,
|
|
2381
|
+
modelOptions: controller.modelSwitcherState.modelOptions,
|
|
2382
|
+
onCancel: controller.cancelModelSwitcher,
|
|
2383
|
+
onConfirm: controller.confirmModelSwitcher,
|
|
2384
|
+
onMove: controller.moveModelSwitcherSelection,
|
|
2385
|
+
reasoningIndex: controller.modelSwitcherState.reasoningIndex,
|
|
2386
|
+
reasoningOptions: controller.modelSwitcherState.reasoningOptions,
|
|
2387
|
+
})
|
|
2388
|
+
: null, createElement(ChatComposer, {
|
|
2389
|
+
entryCount: controller.entries.length,
|
|
2390
|
+
modelSwitcherActive: controller.modelSwitcherState !== null,
|
|
2391
|
+
onChange: controller.setComposerValue,
|
|
2392
|
+
onEditLastQueuedPrompt: controller.editLastQueuedPrompt,
|
|
2393
|
+
onSubmit: controller.submitPrompt,
|
|
2394
|
+
value: controller.composerValue,
|
|
2395
|
+
}), createElement(ChatFooter, {
|
|
2396
|
+
badges: controller.metadataBadges,
|
|
2397
|
+
}))));
|
|
2398
|
+
};
|
|
2399
|
+
try {
|
|
2400
|
+
instance = render(React.createElement(App), {
|
|
2401
|
+
stderr: process.stderr,
|
|
2402
|
+
stdin: inkStdin,
|
|
2403
|
+
stdout: process.stderr,
|
|
2404
|
+
patchConsole: false,
|
|
2405
|
+
});
|
|
2406
|
+
void instance.waitUntilExit().catch(rejectOnce);
|
|
2407
|
+
}
|
|
2408
|
+
catch (error) {
|
|
2409
|
+
rejectOnce(error);
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
if (!instance) {
|
|
2413
|
+
rejectOnce(new Error('Ink chat failed to initialize.'));
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
//# sourceMappingURL=ink.js.map
|