maka-agent 0.2.0-dev.14.20260902 → 0.2.0-dev.16.20260903
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/dist/cli-core.js +0 -1
- package/dist/pi-transcript.js +48 -0
- package/dist/pi-tui-form-interaction.js +497 -0
- package/dist/pi-tui-pickers.js +256 -55
- package/dist/pi-tui-runner.js +215 -12
- package/dist/runtime-host-cli.js +17 -42
- package/dist/runtime-host-lifecycle-transaction.js +31 -13
- package/dist/runtime-host-managed-lifecycle-manager.js +1 -1
- package/dist/runtime-host-onboarding.js +3 -1
- package/dist/runtime-host-openrc-service.js +469 -0
- package/dist/runtime-host-run-command.js +6 -2
- package/dist/runtime-host-service-command.js +3 -3
- package/dist/runtime-host-service-management-command.js +43 -15
- package/dist/runtime-host-session-channel.js +3 -0
- package/dist/runtime-host-session-driver.js +21 -6
- package/dist/runtime-host-setup-command.js +30 -31
- package/dist/tui-context-refresh.js +103 -0
- package/dist/tui-copy-catalog.js +73 -0
- package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/node_modules/@maka/core/dist/chat-model-choice.js +4 -0
- package/node_modules/@maka/core/dist/computer-use.js +27 -92
- package/node_modules/@maka/core/dist/interaction.js +577 -3
- package/node_modules/@maka/core/dist/llm-connections.js +4 -0
- package/node_modules/@maka/core/dist/local-memory.js +2 -2
- package/node_modules/@maka/core/dist/model-call-usage-projection.js +3 -0
- package/node_modules/@maka/core/dist/model-metadata.js +26 -7
- package/node_modules/@maka/core/dist/model-thinking.js +17 -0
- package/node_modules/@maka/core/dist/provider-registry.js +1 -0
- package/node_modules/@maka/core/dist/runtime-event.js +23 -1
- package/node_modules/@maka/core/dist/scheduled-task.js +4 -0
- package/node_modules/@maka/core/dist/session.js +95 -2
- package/node_modules/@maka/core/dist/text-sanitize.js +19 -0
- package/node_modules/@maka/core/dist/usage-ledger-merge.js +3 -0
- package/node_modules/@maka/core/dist/usage-record-schema.js +11 -5
- package/node_modules/@maka/core/dist/workhub-creation-intent.js +75 -9
- package/node_modules/@maka/core/dist/workhub-session-resolver.js +50 -0
- package/node_modules/@maka/core/package.json +2 -0
- package/node_modules/@maka/eval/dist/harbor-external-subject.js +2 -1
- package/node_modules/@maka/eval/dist/harness-executor.js +3 -2
- package/node_modules/@maka/eval/dist/install-preflight.js +39 -10
- package/node_modules/@maka/eval/dist/process-termination.js +83 -0
- package/node_modules/@maka/mcp/dist/sep-2243.js +3 -3
- package/node_modules/@maka/runtime/dist/agent-run.js +6 -3
- package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +332 -118
- package/node_modules/@maka/runtime/dist/ai-sdk-compaction-contract.js +6 -1
- package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +171 -341
- package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +3 -26
- package/node_modules/@maka/runtime/dist/bundled-skill-catalog.generated.js +1 -1
- package/node_modules/@maka/runtime/dist/computer-use-codec.js +1 -141
- package/node_modules/@maka/runtime/dist/computer-use-tools.js +23 -105
- package/node_modules/@maka/runtime/dist/context-budget-policy.js +20 -26
- package/node_modules/@maka/runtime/dist/context-budget.js +18 -20
- package/node_modules/@maka/runtime/dist/conversation-copy.js +23 -19
- package/node_modules/@maka/runtime/dist/cua-frame-state.js +1 -69
- package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +267 -0
- package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +6 -9
- package/node_modules/@maka/runtime/dist/history-compact-summarizer.js +67 -23
- package/node_modules/@maka/runtime/dist/history-compact-summary-validation.js +11 -18
- package/node_modules/@maka/runtime/dist/history-compaction.js +66 -73
- package/node_modules/@maka/runtime/dist/interaction-authority.js +69 -0
- package/node_modules/@maka/runtime/dist/mcp-tools.js +13 -1
- package/node_modules/@maka/runtime/dist/memory-extraction.js +11 -11
- package/node_modules/@maka/runtime/dist/model-adapter.js +24 -6
- package/node_modules/@maka/runtime/dist/model-factory.js +14 -5
- package/node_modules/@maka/runtime/dist/oauth-provider-contracts.js +28 -1
- package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +7 -6
- package/node_modules/@maka/runtime/dist/opencode-session-header.js +29 -0
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +11 -1
- package/node_modules/@maka/runtime/dist/runtime-kernel.js +12 -11
- package/node_modules/@maka/runtime/dist/runtime-read-model.js +51 -13
- package/node_modules/@maka/runtime/dist/scheduled-task-tools.js +3 -0
- package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +26 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +1 -0
- package/node_modules/@maka/runtime/dist/session-projection-helpers.js +16 -1
- package/node_modules/@maka/runtime/dist/stream-graph-projection.js +6 -0
- package/node_modules/@maka/runtime/dist/stream-graph-read-model.js +5 -0
- package/node_modules/@maka/runtime/dist/stream-usage-fallback-fetch.js +74 -0
- package/node_modules/@maka/runtime/dist/subscription-credentials.js +51 -4
- package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +47 -3
- package/node_modules/@maka/runtime/dist/system-prompt/main-session-prompt.js +20 -1
- package/node_modules/@maka/runtime/dist/test-connection.js +18 -11
- package/node_modules/@maka/runtime/dist/tool-runtime.js +183 -0
- package/node_modules/@maka/runtime/package.json +1 -0
- package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +14 -0
- package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +84 -1
- package/node_modules/@maka/runtime-host/dist/client/host-profile.js +68 -51
- package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
- package/node_modules/@maka/runtime-host/dist/client/owner-connection-code.js +8 -5
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +342 -36
- package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +60 -4
- package/node_modules/@maka/runtime-host/dist/peer-mesh/index.js +3 -3
- package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +1 -1
- package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +48 -60
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +847 -431
- package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +33 -74
- package/node_modules/@maka/runtime-host/dist/peer-mesh/store.js +82 -69
- package/node_modules/@maka/runtime-host/dist/peer-reachability/index.js +21 -0
- package/node_modules/@maka/runtime-host/dist/peer-reachability/model.js +202 -0
- package/node_modules/@maka/runtime-host/dist/peer-reachability/owner.js +119 -0
- package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +245 -0
- package/node_modules/@maka/runtime-host/dist/protocol/agent-graph.js +4 -1
- package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +68 -0
- package/node_modules/@maka/runtime-host/dist/protocol/connection-effects.js +15 -8
- package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +7 -23
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +47 -1
- package/node_modules/@maka/runtime-host/dist/protocol/oauth.js +25 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/peer-mesh.js +14 -31
- package/node_modules/@maka/runtime-host/dist/protocol/scheduled-task.js +19 -6
- package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +36 -1
- package/node_modules/@maka/runtime-host/dist/protocol/session-revision.js +11 -2
- package/node_modules/@maka/runtime-host/dist/protocol/usage-pricing.js +16 -2
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +51 -2
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +35 -2
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +12 -10
- package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +163 -30
- package/node_modules/@maka/runtime-host/dist/server/connection-effect-coordinator.js +38 -14
- package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +45 -22
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +78 -20
- package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +4 -2
- package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +2 -1
- package/node_modules/@maka/runtime-host/dist/server/execution-service.js +49 -28
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +3 -6
- package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +130 -27
- package/node_modules/@maka/runtime-host/dist/server/interaction-projection.js +30 -1
- package/node_modules/@maka/runtime-host/dist/server/listener-set.js +2 -4
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +16 -3
- package/node_modules/@maka/runtime-host/dist/server/oauth-coordinator.js +66 -3
- package/node_modules/@maka/runtime-host/dist/server/oauth-execution-authority.js +20 -3
- package/node_modules/@maka/runtime-host/dist/server/peer-listener.js +8 -21
- package/node_modules/@maka/runtime-host/dist/server/peer-mesh-authority.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +6 -3
- package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +72 -12
- package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +48 -18
- package/node_modules/@maka/runtime-host/dist/server/session-turn-access-request-coordinator.js +43 -26
- package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +15 -1
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +229 -4
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +146 -8
- package/node_modules/@maka/runtime-host/dist/transport/peer-native.js +32 -6
- package/node_modules/@maka/runtime-host/package.json +1 -0
- package/node_modules/@maka/storage/dist/agent-run-store.js +18 -5
- package/node_modules/@maka/storage/dist/codex-session-adapter.js +14 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +5 -0
- package/node_modules/@maka/storage/dist/interaction-store.js +13 -1
- package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +9 -3
- package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +49 -19
- package/node_modules/@maka/storage/dist/runtime-policy/oauth-login-receipt-document.js +3 -1
- package/node_modules/@maka/storage/dist/runtime-policy/onboarding-transaction.js +13 -5
- package/node_modules/@maka/storage/dist/session-copy-cleanup.js +4 -2
- package/node_modules/@maka/storage/dist/session-store.js +39 -6
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +16 -9
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +30 -3
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +117 -0
- package/node_modules/@maka/storage/dist/sqlite-usage-store.js +29 -6
- package/node_modules/@maka/storage/dist/usage-stores.js +3 -1
- package/package.json +1 -1
package/dist/cli-core.js
CHANGED
|
@@ -131,7 +131,6 @@ function helpText(cliCommand) {
|
|
|
131
131
|
` ${cliCommand} runtime-host profile set --id <id> --name <name> --tls-url <wss-url> --expected-root <root-id> [--credential-env <name>]`,
|
|
132
132
|
` ${cliCommand} runtime-host profile set --id <id> --name <name> --ssh-destination <user@host> --ssh-remote-port <port> --expected-root <root-id> [--ssh-port <port>] [--credential-env <name>]`,
|
|
133
133
|
` ${cliCommand} runtime-host profile set --id <id> --name <name> --plaintext-url <ws-url> --acknowledge-plaintext --expected-root <root-id> [--credential-env <name>]`,
|
|
134
|
-
` ${cliCommand} runtime-host profile set --id <id> --name <name> --peer-id <peer-id> --peer-route <multiaddr> --expected-root <root-id> [--credential-env <name>]`,
|
|
135
134
|
` ${cliCommand} runtime-host profile remove --id <id>`,
|
|
136
135
|
` ${cliCommand} runtime-host capability-provider serve --url <ws-url> --mcp-config <path> --expected-root <root-id>`,
|
|
137
136
|
'',
|
package/dist/pi-transcript.js
CHANGED
|
@@ -614,6 +614,9 @@ export function applyMakaSessionEventToTranscript(state, event) {
|
|
|
614
614
|
case 'user_question_request':
|
|
615
615
|
enqueuePendingInteraction(state, event);
|
|
616
616
|
break;
|
|
617
|
+
case 'form_request':
|
|
618
|
+
enqueuePendingInteraction(state, event);
|
|
619
|
+
break;
|
|
617
620
|
case 'sandbox_boundary_decision_ack':
|
|
618
621
|
{
|
|
619
622
|
const request = findPendingInteraction(state, event.requestId);
|
|
@@ -630,6 +633,9 @@ export function applyMakaSessionEventToTranscript(state, event) {
|
|
|
630
633
|
case 'user_question_answer_ack':
|
|
631
634
|
completePendingInteraction(state, event.requestId);
|
|
632
635
|
break;
|
|
636
|
+
case 'form_answer_ack':
|
|
637
|
+
completePendingInteraction(state, event.requestId);
|
|
638
|
+
break;
|
|
633
639
|
case 'plan_submitted':
|
|
634
640
|
state.entries.push({
|
|
635
641
|
kind: 'notice',
|
|
@@ -937,6 +943,45 @@ function systemNoteText(message) {
|
|
|
937
943
|
return 'Context compacted to keep this task within the model window.';
|
|
938
944
|
case 'context_compaction_failed_open':
|
|
939
945
|
return 'Context summary failed; the session continued without a new summary.';
|
|
946
|
+
case 'context_provider_dropping': {
|
|
947
|
+
const data = message.data;
|
|
948
|
+
const used = typeof data?.inputTokens === 'number' ? data.inputTokens : undefined;
|
|
949
|
+
const prior = typeof data?.priorInputTokens === 'number' ? data.priorInputTokens : undefined;
|
|
950
|
+
if (used === undefined || prior === undefined) {
|
|
951
|
+
return 'The provider is dropping or rewriting context: content was appended but its reported usage did not grow. Declare a context window for this model so Maka compacts first.';
|
|
952
|
+
}
|
|
953
|
+
return `The provider is dropping or rewriting context: content was appended, and it counted ${used} input tokens against ${prior} before, which is no growth. Declare a context window for this model so Maka compacts first.`;
|
|
954
|
+
}
|
|
955
|
+
case 'context_overflow_after_compaction':
|
|
956
|
+
return 'History was compacted and the provider still called this request too large. What remains also carries the system prompt, the tool schemas, the summary and the recent tail; shortening this message is the part you control.';
|
|
957
|
+
case 'context_reported_window_exceeded': {
|
|
958
|
+
const data = message.data;
|
|
959
|
+
const used = typeof data?.usedTokens === 'number' ? data.usedTokens : undefined;
|
|
960
|
+
const reported = typeof data?.reportedContextWindow === 'number' ? data.reportedContextWindow : undefined;
|
|
961
|
+
if (used === undefined || reported === undefined) {
|
|
962
|
+
return 'This exchange ran past the context window this model reports, and the provider accepted it anyway.';
|
|
963
|
+
}
|
|
964
|
+
return `This exchange used about ${used} tokens, past the ${reported} this model reports, and the provider accepted it without complaint. Nothing is declared, so Maka does not compact on its own; declare a context window to have it compact first.`;
|
|
965
|
+
}
|
|
966
|
+
case 'context_window_overrun': {
|
|
967
|
+
const data = message.data;
|
|
968
|
+
const used = typeof data?.usedTokens === 'number' ? data.usedTokens : undefined;
|
|
969
|
+
const declared = typeof data?.declaredContextWindow === 'number' ? data.declaredContextWindow : undefined;
|
|
970
|
+
if (used === undefined || declared === undefined) {
|
|
971
|
+
return 'This exchange ran past the context window declared for this model.';
|
|
972
|
+
}
|
|
973
|
+
return `This exchange used about ${used} tokens against the declared window of ${declared}: the reply needed more room than was left. Maka compacts before the next request; raise the window if the replies should stay whole.`;
|
|
974
|
+
}
|
|
975
|
+
case 'context_window_suggestion': {
|
|
976
|
+
const data = message.data;
|
|
977
|
+
const tokens = typeof data?.suggestedContextWindow === 'number' ? data.suggestedContextWindow : undefined;
|
|
978
|
+
const declared = typeof data?.declaredContextWindow === 'number' ? data.declaredContextWindow : undefined;
|
|
979
|
+
if (tokens === undefined)
|
|
980
|
+
return 'The provider rejected this request as too large.';
|
|
981
|
+
return declared === undefined
|
|
982
|
+
? `The provider rejected this request. No context window is declared for this model; the last accepted request was about ${tokens} tokens — declare that as the window so Maka compacts first.`
|
|
983
|
+
: `The provider rejected this request at about ${tokens} tokens, below the declared window of ${declared}. The declaration is likely larger than the provider's window; consider lowering it to ${tokens}.`;
|
|
984
|
+
}
|
|
940
985
|
case 'step_limit':
|
|
941
986
|
return STEP_LIMIT_NOTICE_TEXT;
|
|
942
987
|
case 'error':
|
|
@@ -1015,6 +1060,9 @@ export function activeUserQuestionRequest(state) {
|
|
|
1015
1060
|
? state.pendingInteraction
|
|
1016
1061
|
: undefined;
|
|
1017
1062
|
}
|
|
1063
|
+
export function activeFormRequest(state) {
|
|
1064
|
+
return state.pendingInteraction?.type === 'form_request' ? state.pendingInteraction : undefined;
|
|
1065
|
+
}
|
|
1018
1066
|
function enqueuePendingInteraction(state, request) {
|
|
1019
1067
|
if (findPendingInteraction(state, request.requestId))
|
|
1020
1068
|
return;
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { Editor, Key, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
|
|
20
|
+
import { isInteractionFormFieldValueValid, } from '@maka/core/interaction';
|
|
21
|
+
import { sanitizeUnicodeText } from '@maka/core/text-sanitize';
|
|
22
|
+
import { defineUiMessageCatalog, formatUiMessage, resolveUiMessageCatalog, } from '@maka/core/ui-locale';
|
|
23
|
+
import { ansi, editorTheme, stripAnsi } from './tui-ansi.js';
|
|
24
|
+
import { TUI_COPY_RESOURCES } from './tui-copy-catalog.js';
|
|
25
|
+
const FORM_COPY = resolveUiMessageCatalog(defineUiMessageCatalog()(TUI_COPY_RESOURCES['form-interaction']));
|
|
26
|
+
const FORM_REVIEW_MAX_VISIBLE_FIELDS = 8;
|
|
27
|
+
const FORM_CHOICE_MAX_VISIBLE_OPTIONS = 10;
|
|
28
|
+
export function createTuiFormDrafts(fields) {
|
|
29
|
+
return fields.map((field) => {
|
|
30
|
+
const included = field.required || field.default !== undefined;
|
|
31
|
+
if (field.kind === 'boolean')
|
|
32
|
+
return { included, value: field.default ?? false };
|
|
33
|
+
if (field.kind === 'multi_select')
|
|
34
|
+
return { included, value: [...(field.default ?? [])] };
|
|
35
|
+
if (field.kind === 'number' || field.kind === 'integer') {
|
|
36
|
+
return { included, value: field.default === undefined ? '' : String(field.default) };
|
|
37
|
+
}
|
|
38
|
+
return { included, value: field.default ?? '' };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function buildTuiFormResponse(request, drafts) {
|
|
42
|
+
if (drafts.length !== request.fields.length)
|
|
43
|
+
return null;
|
|
44
|
+
const entries = [];
|
|
45
|
+
for (const [index, field] of request.fields.entries()) {
|
|
46
|
+
const draft = drafts[index];
|
|
47
|
+
if (!draft)
|
|
48
|
+
return null;
|
|
49
|
+
if (!draft.included) {
|
|
50
|
+
if (field.required)
|
|
51
|
+
return null;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const value = draftValue(field, draft);
|
|
55
|
+
if (!isInteractionFormFieldValueValid(field, value))
|
|
56
|
+
return null;
|
|
57
|
+
entries.push([field.name, value]);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
requestId: request.requestId,
|
|
61
|
+
action: 'accept',
|
|
62
|
+
values: Object.fromEntries(entries),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export class FormInteractionOverlay {
|
|
66
|
+
input;
|
|
67
|
+
#copy;
|
|
68
|
+
#editor;
|
|
69
|
+
#drafts;
|
|
70
|
+
#activeIndex = 0;
|
|
71
|
+
#mode = { kind: 'review' };
|
|
72
|
+
#invalid = new Set();
|
|
73
|
+
#submitting = false;
|
|
74
|
+
constructor(tui, input) {
|
|
75
|
+
this.input = input;
|
|
76
|
+
this.#copy = FORM_COPY[input.locale];
|
|
77
|
+
this.#drafts =
|
|
78
|
+
input.initialDrafts?.length === input.request.fields.length
|
|
79
|
+
? cloneTuiFormDrafts(input.initialDrafts)
|
|
80
|
+
: createTuiFormDrafts(input.request.fields);
|
|
81
|
+
this.#editor = new Editor(tui, editorTheme(), { paddingX: 0 });
|
|
82
|
+
this.#editor.onChange = (value) => {
|
|
83
|
+
if (this.#mode.kind !== 'text')
|
|
84
|
+
return;
|
|
85
|
+
this.#replaceDraft(this.#mode.index, { value });
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
invalidate() {
|
|
89
|
+
this.#editor.invalidate();
|
|
90
|
+
}
|
|
91
|
+
setSubmissionFailed() {
|
|
92
|
+
this.#submitting = false;
|
|
93
|
+
}
|
|
94
|
+
snapshotDrafts() {
|
|
95
|
+
return cloneTuiFormDrafts(this.#drafts);
|
|
96
|
+
}
|
|
97
|
+
handleInput(data) {
|
|
98
|
+
if (this.#submitting)
|
|
99
|
+
return;
|
|
100
|
+
if (this.#mode.kind === 'review') {
|
|
101
|
+
this.#handleReviewInput(data);
|
|
102
|
+
}
|
|
103
|
+
else if (this.#mode.kind === 'text') {
|
|
104
|
+
if (matchesKey(data, Key.escape)) {
|
|
105
|
+
this.#mode = { kind: 'review' };
|
|
106
|
+
}
|
|
107
|
+
else if ((matchesKey(data, Key.enter) || matchesKey(data, Key.return)) &&
|
|
108
|
+
!isKeyRepeat(data)) {
|
|
109
|
+
this.#replaceDraft(this.#mode.index, { value: this.#editor.getText() });
|
|
110
|
+
this.#mode = { kind: 'review' };
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.#editor.handleInput(data);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (this.#mode.kind === 'choice') {
|
|
117
|
+
this.#handleChoiceInput(data, this.#mode);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
this.#handleMultiInput(data, this.#mode);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
render(width) {
|
|
124
|
+
const safeWidth = Math.max(1, width);
|
|
125
|
+
const lines = this.#renderHeader(safeWidth);
|
|
126
|
+
if (this.#mode.kind === 'review')
|
|
127
|
+
lines.push(...this.#renderReview(safeWidth));
|
|
128
|
+
else if (this.#mode.kind === 'text')
|
|
129
|
+
lines.push(...this.#renderTextEditor(this.#mode.index, safeWidth));
|
|
130
|
+
else if (this.#mode.kind === 'choice')
|
|
131
|
+
lines.push(...this.#renderChoice(this.#mode, safeWidth));
|
|
132
|
+
else
|
|
133
|
+
lines.push(...this.#renderMulti(this.#mode, safeWidth));
|
|
134
|
+
lines.push(padLine(ansi.accent('-'.repeat(safeWidth)), safeWidth));
|
|
135
|
+
return lines;
|
|
136
|
+
}
|
|
137
|
+
#handleReviewInput(data) {
|
|
138
|
+
const fields = this.input.request.fields;
|
|
139
|
+
if (matchesKey(data, Key.escape)) {
|
|
140
|
+
this.#respond({ requestId: this.input.request.requestId, action: 'cancel' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (data === 'd' || data === 'D') {
|
|
144
|
+
this.#respond({ requestId: this.input.request.requestId, action: 'decline' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (data === 's' || data === 'S') {
|
|
148
|
+
this.#submit();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (fields.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
if (matchesKey(data, Key.up)) {
|
|
154
|
+
this.#activeIndex = this.#activeIndex === 0 ? fields.length - 1 : this.#activeIndex - 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (matchesKey(data, Key.down)) {
|
|
158
|
+
this.#activeIndex = this.#activeIndex === fields.length - 1 ? 0 : this.#activeIndex + 1;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const field = fields[this.#activeIndex];
|
|
162
|
+
const draft = this.#drafts[this.#activeIndex];
|
|
163
|
+
if (!field || !draft)
|
|
164
|
+
return;
|
|
165
|
+
if (matchesKey(data, Key.space) && !field.required) {
|
|
166
|
+
this.#replaceDraft(this.#activeIndex, { included: !draft.included });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if ((matchesKey(data, Key.enter) || matchesKey(data, Key.return)) && !isKeyRepeat(data)) {
|
|
170
|
+
if (!draft.included)
|
|
171
|
+
this.#replaceDraft(this.#activeIndex, { included: true });
|
|
172
|
+
this.#openEditor(field, this.#activeIndex);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
#handleChoiceInput(data, mode) {
|
|
176
|
+
if (matchesKey(data, Key.escape)) {
|
|
177
|
+
this.#mode = { kind: 'review' };
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const field = this.input.request.fields[mode.index];
|
|
181
|
+
if (!field || (field.kind !== 'boolean' && field.kind !== 'single_select'))
|
|
182
|
+
return;
|
|
183
|
+
const count = field.kind === 'boolean' ? 2 : field.options.length;
|
|
184
|
+
if (count === 0)
|
|
185
|
+
return;
|
|
186
|
+
if (matchesKey(data, Key.up))
|
|
187
|
+
mode.optionIndex = mode.optionIndex === 0 ? count - 1 : mode.optionIndex - 1;
|
|
188
|
+
else if (matchesKey(data, Key.down))
|
|
189
|
+
mode.optionIndex = mode.optionIndex === count - 1 ? 0 : mode.optionIndex + 1;
|
|
190
|
+
else if ((matchesKey(data, Key.enter) || matchesKey(data, Key.return)) && !isKeyRepeat(data)) {
|
|
191
|
+
const value = field.kind === 'boolean' ? mode.optionIndex === 0 : field.options[mode.optionIndex]?.value;
|
|
192
|
+
if (value !== undefined)
|
|
193
|
+
this.#replaceDraft(mode.index, { included: true, value });
|
|
194
|
+
this.#mode = { kind: 'review' };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
#handleMultiInput(data, mode) {
|
|
198
|
+
if (matchesKey(data, Key.escape) ||
|
|
199
|
+
matchesKey(data, Key.enter) ||
|
|
200
|
+
matchesKey(data, Key.return)) {
|
|
201
|
+
if (!isKeyRepeat(data))
|
|
202
|
+
this.#mode = { kind: 'review' };
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const field = this.input.request.fields[mode.index];
|
|
206
|
+
const draft = this.#drafts[mode.index];
|
|
207
|
+
if (!field || field.kind !== 'multi_select' || !draft || !Array.isArray(draft.value))
|
|
208
|
+
return;
|
|
209
|
+
if (field.options.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
if (matchesKey(data, Key.up))
|
|
212
|
+
mode.optionIndex = mode.optionIndex === 0 ? field.options.length - 1 : mode.optionIndex - 1;
|
|
213
|
+
else if (matchesKey(data, Key.down))
|
|
214
|
+
mode.optionIndex = mode.optionIndex === field.options.length - 1 ? 0 : mode.optionIndex + 1;
|
|
215
|
+
else if (matchesKey(data, Key.space)) {
|
|
216
|
+
const value = field.options[mode.optionIndex]?.value;
|
|
217
|
+
if (value === undefined)
|
|
218
|
+
return;
|
|
219
|
+
const selected = draft.value.includes(value)
|
|
220
|
+
? draft.value.filter((candidate) => candidate !== value)
|
|
221
|
+
: [...draft.value, value];
|
|
222
|
+
this.#replaceDraft(mode.index, { included: true, value: selected });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
#openEditor(field, index) {
|
|
226
|
+
const draft = this.#drafts[index];
|
|
227
|
+
if (!draft)
|
|
228
|
+
return;
|
|
229
|
+
if (field.kind === 'boolean') {
|
|
230
|
+
this.#mode = { kind: 'choice', index, optionIndex: draft.value === true ? 0 : 1 };
|
|
231
|
+
}
|
|
232
|
+
else if (field.kind === 'single_select') {
|
|
233
|
+
this.#mode = {
|
|
234
|
+
kind: 'choice',
|
|
235
|
+
index,
|
|
236
|
+
optionIndex: Math.max(0, field.options.findIndex((option) => option.value === draft.value)),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
else if (field.kind === 'multi_select') {
|
|
240
|
+
this.#mode = { kind: 'multi', index, optionIndex: 0 };
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this.#editor.setText(typeof draft.value === 'string' ? draft.value : '');
|
|
244
|
+
this.#mode = { kind: 'text', index };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
#submit() {
|
|
248
|
+
const response = buildTuiFormResponse(this.input.request, this.#drafts);
|
|
249
|
+
if (response) {
|
|
250
|
+
this.#respond(response);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.#invalid = new Set(this.input.request.fields.flatMap((field, index) => {
|
|
254
|
+
const draft = this.#drafts[index];
|
|
255
|
+
if (!draft || (!draft.included && field.required))
|
|
256
|
+
return [index];
|
|
257
|
+
if (!draft.included)
|
|
258
|
+
return [];
|
|
259
|
+
return isInteractionFormFieldValueValid(field, draftValue(field, draft)) ? [] : [index];
|
|
260
|
+
}));
|
|
261
|
+
const first = this.#invalid.values().next().value;
|
|
262
|
+
if (typeof first === 'number')
|
|
263
|
+
this.#activeIndex = first;
|
|
264
|
+
}
|
|
265
|
+
#respond(response) {
|
|
266
|
+
this.#submitting = true;
|
|
267
|
+
this.input.onRespond(response);
|
|
268
|
+
}
|
|
269
|
+
#replaceDraft(index, patch) {
|
|
270
|
+
const current = this.#drafts[index];
|
|
271
|
+
if (!current)
|
|
272
|
+
return;
|
|
273
|
+
this.#drafts = this.#drafts.map((draft, candidate) => candidate === index ? { ...current, ...patch } : draft);
|
|
274
|
+
this.#invalid.delete(index);
|
|
275
|
+
}
|
|
276
|
+
#renderHeader(width) {
|
|
277
|
+
const requester = this.input.request.requester;
|
|
278
|
+
const detail = requester.source
|
|
279
|
+
? `${safeDisplay(requester.name)} · ${safeDisplay(requester.source)}`
|
|
280
|
+
: safeDisplay(requester.name);
|
|
281
|
+
const provenance = formatUiMessage(this.#copy.requestedBy, { detail }, this.input.locale);
|
|
282
|
+
return [
|
|
283
|
+
padLine(ansi.bold(safeDisplay(this.input.request.message)), width),
|
|
284
|
+
padLine(ansi.dim(provenance), width),
|
|
285
|
+
padLine(ansi.red(this.#copy.sensitiveWarning), width),
|
|
286
|
+
padLine('', width),
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
#renderReview(width) {
|
|
290
|
+
const lines = [];
|
|
291
|
+
const window = visibleWindow(this.input.request.fields.length, this.#activeIndex, FORM_REVIEW_MAX_VISIBLE_FIELDS);
|
|
292
|
+
if (window.start > 0)
|
|
293
|
+
lines.push(padLine(ansi.dim(` … ↑ ${window.start}`), width));
|
|
294
|
+
this.input.request.fields.slice(window.start, window.end).forEach((field, offset) => {
|
|
295
|
+
const index = window.start + offset;
|
|
296
|
+
const draft = this.#drafts[index];
|
|
297
|
+
if (!draft)
|
|
298
|
+
return;
|
|
299
|
+
const requirement = field.required ? this.#copy.required : this.#copy.optional;
|
|
300
|
+
const prefix = index === this.#activeIndex ? '→ ' : ' ';
|
|
301
|
+
const summary = this.#summary(field, draft);
|
|
302
|
+
const invalid = this.#invalid.has(index);
|
|
303
|
+
const row = `${prefix}${safeDisplay(field.label)} (${requirement}): ${summary}${invalid ? ` · ${this.#copy.invalid}` : ''}`;
|
|
304
|
+
lines.push(formatReviewRow(invalid ? ansi.red(row) : row, index === this.#activeIndex, width));
|
|
305
|
+
if (index === this.#activeIndex && field.description) {
|
|
306
|
+
lines.push(padLine(` ${ansi.dim(safeDisplay(field.description))}`, width));
|
|
307
|
+
}
|
|
308
|
+
if (index === this.#activeIndex) {
|
|
309
|
+
const constraint = this.#constraint(field);
|
|
310
|
+
if (constraint)
|
|
311
|
+
lines.push(padLine(` ${ansi.dim(constraint)}`, width));
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
if (window.end < this.input.request.fields.length) {
|
|
315
|
+
lines.push(padLine(ansi.dim(` … ↓ ${this.input.request.fields.length - window.end}`), width));
|
|
316
|
+
}
|
|
317
|
+
if (this.input.request.fields.length === 0)
|
|
318
|
+
lines.push(padLine(ansi.dim('(no fields)'), width));
|
|
319
|
+
lines.push(padLine('', width));
|
|
320
|
+
lines.push(padLine(ansi.dim(this.#copy.reviewHint), width));
|
|
321
|
+
return lines;
|
|
322
|
+
}
|
|
323
|
+
#renderTextEditor(index, width) {
|
|
324
|
+
const field = this.input.request.fields[index];
|
|
325
|
+
if (!field)
|
|
326
|
+
return [];
|
|
327
|
+
this.#editor.focused = true;
|
|
328
|
+
return [
|
|
329
|
+
padLine(`${safeDisplay(field.label)} (${field.required ? this.#copy.required : this.#copy.optional})`, width),
|
|
330
|
+
...this.#fieldDetails(field, width),
|
|
331
|
+
...this.#editor.render(width),
|
|
332
|
+
padLine(ansi.dim(this.#copy.textHint), width),
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
#renderChoice(mode, width) {
|
|
336
|
+
const field = this.input.request.fields[mode.index];
|
|
337
|
+
if (!field || (field.kind !== 'boolean' && field.kind !== 'single_select'))
|
|
338
|
+
return [];
|
|
339
|
+
const options = field.kind === 'boolean'
|
|
340
|
+
? [this.#copy.trueValue, this.#copy.falseValue]
|
|
341
|
+
: field.options.map((option) => safeDisplay(option.label));
|
|
342
|
+
const window = visibleWindow(options.length, mode.optionIndex, FORM_CHOICE_MAX_VISIBLE_OPTIONS);
|
|
343
|
+
return [
|
|
344
|
+
padLine(safeDisplay(field.label), width),
|
|
345
|
+
...this.#fieldDetails(field, width),
|
|
346
|
+
...(window.start > 0 ? [padLine(ansi.dim(` … ↑ ${window.start}`), width)] : []),
|
|
347
|
+
...options.slice(window.start, window.end).map((label, offset) => {
|
|
348
|
+
const index = window.start + offset;
|
|
349
|
+
return formatReviewRow(`${index === mode.optionIndex ? '→ ' : ' '}${label}`, index === mode.optionIndex, width);
|
|
350
|
+
}),
|
|
351
|
+
...(window.end < options.length
|
|
352
|
+
? [padLine(ansi.dim(` … ↓ ${options.length - window.end}`), width)]
|
|
353
|
+
: []),
|
|
354
|
+
padLine(ansi.dim(this.#copy.choiceHint), width),
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
#renderMulti(mode, width) {
|
|
358
|
+
const field = this.input.request.fields[mode.index];
|
|
359
|
+
const draft = this.#drafts[mode.index];
|
|
360
|
+
if (!field || field.kind !== 'multi_select' || !draft || !Array.isArray(draft.value))
|
|
361
|
+
return [];
|
|
362
|
+
const selected = draft.value;
|
|
363
|
+
const window = visibleWindow(field.options.length, mode.optionIndex, FORM_CHOICE_MAX_VISIBLE_OPTIONS);
|
|
364
|
+
return [
|
|
365
|
+
padLine(safeDisplay(field.label), width),
|
|
366
|
+
...this.#fieldDetails(field, width),
|
|
367
|
+
...(window.start > 0 ? [padLine(ansi.dim(` … ↑ ${window.start}`), width)] : []),
|
|
368
|
+
...field.options.slice(window.start, window.end).map((option, offset) => {
|
|
369
|
+
const index = window.start + offset;
|
|
370
|
+
const row = `${index === mode.optionIndex ? '→ ' : ' '}[${selected.includes(option.value) ? 'x' : ' '}] ${safeDisplay(option.label)}`;
|
|
371
|
+
return formatReviewRow(row, index === mode.optionIndex, width);
|
|
372
|
+
}),
|
|
373
|
+
...(window.end < field.options.length
|
|
374
|
+
? [padLine(ansi.dim(` … ↓ ${field.options.length - window.end}`), width)]
|
|
375
|
+
: []),
|
|
376
|
+
padLine(ansi.dim(this.#copy.multiHint), width),
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
#summary(field, draft) {
|
|
380
|
+
if (!draft.included)
|
|
381
|
+
return ansi.dim(this.#copy.omitted);
|
|
382
|
+
if (field.kind === 'boolean')
|
|
383
|
+
return draft.value === true ? this.#copy.trueValue : this.#copy.falseValue;
|
|
384
|
+
if (field.kind === 'single_select') {
|
|
385
|
+
const option = field.options.find((candidate) => candidate.value === draft.value);
|
|
386
|
+
return option ? safeDisplay(option.label) : ansi.dim(this.#copy.empty);
|
|
387
|
+
}
|
|
388
|
+
if (field.kind === 'multi_select' && Array.isArray(draft.value)) {
|
|
389
|
+
return formatUiMessage(this.#copy.selectedCount, { count: draft.value.length }, this.input.locale);
|
|
390
|
+
}
|
|
391
|
+
return typeof draft.value === 'string' && draft.value.length > 0
|
|
392
|
+
? safeDisplay(draft.value)
|
|
393
|
+
: ansi.dim(this.#copy.empty);
|
|
394
|
+
}
|
|
395
|
+
#fieldDetails(field, width) {
|
|
396
|
+
const details = field.description ? [safeDisplay(field.description)] : [];
|
|
397
|
+
const constraint = this.#constraint(field);
|
|
398
|
+
if (constraint)
|
|
399
|
+
details.push(constraint);
|
|
400
|
+
return details.map((detail) => padLine(ansi.dim(detail), width));
|
|
401
|
+
}
|
|
402
|
+
#constraint(field) {
|
|
403
|
+
const constraints = [];
|
|
404
|
+
if (field.kind === 'string') {
|
|
405
|
+
if (field.minLength !== undefined && field.maxLength !== undefined) {
|
|
406
|
+
constraints.push(formatUiMessage(this.#copy.lengthRange, {
|
|
407
|
+
minimum: field.minLength,
|
|
408
|
+
maximum: field.maxLength,
|
|
409
|
+
}, this.input.locale));
|
|
410
|
+
}
|
|
411
|
+
else if (field.minLength !== undefined) {
|
|
412
|
+
constraints.push(formatUiMessage(this.#copy.minimumLength, {
|
|
413
|
+
minimum: field.minLength,
|
|
414
|
+
}, this.input.locale));
|
|
415
|
+
}
|
|
416
|
+
else if (field.maxLength !== undefined) {
|
|
417
|
+
constraints.push(formatUiMessage(this.#copy.maximumLength, {
|
|
418
|
+
maximum: field.maxLength,
|
|
419
|
+
}, this.input.locale));
|
|
420
|
+
}
|
|
421
|
+
if (field.format !== undefined) {
|
|
422
|
+
constraints.push(formatUiMessage(this.#copy.format, {
|
|
423
|
+
format: field.format,
|
|
424
|
+
}, this.input.locale));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else if (field.kind === 'number' || field.kind === 'integer') {
|
|
428
|
+
if (field.minimum !== undefined && field.maximum !== undefined) {
|
|
429
|
+
constraints.push(formatUiMessage(this.#copy.valueRange, {
|
|
430
|
+
minimum: field.minimum,
|
|
431
|
+
maximum: field.maximum,
|
|
432
|
+
}, this.input.locale));
|
|
433
|
+
}
|
|
434
|
+
else if (field.minimum !== undefined) {
|
|
435
|
+
constraints.push(formatUiMessage(this.#copy.minimumValue, {
|
|
436
|
+
minimum: field.minimum,
|
|
437
|
+
}, this.input.locale));
|
|
438
|
+
}
|
|
439
|
+
else if (field.maximum !== undefined) {
|
|
440
|
+
constraints.push(formatUiMessage(this.#copy.maximumValue, {
|
|
441
|
+
maximum: field.maximum,
|
|
442
|
+
}, this.input.locale));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (field.kind === 'multi_select') {
|
|
446
|
+
if (field.minItems !== undefined && field.maxItems !== undefined) {
|
|
447
|
+
constraints.push(formatUiMessage(this.#copy.itemRange, {
|
|
448
|
+
minimum: field.minItems,
|
|
449
|
+
maximum: field.maxItems,
|
|
450
|
+
}, this.input.locale));
|
|
451
|
+
}
|
|
452
|
+
else if (field.minItems !== undefined) {
|
|
453
|
+
constraints.push(formatUiMessage(this.#copy.minimumItems, {
|
|
454
|
+
minimum: field.minItems,
|
|
455
|
+
}, this.input.locale));
|
|
456
|
+
}
|
|
457
|
+
else if (field.maxItems !== undefined) {
|
|
458
|
+
constraints.push(formatUiMessage(this.#copy.maximumItems, {
|
|
459
|
+
maximum: field.maxItems,
|
|
460
|
+
}, this.input.locale));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return constraints.length === 0 ? undefined : constraints.join(' · ');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function cloneTuiFormDrafts(drafts) {
|
|
467
|
+
return drafts.map((draft) => ({
|
|
468
|
+
included: draft.included,
|
|
469
|
+
value: Array.isArray(draft.value) ? [...draft.value] : draft.value,
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
function draftValue(field, draft) {
|
|
473
|
+
if (field.kind === 'number' || field.kind === 'integer') {
|
|
474
|
+
return typeof draft.value === 'string' && draft.value.trim() !== ''
|
|
475
|
+
? Number(draft.value)
|
|
476
|
+
: Number.NaN;
|
|
477
|
+
}
|
|
478
|
+
return draft.value;
|
|
479
|
+
}
|
|
480
|
+
function safeDisplay(value) {
|
|
481
|
+
return sanitizeUnicodeText(stripAnsi(value), { maxCodePoints: 1_024 });
|
|
482
|
+
}
|
|
483
|
+
function formatReviewRow(text, active, width) {
|
|
484
|
+
const padded = padLine(text, width);
|
|
485
|
+
return active ? ansi.reverse(padded) : padded;
|
|
486
|
+
}
|
|
487
|
+
function padLine(text, width) {
|
|
488
|
+
const safeWidth = Math.max(1, width);
|
|
489
|
+
const trimmed = visibleWidth(text) > safeWidth ? truncateToWidth(text, safeWidth, '') : text;
|
|
490
|
+
return `${trimmed}${' '.repeat(Math.max(0, safeWidth - visibleWidth(trimmed)))}`;
|
|
491
|
+
}
|
|
492
|
+
function visibleWindow(length, activeIndex, limit) {
|
|
493
|
+
if (length <= limit)
|
|
494
|
+
return { start: 0, end: length };
|
|
495
|
+
const start = Math.min(Math.max(0, activeIndex - Math.floor(limit / 2)), Math.max(0, length - limit));
|
|
496
|
+
return { start, end: Math.min(length, start + limit) };
|
|
497
|
+
}
|