mixdog 0.9.1 → 0.9.3
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/package.json +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* components/ConfirmBar.jsx — horizontal text-button bar for onboarding steps.
|
|
3
|
+
*
|
|
4
|
+
* Pure render + helpers. Owns NO keyboard state: the parent Picker manages
|
|
5
|
+
* `focusedIndex` (0..n-1) and dispatches Enter to `onConfirm`. Kept side-effect
|
|
6
|
+
* free so it can be reused under any picker without stealing input focus.
|
|
7
|
+
*
|
|
8
|
+
* Props:
|
|
9
|
+
* buttons: [{ value, label }] — button descriptors
|
|
10
|
+
* focusedIndex: number — highlighted button (-1 = none/list focus)
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { Box, Text } from 'ink';
|
|
14
|
+
import { theme } from '../theme.mjs';
|
|
15
|
+
|
|
16
|
+
/** Clamp a focus index into the valid button range, or -1 when list-focused. */
|
|
17
|
+
export function clampConfirmFocus(index, count) {
|
|
18
|
+
const n = Math.max(0, Number(count) || 0);
|
|
19
|
+
if (n === 0) return -1;
|
|
20
|
+
const i = Number(index);
|
|
21
|
+
if (!Number.isFinite(i) || i < 0) return -1;
|
|
22
|
+
return Math.min(i, n - 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ConfirmBar({ buttons = [], focusedIndex = -1 }) {
|
|
26
|
+
const list = Array.isArray(buttons) ? buttons.filter(Boolean) : [];
|
|
27
|
+
if (list.length === 0) return null;
|
|
28
|
+
return (
|
|
29
|
+
<Box flexDirection="row" width="100%" justifyContent="flex-end">
|
|
30
|
+
{list.map((button, index) => {
|
|
31
|
+
const isFocused = index === focusedIndex;
|
|
32
|
+
return (
|
|
33
|
+
<Text key={button.value ?? index}>
|
|
34
|
+
{index > 0 ? <Text color={theme.subtle}>{' '}</Text> : null}
|
|
35
|
+
<Text
|
|
36
|
+
color={isFocused ? theme.selectionText : theme.text}
|
|
37
|
+
backgroundColor={isFocused ? theme.selectionBackground : undefined}
|
|
38
|
+
bold={isFocused}
|
|
39
|
+
>
|
|
40
|
+
{` ${button.label} `}
|
|
41
|
+
</Text>
|
|
42
|
+
</Text>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
</Box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -47,7 +47,7 @@ function percent(value, total) {
|
|
|
47
47
|
|
|
48
48
|
function percentLabel(value, total) {
|
|
49
49
|
const pct = percent(value, total);
|
|
50
|
-
if (pct === null) return '
|
|
50
|
+
if (pct === null) return 'N/A';
|
|
51
51
|
return `${pct > 0 && pct < 1 ? pct.toFixed(1) : Math.round(pct)}%`;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -185,8 +185,10 @@ function ContextUsageView({ detail, columns }) {
|
|
|
185
185
|
]);
|
|
186
186
|
const apiLine = metricValue([
|
|
187
187
|
`last ctx ${formatTokens(lastApi.contextTokens)}`,
|
|
188
|
-
`
|
|
189
|
-
`
|
|
188
|
+
`uncached/out ${formatTokens(lastApi.inputTokens)}/${formatTokens(lastApi.outputTokens)}`,
|
|
189
|
+
lastApi.rawInputTokens && lastApi.rawInputTokens !== lastApi.inputTokens ? `raw in ${formatTokens(lastApi.rawInputTokens)}` : '',
|
|
190
|
+
cache.writeTokens ? `write ${formatTokens(cache.writeTokens)}` : '',
|
|
191
|
+
`cache ${cache.hitRate || 'N/A'}`,
|
|
190
192
|
]);
|
|
191
193
|
const categories = [
|
|
192
194
|
{ label: 'Messages', tokens: semanticTokens(semantic, ['chat', 'assistant']), meta: '' },
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overprint a truncated right-side hint on the last row of a transcript item
|
|
3
|
+
* without reserving an extra layout row (negative margin pulls the hint band up).
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
import { theme } from '../theme.mjs';
|
|
8
|
+
|
|
9
|
+
function hintColor(tone) {
|
|
10
|
+
if (tone === 'error') return theme.error;
|
|
11
|
+
if (tone === 'warn' || tone === 'cancel') return theme.warning;
|
|
12
|
+
if (tone === 'plain') return theme.subtle;
|
|
13
|
+
return theme.inactive;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cleanRightMessage(value) {
|
|
17
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ItemRightHintOverprint({
|
|
21
|
+
children,
|
|
22
|
+
rightMessage = '',
|
|
23
|
+
rightTone = 'info',
|
|
24
|
+
rightMessageWidth = 24,
|
|
25
|
+
}) {
|
|
26
|
+
const rightText = cleanRightMessage(rightMessage);
|
|
27
|
+
if (!rightText) return children;
|
|
28
|
+
const rightWidth = Math.max(1, Number(rightMessageWidth) || 24);
|
|
29
|
+
return (
|
|
30
|
+
<Box flexDirection="column" width="100%" flexShrink={0}>
|
|
31
|
+
{children}
|
|
32
|
+
<Box
|
|
33
|
+
height={1}
|
|
34
|
+
marginTop={-1}
|
|
35
|
+
flexDirection="row"
|
|
36
|
+
width="100%"
|
|
37
|
+
flexShrink={0}
|
|
38
|
+
overflow="hidden"
|
|
39
|
+
>
|
|
40
|
+
<Box flexGrow={1} flexShrink={1} overflow="hidden" />
|
|
41
|
+
<Box
|
|
42
|
+
flexShrink={0}
|
|
43
|
+
width={rightWidth}
|
|
44
|
+
marginLeft={1}
|
|
45
|
+
marginRight={1}
|
|
46
|
+
justifyContent="flex-end"
|
|
47
|
+
overflow="hidden"
|
|
48
|
+
>
|
|
49
|
+
<Text color={hintColor(rightTone)} wrap="truncate">{rightText}</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
</Box>
|
|
52
|
+
</Box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -12,19 +12,28 @@
|
|
|
12
12
|
* Syntax highlighting is omitted, but token cache + streaming-split are
|
|
13
13
|
* kept so partial markdown does not repaint stable text on every delta.
|
|
14
14
|
*/
|
|
15
|
-
import React
|
|
15
|
+
import React from 'react';
|
|
16
16
|
import { Box, Text } from 'ink';
|
|
17
|
-
import { marked } from 'marked';
|
|
18
17
|
import {
|
|
19
|
-
configureMarked,
|
|
20
|
-
hasMarkdownSyntax,
|
|
21
18
|
renderTokenAnsiSegments,
|
|
22
19
|
} from '../markdown/render-ansi.mjs';
|
|
23
|
-
import {
|
|
20
|
+
import { resolveStreamingMarkdownParts } from '../markdown/streaming-markdown.mjs';
|
|
24
21
|
import { AnsiText } from './AnsiText.jsx';
|
|
25
22
|
import { MarkdownTable } from './MarkdownTable.jsx';
|
|
26
23
|
import { theme } from '../theme.mjs';
|
|
27
24
|
|
|
25
|
+
export {
|
|
26
|
+
balanceStreamingMarkdown,
|
|
27
|
+
resolveStreamingMarkdownParts,
|
|
28
|
+
resetStreamingMarkdownStablePrefix,
|
|
29
|
+
resetAllStreamingMarkdownStablePrefixes,
|
|
30
|
+
streamingLayoutText,
|
|
31
|
+
} from '../markdown/streaming-markdown.mjs';
|
|
32
|
+
export {
|
|
33
|
+
measureMarkdownRenderedRows,
|
|
34
|
+
measureStreamingMarkdownRenderedRows,
|
|
35
|
+
} from '../markdown/measure-rendered-rows.mjs';
|
|
36
|
+
|
|
28
37
|
function renderMarkdownElements(content, trimPartialFences = false, tableWidth) {
|
|
29
38
|
// `tableWidth` is the App's body/content width; it doubles as the hr fill
|
|
30
39
|
// width. Fall back to 80 when not provided so an hr still spans a sane rule.
|
|
@@ -50,60 +59,6 @@ function renderMarkdownElements(content, trimPartialFences = false, tableWidth)
|
|
|
50
59
|
return result;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
function hasOpenFence(text) {
|
|
54
|
-
let ticks = 0;
|
|
55
|
-
let tildes = 0;
|
|
56
|
-
for (const line of String(text ?? '').split('\n')) {
|
|
57
|
-
if (/^\s*```/.test(line)) ticks += 1;
|
|
58
|
-
if (/^\s*~~~/.test(line)) tildes += 1;
|
|
59
|
-
}
|
|
60
|
-
return ticks % 2 === 1 || tildes % 2 === 1;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function hasOpenInlineCode(text) {
|
|
64
|
-
let count = 0;
|
|
65
|
-
const value = String(text ?? '');
|
|
66
|
-
for (let i = 0; i < value.length; i++) {
|
|
67
|
-
const ch = value[i];
|
|
68
|
-
if (ch === '\\') {
|
|
69
|
-
i += 1;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (ch !== '`') continue;
|
|
73
|
-
let run = 1;
|
|
74
|
-
while (value[i + run] === '`') run += 1;
|
|
75
|
-
if (run === 1) count += 1;
|
|
76
|
-
i += run - 1;
|
|
77
|
-
}
|
|
78
|
-
return count % 2 === 1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function hasUnclosedDelimiter(text, marker) {
|
|
82
|
-
let count = 0;
|
|
83
|
-
const value = String(text ?? '');
|
|
84
|
-
for (let i = 0; i < value.length; i++) {
|
|
85
|
-
if (value[i] === '\\') {
|
|
86
|
-
i += 1;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (value.startsWith(marker, i)) {
|
|
90
|
-
count += 1;
|
|
91
|
-
i += marker.length - 1;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return count % 2 === 1;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function balanceStreamingMarkdown(text) {
|
|
98
|
-
const value = String(text ?? '');
|
|
99
|
-
if (!value || hasOpenFence(value)) return value;
|
|
100
|
-
if (hasOpenInlineCode(value)) return `${value}\``;
|
|
101
|
-
let rendered = value;
|
|
102
|
-
if (hasUnclosedDelimiter(rendered, '**')) rendered += '**';
|
|
103
|
-
if (hasUnclosedDelimiter(rendered, '__')) rendered += '__';
|
|
104
|
-
return rendered;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
62
|
export function Markdown({ children, themeEpoch = 0, trimPartialFences = false, columns }) {
|
|
108
63
|
const elements = React.useMemo(() => {
|
|
109
64
|
try {
|
|
@@ -124,48 +79,17 @@ export function Markdown({ children, themeEpoch = 0, trimPartialFences = false,
|
|
|
124
79
|
);
|
|
125
80
|
}
|
|
126
81
|
|
|
127
|
-
export function StreamingMarkdown({ children, themeEpoch = 0, columns }) {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!hasMarkdownSyntax(text)) {
|
|
132
|
-
stablePrefixRef.current = '';
|
|
133
|
-
return <Markdown themeEpoch={themeEpoch} columns={columns}>{text}</Markdown>;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!text.startsWith(stablePrefixRef.current)) {
|
|
137
|
-
stablePrefixRef.current = '';
|
|
82
|
+
export function StreamingMarkdown({ children, themeEpoch = 0, columns, streamKey }) {
|
|
83
|
+
const parts = resolveStreamingMarkdownParts(children, streamKey);
|
|
84
|
+
if (parts.plain) {
|
|
85
|
+
return <Markdown themeEpoch={themeEpoch} columns={columns}>{parts.unstableForRender}</Markdown>;
|
|
138
86
|
}
|
|
139
|
-
|
|
140
|
-
let stablePrefix = stablePrefixRef.current;
|
|
141
|
-
try {
|
|
142
|
-
configureMarked();
|
|
143
|
-
const boundary = stablePrefix.length;
|
|
144
|
-
// Lex the still-streaming suffix and trim any partial closing fence so an
|
|
145
|
-
// open code block does not grow-then-shrink as the final backtick(s)
|
|
146
|
-
// stream in. Operates on a fresh lex so the shared tokenCache is untouched.
|
|
147
|
-
const tokens = marked.lexer(text.substring(boundary));
|
|
148
|
-
trimPartialClosingFences(tokens);
|
|
149
|
-
let lastContentIdx = tokens.length - 1;
|
|
150
|
-
while (lastContentIdx >= 0 && tokens[lastContentIdx]?.type === 'space') lastContentIdx--;
|
|
151
|
-
let advance = 0;
|
|
152
|
-
for (let i = 0; i < lastContentIdx; i++) {
|
|
153
|
-
advance += tokens[i]?.raw?.length ?? 0;
|
|
154
|
-
}
|
|
155
|
-
if (advance > 0) {
|
|
156
|
-
stablePrefixRef.current = text.substring(0, boundary + advance);
|
|
157
|
-
stablePrefix = stablePrefixRef.current;
|
|
158
|
-
}
|
|
159
|
-
} catch {
|
|
160
|
-
stablePrefix = '';
|
|
161
|
-
stablePrefixRef.current = '';
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const unstableSuffix = text.substring(stablePrefix.length);
|
|
165
87
|
return (
|
|
166
88
|
<Box flexDirection="column" gap={1}>
|
|
167
|
-
{stablePrefix ? <Markdown themeEpoch={themeEpoch} columns={columns}>{stablePrefix}</Markdown> : null}
|
|
168
|
-
{unstableSuffix
|
|
89
|
+
{parts.stablePrefix ? <Markdown themeEpoch={themeEpoch} columns={columns}>{parts.stablePrefix}</Markdown> : null}
|
|
90
|
+
{parts.unstableSuffix
|
|
91
|
+
? <Markdown themeEpoch={themeEpoch} columns={columns} trimPartialFences>{parts.unstableForRender}</Markdown>
|
|
92
|
+
: null}
|
|
169
93
|
</Box>
|
|
170
94
|
);
|
|
171
95
|
}
|
|
@@ -13,18 +13,22 @@
|
|
|
13
13
|
* not under the dot (2-wide gutter alignment).
|
|
14
14
|
*/
|
|
15
15
|
import React from 'react';
|
|
16
|
-
import { Box, Text
|
|
16
|
+
import { Box, Text } from 'ink';
|
|
17
17
|
import { theme, TURN_MARKER } from '../theme.mjs';
|
|
18
|
-
import { Markdown, StreamingMarkdown } from './Markdown.jsx';
|
|
18
|
+
import { Markdown, StreamingMarkdown, resetStreamingMarkdownStablePrefix } from './Markdown.jsx';
|
|
19
19
|
import { assistantBodyWidth } from '../markdown/table-layout.mjs';
|
|
20
|
-
import { THEREFORE } from '../figures.mjs';
|
|
21
|
-
import { formatDuration } from '../time-format.mjs';
|
|
22
20
|
|
|
23
21
|
// `themeEpoch` is a memo-busting prop (threaded from App): the active theme
|
|
24
22
|
// mutates `theme` in-place, so a /theme switch must re-render this memoized row
|
|
25
23
|
// and recompute its markdown colors. It is forwarded into Markdown so the
|
|
26
24
|
// token/AnsiText caches include it as a dep.
|
|
27
|
-
export const AssistantMessage = React.memo(function AssistantMessage({
|
|
25
|
+
export const AssistantMessage = React.memo(function AssistantMessage({
|
|
26
|
+
text,
|
|
27
|
+
streaming = false,
|
|
28
|
+
columns = 80,
|
|
29
|
+
themeEpoch = 0,
|
|
30
|
+
assistantId,
|
|
31
|
+
}) {
|
|
28
32
|
// The body column needs an EXPLICIT numeric width. Without it, ink/Yoga
|
|
29
33
|
// measures the wrapped markdown body before the row's available width is
|
|
30
34
|
// resolved and caches its height as a single row — so a multi-line assistant
|
|
@@ -34,6 +38,10 @@ export const AssistantMessage = React.memo(function AssistantMessage({ text, str
|
|
|
34
38
|
// assistant line reach the terminal's last column can auto-wrap/scroll on
|
|
35
39
|
// Windows Terminal/conhost and make the next redraw appear to lose leading
|
|
36
40
|
// CJK characters even though the backing transcript is intact.
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
if (!streaming && assistantId) resetStreamingMarkdownStablePrefix(assistantId);
|
|
43
|
+
}, [streaming, assistantId]);
|
|
44
|
+
|
|
37
45
|
const bodyWidth = assistantBodyWidth(columns);
|
|
38
46
|
return (
|
|
39
47
|
<Box flexDirection="row" marginTop={1}>
|
|
@@ -42,7 +50,7 @@ export const AssistantMessage = React.memo(function AssistantMessage({ text, str
|
|
|
42
50
|
</Box>
|
|
43
51
|
<Box flexDirection="column" flexShrink={0} width={bodyWidth}>
|
|
44
52
|
{streaming
|
|
45
|
-
? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</StreamingMarkdown>
|
|
53
|
+
? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth} streamKey={assistantId}>{text}</StreamingMarkdown>
|
|
46
54
|
: <Markdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</Markdown>}
|
|
47
55
|
</Box>
|
|
48
56
|
</Box>
|
|
@@ -68,35 +76,6 @@ export const UserMessage = React.memo(function UserMessage({ text, attached = fa
|
|
|
68
76
|
);
|
|
69
77
|
});
|
|
70
78
|
|
|
71
|
-
function formatThinkingElapsed(ms) {
|
|
72
|
-
const label = formatDuration(ms);
|
|
73
|
-
if (!label) return '';
|
|
74
|
-
const totalSec = Math.floor(Math.max(0, Number(ms || 0)) / 1000);
|
|
75
|
-
if (totalSec < 60) return label;
|
|
76
|
-
const m = Math.floor(totalSec / 60);
|
|
77
|
-
const s = totalSec % 60;
|
|
78
|
-
return `${m}:${String(s).padStart(2, '0')}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function ThinkingMessage({ text, elapsedMs = 0, activeSince = 0 }) {
|
|
82
|
-
useAnimation({ interval: 250 });
|
|
83
|
-
const liveElapsedMs = Number(elapsedMs || 0) + (activeSince ? Math.max(0, Date.now() - activeSince) : 0);
|
|
84
|
-
const elapsed = formatThinkingElapsed(liveElapsedMs);
|
|
85
|
-
return (
|
|
86
|
-
<Box flexDirection="column" marginTop={1} gap={1} width="100%">
|
|
87
|
-
<Text>
|
|
88
|
-
<Text color={theme.spinnerGlyph}>{THEREFORE} </Text>
|
|
89
|
-
<Text color={theme.thinkingAccent}>{`Thinking${elapsed ? ` ${elapsed}` : ''}…`}</Text>
|
|
90
|
-
</Text>
|
|
91
|
-
{text ? (
|
|
92
|
-
<Box paddingLeft={2}>
|
|
93
|
-
<Text color={theme.thinkingText} italic>{text}</Text>
|
|
94
|
-
</Box>
|
|
95
|
-
) : null}
|
|
96
|
-
</Box>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
79
|
export function NoticeMessage({ text, tone, columns = 80 }) {
|
|
101
80
|
const accentColor = tone === 'error' ? theme.error : tone === 'warn' ? theme.warning : theme.inactive;
|
|
102
81
|
const bodyColor = tone === 'info' || tone === 'plain' ? theme.inactive : theme.statusText;
|
|
@@ -17,12 +17,14 @@ import React, { useState, useCallback, useEffect } from 'react';
|
|
|
17
17
|
import { Box, Text, useInput } from 'ink';
|
|
18
18
|
import stringWidth from 'string-width';
|
|
19
19
|
import { theme } from '../theme.mjs';
|
|
20
|
+
import { ConfirmBar, clampConfirmFocus } from './ConfirmBar.jsx';
|
|
20
21
|
|
|
21
22
|
/** Max items visible at once before scrolling kicks in. */
|
|
22
23
|
const MAX_VISIBLE = 8;
|
|
23
24
|
const DEFAULT_LABEL_WIDTH = 28;
|
|
24
25
|
const SELECT_HELP = '↑/↓ Select · Enter Choose · Esc Back';
|
|
25
26
|
const ADJUST_HELP = '↑/↓ Select · ←/→ Adjust · Enter Choose · Esc Back';
|
|
27
|
+
const CONFIRM_HELP = '↑/↓ Select · ←/→ Back/Next · Enter Choose · Esc Skip';
|
|
26
28
|
|
|
27
29
|
function truncateText(value, width) {
|
|
28
30
|
const text = String(value || '');
|
|
@@ -92,6 +94,10 @@ export function Picker({
|
|
|
92
94
|
indexMode = 'auto',
|
|
93
95
|
fillHeight = false,
|
|
94
96
|
visibleCount = MAX_VISIBLE,
|
|
97
|
+
// Onboarding confirm bar: { buttons:[{value,label}], onConfirm(button,index) }.
|
|
98
|
+
// When present, ←/→ and Tab drive button focus (mutually exclusive with
|
|
99
|
+
// onLeft/onRight), and Enter fires onConfirm while a button is focused.
|
|
100
|
+
confirmBar = null,
|
|
95
101
|
// Memo-busting epoch: ItemRow is React.memo and reads theme.* directly, so a
|
|
96
102
|
// live /theme switch (or picker preview) must re-render every row. Threading
|
|
97
103
|
// the epoch into each ItemRow breaks its shallow-equality on a theme change.
|
|
@@ -99,6 +105,14 @@ export function Picker({
|
|
|
99
105
|
}) {
|
|
100
106
|
const visibleLimit = Math.max(1, Math.floor(Number(visibleCount) || MAX_VISIBLE));
|
|
101
107
|
const [selectedIndex, setSelectedIndex] = useState(() => Math.max(0, Math.min(Number(initialIndex) || 0, Math.max(0, items.length - 1))));
|
|
108
|
+
const confirmButtons = Array.isArray(confirmBar?.buttons) ? confirmBar.buttons.filter(Boolean) : [];
|
|
109
|
+
const hasConfirm = confirmButtons.length > 0;
|
|
110
|
+
// -1 = list focus; 0..n-1 = confirm-bar button focus.
|
|
111
|
+
const [confirmFocus, setConfirmFocus] = useState(-1);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
// Reset to list focus whenever the bar identity/shape changes (step switch).
|
|
114
|
+
setConfirmFocus(-1);
|
|
115
|
+
}, [confirmButtons.length, confirmBar]);
|
|
102
116
|
|
|
103
117
|
useEffect(() => {
|
|
104
118
|
setSelectedIndex((i) => Math.min(Math.max(0, i), Math.max(0, items.length - 1)));
|
|
@@ -122,13 +136,24 @@ export function Picker({
|
|
|
122
136
|
const footerLines = normalizeFooterLines(activeFooter, columns);
|
|
123
137
|
const footerGap = footerLines.length > 0 ? Math.max(0, Math.floor(Number(footerGapRows) || 0)) : 0;
|
|
124
138
|
const footerReserveRows = footerLines.length > 0 ? footerLines.length + footerGap : 0;
|
|
125
|
-
const
|
|
126
|
-
const
|
|
139
|
+
const confirmReserveRows = hasConfirm ? 2 : 0;
|
|
140
|
+
const effectiveVisibleLimit = Math.max(1, visibleLimit - footerReserveRows - confirmReserveRows);
|
|
141
|
+
const helpText = help || (hasConfirm ? CONFIRM_HELP : (onLeft || onRight || onTab ? ADJUST_HELP : SELECT_HELP));
|
|
127
142
|
|
|
128
143
|
useInput(
|
|
129
144
|
useCallback(
|
|
130
145
|
(input, key) => {
|
|
131
146
|
if (key.upArrow) {
|
|
147
|
+
// Single vertical loop over [list items...] + [confirm buttons...].
|
|
148
|
+
if (hasConfirm) {
|
|
149
|
+
const last = items.length - 1;
|
|
150
|
+
if (confirmFocus > 0) { setConfirmFocus((f) => f - 1); return; }
|
|
151
|
+
if (confirmFocus === 0) { setConfirmFocus(-1); setSelectedIndex(Math.max(0, last)); return; }
|
|
152
|
+
// list focus: first row ↑ → last confirm button.
|
|
153
|
+
if (items.length === 0 || selectedIndex === 0) { setConfirmFocus(confirmButtons.length - 1); return; }
|
|
154
|
+
setSelectedIndex((i) => i - 1);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
132
157
|
setSelectedIndex((i) => {
|
|
133
158
|
const total = items.length;
|
|
134
159
|
return total > 0 ? (i - 1 + total) % total : 0;
|
|
@@ -136,6 +161,19 @@ export function Picker({
|
|
|
136
161
|
return;
|
|
137
162
|
}
|
|
138
163
|
if (key.downArrow) {
|
|
164
|
+
if (hasConfirm) {
|
|
165
|
+
const last = items.length - 1;
|
|
166
|
+
const lastBtn = confirmButtons.length - 1;
|
|
167
|
+
if (confirmFocus >= 0) {
|
|
168
|
+
if (confirmFocus < lastBtn) { setConfirmFocus((f) => f + 1); return; }
|
|
169
|
+
// last button ↓ → first list row.
|
|
170
|
+
setConfirmFocus(-1); setSelectedIndex(0); return;
|
|
171
|
+
}
|
|
172
|
+
// list focus: last row ↓ → first confirm button.
|
|
173
|
+
if (items.length === 0 || selectedIndex === last) { setConfirmFocus(0); return; }
|
|
174
|
+
setSelectedIndex((i) => i + 1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
139
177
|
setSelectedIndex((i) => {
|
|
140
178
|
const total = items.length;
|
|
141
179
|
return total > 0 ? (i + 1) % total : 0;
|
|
@@ -159,18 +197,35 @@ export function Picker({
|
|
|
159
197
|
return;
|
|
160
198
|
}
|
|
161
199
|
if (key.leftArrow) {
|
|
200
|
+
if (hasConfirm) {
|
|
201
|
+
setConfirmFocus((f) => (f <= 0 ? -1 : f - 1));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
162
204
|
if (onLeft) onLeft(items[selectedIndex], selectedIndex);
|
|
163
205
|
return;
|
|
164
206
|
}
|
|
165
207
|
if (key.rightArrow) {
|
|
208
|
+
if (hasConfirm) {
|
|
209
|
+
setConfirmFocus((f) => (f < 0 ? 0 : Math.min(confirmButtons.length - 1, f + 1)));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
166
212
|
if (onRight) onRight(items[selectedIndex], selectedIndex);
|
|
167
213
|
return;
|
|
168
214
|
}
|
|
169
215
|
if (key.tab || input === '\t') {
|
|
216
|
+
if (hasConfirm) {
|
|
217
|
+
setConfirmFocus((f) => (f < 0 ? 0 : (f + 1 > confirmButtons.length - 1 ? -1 : f + 1)));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
170
220
|
if (onTab) onTab(items[selectedIndex], selectedIndex);
|
|
171
221
|
return;
|
|
172
222
|
}
|
|
173
223
|
if (key.return) {
|
|
224
|
+
if (hasConfirm && confirmFocus >= 0) {
|
|
225
|
+
const button = confirmButtons[confirmFocus];
|
|
226
|
+
if (button && confirmBar?.onConfirm) confirmBar.onConfirm(button, confirmFocus);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
174
229
|
const selected = items[selectedIndex];
|
|
175
230
|
if (selected && onSelect) onSelect(selected.value, selected);
|
|
176
231
|
return;
|
|
@@ -187,7 +242,7 @@ export function Picker({
|
|
|
187
242
|
return;
|
|
188
243
|
}
|
|
189
244
|
},
|
|
190
|
-
[items, selectedIndex, onSelect, onCancel, onLeft, onRight, onTab, onKey, effectiveVisibleLimit],
|
|
245
|
+
[items, selectedIndex, onSelect, onCancel, onLeft, onRight, onTab, onKey, effectiveVisibleLimit, hasConfirm, confirmFocus, confirmButtons, confirmBar],
|
|
191
246
|
),
|
|
192
247
|
);
|
|
193
248
|
|
|
@@ -214,6 +269,13 @@ export function Picker({
|
|
|
214
269
|
<Text color={theme.text}>{emptyLine || ' '}</Text>
|
|
215
270
|
<Text> </Text>
|
|
216
271
|
<Text color={theme.inactive}>(empty)</Text>
|
|
272
|
+
{hasConfirm ? (
|
|
273
|
+
<>
|
|
274
|
+
<Box flexGrow={1} />
|
|
275
|
+
<Text> </Text>
|
|
276
|
+
<ConfirmBar buttons={confirmButtons} focusedIndex={clampConfirmFocus(confirmFocus, confirmButtons.length)} />
|
|
277
|
+
</>
|
|
278
|
+
) : null}
|
|
217
279
|
</Box>
|
|
218
280
|
</Box>
|
|
219
281
|
);
|
|
@@ -270,7 +332,7 @@ export function Picker({
|
|
|
270
332
|
<Text> </Text>
|
|
271
333
|
{visible.map((item, i) => {
|
|
272
334
|
const idx = start + i;
|
|
273
|
-
const isSelected = idx === selectedIndex;
|
|
335
|
+
const isSelected = idx === selectedIndex && confirmFocus < 0;
|
|
274
336
|
return (
|
|
275
337
|
<ItemRow
|
|
276
338
|
key={item.value}
|
|
@@ -305,12 +367,21 @@ export function Picker({
|
|
|
305
367
|
))}
|
|
306
368
|
</>
|
|
307
369
|
) : null}
|
|
370
|
+
{hasConfirm ? (
|
|
371
|
+
<>
|
|
372
|
+
{footerLines.length > 0 ? null : <Box flexGrow={1} />}
|
|
373
|
+
<Text> </Text>
|
|
374
|
+
<ConfirmBar buttons={confirmButtons} focusedIndex={clampConfirmFocus(confirmFocus, confirmButtons.length)} />
|
|
375
|
+
</>
|
|
376
|
+
) : null}
|
|
308
377
|
</Box>
|
|
309
378
|
</Box>
|
|
310
379
|
);
|
|
311
380
|
}
|
|
312
381
|
|
|
313
382
|
const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, marker, markerColor, markerWidth, label, labelSuffix, labelSuffixColor, meta, metaParts, description, labelWidth, metaWidth, descriptionWidth, showMeta, isSelected, themeEpoch = 0 }) {
|
|
383
|
+
const rowText = isSelected ? theme.selectionText : theme.text;
|
|
384
|
+
const rowIndexColor = isSelected ? theme.selectionText : theme.subtle;
|
|
314
385
|
const rawSuffix = String(labelSuffix || '');
|
|
315
386
|
const suffix = rawSuffix ? truncateText(rawSuffix, labelWidth) : '';
|
|
316
387
|
const suffixGap = suffix && stringWidth(suffix) < labelWidth ? ' ' : '';
|
|
@@ -323,22 +394,26 @@ const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, marker, mar
|
|
|
323
394
|
const parts = Array.isArray(metaParts) ? metaParts : null;
|
|
324
395
|
|
|
325
396
|
return (
|
|
326
|
-
<Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.
|
|
397
|
+
<Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.selectionBackground : undefined}>
|
|
327
398
|
{indexWidth > 0 ? (
|
|
328
|
-
<Text color={
|
|
399
|
+
<Text color={rowIndexColor}>
|
|
329
400
|
{padCells(indexText, indexWidth)}{' '}
|
|
330
401
|
</Text>
|
|
331
402
|
) : null}
|
|
332
403
|
{markerWidth > 0 ? (
|
|
333
|
-
<Text color={marker ? (markerColor || theme.success) :
|
|
404
|
+
<Text color={isSelected ? theme.selectionText : (marker ? (markerColor || theme.success) : rowText)}>
|
|
334
405
|
{padCells(displayMarker, markerWidth)}
|
|
335
406
|
</Text>
|
|
336
407
|
) : null}
|
|
337
|
-
<Text color={
|
|
338
|
-
{suffix ?
|
|
339
|
-
|
|
408
|
+
<Text color={rowText}>{displayLabel}</Text>
|
|
409
|
+
{suffix ? (
|
|
410
|
+
<Text color={isSelected ? theme.selectionText : (labelSuffixColor || theme.success)}>
|
|
411
|
+
{suffixGap}{suffix}
|
|
412
|
+
</Text>
|
|
413
|
+
) : null}
|
|
414
|
+
<Text color={rowText}>{labelPadding}</Text>
|
|
340
415
|
{showMeta ? (
|
|
341
|
-
<Text color={
|
|
416
|
+
<Text color={rowText}>
|
|
342
417
|
{' '}
|
|
343
418
|
{parts
|
|
344
419
|
? padCells(parts.map((part) => padCells(truncateText(part?.text || '', Number(part?.width) || 1), Number(part?.width) || 1)).join(' '), metaWidth)
|
|
@@ -346,7 +421,7 @@ const ItemRow = React.memo(function ItemRow({ indexText, indexWidth, marker, mar
|
|
|
346
421
|
</Text>
|
|
347
422
|
) : null}
|
|
348
423
|
{displayDescription ? (
|
|
349
|
-
<Text color={
|
|
424
|
+
<Text color={rowText}>
|
|
350
425
|
{' '}
|
|
351
426
|
{displayDescription}
|
|
352
427
|
</Text>
|