@namzu/cli 26.2.0 → 27.0.0
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 +312 -0
- package/README.md +49 -34
- package/dist/bin.js +7 -0
- package/dist/bin.js.map +1 -1
- package/dist/checkpoints/store.d.ts +17 -4
- package/dist/checkpoints/store.d.ts.map +1 -1
- package/dist/checkpoints/store.js +20 -5
- package/dist/checkpoints/store.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts +23 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +41 -2
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts +69 -2
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +214 -134
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/eval.js +1 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/provider-wait.d.ts +5 -5
- package/dist/commands/provider-wait.d.ts.map +1 -1
- package/dist/commands/provider-wait.js +2 -2
- package/dist/commands/resident.d.ts.map +1 -1
- package/dist/commands/resident.js +2 -1
- package/dist/commands/resident.js.map +1 -1
- package/dist/commands/run-flags.d.ts +7 -7
- package/dist/commands/run-flags.js +4 -4
- package/dist/commands/run-stream.d.ts +16 -12
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +142 -98
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts +6 -4
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +129 -45
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/serve.js +2 -2
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/state.js +8 -8
- package/dist/commands/state.js.map +1 -1
- package/dist/config/load.d.ts +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +17 -5
- package/dist/config/load.js.map +1 -1
- package/dist/config/run-limits.d.ts +20 -8
- package/dist/config/run-limits.d.ts.map +1 -1
- package/dist/config/run-limits.js +28 -46
- package/dist/config/run-limits.js.map +1 -1
- package/dist/config/schema.d.ts +44 -19
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/tool-result-screens.js +1 -1
- package/dist/context/directories.d.ts +37 -0
- package/dist/context/directories.d.ts.map +1 -0
- package/dist/context/directories.js +69 -0
- package/dist/context/directories.js.map +1 -0
- package/dist/context/environment.d.ts +55 -0
- package/dist/context/environment.d.ts.map +1 -1
- package/dist/context/environment.js +96 -0
- package/dist/context/environment.js.map +1 -1
- package/dist/context/project-tracker.d.ts +3 -3
- package/dist/context/project-tracker.d.ts.map +1 -1
- package/dist/context/project-tracker.js +3 -3
- package/dist/context/project-tracker.js.map +1 -1
- package/dist/context/project.d.ts +1 -1
- package/dist/context/project.js +1 -1
- package/dist/context/sandbox.d.ts +22 -3
- package/dist/context/sandbox.d.ts.map +1 -1
- package/dist/context/sandbox.js +42 -4
- package/dist/context/sandbox.js.map +1 -1
- package/dist/doctor/checks/chain.d.ts +1 -1
- package/dist/doctor/checks/chain.js +5 -5
- package/dist/doctor/checks/chain.js.map +1 -1
- package/dist/doctor/checks/invariants.d.ts +2 -2
- package/dist/doctor/checks/invariants.js +5 -5
- package/dist/doctor/checks/invariants.js.map +1 -1
- package/dist/doctor/checks/logging.js +1 -1
- package/dist/doctor/checks/logging.js.map +1 -1
- package/dist/doctor/checks/sandbox.js +1 -1
- package/dist/doctor/checks/sandbox.js.map +1 -1
- package/dist/doctor/checks/session-export.d.ts +1 -1
- package/dist/doctor/checks/session-export.js +2 -2
- package/dist/doctor/checks/session-export.js.map +1 -1
- package/dist/doctor/checks/telemetry.d.ts +1 -1
- package/dist/doctor/checks/telemetry.js +1 -1
- package/dist/doctor/registry.js +2 -2
- package/dist/doctor/registry.js.map +1 -1
- package/dist/exit-codes.d.ts +2 -2
- package/dist/exit-codes.js +2 -2
- package/dist/integrations/mcp/servers.d.ts +1 -1
- package/dist/integrations/providers/chain-capabilities.d.ts +6 -6
- package/dist/integrations/providers/chain-capabilities.js +8 -8
- package/dist/integrations/providers/chain-capabilities.js.map +1 -1
- package/dist/integrations/providers/index.d.ts +1 -1
- package/dist/integrations/providers/index.d.ts.map +1 -1
- package/dist/integrations/providers/index.js +1 -1
- package/dist/integrations/providers/index.js.map +1 -1
- package/dist/integrations/providers/register.d.ts +1 -1
- package/dist/integrations/providers/register.js +1 -1
- package/dist/integrations/providers/registry.d.ts +74 -1
- package/dist/integrations/providers/registry.d.ts.map +1 -1
- package/dist/integrations/providers/registry.js +75 -0
- package/dist/integrations/providers/registry.js.map +1 -1
- package/dist/integrations/resident/inspection.d.ts +4 -1
- package/dist/integrations/resident/inspection.d.ts.map +1 -1
- package/dist/integrations/resident/inspection.js +53 -34
- package/dist/integrations/resident/inspection.js.map +1 -1
- package/dist/integrations/resident/learning-storage.d.ts +7 -2
- package/dist/integrations/resident/learning-storage.d.ts.map +1 -1
- package/dist/integrations/resident/learning-storage.js +12 -8
- package/dist/integrations/resident/learning-storage.js.map +1 -1
- package/dist/integrations/resident/runner-store.d.ts +1 -1
- package/dist/integrations/resident/runner-store.d.ts.map +1 -1
- package/dist/integrations/resident/runner-store.js +5 -2
- package/dist/integrations/resident/runner-store.js.map +1 -1
- package/dist/integrations/resident/runner-worker.js +1 -0
- package/dist/integrations/resident/runner-worker.js.map +1 -1
- package/dist/integrations/resident/session-log-reads.d.ts +37 -0
- package/dist/integrations/resident/session-log-reads.d.ts.map +1 -0
- package/dist/integrations/resident/session-log-reads.js +125 -0
- package/dist/integrations/resident/session-log-reads.js.map +1 -0
- package/dist/integrations/resident/session-step.d.ts +2 -0
- package/dist/integrations/resident/session-step.d.ts.map +1 -1
- package/dist/integrations/resident/session-step.js +15 -13
- package/dist/integrations/resident/session-step.js.map +1 -1
- package/dist/integrations/resident/storage.d.ts +23 -1
- package/dist/integrations/resident/storage.d.ts.map +1 -1
- package/dist/integrations/resident/storage.js +60 -17
- package/dist/integrations/resident/storage.js.map +1 -1
- package/dist/integrations/resident/tool-evidence.d.ts +4 -2
- package/dist/integrations/resident/tool-evidence.d.ts.map +1 -1
- package/dist/integrations/resident/tool-evidence.js +36 -30
- package/dist/integrations/resident/tool-evidence.js.map +1 -1
- package/dist/integrations/resident/verification.d.ts +2 -2
- package/dist/integrations/resident/verification.d.ts.map +1 -1
- package/dist/integrations/resident/verification.js +4 -4
- package/dist/integrations/resident/verification.js.map +1 -1
- package/dist/integrations/sessions/checkpoint-view.d.ts +13 -0
- package/dist/integrations/sessions/checkpoint-view.d.ts.map +1 -0
- package/dist/integrations/sessions/checkpoint-view.js +48 -0
- package/dist/integrations/sessions/checkpoint-view.js.map +1 -0
- package/dist/integrations/sessions/context-inventory.js +1 -1
- package/dist/integrations/sessions/context-inventory.js.map +1 -1
- package/dist/integrations/sessions/conversation-search.d.ts +16 -20
- package/dist/integrations/sessions/conversation-search.d.ts.map +1 -1
- package/dist/integrations/sessions/conversation-search.js +305 -731
- package/dist/integrations/sessions/conversation-search.js.map +1 -1
- package/dist/integrations/sessions/evidence-page-validation.d.ts +4 -4
- package/dist/integrations/sessions/evidence-page-validation.d.ts.map +1 -1
- package/dist/integrations/sessions/evidence-page-validation.js +1 -1
- package/dist/integrations/sessions/evidence-page-validation.js.map +1 -1
- package/dist/integrations/sessions/evidence-recall.d.ts +40 -2
- package/dist/integrations/sessions/evidence-recall.d.ts.map +1 -1
- package/dist/integrations/sessions/evidence-recall.js +41 -97
- package/dist/integrations/sessions/evidence-recall.js.map +1 -1
- package/dist/integrations/sessions/store.d.ts +153 -85
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +414 -484
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/task-context.d.ts +15 -2
- package/dist/integrations/sessions/task-context.d.ts.map +1 -1
- package/dist/integrations/sessions/task-context.js +34 -11
- package/dist/integrations/sessions/task-context.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts +13 -6
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +148 -263
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/state/home.d.ts +4 -16
- package/dist/integrations/state/home.d.ts.map +1 -1
- package/dist/integrations/state/home.js +6 -42
- package/dist/integrations/state/home.js.map +1 -1
- package/dist/integrations/state/project.d.ts +6 -7
- package/dist/integrations/state/project.d.ts.map +1 -1
- package/dist/integrations/state/project.js +7 -9
- package/dist/integrations/state/project.js.map +1 -1
- package/dist/integrations/state/report.d.ts +27 -41
- package/dist/integrations/state/report.d.ts.map +1 -1
- package/dist/integrations/state/report.js +172 -464
- package/dist/integrations/state/report.js.map +1 -1
- package/dist/integrations/state/retention.d.ts +18 -0
- package/dist/integrations/state/retention.d.ts.map +1 -0
- package/dist/integrations/state/retention.js +18 -0
- package/dist/integrations/state/retention.js.map +1 -0
- package/dist/integrations/subagents/activity.d.ts +36 -31
- package/dist/integrations/subagents/activity.d.ts.map +1 -1
- package/dist/integrations/subagents/activity.js +27 -23
- package/dist/integrations/subagents/activity.js.map +1 -1
- package/dist/integrations/subagents/batches.d.ts +91 -0
- package/dist/integrations/subagents/batches.d.ts.map +1 -0
- package/dist/integrations/subagents/batches.js +172 -0
- package/dist/integrations/subagents/batches.js.map +1 -0
- package/dist/integrations/subagents/parent.d.ts +26 -11
- package/dist/integrations/subagents/parent.d.ts.map +1 -1
- package/dist/integrations/subagents/parent.js +13 -28
- package/dist/integrations/subagents/parent.js.map +1 -1
- package/dist/integrations/subagents/policy.d.ts +2 -2
- package/dist/integrations/subagents/policy.d.ts.map +1 -1
- package/dist/integrations/subagents/policy.js +2 -2
- package/dist/integrations/subagents/policy.js.map +1 -1
- package/dist/integrations/subagents/replay.d.ts +91 -26
- package/dist/integrations/subagents/replay.d.ts.map +1 -1
- package/dist/integrations/subagents/replay.js +216 -114
- package/dist/integrations/subagents/replay.js.map +1 -1
- package/dist/integrations/subagents/runtime.d.ts +50 -26
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +132 -126
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/integrations/subagents/saved-agents.d.ts +57 -0
- package/dist/integrations/subagents/saved-agents.d.ts.map +1 -0
- package/dist/integrations/subagents/saved-agents.js +102 -0
- package/dist/integrations/subagents/saved-agents.js.map +1 -0
- package/dist/integrations/subagents/slash.d.ts +55 -0
- package/dist/integrations/subagents/slash.d.ts.map +1 -0
- package/dist/integrations/subagents/slash.js +45 -0
- package/dist/integrations/subagents/slash.js.map +1 -0
- package/dist/integrations/telemetry/session-export.d.ts +6 -6
- package/dist/integrations/telemetry/session-export.d.ts.map +1 -1
- package/dist/integrations/telemetry/session-export.js +2 -2
- package/dist/memory/presentation.d.ts +11 -0
- package/dist/memory/presentation.d.ts.map +1 -1
- package/dist/memory/presentation.js +25 -0
- package/dist/memory/presentation.js.map +1 -1
- package/dist/memory/store.d.ts +13 -7
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +37 -11
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/typed.d.ts +116 -0
- package/dist/memory/typed.d.ts.map +1 -0
- package/dist/memory/typed.js +336 -0
- package/dist/memory/typed.js.map +1 -0
- package/dist/permissions/checks.d.ts +1 -1
- package/dist/permissions/checks.js +1 -1
- package/dist/permissions/headless-trust.d.ts +2 -2
- package/dist/permissions/headless-trust.d.ts.map +1 -1
- package/dist/permissions/headless-trust.js +3 -3
- package/dist/permissions/headless-trust.js.map +1 -1
- package/dist/permissions/mode.d.ts +4 -4
- package/dist/permissions/mode.js +1 -1
- package/dist/permissions/rules.d.ts +1 -1
- package/dist/permissions/rules.js +1 -1
- package/dist/termination.d.ts +75 -0
- package/dist/termination.d.ts.map +1 -0
- package/dist/termination.js +143 -0
- package/dist/termination.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts +2 -2
- package/dist/tui/AgentExplorer.js +4 -4
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts +9 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +458 -304
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +1 -1
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +8 -0
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +25 -2
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +14 -1
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +598 -132
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/agent.d.ts +208 -132
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +523 -313
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/conversation-evidence-view.js +3 -3
- package/dist/tui/conversation-evidence-view.js.map +1 -1
- package/dist/tui/conversation-history.d.ts +4 -21
- package/dist/tui/conversation-history.d.ts.map +1 -1
- package/dist/tui/conversation-history.js +4 -28
- package/dist/tui/conversation-history.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +27 -1
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/log-pane.js +1 -1
- package/dist/tui/markdownParser.d.ts +1 -1
- package/dist/tui/markdownParser.js +1 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +20 -1
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/permission-review.d.ts +16 -0
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +25 -2
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/provider-list.d.ts +206 -41
- package/dist/tui/provider-list.d.ts.map +1 -1
- package/dist/tui/provider-list.js +248 -52
- package/dist/tui/provider-list.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +29 -40
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +70 -61
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/terminal-choice-text.d.ts +22 -0
- package/dist/tui/terminal-choice-text.d.ts.map +1 -1
- package/dist/tui/terminal-choice-text.js +81 -0
- package/dist/tui/terminal-choice-text.js.map +1 -1
- package/dist/tui/{run-interruption.d.ts → turn-interruption.d.ts} +4 -4
- package/dist/tui/turn-interruption.d.ts.map +1 -0
- package/dist/tui/{run-interruption.js → turn-interruption.js} +20 -20
- package/dist/tui/turn-interruption.js.map +1 -0
- package/dist/tui/turn-limits-settings.d.ts +39 -0
- package/dist/tui/turn-limits-settings.d.ts.map +1 -0
- package/dist/tui/{run-limits-settings.js → turn-limits-settings.js} +18 -18
- package/dist/tui/turn-limits-settings.js.map +1 -0
- package/dist/tui/types.d.ts +3 -3
- package/dist/tui/types.d.ts.map +1 -1
- package/dist/user-commands/store.d.ts +1 -1
- package/dist/user-commands/store.js +1 -1
- package/package.json +8 -8
- package/dist/integrations/sessions/compaction-evidence.d.ts +0 -5
- package/dist/integrations/sessions/compaction-evidence.d.ts.map +0 -1
- package/dist/integrations/sessions/compaction-evidence.js +0 -59
- package/dist/integrations/sessions/compaction-evidence.js.map +0 -1
- package/dist/integrations/sessions/database.d.ts +0 -5
- package/dist/integrations/sessions/database.d.ts.map +0 -1
- package/dist/integrations/sessions/database.js +0 -34
- package/dist/integrations/sessions/database.js.map +0 -1
- package/dist/integrations/sessions/paths.d.ts +0 -7
- package/dist/integrations/sessions/paths.d.ts.map +0 -1
- package/dist/integrations/sessions/paths.js +0 -14
- package/dist/integrations/sessions/paths.js.map +0 -1
- package/dist/integrations/sessions/run-discovery.d.ts +0 -24
- package/dist/integrations/sessions/run-discovery.d.ts.map +0 -1
- package/dist/integrations/sessions/run-discovery.js +0 -142
- package/dist/integrations/sessions/run-discovery.js.map +0 -1
- package/dist/integrations/sessions/turn-evidence.d.ts +0 -129
- package/dist/integrations/sessions/turn-evidence.d.ts.map +0 -1
- package/dist/integrations/sessions/turn-evidence.js +0 -517
- package/dist/integrations/sessions/turn-evidence.js.map +0 -1
- package/dist/integrations/subagents/history.d.ts +0 -26
- package/dist/integrations/subagents/history.d.ts.map +0 -1
- package/dist/integrations/subagents/history.js +0 -76
- package/dist/integrations/subagents/history.js.map +0 -1
- package/dist/integrations/subagents/runs.d.ts +0 -72
- package/dist/integrations/subagents/runs.d.ts.map +0 -1
- package/dist/integrations/subagents/runs.js +0 -155
- package/dist/integrations/subagents/runs.js.map +0 -1
- package/dist/tui/run-interruption.d.ts.map +0 -1
- package/dist/tui/run-interruption.js.map +0 -1
- package/dist/tui/run-limits-settings.d.ts +0 -39
- package/dist/tui/run-limits-settings.d.ts.map +0 -1
- package/dist/tui/run-limits-settings.js.map +0 -1
package/dist/tui/Picker.js
CHANGED
|
@@ -8,19 +8,121 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
8
8
|
* live agent session lives in `agent.ts`.
|
|
9
9
|
*/
|
|
10
10
|
import { Box, Text, useInput, useWindowSize } from 'ink';
|
|
11
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
|
+
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
|
12
12
|
import { canSelectModel } from '../integrations/providers/access.js';
|
|
13
13
|
import { ALL_PROVIDER_IDS, PROVIDER_REGISTRY, signedInSubscriptionProviders, unsupportedProviderMessage, } from '../integrations/providers/index.js';
|
|
14
14
|
import { describeProviderModels, verifyCredential } from './agent.js';
|
|
15
15
|
import { classifyCredential, describeDisposition, keyLooksUsable, maskKey, sessionCredential, } from './credential-entry.js';
|
|
16
16
|
import { modelStep } from './model-choices.js';
|
|
17
17
|
import { filterModelChoices } from './model-search.js';
|
|
18
|
-
import { credentialNeed, initialProviderRow, providerListRows,
|
|
19
|
-
import { moveSelection, selectionWindow } from './selection-window.js';
|
|
20
|
-
import { choiceDisplayWidth, eraseLastChoiceGrapheme, truncateChoiceText, } from './terminal-choice-text.js';
|
|
18
|
+
import { credentialNeed, initialProviderRow, pathProviderId, providerListRows, rowCredentialNeed, rowHasProvider, rowIndexOfProvider, rowIsUsable, rowNeedsChoice, typedCredentialEntry, } from './provider-list.js';
|
|
19
|
+
import { moveSelection, selectionWindow, } from './selection-window.js';
|
|
20
|
+
import { capChoiceLines, choiceDisplayWidth, eraseLastChoiceGrapheme, truncateChoiceText, wrapChoiceText, } from './terminal-choice-text.js';
|
|
21
21
|
import { terminalDisplayText } from './terminal-display.js';
|
|
22
22
|
import { theme } from './theme.js';
|
|
23
23
|
import { useSelectionIndex } from './use-selection-index.js';
|
|
24
|
+
/**
|
|
25
|
+
* Columns this screen's text area has, given the terminal's width.
|
|
26
|
+
*
|
|
27
|
+
* Derived rather than written down, because the number this replaces was a
|
|
28
|
+
* literal that counted two of the three things that take columns and forgot the
|
|
29
|
+
* third. In order:
|
|
30
|
+
*
|
|
31
|
+
* - `HOST_INSET_X` — the app draws every screen inside a box with one column
|
|
32
|
+
* of padding on each side (`App.tsx`'s root), so this picker's own box starts
|
|
33
|
+
* one column in and ends one column short of the terminal.
|
|
34
|
+
* - `BORDER_X` — this box's round border, one column per side.
|
|
35
|
+
* - `BOX_PADDING_X` — this box's `paddingX={1}`, one column per side.
|
|
36
|
+
*
|
|
37
|
+
* The old arithmetic was `columns - 4`: the border and the padding, minus the
|
|
38
|
+
* host's inset. At 60 columns that budgeted 56 columns of text for an area of
|
|
39
|
+
* 54 — measured from a 60-column capture, where the two borders sit at columns
|
|
40
|
+
* 1 and 58, leaving 56 between them and 54 inside the padding. Every row the
|
|
41
|
+
* screen measured as fitting therefore wrapped, and the text it wrapped was the
|
|
42
|
+
* row the cursor was on.
|
|
43
|
+
*/
|
|
44
|
+
const HOST_INSET_X = 2;
|
|
45
|
+
const BORDER_X = 2;
|
|
46
|
+
const BOX_PADDING_X = 2;
|
|
47
|
+
/** Columns a row of this screen's content may occupy. */
|
|
48
|
+
export function pickerContentWidth(columns) {
|
|
49
|
+
return Math.max(1, columns - HOST_INSET_X - BORDER_X - BOX_PADDING_X);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Columns a row spends before its source: the marker, the number and the
|
|
53
|
+
* vendor's name, each written over its own width.
|
|
54
|
+
*
|
|
55
|
+
* Derived from the three, because 32 was written down here once and was the
|
|
56
|
+
* width of the BROKEN row — the one whose marker had lost the space beside it.
|
|
57
|
+
* A source measured against a row one column narrower than it is a source that
|
|
58
|
+
* wraps in the renderer after the screen has counted it as one line, which is
|
|
59
|
+
* the defect this whole window exists to prevent.
|
|
60
|
+
*/
|
|
61
|
+
const ROW_MARKER_WIDTH = 2;
|
|
62
|
+
const ROW_NUMBER_WIDTH = 3;
|
|
63
|
+
const ROW_LABEL_WIDTH = 28;
|
|
64
|
+
const ROW_PREFIX_WIDTH = ROW_MARKER_WIDTH + ROW_NUMBER_WIDTH + ROW_LABEL_WIDTH;
|
|
65
|
+
/** What a wrapped source line is indented by, so it lines up under the name. */
|
|
66
|
+
const ROW_INDENT = ' '.repeat(ROW_PREFIX_WIDTH);
|
|
67
|
+
/**
|
|
68
|
+
* Lines a row's source column may take before the rest is cut.
|
|
69
|
+
*
|
|
70
|
+
* Three is what a variable name needs at the width this was measured against
|
|
71
|
+
* (54 columns leaves 22 for the source, so `env · OPENROUTER_API_KEY` wraps to
|
|
72
|
+
* two). It is a cap rather than a measurement because a row's height is part of
|
|
73
|
+
* the box's height: without one, a forty-column terminal could make a single
|
|
74
|
+
* source a dozen lines and no window could keep the header on screen.
|
|
75
|
+
*/
|
|
76
|
+
const MAX_SOURCE_LINES = 3;
|
|
77
|
+
/** The words above the not-detected block, which the row window reserves lines for. */
|
|
78
|
+
const NOT_DETECTED_HEADING = 'Not detected — enter a credential to use these:';
|
|
79
|
+
/**
|
|
80
|
+
* The line above the box that is not the box's.
|
|
81
|
+
*
|
|
82
|
+
* Ink draws this screen below whatever the app has already written — the boot
|
|
83
|
+
* banner, on the turn that shows this picker first — and a terminal keeps the
|
|
84
|
+
* last `rows` lines of all of it. A box that fills the terminal exactly
|
|
85
|
+
* therefore loses its own top border, and under that border is the notice the
|
|
86
|
+
* screen exists to deliver.
|
|
87
|
+
*/
|
|
88
|
+
const TRANSCRIPT_LINE_ABOVE = 1;
|
|
89
|
+
/** The mark a row carries when it is the provider in force. */
|
|
90
|
+
const CURRENT_MARK = ' ← current';
|
|
91
|
+
/**
|
|
92
|
+
* Columns a row's source must keep before it shares its line with that mark.
|
|
93
|
+
*
|
|
94
|
+
* Twenty is the shortest this column can be and still be read: an environment
|
|
95
|
+
* variable name (`DEEPSEEK_API_KEY` is sixteen, with the word `needs` before it
|
|
96
|
+
* and the `←` mark is twelve columns of its own. Below twenty the mark moves
|
|
97
|
+
* down a line rather than cutting the answer in half.
|
|
98
|
+
*/
|
|
99
|
+
const MIN_SOURCE_COLUMNS = 20;
|
|
100
|
+
/** What `s` does, printed above the list on the screens that offer it. */
|
|
101
|
+
const SETUP_HINT = 's provider setup · check installations and access';
|
|
102
|
+
/**
|
|
103
|
+
* What the screen that found nothing says, and the reason it says it.
|
|
104
|
+
*
|
|
105
|
+
* Here rather than inline because the row window counts these lines before it
|
|
106
|
+
* draws a single row: they are the operator's only way off that screen, so they
|
|
107
|
+
* are reserved first and a row is drawn with what is left over.
|
|
108
|
+
*/
|
|
109
|
+
const EMPTY_SCREEN_INTRO = 'namzu scans these sources, in order, for an LLM credential:';
|
|
110
|
+
const EMPTY_SCREEN_HINTS = {
|
|
111
|
+
signIn: 'to sign in with a subscription — no API key, and namzu keeps it for next time.',
|
|
112
|
+
paste: 'to paste a credential now and use it for this session.',
|
|
113
|
+
durable: 'You can also set one of the env vars above (or start a local server) and restart.',
|
|
114
|
+
};
|
|
115
|
+
/** The sources the empty screen lists — the same set the summary line names. */
|
|
116
|
+
function emptyScreenSourceLines() {
|
|
117
|
+
return [
|
|
118
|
+
` · existing ${subscriptionProviders()
|
|
119
|
+
.map((entry) => entry.label)
|
|
120
|
+
.join(' and ')} sessions on this device`,
|
|
121
|
+
' · subscriptions signed in to from namzu (~/.namzu/credentials.json)',
|
|
122
|
+
' · env vars / API keys (optional alternatives: ANTHROPIC_API_KEY, OPENAI_API_KEY, …)',
|
|
123
|
+
' · local servers (Ollama localhost:11434, LM Studio localhost:1234)',
|
|
124
|
+
];
|
|
125
|
+
}
|
|
24
126
|
function subscriptionProviders() {
|
|
25
127
|
return ALL_PROVIDER_IDS.map((id) => PROVIDER_REGISTRY[id]).filter((entry) => entry.subscriptionLogin !== undefined);
|
|
26
128
|
}
|
|
@@ -48,26 +150,25 @@ function signInChoiceIndex(choices, provider) {
|
|
|
48
150
|
/**
|
|
49
151
|
* The provider `k` opens entry for.
|
|
50
152
|
*
|
|
51
|
-
* The highlighted row whenever
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
153
|
+
* The highlighted row's own provider whenever the vendor takes a typed
|
|
154
|
+
* credential. This screen used to answer the question from the registry
|
|
155
|
+
* instead: the saved provider if the picker was open because of one, and
|
|
156
|
+
* otherwise the first key-capable entry, which is a provider the operator may
|
|
157
|
+
* not have been looking at. A list you can move through is a list the key
|
|
56
158
|
* beside it has to follow.
|
|
57
159
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
160
|
+
* A row whose vendor takes none — a local server — falls through to the saved
|
|
161
|
+
* provider this picker was opened for: that route is why the notice above says
|
|
162
|
+
* `k`, and it must keep working when the cursor happens to sit on a row that
|
|
163
|
+
* cannot use it.
|
|
62
164
|
*
|
|
63
165
|
* Returns null when there is nothing here to enter a credential for, which the
|
|
64
166
|
* caller says out loud rather than presenting a field that leads nowhere.
|
|
65
167
|
*/
|
|
66
168
|
function keyEntryTarget(row, keyEntryFor) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return row.detected.entry;
|
|
169
|
+
const onThisRow = typedCredentialEntry(row);
|
|
170
|
+
if (onThisRow)
|
|
171
|
+
return onThisRow;
|
|
71
172
|
if (keyEntryFor) {
|
|
72
173
|
const entry = PROVIDER_REGISTRY[keyEntryFor];
|
|
73
174
|
if (entry?.acceptsTypedCredential)
|
|
@@ -75,11 +176,7 @@ function keyEntryTarget(row, keyEntryFor) {
|
|
|
75
176
|
}
|
|
76
177
|
return null;
|
|
77
178
|
}
|
|
78
|
-
|
|
79
|
-
function rowLabel(row) {
|
|
80
|
-
return row.kind === 'detected' ? row.detected.entry.label : row.entry.label;
|
|
81
|
-
}
|
|
82
|
-
export function Picker({ detected, currentProvider, selectionKind = 'provider-and-model', currentModel, initialView = 'providers', onSubmit, onCancel, onSetup, describeModels = describeProviderModels, onCredential, onLogin, onLoginComplete, verify = verifyCredential, keyEntryFor, notice, }) {
|
|
179
|
+
export function Picker({ detected, currentProvider, selectionKind = 'provider-and-model', currentModel, initialView = 'providers', onSubmit, onCancel, onSetup, describeModels = describeProviderModels, onCredential, onLogin, onLoginComplete, verify = verifyCredential, keyEntryFor, notice, cancelExits = false, }) {
|
|
83
180
|
/**
|
|
84
181
|
* The one foreign operation still allowed to publish into this picker.
|
|
85
182
|
*
|
|
@@ -138,6 +235,11 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
138
235
|
// of a three-row sign-in list.
|
|
139
236
|
const initialSelection = initialView === 'subscriptions' ? 0 : initialProviderRow(rows, currentProvider, keyEntryFor);
|
|
140
237
|
const { selection: cursor, selectionRef: cursorRef, setSelection: setCursor, } = useSelectionIndex(initialSelection);
|
|
238
|
+
// How wide this screen's text is, and how many lines its notice takes, both
|
|
239
|
+
// of which the row window is sized against — see the list below.
|
|
240
|
+
const terminal = useWindowSize();
|
|
241
|
+
const contentWidth = pickerContentWidth(terminal.columns ?? 80);
|
|
242
|
+
const noticeLines = notice ? wrapChoiceText(notice, contentWidth).length : 0;
|
|
141
243
|
const [errorHint, setErrorHint] = useState(null);
|
|
142
244
|
// The ref makes a pasted query followed immediately by Enter use the new
|
|
143
245
|
// filtered list, even before React has drawn another frame. Null keeps the
|
|
@@ -162,6 +264,15 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
162
264
|
// provider, because nothing on this screen changed it — so a credential typed
|
|
163
265
|
// for a saved provider would have been built for a different one.
|
|
164
266
|
const [keyEntry, setKeyEntry] = useState(null);
|
|
267
|
+
// The vendor whose ways in are being chosen, while that screen is up.
|
|
268
|
+
//
|
|
269
|
+
// Held as the vendor's id and resolved against `rows` on every render, not as
|
|
270
|
+
// a row object: `rows` is derived from the props on each render, and a stored
|
|
271
|
+
// row would be the list as it looked on the keystroke that opened this screen.
|
|
272
|
+
// The screen is only ever reached from a row, so the lookup cannot come back
|
|
273
|
+
// empty while it is up.
|
|
274
|
+
const [pathVendor, setPathVendor] = useState(null);
|
|
275
|
+
const pathRow = pathVendor === null ? null : (rows.find((row) => row.vendor === pathVendor) ?? null);
|
|
165
276
|
const openModels = useCallback((current, returnToProviders = true) => {
|
|
166
277
|
const operation = beginOperation();
|
|
167
278
|
modelQueryRef.current = null;
|
|
@@ -294,6 +405,81 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
294
405
|
});
|
|
295
406
|
}
|
|
296
407
|
};
|
|
408
|
+
/**
|
|
409
|
+
* Start a subscription sign-in for one provider.
|
|
410
|
+
*
|
|
411
|
+
* Written once and reached from two places — the sign-in screen's own Enter,
|
|
412
|
+
* and the sign-in path inside a vendor row — because two copies of this would
|
|
413
|
+
* be two places for the device-code wording, the operation's ownership and the
|
|
414
|
+
* failure sentence to drift apart. It is one operation either way, so an esc
|
|
415
|
+
* from either leaves nothing running behind the screen.
|
|
416
|
+
*/
|
|
417
|
+
const startSignIn = (entry) => {
|
|
418
|
+
if (!entry.subscriptionLogin)
|
|
419
|
+
return;
|
|
420
|
+
if (!onLogin) {
|
|
421
|
+
setErrorHint('Subscription sign-in is not available on this screen.');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const operation = beginOperation();
|
|
425
|
+
setPathVendor(null);
|
|
426
|
+
setLoginEntry({ entry, value: '', status: 'starting' });
|
|
427
|
+
void onLogin(entry.id, operation.controller.signal)
|
|
428
|
+
.then((disposition) => {
|
|
429
|
+
if (!ownsOperation(operation))
|
|
430
|
+
return;
|
|
431
|
+
if (disposition === 'awaiting-input') {
|
|
432
|
+
setLoginEntry({
|
|
433
|
+
entry,
|
|
434
|
+
value: '',
|
|
435
|
+
status: 'typing',
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
finishOperation(operation);
|
|
440
|
+
setLoginEntry(null);
|
|
441
|
+
})
|
|
442
|
+
.catch((error) => {
|
|
443
|
+
if (!ownsOperation(operation))
|
|
444
|
+
return;
|
|
445
|
+
finishOperation(operation);
|
|
446
|
+
setLoginEntry(null);
|
|
447
|
+
setErrorHint(`Could not start sign-in: ${error instanceof Error ? error.message : String(error)}`);
|
|
448
|
+
});
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* Follow one way in to the flow that already exists for it.
|
|
452
|
+
*
|
|
453
|
+
* The three arms are the three flows this screen already had; nothing here
|
|
454
|
+
* builds a credential. A detected path goes on to the model step exactly as
|
|
455
|
+
* Enter on a detected row always has. A credential path opens the paste field
|
|
456
|
+
* for its provider, so the value still travels `setKeyEntry` → `acceptKey` →
|
|
457
|
+
* the session credential. A sign-in path starts the operation `l` starts.
|
|
458
|
+
*/
|
|
459
|
+
const takePath = (path) => {
|
|
460
|
+
if (path.kind === 'detected') {
|
|
461
|
+
const current = path.detected;
|
|
462
|
+
if (selectionKind === 'signed-in-subscription') {
|
|
463
|
+
const operation = beginOperation();
|
|
464
|
+
onSubmit({ provider: current.entry.id }, operation.controller.signal);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
setPathVendor(null);
|
|
468
|
+
openModels(current);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (path.kind === 'credential') {
|
|
472
|
+
if (!onCredential) {
|
|
473
|
+
setErrorHint(`No credential can be entered for ${path.entry.label} on this screen.`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
invalidateOperation();
|
|
477
|
+
setPathVendor(null);
|
|
478
|
+
setKeyEntry({ entry: path.entry, value: '', status: 'typing' });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
startSignIn(path.entry);
|
|
482
|
+
};
|
|
297
483
|
useInput((input, key) => {
|
|
298
484
|
if (loginEntry) {
|
|
299
485
|
if (key.escape) {
|
|
@@ -345,6 +531,43 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
345
531
|
}
|
|
346
532
|
return;
|
|
347
533
|
}
|
|
534
|
+
// One vendor's ways in. Modal like the two screens above: while it is up it
|
|
535
|
+
// owns the keyboard, so `k`, `l` and `s` cannot act on a list that is not
|
|
536
|
+
// the one on screen. Escape returns to the row that opened it rather than
|
|
537
|
+
// to the top, because the cursor here is an index into this vendor's paths
|
|
538
|
+
// and would otherwise be read as a position in the list.
|
|
539
|
+
if (pathRow) {
|
|
540
|
+
if (key.escape) {
|
|
541
|
+
invalidateOperation();
|
|
542
|
+
setPathVendor(null);
|
|
543
|
+
setCursor(Math.max(0, rows.findIndex((row) => row.vendor === pathRow.vendor)));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (key.home || key.end || key.pageUp || key.pageDown) {
|
|
547
|
+
setCursor((current) => moveSelection(current, pathRow.paths.length, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page'));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (key.upArrow) {
|
|
551
|
+
setCursor((current) => moveSelection(current, pathRow.paths.length, 'previous'));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (key.downArrow) {
|
|
555
|
+
setCursor((current) => moveSelection(current, pathRow.paths.length, 'next'));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (key.return) {
|
|
559
|
+
const chosen = pathRow.paths[cursorRef.current];
|
|
560
|
+
if (!chosen)
|
|
561
|
+
return;
|
|
562
|
+
takePath(chosen);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const selected = Number.parseInt(input, 10);
|
|
566
|
+
if (Number.isFinite(selected) && selected >= 1 && selected <= pathRow.paths.length) {
|
|
567
|
+
setCursor(selected - 1);
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
348
571
|
// `k` opens credential entry for the highlighted row — see
|
|
349
572
|
// `keyEntryTarget` for which provider that resolves to when the row takes
|
|
350
573
|
// no typed credential. The empty screen has always offered it (there,
|
|
@@ -397,7 +620,7 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
397
620
|
}
|
|
398
621
|
else {
|
|
399
622
|
setErrorHint(highlighted
|
|
400
|
-
? `${
|
|
623
|
+
? `${highlighted.label} does not take a typed credential — choose a provider that does.`
|
|
401
624
|
: 'No provider here takes a typed credential.');
|
|
402
625
|
}
|
|
403
626
|
return;
|
|
@@ -405,7 +628,7 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
405
628
|
if (modelPhase &&
|
|
406
629
|
(key.leftArrow || (modelQueryRef.current === null && (input === 'p' || input === 'P')))) {
|
|
407
630
|
invalidateOperation();
|
|
408
|
-
setCursor(Math.max(0, rows
|
|
631
|
+
setCursor(Math.max(0, rowIndexOfProvider(rows, modelPhase.provider.entry.id)));
|
|
409
632
|
setModelPhase(null);
|
|
410
633
|
return;
|
|
411
634
|
}
|
|
@@ -424,7 +647,7 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
424
647
|
onCancel();
|
|
425
648
|
return;
|
|
426
649
|
}
|
|
427
|
-
setCursor(Math.max(0, rows
|
|
650
|
+
setCursor(Math.max(0, rowIndexOfProvider(rows, modelPhase.provider.entry.id)));
|
|
428
651
|
setModelPhase(null);
|
|
429
652
|
return;
|
|
430
653
|
}
|
|
@@ -455,36 +678,7 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
455
678
|
onSubmit({ provider: chosen.detected.entry.id }, operation.controller.signal);
|
|
456
679
|
return;
|
|
457
680
|
}
|
|
458
|
-
|
|
459
|
-
return;
|
|
460
|
-
if (!onLogin) {
|
|
461
|
-
setErrorHint('Subscription sign-in is not available on this screen.');
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
const operation = beginOperation();
|
|
465
|
-
setLoginEntry({ entry: chosen.entry, value: '', status: 'starting' });
|
|
466
|
-
void onLogin(chosen.entry.id, operation.controller.signal)
|
|
467
|
-
.then((disposition) => {
|
|
468
|
-
if (!ownsOperation(operation))
|
|
469
|
-
return;
|
|
470
|
-
if (disposition === 'awaiting-input') {
|
|
471
|
-
setLoginEntry({
|
|
472
|
-
entry: chosen.entry,
|
|
473
|
-
value: '',
|
|
474
|
-
status: 'typing',
|
|
475
|
-
});
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
finishOperation(operation);
|
|
479
|
-
setLoginEntry(null);
|
|
480
|
-
})
|
|
481
|
-
.catch((error) => {
|
|
482
|
-
if (!ownsOperation(operation))
|
|
483
|
-
return;
|
|
484
|
-
finishOperation(operation);
|
|
485
|
-
setLoginEntry(null);
|
|
486
|
-
setErrorHint(`Could not start sign-in: ${error instanceof Error ? error.message : String(error)}`);
|
|
487
|
-
});
|
|
681
|
+
startSignIn(chosen.entry);
|
|
488
682
|
return;
|
|
489
683
|
}
|
|
490
684
|
const selected = Number.parseInt(input, 10);
|
|
@@ -571,39 +765,30 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
571
765
|
setErrorHint('No provider available.');
|
|
572
766
|
return;
|
|
573
767
|
}
|
|
574
|
-
//
|
|
575
|
-
// ask
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
invalidateOperation();
|
|
585
|
-
setKeyEntry({ entry: row.entry, value: '', status: 'typing' });
|
|
768
|
+
// Nothing on this row can be constructed, so there is no session to open
|
|
769
|
+
// and no catalogue to ask. The row stays visible on purpose — see the
|
|
770
|
+
// list below — so this is the only place that can decline it, and
|
|
771
|
+
// declining with the reason is the point: accepting would write the
|
|
772
|
+
// choice to preferences and hand the operator a session that refuses to
|
|
773
|
+
// start.
|
|
774
|
+
if (!rowIsUsable(row)) {
|
|
775
|
+
setErrorHint(unsupportedProviderMessage(row.detected[0]?.entry.id ?? row.vendor));
|
|
586
776
|
return;
|
|
587
777
|
}
|
|
588
|
-
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
// it
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
if (selectionKind === 'signed-in-subscription') {
|
|
599
|
-
const operation = beginOperation();
|
|
600
|
-
onSubmit({ provider: current.entry.id }, operation.controller.signal);
|
|
778
|
+
// More than one provider behind this row: which of them the operator
|
|
779
|
+
// means is theirs to say. Skipping the question was the defect this
|
|
780
|
+
// screen was reported with — one row asked for a key while the session
|
|
781
|
+
// it could have used sat above it — so the row asks instead of picking.
|
|
782
|
+
if (rowNeedsChoice(row)) {
|
|
783
|
+
invalidateOperation();
|
|
784
|
+
setPathVendor(row.vendor);
|
|
785
|
+
setCursor(0);
|
|
601
786
|
return;
|
|
602
787
|
}
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
|
|
788
|
+
// One provider, so Enter means exactly what it has always meant: the
|
|
789
|
+
// detected session's models, or the field that takes the credential this
|
|
790
|
+
// row is missing. `takePath` is the same call the choice screen makes.
|
|
791
|
+
takePath(row.paths[0]);
|
|
607
792
|
return;
|
|
608
793
|
}
|
|
609
794
|
// Numeric quick-select, one keystroke per row and therefore the first nine.
|
|
@@ -618,14 +803,16 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
618
803
|
}
|
|
619
804
|
});
|
|
620
805
|
// Above every screen this picker draws, so the reason is on the same frame as
|
|
621
|
-
// the choice it is asking for.
|
|
622
|
-
|
|
806
|
+
// the choice it is asking for. Whether the setup hint fits beside it is
|
|
807
|
+
// decided below, where the screen's line budget is; the screens that return
|
|
808
|
+
// early cannot be showing it in any case, so they ask for it unconditionally.
|
|
809
|
+
const buildNoticeBox = (withSetupHint) => notice || onSetup ? (_jsx(Box, { paddingBottom: 1, children: _jsxs(Box, { flexDirection: "column", children: [notice ? (_jsx(WrappedLines, { text: notice, width: contentWidth, color: theme.status.warn })) : null, onSetup && withSetupHint && !modelPhase && !loginPhase && !loginEntry && !keyEntry ? (_jsx(Text, { dimColor: true, children: SETUP_HINT })) : null] }) })) : null;
|
|
623
810
|
if (loginEntry) {
|
|
624
811
|
const waitsForDeviceApproval = loginEntry.entry.subscriptionLogin === 'device';
|
|
625
812
|
const painted = loginEntry.value
|
|
626
813
|
? `${'•'.repeat(Math.min(loginEntry.value.length, 32))}${loginEntry.value.length > 32 ? '…' : ''}`
|
|
627
814
|
: '(nothing pasted yet)';
|
|
628
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [
|
|
815
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [buildNoticeBox(true), _jsxs(Text, { color: theme.accent.system, bold: true, children: ["Complete ", loginEntry.entry.label, " sign-in"] }), waitsForDeviceApproval ? (_jsx(Text, { color: theme.text.muted, children: "Approve the device code shown above in your browser. esc cancels." })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: "Paste the authorization code, or the finished browser address, then press enter. esc cancels." }), _jsx(Box, { paddingTop: 1, children: _jsx(Text, { color: loginEntry.value ? theme.text.primary : theme.text.muted, children: painted }) })] })), _jsxs(Box, { paddingTop: 1, flexDirection: "column", children: [loginEntry.status === 'starting' ? (_jsx(Text, { color: theme.text.muted, children: waitsForDeviceApproval
|
|
629
816
|
? 'Waiting for browser approval…'
|
|
630
817
|
: 'Starting browser sign-in…' })) : loginEntry.status === 'checking' ? (_jsx(Text, { color: theme.text.muted, children: "Finishing sign-in\u2026" })) : null, loginEntry.problem ? _jsx(Text, { color: theme.status.warn, children: loginEntry.problem }) : null] })] }));
|
|
631
818
|
}
|
|
@@ -636,19 +823,27 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
636
823
|
// line below is a preview of the sentence they will get, not a second
|
|
637
824
|
// opinion about it.
|
|
638
825
|
const kind = classifyCredential(entry, keyEntry.value);
|
|
639
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [
|
|
826
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [buildNoticeBox(true), _jsxs(Box, { flexDirection: "column", paddingBottom: 1, children: [_jsxs(Text, { color: theme.accent.system, bold: true, children: ["Paste a credential for ", entry.label] }), _jsx(Text, { color: theme.text.muted, children: "An API key or a subscription token. Type or paste, then enter. esc cancels." })] }), _jsx(Text, { color: theme.text.primary, children: maskKey(keyEntry.value) || _jsx(Text, { color: theme.text.muted, children: "(nothing typed yet)" }) }), _jsxs(Box, { paddingTop: 1, flexDirection: "column", children: [keyEntry.status === 'checking' ? (_jsxs(Text, { color: theme.text.muted, children: ["Checking it with ", entry.label, "\u2026"] })) : (_jsx(Text, { color: theme.text.secondary, children: "Used for this session only \u2014 it is not written anywhere." })), keyEntry.value.length > 0 && kind === 'subscription-token' ? (_jsx(Text, { color: theme.status.warn, children: "Reads as a subscription token \u2014 it expires in a few hours and namzu has no refresh data for a pasted one." })) : null, keyEntry.problem ? _jsx(Text, { color: theme.status.warn, children: keyEntry.problem }) : null] })] }));
|
|
640
827
|
}
|
|
641
828
|
if (modelPhase) {
|
|
642
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
829
|
+
return (_jsxs(Box, { flexDirection: "column", children: [buildNoticeBox(true), _jsx(ModelStepView, { providerLabel: modelPhase.provider.entry.label, otherProviders: detected
|
|
643
830
|
.filter((provider) => provider.entry.constructible && provider.entry.id !== modelPhase.provider.entry.id)
|
|
644
831
|
.map((provider) => provider.entry.label), step: modelPhase.step, query: modelQuery, currentModel: currentProvider == null || currentProvider === modelPhase.provider.entry.id
|
|
645
832
|
? currentModel
|
|
646
833
|
: undefined, cursor: cursor, errorHint: errorHint, returnToProviders: modelPhase.returnToProviders, sessionOnly: modelPhase.provider.source.kind === 'session' })] }));
|
|
647
834
|
}
|
|
835
|
+
// One vendor's ways in. Drawn only where the row's paths name more than one
|
|
836
|
+
// provider — every other row is answered by Enter as it always was — so this
|
|
837
|
+
// is the one screen this change adds, and it is reached one way.
|
|
838
|
+
if (pathRow) {
|
|
839
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [buildNoticeBox(true), _jsxs(Text, { color: theme.accent.system, bold: true, children: ["Choose a way to use ", pathRow.label] }), _jsx(Text, { color: theme.text.muted, children: pathRow.detected.length > 0
|
|
840
|
+
? `Already usable here — ${rowSourceText(pathRow)}.`
|
|
841
|
+
: 'Nothing on this device is set up for it yet.' }), _jsx(Box, { flexDirection: "column", paddingTop: 1, children: pathRow.paths.map((path, index) => (_jsxs(Text, { color: index === cursor ? theme.text.primary : theme.text.muted, children: [index === cursor ? '❯' : ' ', " ", index + 1, ". ", describePath(path)] }, `${path.kind}-${pathProviderId(path)}`))) }), _jsxs(Box, { paddingTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.text.muted, children: ["\u2191\u2193 or 1-", pathRow.paths.length, " navigate \u00B7 enter use \u00B7 esc back"] }), errorHint ? _jsx(Text, { color: theme.status.warn, children: errorHint }) : null] })] }));
|
|
842
|
+
}
|
|
648
843
|
if (loginPhase) {
|
|
649
844
|
const choices = subscriptionChoices(detected);
|
|
650
845
|
const existingCount = choices.filter((choice) => choice.kind === 'existing').length;
|
|
651
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [
|
|
846
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [buildNoticeBox(true), _jsx(Text, { color: theme.accent.system, bold: true, children: "Choose a subscription session" }), _jsx(Text, { color: theme.text.muted, children: existingCount > 0
|
|
652
847
|
? 'Reuse a signed-in device session, or start a separate Namzu-owned sign-in.'
|
|
653
848
|
: 'No usable model session was found on this device. Start a new subscription sign-in.' }), _jsx(Box, { flexDirection: "column", paddingTop: 1, children: choices.map((choice, index) => (_jsxs(Text, { color: index === cursor ? theme.text.primary : theme.text.muted, children: [index === cursor ? '❯' : ' ', " ", index + 1, ".", ' ', choice.kind === 'existing'
|
|
654
849
|
? `Use existing ${choice.detected.entry.label} · ${describeSource(choice.detected)}`
|
|
@@ -667,26 +862,107 @@ export function Picker({ detected, currentProvider, selectionKind = 'provider-an
|
|
|
667
862
|
// the cursor is the answer, and saying so again costs the line its tail: the
|
|
668
863
|
// footer is one row, and the sentence with a label in it wraps.
|
|
669
864
|
const namesAnotherRow = entryTarget !== null &&
|
|
670
|
-
(highlightedRow === undefined ||
|
|
865
|
+
(highlightedRow === undefined || !rowHasProvider(highlightedRow, entryTarget.id));
|
|
866
|
+
// The lines this screen spends on everything that is not a row, counted from
|
|
867
|
+
// the very words it is about to draw rather than estimated, then the rows get
|
|
868
|
+
// what is left.
|
|
869
|
+
//
|
|
870
|
+
// Two defects met here. The box used to be drawn at whatever height its rows
|
|
871
|
+
// needed, and on a 60x20 terminal it was taller than the terminal: Ink
|
|
872
|
+
// scrolled it, so the top border, the title, the count and the notice — the
|
|
873
|
+
// sentence telling the operator what to do about a missing credential — were
|
|
874
|
+
// above the top of the screen. And counting rows rather than lines was not
|
|
875
|
+
// enough either, because a row whose source column wraps is two lines: the
|
|
876
|
+
// count has to be the height the row actually draws, which is why the sources
|
|
877
|
+
// are wrapped here rather than by the renderer.
|
|
878
|
+
const oneLine = (text) => wrapChoiceText(text, contentWidth).length;
|
|
879
|
+
const emptyScreenFooter = `${onLogin ? 'l: sign in · ' : ''}↑↓ or 1-9 navigate · enter or k: enter a credential · ${cancelExits ? 'esc: exit namzu' : 'esc: exit picker'}`;
|
|
880
|
+
// Everything the screen draws that is not a row, in the order it is drawn:
|
|
881
|
+
// the box's two borders, the notice and its blank, the title, the blank and
|
|
882
|
+
// the intro, the sources, then the two or three ways off the screen with the
|
|
883
|
+
// blank each one sits under, the footer with its blank, the app's status bar
|
|
884
|
+
// underneath, and the line above the box that belongs to what the app has
|
|
885
|
+
// already written — Ink draws this box below that, and a terminal shows the
|
|
886
|
+
// last `rows` of it, so a box that fills the screen exactly loses its own top
|
|
887
|
+
// border to the transcript above it. Measured at 60x20, where the boot banner
|
|
888
|
+
// is two lines and the reserved line is what keeps the notice on screen.
|
|
889
|
+
const emptyScreenChromeRows = 2 +
|
|
890
|
+
(notice ? noticeLines : 0) +
|
|
891
|
+
(notice || onSetup ? 1 : 0) +
|
|
892
|
+
(onSetup ? oneLine(SETUP_HINT) : 0) +
|
|
893
|
+
1 +
|
|
894
|
+
1 +
|
|
895
|
+
oneLine(EMPTY_SCREEN_INTRO) +
|
|
896
|
+
emptyScreenSourceLines().reduce((rows, line) => rows + oneLine(line), 0) +
|
|
897
|
+
(onLogin ? 1 + oneLine(`Press l ${EMPTY_SCREEN_HINTS.signIn}`) : 0) +
|
|
898
|
+
1 +
|
|
899
|
+
oneLine(`Or press k ${EMPTY_SCREEN_HINTS.paste}`) +
|
|
900
|
+
1 +
|
|
901
|
+
oneLine(EMPTY_SCREEN_HINTS.durable) +
|
|
902
|
+
1 +
|
|
903
|
+
oneLine(emptyScreenFooter) +
|
|
904
|
+
1 +
|
|
905
|
+
TRANSCRIPT_LINE_ABOVE;
|
|
906
|
+
// The same, for the screen that found something: two borders, the notice and
|
|
907
|
+
// its blank, the title, the subtitle and the blank under the header, the
|
|
908
|
+
// heading above the not-detected block and the blank above that, the footer's
|
|
909
|
+
// blank and its line, and the status bar. The heading is reserved whenever
|
|
910
|
+
// the list has a not-detected block, even if the window does not reach it: a
|
|
911
|
+
// row too few visible is a scroll the arrows fix, and a line too many is the
|
|
912
|
+
// header scrolled away.
|
|
913
|
+
const listSubtitle = selectionKind === 'signed-in-subscription'
|
|
914
|
+
? `${detected.length} already usable · no API key required`
|
|
915
|
+
: `${detected.length} detected · device sessions / Namzu sign-ins / optional keys / local probes`;
|
|
916
|
+
const listFooter = `↑↓ or 1-9 navigate · enter ${selectionKind === 'signed-in-subscription' ? 'use' : 'accept'}${entryTarget
|
|
917
|
+
? ` · k enter a credential${namesAnotherRow ? ` for ${entryTarget.label}` : ''}`
|
|
918
|
+
: ''}${onLogin && selectionKind !== 'signed-in-subscription' ? ' · l create a Namzu sign-in' : ''} · ${cancelExits ? 'esc exit namzu' : 'esc cancel'}`;
|
|
919
|
+
// What the screen keeps, in the order it gives things up.
|
|
920
|
+
//
|
|
921
|
+
// Overflow cuts the TOP of the box, and the top is where the notice sits —
|
|
922
|
+
// the sentence that says why this screen is open and what to do about it.
|
|
923
|
+
// Everything below is ranked against that: the title and the rows are the
|
|
924
|
+
// screen, the footer is how it is left, and the two blocks that only describe
|
|
925
|
+
// the screen are what go when the terminal is too short to hold them all.
|
|
926
|
+
// `5 detected · device sessions / Namzu sign-ins / optional keys / local
|
|
927
|
+
// probes` is a summary of what is already listed beneath it, and
|
|
928
|
+
// `s provider setup` advertises a key that works from other screens too.
|
|
929
|
+
const headingRows = rows.some((row) => row.detected.length === 0) ? 1 + oneLine(NOT_DETECTED_HEADING) : 0;
|
|
930
|
+
// Everything above and below the rows except those two blocks.
|
|
931
|
+
const listCoreRows = 2 +
|
|
932
|
+
(notice ? noticeLines : 0) +
|
|
933
|
+
(notice || onSetup ? 1 : 0) +
|
|
934
|
+
1 +
|
|
935
|
+
headingRows +
|
|
936
|
+
1 +
|
|
937
|
+
oneLine(listFooter) +
|
|
938
|
+
1 +
|
|
939
|
+
TRANSCRIPT_LINE_ABOVE;
|
|
940
|
+
const subtitleRows = 1 + oneLine(listSubtitle);
|
|
941
|
+
const setupHintRows = onSetup ? oneLine(SETUP_HINT) : 0;
|
|
942
|
+
const screenRows = terminal.rows ?? 24;
|
|
943
|
+
// The row under the cursor is the one the window will not drop, so it is the
|
|
944
|
+
// height the gives-way tests are measured against — a row is one line only
|
|
945
|
+
// while its source column fits, and at 50 columns a variable name is three.
|
|
946
|
+
const cursorRow = rows[Math.max(0, Math.min(cursor, rows.length - 1))];
|
|
947
|
+
const mustKeep = cursorRow ? rowHeight(cursorRow, contentWidth, CURRENT_MARK) : 1;
|
|
948
|
+
const showSubtitle = screenRows - listCoreRows - setupHintRows - subtitleRows >= mustKeep;
|
|
949
|
+
const showSetupHint = screenRows - listCoreRows - (showSubtitle ? subtitleRows : 0) - setupHintRows >= mustKeep;
|
|
950
|
+
const listChromeRows = listCoreRows + (showSubtitle ? subtitleRows : 0) + (showSetupHint ? setupHintRows : 0);
|
|
951
|
+
const rowBudget = Math.max(1, screenRows - (detected.length === 0 ? emptyScreenChromeRows : listChromeRows));
|
|
952
|
+
const listWindow = rowWindow(rows, cursor, rowBudget, contentWidth);
|
|
671
953
|
if (detected.length === 0) {
|
|
672
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.status.warn, paddingX: 1, children: [
|
|
673
|
-
.map((entry) => entry.label)
|
|
674
|
-
.join(' and '), ' ', "sessions on this device"] }), _jsxs(Text, { color: theme.text.muted, children: [' ', "\u00B7 subscriptions signed in to from namzu (~/.namzu/credentials.json)"] }), _jsxs(Text, { color: theme.text.muted, children: [' ', "\u00B7 env vars / API keys (optional alternatives: ANTHROPIC_API_KEY, OPENAI_API_KEY, \u2026)"] }), _jsxs(Text, { color: theme.text.muted, children: [' ', "\u00B7 local servers (Ollama localhost:11434, LM Studio localhost:1234)"] })] }), onLogin ? (_jsx(Box, { paddingTop: 1, children: _jsxs(Text, { color: theme.text.primary, children: ["Press ", _jsx(Text, { color: theme.accent.system, children: "l" }), " to sign in with a subscription \u2014 no API key, and namzu keeps it for next time."] }) })) : null, _jsx(Box, { paddingTop: 1, children: _jsxs(Text, { color: theme.text.primary, children: ["Or press ", _jsx(Text, { color: theme.accent.system, children: "k" }), " to paste a credential now and use it for this session."] }) }), _jsx(Box, { paddingTop: 1, children: _jsx(Text, { color: theme.text.secondary, children: "You can also set one of the env vars above (or start a local server) and restart." }) }), _jsx(ProviderSetupRows, { rows: rows, cursor: cursor, currentProvider: currentProvider }), _jsx(Box, { paddingTop: 1, children: _jsxs(Text, { color: theme.text.muted, children: [onLogin ? 'l: sign in · ' : '', "\u2191\u2193 or 1-9 navigate \u00B7 enter or k: enter a credential \u00B7 esc: exit picker"] }) })] }));
|
|
954
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.status.warn, paddingX: 1, children: [buildNoticeBox(showSetupHint), _jsx(Text, { color: theme.status.warn, bold: true, children: "No providers detected" }), _jsxs(Box, { paddingTop: 1, flexDirection: "column", children: [_jsx(WrappedLines, { text: EMPTY_SCREEN_INTRO, width: contentWidth, color: theme.text.primary }), emptyScreenSourceLines().map((line) => (_jsx(WrappedLines, { text: line, width: contentWidth, color: theme.text.muted }, line)))] }), onLogin ? (_jsx(Box, { paddingTop: 1, flexDirection: "column", children: _jsx(HintLines, { text: `Press l ${EMPTY_SCREEN_HINTS.signIn}`, keyLetter: "l", width: contentWidth, color: theme.text.primary }) })) : null, _jsx(Box, { paddingTop: 1, flexDirection: "column", children: _jsx(HintLines, { text: `Or press k ${EMPTY_SCREEN_HINTS.paste}`, keyLetter: "k", width: contentWidth, color: theme.text.primary }) }), _jsx(Box, { paddingTop: 1, children: _jsx(WrappedLines, { text: EMPTY_SCREEN_HINTS.durable, width: contentWidth, color: theme.text.secondary }) }), _jsx(ProviderSetupRows, { rows: rows, cursor: cursor, currentProvider: currentProvider, visible: listWindow, contentWidth: contentWidth }), _jsx(Box, { paddingTop: 1, children: _jsx(Text, { color: theme.text.muted, children: emptyScreenFooter }) })] }));
|
|
675
955
|
}
|
|
676
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [
|
|
956
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focus, paddingX: 1, children: [buildNoticeBox(showSetupHint), _jsxs(Box, { flexDirection: "column", paddingBottom: 1, children: [_jsx(Text, { color: theme.accent.system, bold: true, children: selectionKind === 'signed-in-subscription'
|
|
677
957
|
? 'Choose a signed-in subscription'
|
|
678
|
-
: 'Choose a provider' }), _jsx(Text, { color: theme.text.muted, children:
|
|
679
|
-
? `${detected.length} already usable · no API key required`
|
|
680
|
-
: `${detected.length} detected · device sessions / Namzu sign-ins / optional keys / local probes` })] }), _jsx(ProviderSetupRows, { rows: rows, cursor: cursor, currentProvider: currentProvider }), _jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsxs(Text, { color: theme.text.muted, children: ["\u2191\u2193 or 1-9 navigate \u00B7 enter ", selectionKind === 'signed-in-subscription' ? 'use' : 'accept', entryTarget
|
|
681
|
-
? ` · k enter a credential${namesAnotherRow ? ` for ${entryTarget.label}` : ''}`
|
|
682
|
-
: '', onLogin && selectionKind !== 'signed-in-subscription'
|
|
683
|
-
? ' · l create a Namzu sign-in'
|
|
684
|
-
: '', ' ', "\u00B7 esc cancel"] }), rows.length > 9 ? (_jsx(Text, { color: theme.text.muted, children: "Rows past 9 are \u2191\u2193 only: two digits cannot be told from two presses." })) : null, errorHint ? _jsx(Text, { color: theme.status.warn, children: errorHint }) : null] })] }));
|
|
958
|
+
: 'Choose a provider' }), showSubtitle ? (_jsx(WrappedLines, { text: listSubtitle, width: contentWidth, color: theme.text.muted })) : null] }), _jsx(ProviderSetupRows, { rows: rows, cursor: cursor, currentProvider: currentProvider, visible: listWindow, contentWidth: contentWidth }), _jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(WrappedLines, { text: listFooter, width: contentWidth, color: theme.text.muted }), rows.length > 9 ? (_jsx(Text, { color: theme.text.muted, children: "Rows past 9 are \u2191\u2193 only: two digits cannot be told from two presses." })) : null, errorHint ? _jsx(Text, { color: theme.status.warn, children: errorHint }) : null] })] }));
|
|
685
959
|
}
|
|
686
960
|
function ModelStepView({ providerLabel, otherProviders, step, query, currentModel, cursor, errorHint, returnToProviders, sessionOnly, }) {
|
|
687
961
|
const terminal = useWindowSize();
|
|
688
962
|
const columns = terminal.columns ?? 80;
|
|
689
|
-
|
|
963
|
+
// The same area the provider list measures against — see `pickerContentWidth`
|
|
964
|
+
// for the three deductions and what counting only two of them cost.
|
|
965
|
+
const width = pickerContentWidth(columns);
|
|
690
966
|
const choices = step ? filterModelChoices(step.choices, query ?? '') : [];
|
|
691
967
|
const noticeRows = step?.notice ? Math.ceil(choiceDisplayWidth(step.notice) / width) : 0;
|
|
692
968
|
const window = selectionWindow(choices, cursor, Math.max(1, Math.min(7, (terminal.rows ?? 24) - 7 - noticeRows)));
|
|
@@ -739,47 +1015,237 @@ function ModelStepView({ providerLabel, otherProviders, step, query, currentMode
|
|
|
739
1015
|
* The appended rows carry no blank line between them. They are a work list
|
|
740
1016
|
* rather than a catalogue, and a machine with six of them would otherwise push
|
|
741
1017
|
* the box past a 24-row terminal and scroll the top of it — the detected rows —
|
|
742
|
-
* out of sight.
|
|
1018
|
+
* out of sight. That argument is now the behaviour: `visible` is the slice that
|
|
1019
|
+
* fits, and when the whole list does not, the block boundary travels with the
|
|
1020
|
+
* window so the heading stays on screen wherever the operator has scrolled to.
|
|
1021
|
+
*
|
|
1022
|
+
* Grouping by vendor only shortened this list, and it shortened it by exactly
|
|
1023
|
+
* the duplicates: with one vendor drawn once instead of twice, nine
|
|
1024
|
+
* vendors is the most this screen can ever draw, and the digit shortcut's
|
|
1025
|
+
* nine-row limit is now a bound the list cannot pass rather than one it can
|
|
1026
|
+
* reach and overshoot. The sentence below still guards it, because a tenth
|
|
1027
|
+
* vendor in the registry would put the boundary back.
|
|
743
1028
|
*/
|
|
744
|
-
function ProviderSetupRows({ rows, cursor, currentProvider, }) {
|
|
745
|
-
// The two
|
|
1029
|
+
function ProviderSetupRows({ rows, cursor, currentProvider, visible, contentWidth, }) {
|
|
1030
|
+
// The two blocks are contiguous by construction (`providerListRows` appends),
|
|
746
1031
|
// so one boundary splits them and every row's number is its position in the
|
|
747
|
-
// whole list, whichever block it is drawn in.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1032
|
+
// whole list, whichever block it is drawn in. The boundary is a row's own
|
|
1033
|
+
// state now — whether this machine has anything for it — rather than which
|
|
1034
|
+
// arm of a union it is in.
|
|
1035
|
+
const firstUndetected = rows.findIndex((row) => row.detected.length === 0);
|
|
1036
|
+
return (_jsx(Box, { flexDirection: "column", children: visible.items.map((row, offset) => {
|
|
1037
|
+
const index = visible.start + offset;
|
|
1038
|
+
// The heading is drawn above the first not-detected row, or at the
|
|
1039
|
+
// top when the window has scrolled past that boundary — so the words
|
|
1040
|
+
// that explain the block below are never scrolled away from it.
|
|
1041
|
+
const heading = firstUndetected !== -1 &&
|
|
1042
|
+
(index === firstUndetected || (offset === 0 && index > firstUndetected)) ? (_jsx(Box, { marginTop: offset === 0 ? 0 : 1, children: _jsx(Text, { color: theme.text.muted, children: NOT_DETECTED_HEADING }) }, "heading")) : null;
|
|
1043
|
+
const detected = row.detected.length > 0;
|
|
1044
|
+
return (_jsxs(Fragment, { children: [heading, detected ? (_jsx(DetectedVendorRow, { row: row, index: index, selected: index === cursor, isCurrent: currentProvider != null && rowHasProvider(row, currentProvider), contentWidth: contentWidth })) : (_jsx(UnconfiguredVendorRow, { row: row, index: index, selected: index === cursor, isCurrent: currentProvider != null && rowHasProvider(row, currentProvider), contentWidth: contentWidth }))] }, row.vendor));
|
|
1045
|
+
}) }));
|
|
751
1046
|
}
|
|
752
1047
|
/**
|
|
753
|
-
* A row for a
|
|
1048
|
+
* A row for a vendor nothing on this machine can serve yet.
|
|
754
1049
|
*
|
|
755
|
-
* Deliberately a separate component from `
|
|
1050
|
+
* Deliberately a separate component from `DetectedVendorRow` rather than one
|
|
756
1051
|
* component with two modes: the detected row's shape is what operators read
|
|
757
1052
|
* every day, and a shared body is a body where a change for the new rows moves
|
|
758
1053
|
* the old ones. The columns line up because both pad the label to the same
|
|
759
1054
|
* width; what differs is the third one, which says what is missing instead of
|
|
760
1055
|
* where the credential came from.
|
|
761
1056
|
*/
|
|
762
|
-
function
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1057
|
+
function UnconfiguredVendorRow({ row, index, selected, isCurrent, contentWidth, }) {
|
|
1058
|
+
// The variable, not "not configured": it is the one thing the operator has
|
|
1059
|
+
// to act on, and it is what makes the credential durable. The colour marks
|
|
1060
|
+
// the work still to do on a screen that is otherwise all done.
|
|
1061
|
+
const mark = isCurrent ? CURRENT_MARK : '';
|
|
1062
|
+
const source = rowSourceLines(row, contentWidth, mark);
|
|
1063
|
+
const markInline = rowMarkPlacement(contentWidth, mark).inline;
|
|
1064
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(RowLine, { cursor: selected ? '›' : ' ', number: `${index + 1}.`, label: row.label, labelColor: selected ? theme.border.focus : theme.text.primary, labelBold: selected, numberColor: selected ? theme.border.focus : theme.text.muted, source: source[0] ?? '', sourceColor: theme.status.warn, currentMark: mark, markInline: markInline }), source.slice(1).map((line, at) => (
|
|
1065
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: wrapped lines have no identity beyond their order.
|
|
1066
|
+
_jsx(Text, { color: theme.status.warn, children: `${ROW_INDENT}${line}` }, at))), mark !== '' && !markInline ? (_jsx(Text, { color: theme.accent.system, children: `${ROW_INDENT}${mark.trimStart()}` })) : null] }));
|
|
767
1067
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return (_jsxs(
|
|
1068
|
+
/**
|
|
1069
|
+
* One row's first line: marker, number, name and source.
|
|
1070
|
+
*
|
|
1071
|
+
* The segments are children of ONE `<Text>` rather than siblings in a `<Box>`,
|
|
1072
|
+
* and that is load-bearing rather than stylistic. A row that reaches the edge of
|
|
1073
|
+
* the box is compressed, and a compressed row loses the trailing whitespace of
|
|
1074
|
+
* the nodes it is built from — so the cursor row, which is one column wider
|
|
1075
|
+
* than every other row because of its marker, came out as `› 6.DeepSeek` while
|
|
1076
|
+
* the rows below kept their `6. `. Children of a text node are squashed into
|
|
1077
|
+
* one string before anything measures or wraps them, so the space between the
|
|
1078
|
+
* marker and the number is not trailing anything, and neither is the gap the
|
|
1079
|
+
* padded name leaves before the source.
|
|
1080
|
+
*/
|
|
1081
|
+
function RowLine({ cursor, number, label, labelColor, labelBold, numberColor, source, sourceColor, currentMark, markInline = true, dim = false, }) {
|
|
1082
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: numberColor, children: cursor }), " ", _jsx(Text, { color: numberColor, children: number }), ' ', _jsx(Text, { color: labelColor, bold: labelBold, dimColor: dim, children: label.padEnd(ROW_LABEL_WIDTH) }), _jsx(Text, { color: sourceColor, dimColor: dim, children: source }), currentMark && markInline ? (_jsx(Text, { color: theme.accent.system, children: currentMark })) : null] }));
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* A row for a vendor something on this machine can already serve.
|
|
1086
|
+
*
|
|
1087
|
+
* What was found stays what is shown. A provider this build cannot construct is
|
|
1088
|
+
* still genuinely on the machine, and replacing "local · localhost:1234" with
|
|
1089
|
+
* the refusal would hide the discovery that makes the refusal make sense. The
|
|
1090
|
+
* reason goes in the source column, where the row says what namzu knows about
|
|
1091
|
+
* it.
|
|
1092
|
+
*
|
|
1093
|
+
* The row's name is the vendor's, which is the same word the operator used to
|
|
1094
|
+
* reach it — and the only place the two could differ is where one vendor is two
|
|
1095
|
+
* registry ids, which is the case this screen was changed for.
|
|
1096
|
+
*/
|
|
1097
|
+
function DetectedVendorRow({ row, index, selected, isCurrent, contentWidth, }) {
|
|
1098
|
+
const usable = rowIsUsable(row);
|
|
1099
|
+
const mark = isCurrent ? CURRENT_MARK : '';
|
|
1100
|
+
const source = rowSourceLines(row, contentWidth, mark);
|
|
1101
|
+
const markInline = rowMarkPlacement(contentWidth, mark).inline;
|
|
1102
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(RowLine, { cursor: selected ? '›' : ' ', number: `${index + 1}.`, label: row.label, labelColor: usable ? (selected ? theme.border.focus : theme.text.primary) : theme.text.muted, labelBold: usable && selected, numberColor: selected ? theme.border.focus : theme.text.muted, source: source[0] ?? '', sourceColor: usable ? theme.text.muted : theme.status.warn, currentMark: mark, markInline: markInline, dim: !usable }), source.slice(1).map((line, at) => (
|
|
1103
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: wrapped lines have no identity beyond their order.
|
|
1104
|
+
_jsx(Text, { color: usable ? theme.text.muted : theme.status.warn, dimColor: !usable, children: `${ROW_INDENT}${line}` }, at))), mark !== '' && !markInline ? (_jsx(Text, { color: theme.accent.system, dimColor: !usable, children: `${ROW_INDENT}${mark.trimStart()}` })) : null] }));
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Text this screen wraps before the renderer sees it.
|
|
1108
|
+
*
|
|
1109
|
+
* Every sentence whose line count the box is sized against goes through here,
|
|
1110
|
+
* for two reasons that are the same reason. Ink's wrapping keeps the space it
|
|
1111
|
+
* broke at, so a sentence that ran onto a second line began it one column to
|
|
1112
|
+
* the right of the first — inside a box whose whole point is that its left edge
|
|
1113
|
+
* is straight. And a line count the screen reserved has to be the line count
|
|
1114
|
+
* the screen draws, which it is only if the screen is the one wrapping.
|
|
1115
|
+
*/
|
|
1116
|
+
function WrappedLines({ text, width, color, }) {
|
|
1117
|
+
return (_jsx(_Fragment, { children: wrapChoiceText(text, width).map((line, at) => (
|
|
1118
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: wrapped lines have no identity beyond their order.
|
|
1119
|
+
_jsx(Text, { color: color, children: line || ' ' }, at))) }));
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* The same, for a sentence that names a key the operator presses.
|
|
1123
|
+
*
|
|
1124
|
+
* The key is picked out in the accent colour wherever the wrap puts it, which
|
|
1125
|
+
* is why this splits the line it wrapped rather than the text it was given: a
|
|
1126
|
+
* span placed before wrapping is a span the wrap can move.
|
|
1127
|
+
*/
|
|
1128
|
+
function HintLines({ text, keyLetter, width, color, }) {
|
|
1129
|
+
return (_jsx(_Fragment, { children: wrapChoiceText(text, width).map((line, at) => {
|
|
1130
|
+
const words = line.split(' ');
|
|
1131
|
+
const key = words.indexOf(keyLetter);
|
|
1132
|
+
if (key < 0) {
|
|
1133
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: wrapped lines have no identity beyond their order.
|
|
1134
|
+
return (_jsx(Text, { color: color, children: line }, at));
|
|
1135
|
+
}
|
|
1136
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: wrapped lines have no identity beyond their order.
|
|
1137
|
+
return (_jsxs(Text, { color: color, children: [`${words.slice(0, key).join(' ')}${key > 0 ? ' ' : ''}`, _jsx(Text, { color: theme.accent.system, children: keyLetter }), key < words.length - 1 ? ` ${words.slice(key + 1).join(' ')}` : ''] }, at));
|
|
1138
|
+
}) }));
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* A row's third column, wrapped to the columns the row has left for it.
|
|
1142
|
+
*
|
|
1143
|
+
* Wrapped here rather than by the renderer because a row's height is part of
|
|
1144
|
+
* the box's height, and the row window budgets in lines: Ink wrapping a row the
|
|
1145
|
+
* screen had measured as one line tall is exactly how the box grew past the
|
|
1146
|
+
* terminal and pushed its own header off the top. The count below draws the
|
|
1147
|
+
* same lines it measures, so the two cannot disagree.
|
|
1148
|
+
*/
|
|
1149
|
+
function rowSourceLines(row, contentWidth, currentMark = '') {
|
|
1150
|
+
const text = row.detected.length > 0 ? rowSourceText(row) : (rowCredentialNeed(row) ?? '');
|
|
1151
|
+
const available = Math.max(1, rowMarkPlacement(contentWidth, currentMark).sourceColumns);
|
|
1152
|
+
return capChoiceLines(wrapChoiceText(text, available), MAX_SOURCE_LINES, available);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Where a row's `← current` mark goes at this width, and what it leaves the
|
|
1156
|
+
* source.
|
|
1157
|
+
*
|
|
1158
|
+
* Inline while the source keeps enough columns to be readable, and on a line of
|
|
1159
|
+
* its own when it does not. The mark is twelve columns, and at 60 columns
|
|
1160
|
+
* sharing the line with it left the source nine — which broke
|
|
1161
|
+
* `DEEPSEEK_API_KEY` across three lines to say what the operator already knew
|
|
1162
|
+
* from choosing this row.
|
|
1163
|
+
*/
|
|
1164
|
+
function rowMarkPlacement(contentWidth, currentMark) {
|
|
1165
|
+
const width = contentWidth - ROW_PREFIX_WIDTH;
|
|
1166
|
+
if (currentMark === '')
|
|
1167
|
+
return { inline: false, sourceColumns: width };
|
|
1168
|
+
const shares = width - choiceDisplayWidth(currentMark) >= MIN_SOURCE_COLUMNS;
|
|
1169
|
+
return {
|
|
1170
|
+
inline: shares,
|
|
1171
|
+
sourceColumns: Math.max(1, width - (shares ? choiceDisplayWidth(currentMark) : 0)),
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* The lines one row occupies at this width.
|
|
1176
|
+
*
|
|
1177
|
+
* A row that carries the mark on a line below its source is one line taller,
|
|
1178
|
+
* which is why the placement is decided by a function both the measurement and
|
|
1179
|
+
* the renderer call: a height the box did not reserve is the header scrolled off
|
|
1180
|
+
* the top of the terminal.
|
|
1181
|
+
*/
|
|
1182
|
+
function rowHeight(row, contentWidth, currentMark = '') {
|
|
1183
|
+
const lines = rowSourceLines(row, contentWidth, currentMark).length;
|
|
1184
|
+
const below = currentMark !== '' && !rowMarkPlacement(contentWidth, currentMark).inline;
|
|
1185
|
+
return below ? lines + 1 : lines;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* The rows that fit a line budget, with the row under the cursor among them.
|
|
1189
|
+
*
|
|
1190
|
+
* A window over ROWS and a budget in LINES, because a row is one line only
|
|
1191
|
+
* while its source column fits beside the label. The cursor's row is always
|
|
1192
|
+
* drawn whole — a cursor scrolled out of its own window is a screen the arrows
|
|
1193
|
+
* cannot explain — and the rows around it fill what is left, half the budget
|
|
1194
|
+
* above it when there is that much to show.
|
|
1195
|
+
*/
|
|
1196
|
+
function rowWindow(rows, cursor, budget, contentWidth) {
|
|
1197
|
+
if (rows.length === 0)
|
|
1198
|
+
return { start: 0, items: [] };
|
|
1199
|
+
// The row under the cursor is the marked one when a provider is in force, and
|
|
1200
|
+
// a marked row may be a line taller: see `rowHeight`.
|
|
1201
|
+
const marked = rows[Math.max(0, Math.min(cursor, rows.length - 1))];
|
|
1202
|
+
const heights = rows.map((row) => rowHeight(row, contentWidth, row === marked ? CURRENT_MARK : ''));
|
|
1203
|
+
const at = Math.max(0, Math.min(cursor, rows.length - 1));
|
|
1204
|
+
let start = at;
|
|
1205
|
+
let used = heights[at] ?? 1;
|
|
1206
|
+
const half = Math.max(1, Math.floor(budget / 2));
|
|
1207
|
+
while (start > 0 && used < half && used + (heights[start - 1] ?? 1) <= budget) {
|
|
1208
|
+
start -= 1;
|
|
1209
|
+
used += heights[start] ?? 1;
|
|
1210
|
+
}
|
|
1211
|
+
let end = at + 1;
|
|
1212
|
+
while (end < rows.length && used + (heights[end] ?? 1) <= budget) {
|
|
1213
|
+
used += heights[end] ?? 1;
|
|
1214
|
+
end += 1;
|
|
1215
|
+
}
|
|
1216
|
+
return { start, items: rows.slice(start, end) };
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* What a detected row's third column says: where what it found came from.
|
|
1220
|
+
*
|
|
1221
|
+
* Every source is named, not the first one. A vendor reached two ways at once —
|
|
1222
|
+
* an exported key and a signed-in session — has two usable credentials, and
|
|
1223
|
+
* naming one of them would be the row choosing for an operator who has not
|
|
1224
|
+
* chosen anything yet. `+` joins them because the sources are joined by ` · `
|
|
1225
|
+
* internally, and a list of four items reads as four discoveries.
|
|
1226
|
+
*/
|
|
1227
|
+
function rowSourceText(row) {
|
|
1228
|
+
const text = row.detected.map(describeSource).join(' + ');
|
|
1229
|
+
return rowIsUsable(row) ? text : `${text} · unavailable in this build`;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* One way in, as the line that offers it.
|
|
1233
|
+
*
|
|
1234
|
+
* Each line borrows the wording of the screen it leads to — "Use existing …" is
|
|
1235
|
+
* the sign-in screen's phrase for a session already on the device, "Sign in to …
|
|
1236
|
+
* · device code" is its phrase for a new one, "a credential for …" is the paste
|
|
1237
|
+
* field's title — so the operator recognises where a line goes before pressing
|
|
1238
|
+
* enter on it.
|
|
1239
|
+
*/
|
|
1240
|
+
function describePath(path) {
|
|
1241
|
+
switch (path.kind) {
|
|
1242
|
+
case 'detected':
|
|
1243
|
+
return `Use existing ${path.detected.entry.label} · ${describeSource(path.detected)}`;
|
|
1244
|
+
case 'credential':
|
|
1245
|
+
return `Enter a credential for ${path.entry.label} · ${credentialNeed(path.entry)}`;
|
|
1246
|
+
case 'sign-in':
|
|
1247
|
+
return `Sign in to ${path.entry.label} · ${path.entry.subscriptionLogin === 'device' ? 'device code' : 'browser'}`;
|
|
1248
|
+
}
|
|
783
1249
|
}
|
|
784
1250
|
function describeSource(d) {
|
|
785
1251
|
switch (d.source.kind) {
|