dsh-ssh-tui 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +67 -2
- package/README.md +56 -3
- package/lib/context-window.js +139 -0
- package/lib/context-window.js.map +1 -0
- package/lib/dialogs.js +13 -7
- package/lib/dialogs.js.map +1 -1
- package/lib/i18n/en.js +69 -3
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +69 -3
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +10 -4
- package/lib/index.js.map +1 -1
- package/lib/job-label.js +45 -0
- package/lib/job-label.js.map +1 -0
- package/lib/paint.js +3 -3
- package/lib/paint.js.map +1 -1
- package/lib/preset-label.js +73 -0
- package/lib/preset-label.js.map +1 -0
- package/lib/preset-rows.js +96 -0
- package/lib/preset-rows.js.map +1 -0
- package/lib/provider-catalog.js +12 -2
- package/lib/provider-catalog.js.map +1 -1
- package/lib/quota.js +149 -1
- package/lib/quota.js.map +1 -1
- package/lib/reasoning.js +9 -4
- package/lib/reasoning.js.map +1 -1
- package/lib/term-text.js +82 -11
- package/lib/term-text.js.map +1 -1
- package/lib/tool-present.js +14 -1
- package/lib/tool-present.js.map +1 -1
- package/lib/tui.js +363 -25
- package/lib/tui.js.map +1 -1
- package/lib/types/context-window.d.ts +46 -0
- package/lib/types/dialogs.d.ts +5 -4
- package/lib/types/job-label.d.ts +20 -0
- package/lib/types/preset-label.d.ts +38 -0
- package/lib/types/preset-rows.d.ts +47 -0
- package/lib/types/provider-catalog.d.ts +2 -0
- package/lib/types/quota.d.ts +49 -0
- package/lib/types/reasoning.d.ts +9 -4
- package/lib/types/term-text.d.ts +33 -4
- package/lib/types/tool-present.d.ts +1 -0
- package/lib/types/tui.d.ts +76 -1
- package/package.json +1 -1
package/lib/tui.js
CHANGED
|
@@ -29,8 +29,11 @@ import { commandAcceptsAttachments, forEachSessionEventAsync, isAssistantStreamE
|
|
|
29
29
|
import { classifyApprovalDetailed, commandForApprovalRequest, isApprovalStatusArg, parseAutoApprovalMode } from './auto-approval.js';
|
|
30
30
|
import { buildReviewUserMessage, parseReviewOutput, reviewSystemPrompt } from './approval-reviewer.js';
|
|
31
31
|
import { loadProviderCatalog, mergeProviderEntries } from './provider-catalog.js';
|
|
32
|
+
import { catalogContextWindow, catalogWindowIndex, suggestedRouteContextWindow, } from './context-window.js';
|
|
32
33
|
import { formatFooterCwd } from './session-list.js';
|
|
33
34
|
import { collectDiag, formatDiag } from './diag.js';
|
|
35
|
+
import { presetLabel, profileFromArgv } from './preset-label.js';
|
|
36
|
+
import { ensureRosterRows, rosterPatchPath } from './preset-rows.js';
|
|
34
37
|
import { SessionStatsTracker, statsRowOf } from './stats.js';
|
|
35
38
|
import { QUESTION_OPTION_KEYS, confirmAnswer, inspectClosesOn, moveQuestionCursor, optionsLength, questionSubmit, selectQuestionOptionByKey, } from './dialogs.js';
|
|
36
39
|
import { commandSuggestions, localizedCommands } from './commands.js';
|
|
@@ -49,13 +52,13 @@ import { clipAnsiToWidth, hrefAtColumn, osc52Clipboard, paintedLinkHits, cursorV
|
|
|
49
52
|
import { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, HANGUP_CANCEL_TIMEOUT_MS, ignoreFurtherHangupSignals, isEscapePrefix, isHangupErrno, parseCursorPositionReply, PICKER_WINDOW, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, } from './paint.js';
|
|
50
53
|
import { TerminalInputGuard } from './terminal-input.js';
|
|
51
54
|
import { contextPressureAlertText, contextPressureRingColor, contextPressureView, formatContextPressureRing, describeProviderRoute, fitFooterStatsLine, fitFooterStatusLine, footerActivity, footerIdentityParts, footerStatsGroups, formatContextPressureChip, formatStatusReport, formatTokens, parseContextPressure, promptPressureTokens, providerUsesLocalOAuth, shouldIdleAutoCompact, } from './footer.js';
|
|
52
|
-
import { crossedQuotaThresholds, DEEPSEEK_PUBLIC_BASE_URL, formatAccountBalance, formatFooterBalance, formatQuotaSnapshot, joinUrl, OPENAI_COMPAT_BALANCE_PATHS, OPENCODE_GO_USAGE_URL, OPENCODE_ZEN_BASE_URL, openCodeApiErrorMessage, openCodeSourceFor, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, reasoningEffortsForDefault, SUPERGROK_BILLING_URL, tightestQuotaWindow, } from './quota.js';
|
|
55
|
+
import { COMMAND_CODE_CREDITS_URL, COMMAND_CODE_SUBSCRIPTIONS_URL, COMMAND_CODE_USAGE_URL, commandCodePeriodStart, commandCodeSourceFor, crossedQuotaThresholds, DEEPSEEK_PUBLIC_BASE_URL, formatAccountBalance, formatFooterBalance, formatQuotaSnapshot, joinUrl, OPENAI_COMPAT_BALANCE_PATHS, OPENCODE_GO_USAGE_URL, OPENCODE_ZEN_BASE_URL, openCodeApiErrorMessage, openCodeSourceFor, parseCommandCodeQuota, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, reasoningEffortsForDefault, SUPERGROK_BILLING_URL, tightestQuotaWindow, } from './quota.js';
|
|
53
56
|
import { appendSubagentLog, applyTurnEndToPlan, cardCategoryLabel, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planMarkdownFromArgs, planDockNote, planIsLive, planTitleFromMarkdown, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoItemKind, todoProgressLabel, TODO_STATUS_MARK, } from './plan.js';
|
|
54
57
|
import { buildToolHeader, compactEditPath, compactToolBursts, countDiffAddDel, countDiffLines, countOutputLines, diffMetaDiffs, diffStatToken, formatModelList, HIDDEN_TOOL_NAMES, parseExitStatus, planReviewOf, presentToolCall, READ_TOOL_NAMES, SHELL_TOOL_NAMES, toolBodyFitsWorkspace, toolBodyLines, toolTitle, wrappedToolBodyLineCount, } from './tool-present.js';
|
|
55
58
|
export { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, hrefAtColumn, osc52Clipboard, osc8Enabled, paintedLinkHits, fmtElapsedCompact, padAnsiToWidth, padToWidth, renderMarkdownLines, repeatToWidth, shimmerText, truncateToWidth, visibleWidth, waitCardCopy, waitSummaryFromReasoning, wrapWaitDetails, } from './term-text.js';
|
|
56
59
|
export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, findCursorPositionReply, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, } from './paint.js';
|
|
57
60
|
export { CONTEXT_IDLE_COMPACT_RATIO, CONTEXT_PRESSURE_DANGER_RATIO, CONTEXT_PRESSURE_WARN_RATIO, CONTEXT_RING_EMPTY, CONTEXT_RING_SEGMENTS, contextPressureAlertText, contextPressureRingColor, contextPressureUsedTokens, contextPressureView, describeProviderRoute, dropFooterQuotaPlanName, fitFooterStatsLine, fitFooterStatusLine, footerActivity, footerIdentityParts, footerStatsGroups, formatContextPressureChip, formatContextPressureRing, formatContextPressureStatusLine, formatDuration, formatFooterQuota, formatQuotaBar, formatStatusReport, formatTokens, formatTokensPerSecond, parseContextPressure, promptPressureTokens, providerShortCode, providerUsesLocalOAuth, shouldIdleAutoCompact, subagentRouteLabel, } from './footer.js';
|
|
58
|
-
export { crossedQuotaThresholds, formatAccountBalance, formatFooterBalance, formatOpenCodeGoUsage, formatQuotaSnapshot, formatQuotaStatusLine, joinUrl, openCodeSourceFor, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, quotaRefreshEveryTurns, remainingPercentFromUsed, tightestQuotaWindow, } from './quota.js';
|
|
61
|
+
export { commandCodePeriodStart, commandCodeSourceFor, crossedQuotaThresholds, formatAccountBalance, formatFooterBalance, formatOpenCodeGoUsage, formatQuotaSnapshot, formatQuotaStatusLine, joinUrl, openCodeSourceFor, parseCommandCodeQuota, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, quotaRefreshEveryTurns, remainingPercentFromUsed, tightestQuotaWindow, } from './quota.js';
|
|
59
62
|
export { commandAcceptsAttachments, forEachSessionEvent, isAssistantStreamEvent, isTokenDeltaChunk, listPersistenceHeaders, inspectPersistenceSession, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
|
|
60
63
|
export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoProgressLabel, todoSummary, } from './plan.js';
|
|
61
64
|
export { buildToolHeader, canMergeToolCall, compactEditPath, compactToolBursts, compactToolGroups, countDiffAddDel, countDiffLines, countOutputLines, diffMetaDiffs, diffStatToken, friendlyJsonLines, parseExitStatus, presentToolCall, READ_TOOL_NAMES, renderToolDiff, toolBodyFitsWorkspace, toolBodyLines, toolStateColor, toolStateLabel, wrappedToolBodyLineCount, } from './tool-present.js';
|
|
@@ -79,6 +82,12 @@ function installUserQuestionAnswerer(ctx, questions, ask) {
|
|
|
79
82
|
});
|
|
80
83
|
}
|
|
81
84
|
const ROUTE_MEMORY_NS = ROUTE_MEMORY_NAMESPACE;
|
|
85
|
+
/**
|
|
86
|
+
* The window llm-pi-ai falls back to when neither a model entry nor the
|
|
87
|
+
* installed catalog sizes a model. The wizard pre-fills it only so a route
|
|
88
|
+
* whose capacity nothing could discover still shows the number it will use.
|
|
89
|
+
*/
|
|
90
|
+
const HARNESS_DEFAULT_CONTEXT_WINDOW = 262_144;
|
|
82
91
|
function providerTemplates() {
|
|
83
92
|
return {
|
|
84
93
|
official: {
|
|
@@ -94,6 +103,16 @@ function providerTemplates() {
|
|
|
94
103
|
api: 'openai-responses',
|
|
95
104
|
defaultModels: ['deepseek-v4-flash', 'deepseek-v4-pro'],
|
|
96
105
|
},
|
|
106
|
+
// A fixed provider: its chat route is OpenAI-completions, but billing uses
|
|
107
|
+
// Command Code's own `/alpha/billing/credits` surface on the API root.
|
|
108
|
+
'command-code': {
|
|
109
|
+
label: t('onboard.providerCommandCode'),
|
|
110
|
+
defaultId: 'command-code',
|
|
111
|
+
defaultBaseUrl: 'https://api.commandcode.ai/provider/v1',
|
|
112
|
+
api: 'openai-completions',
|
|
113
|
+
defaultModels: ['deepseek/deepseek-v4.1-flash'],
|
|
114
|
+
defaultModelCapacity: { 'deepseek/deepseek-v4.1-flash': { contextWindow: 1_048_576 } },
|
|
115
|
+
},
|
|
97
116
|
'openai-completions': {
|
|
98
117
|
label: t('onboard.providerCompletions'),
|
|
99
118
|
defaultId: 'my-gateway',
|
|
@@ -327,6 +346,62 @@ function effortChoices(ids) {
|
|
|
327
346
|
function undeclaredEffortChoices() {
|
|
328
347
|
return effortChoices(UNDECLARED_EFFORT_IDS);
|
|
329
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* The `reasoningEfforts` map `/setup` writes for a hand-declared
|
|
351
|
+
* OpenAI-compatible route.
|
|
352
|
+
*
|
|
353
|
+
* Such a route is absent from pi-ai's catalog, so `defaultReasoningEffort`
|
|
354
|
+
* resolves nothing before the profile is saved and the model would land as a
|
|
355
|
+
* bare `{ id }` entry. The Harness then reports that model as non-reasoning
|
|
356
|
+
* (`levels [off]`) and refuses every effort the TUI itself offers for an
|
|
357
|
+
* undeclared model. Declaring the offered vocabulary up front keeps the model
|
|
358
|
+
* dispatchable; no parameter is sent until an effort is selected.
|
|
359
|
+
* @returns the level-to-wire map, with `off` meaning "send no parameter".
|
|
360
|
+
*/
|
|
361
|
+
export function handDeclaredReasoningEfforts() {
|
|
362
|
+
return {
|
|
363
|
+
off: null,
|
|
364
|
+
...Object.fromEntries(UNDECLARED_EFFORT_IDS.filter(id => id !== 'off').map(id => [id, id])),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Whether a wizard template speaks the OpenAI completions dialect whose
|
|
369
|
+
* `reasoning_effort` the TUI may declare without a catalog.
|
|
370
|
+
*
|
|
371
|
+
* The Responses dialect maps levels too, but its unset state materializes as
|
|
372
|
+
* `reasoning: { effort: "none" }`, which a custom gateway need not accept, so
|
|
373
|
+
* it keeps the existing undeclared behavior until an endpoint verifies it.
|
|
374
|
+
* @param providerType - the wizard template under test.
|
|
375
|
+
*/
|
|
376
|
+
function declaresOfferedReasoning(providerType) {
|
|
377
|
+
return providerType === 'openai-completions' || providerType === 'command-code';
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The `reasoningEfforts` map `/setup` persists for one model entry.
|
|
381
|
+
*
|
|
382
|
+
* A hand-declared OpenAI-compatible completions route always receives the
|
|
383
|
+
* offered vocabulary; a catalog-backed route keeps whatever live model info
|
|
384
|
+
* resolved, preserving the existing behavior of redeclaring only the chosen
|
|
385
|
+
* default.
|
|
386
|
+
* @param providerType - the wizard template that produced the route.
|
|
387
|
+
* @param defaultEffort - the level resolved from live model info, if any.
|
|
388
|
+
* @returns the level-to-wire map, or `undefined` to declare nothing.
|
|
389
|
+
*/
|
|
390
|
+
export function onboardingReasoningEfforts(providerType, defaultEffort) {
|
|
391
|
+
if (declaresOfferedReasoning(providerType)) {
|
|
392
|
+
return handDeclaredReasoningEfforts();
|
|
393
|
+
}
|
|
394
|
+
return defaultEffort === undefined ? undefined : { off: null, [defaultEffort]: defaultEffort };
|
|
395
|
+
}
|
|
396
|
+
/** The id of one configured llm-pi-ai models entry, in either stored form. */
|
|
397
|
+
function modelEntryId(raw) {
|
|
398
|
+
if (typeof raw === 'string')
|
|
399
|
+
return raw;
|
|
400
|
+
if (typeof raw === 'object' && raw !== null && typeof raw.id === 'string') {
|
|
401
|
+
return raw.id;
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
330
405
|
function localOAuthEffortChoices(modelId) {
|
|
331
406
|
return effortChoices(modelId === 'grok-4.6'
|
|
332
407
|
? ['off', 'low', 'medium', 'high', 'xhigh']
|
|
@@ -369,6 +444,8 @@ export class SshTui {
|
|
|
369
444
|
/** Web-aligned provider presets from the host's pi-ai catalog (undefined until loaded / when unreachable). */
|
|
370
445
|
catalogPresets;
|
|
371
446
|
catalogLoad;
|
|
447
|
+
/** Memoized id→window index over {@link catalogPresets}. */
|
|
448
|
+
catalogWindows;
|
|
372
449
|
input = '';
|
|
373
450
|
cursor = 0;
|
|
374
451
|
inputFolded = false;
|
|
@@ -417,7 +494,7 @@ export class SshTui {
|
|
|
417
494
|
disposers = [];
|
|
418
495
|
userQuestionDisposer;
|
|
419
496
|
presetId = 'standard';
|
|
420
|
-
presetName = t('mode.standard');
|
|
497
|
+
presetName = t('mode.preset.standard');
|
|
421
498
|
useAlternateScreen;
|
|
422
499
|
agentGone = false;
|
|
423
500
|
onboarding;
|
|
@@ -531,7 +608,7 @@ export class SshTui {
|
|
|
531
608
|
this.headlessDisplay = config.headlessDisplay === true;
|
|
532
609
|
this.disconnectPolicy = config.disconnectPolicy ?? this.readDisconnectPolicy();
|
|
533
610
|
this.presetId = config.presetId ?? 'standard';
|
|
534
|
-
this.presetName = config.presetName
|
|
611
|
+
this.presetName = presetLabel(this.presetId, config.presetName, config.presetTrust);
|
|
535
612
|
this.useAlternateScreen = process.env.DSH_TUI_NO_ALT_SCREEN !== '1' && process.env.DSH_TUI_NO_ALT_SCREEN !== 'true';
|
|
536
613
|
this.paintLink = detectSshSession() ? 'ssh' : 'local';
|
|
537
614
|
this.paintIntervalMs = resolvePaintIntervalMs(config.paintIntervalMs, process.env, {
|
|
@@ -540,6 +617,13 @@ export class SshTui {
|
|
|
540
617
|
this.pushRow({ kind: 'brand-logo' });
|
|
541
618
|
this.pushRow({ kind: 'system', text: t('boot.banner') });
|
|
542
619
|
this.pushRow({ kind: 'system', text: t('boot.help') });
|
|
620
|
+
// The roster is a profile-layer row, so an install that predates it (or an
|
|
621
|
+
// in-app update, which only runs `dsh plugin add`) boots without one. Say
|
|
622
|
+
// so at boot: the banner's localized default hides the missing service, and
|
|
623
|
+
// the loss is not only `/mode` — the preset-owned tools are absent too.
|
|
624
|
+
if (this.ctx.get('agentPresets') === undefined) {
|
|
625
|
+
this.pushRow({ kind: 'system', text: t('mode.bootMissing') });
|
|
626
|
+
}
|
|
543
627
|
if (config.cwdNotice !== undefined && config.cwdNotice !== '') {
|
|
544
628
|
this.pushRow({ kind: /进入|Entered/u.test(config.cwdNotice) ? 'system' : 'error', text: config.cwdNotice });
|
|
545
629
|
}
|
|
@@ -1028,6 +1112,7 @@ export class SshTui {
|
|
|
1028
1112
|
baseUrl: '',
|
|
1029
1113
|
key: '',
|
|
1030
1114
|
models: [],
|
|
1115
|
+
modelCapacity: new Map(),
|
|
1031
1116
|
catalogPresets: undefined,
|
|
1032
1117
|
catalog: undefined,
|
|
1033
1118
|
providerCursor: 0,
|
|
@@ -2447,6 +2532,15 @@ export class SshTui {
|
|
|
2447
2532
|
addDialog(t('onboard.modelsCatalogHint'));
|
|
2448
2533
|
addDialog(t('onboard.enterEsc'));
|
|
2449
2534
|
break;
|
|
2535
|
+
case 'context':
|
|
2536
|
+
addDialog(t('onboard.providerLine', { label: providerLabel }));
|
|
2537
|
+
addDialog(t('onboard.contextPrompt'));
|
|
2538
|
+
addDialog(t('onboard.contextValue', {
|
|
2539
|
+
value: String(ob.routeContextWindow ?? HARNESS_DEFAULT_CONTEXT_WINDOW),
|
|
2540
|
+
}));
|
|
2541
|
+
addDialog(t('onboard.contextHint'));
|
|
2542
|
+
addDialog(t('onboard.enterEsc'));
|
|
2543
|
+
break;
|
|
2450
2544
|
case 'confirm':
|
|
2451
2545
|
addDialog(t('onboard.confirmTitle'));
|
|
2452
2546
|
addDialog(t('onboard.confirmProvider', { label: providerLabel }));
|
|
@@ -2456,6 +2550,9 @@ export class SshTui {
|
|
|
2456
2550
|
api: template.api ?? (ob.providerType === 'catalog' ? t('onboard.apiCatalog') : 'deepseek-official'),
|
|
2457
2551
|
}));
|
|
2458
2552
|
addDialog(t('onboard.confirmModels', { list: formatModelList(ob.models, 8) }));
|
|
2553
|
+
if (ob.routeContextWindow !== undefined) {
|
|
2554
|
+
addDialog(t('onboard.confirmContext', { value: String(ob.routeContextWindow) }));
|
|
2555
|
+
}
|
|
2459
2556
|
addDialog(t('onboard.confirmKey', {
|
|
2460
2557
|
head: sliceCodePoints(ob.key, 6),
|
|
2461
2558
|
tail: lastCodePoints(ob.key, 4),
|
|
@@ -4431,6 +4528,14 @@ export class SshTui {
|
|
|
4431
4528
|
if (ids.has(modelId))
|
|
4432
4529
|
return true;
|
|
4433
4530
|
const modelEntry = { id: modelId };
|
|
4531
|
+
// A model added here skips the wizard, so size it from the installed
|
|
4532
|
+
// catalog the same way `/setup` does; the route default covers a miss.
|
|
4533
|
+
const catalogWindows = this.catalogWindowIndexSource();
|
|
4534
|
+
if (catalogWindows.size > 0) {
|
|
4535
|
+
const contextWindow = catalogContextWindow(modelId, catalogWindows);
|
|
4536
|
+
if (contextWindow !== undefined)
|
|
4537
|
+
modelEntry.contextWindow = contextWindow;
|
|
4538
|
+
}
|
|
4434
4539
|
const reasoningEfforts = reasoningEffortsForDefault(profile.reasoning);
|
|
4435
4540
|
if (reasoningEfforts !== undefined)
|
|
4436
4541
|
modelEntry.reasoningEfforts = reasoningEfforts;
|
|
@@ -4682,6 +4787,8 @@ export class SshTui {
|
|
|
4682
4787
|
})),
|
|
4683
4788
|
}, 0, 1, currentIndex);
|
|
4684
4789
|
const effort = choices.find(option => option.label === effortAnswer.selected[0])?.id;
|
|
4790
|
+
if (isUndeclared && effort !== undefined)
|
|
4791
|
+
await this.declareReasoningEffort(provider, modelId, effort);
|
|
4685
4792
|
const next = {
|
|
4686
4793
|
provider,
|
|
4687
4794
|
model: modelId,
|
|
@@ -4977,7 +5084,61 @@ export class SshTui {
|
|
|
4977
5084
|
return;
|
|
4978
5085
|
await this.setReasoningEffort(provider, modelId, picked.id, isUndeclared);
|
|
4979
5086
|
}
|
|
5087
|
+
/**
|
|
5088
|
+
* Persist one explicitly chosen level as a model's `reasoningEfforts`.
|
|
5089
|
+
*
|
|
5090
|
+
* `/effort` offers `low`…`max` for a model whose route declares nothing, but
|
|
5091
|
+
* the selection alone cannot make the request legal: the Harness still
|
|
5092
|
+
* resolves the model as non-reasoning and refuses it. Writing the chosen
|
|
5093
|
+
* level — merged with whatever the entry already declares — keeps the
|
|
5094
|
+
* selection and the declaration consistent. Only a route that explicitly
|
|
5095
|
+
* speaks OpenAI Completions is amended: its dialect omits the parameter for
|
|
5096
|
+
* `off` and was verified against a gateway, while another protocol may
|
|
5097
|
+
* materialize an unset level as a value its endpoint refuses.
|
|
5098
|
+
* @returns whether the entry now declares the level.
|
|
5099
|
+
*/
|
|
5100
|
+
async declareReasoningEffort(provider, modelId, effort) {
|
|
5101
|
+
const settings = this.ctx.get('settings');
|
|
5102
|
+
const profile = this.piAiProviderProfile(provider);
|
|
5103
|
+
if (settings === undefined || profile === undefined)
|
|
5104
|
+
return false;
|
|
5105
|
+
if (profile.api !== 'openai-completions')
|
|
5106
|
+
return false;
|
|
5107
|
+
const models = Array.isArray(profile.models) ? [...profile.models] : [];
|
|
5108
|
+
const index = models.findIndex(raw => modelEntryId(raw) === modelId);
|
|
5109
|
+
const existing = index >= 0 && typeof models[index] === 'object' && models[index] !== null
|
|
5110
|
+
? { ...models[index] }
|
|
5111
|
+
: {};
|
|
5112
|
+
const declared = existing.reasoningEfforts;
|
|
5113
|
+
const efforts = typeof declared === 'object' && declared !== null && !Array.isArray(declared)
|
|
5114
|
+
? { ...declared }
|
|
5115
|
+
: {};
|
|
5116
|
+
if (efforts[effort] === effort && 'off' in efforts)
|
|
5117
|
+
return true;
|
|
5118
|
+
const entry = {
|
|
5119
|
+
...existing,
|
|
5120
|
+
id: modelId,
|
|
5121
|
+
reasoningEfforts: { ...efforts, off: null, [effort]: effort },
|
|
5122
|
+
};
|
|
5123
|
+
if (index >= 0)
|
|
5124
|
+
models[index] = entry;
|
|
5125
|
+
else
|
|
5126
|
+
models.push(entry);
|
|
5127
|
+
try {
|
|
5128
|
+
await settings.mutate(settingsNamespace('llm-pi-ai'), [
|
|
5129
|
+
{ op: 'set', path: ['providers', provider, 'models'], value: models },
|
|
5130
|
+
]);
|
|
5131
|
+
return true;
|
|
5132
|
+
}
|
|
5133
|
+
catch (error) {
|
|
5134
|
+
this.pushRow({ kind: 'error', text: t('effort.declareFailed', { provider, model: modelId, error: errorChain(error) }) });
|
|
5135
|
+
this.markDirty();
|
|
5136
|
+
return false;
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
4980
5139
|
async setReasoningEffort(provider, modelId, effort, isUndeclared) {
|
|
5140
|
+
if (isUndeclared && effort !== undefined)
|
|
5141
|
+
await this.declareReasoningEffort(provider, modelId, effort);
|
|
4981
5142
|
const next = {
|
|
4982
5143
|
provider,
|
|
4983
5144
|
model: modelId,
|
|
@@ -5025,6 +5186,9 @@ export class SshTui {
|
|
|
5025
5186
|
return;
|
|
5026
5187
|
}
|
|
5027
5188
|
const targetEffort = parsed.kind === 'default' ? undefined : parsed.id;
|
|
5189
|
+
if (effortOptions.length === 0 && targetEffort !== undefined) {
|
|
5190
|
+
await this.declareReasoningEffort(provider, current.model, targetEffort);
|
|
5191
|
+
}
|
|
5028
5192
|
const next = {
|
|
5029
5193
|
...current,
|
|
5030
5194
|
...(targetEffort === undefined ? { reasoningEffort: undefined } : { reasoningEffort: ReasoningEffortId(targetEffort) }),
|
|
@@ -5064,6 +5228,9 @@ export class SshTui {
|
|
|
5064
5228
|
const picked = choices.find(option => option.label === answer.selected[0]);
|
|
5065
5229
|
if (picked === undefined)
|
|
5066
5230
|
return;
|
|
5231
|
+
if (effortOptions.length === 0 && picked.id !== undefined) {
|
|
5232
|
+
await this.declareReasoningEffort(provider, current.model, picked.id);
|
|
5233
|
+
}
|
|
5067
5234
|
const next = {
|
|
5068
5235
|
...current,
|
|
5069
5236
|
...(picked.id === undefined
|
|
@@ -5176,8 +5343,28 @@ export class SshTui {
|
|
|
5176
5343
|
/** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
|
|
5177
5344
|
async runModeCommand(arg = '') {
|
|
5178
5345
|
const agentPresets = this.ctx.get('agentPresets');
|
|
5346
|
+
const direct = arg.trim().toLowerCase();
|
|
5347
|
+
if (direct === 'fix' || direct === 'repair') {
|
|
5348
|
+
await this.repairRoster();
|
|
5349
|
+
return;
|
|
5350
|
+
}
|
|
5179
5351
|
if (agentPresets === undefined) {
|
|
5180
|
-
|
|
5352
|
+
// A terminal profile built on dsh-base composes no roster, and the
|
|
5353
|
+
// plugin's bundle patch may not mount one; only the profile's user layer
|
|
5354
|
+
// can. Report the exact row and offer the in-app repair: the install
|
|
5355
|
+
// scripts are the checkout path, while an npm install and the in-app
|
|
5356
|
+
// `dsh plugin add` update never run them.
|
|
5357
|
+
const profile = profileFromArgv();
|
|
5358
|
+
this.pushRow({
|
|
5359
|
+
kind: 'error',
|
|
5360
|
+
text: [
|
|
5361
|
+
t('mode.missingService'),
|
|
5362
|
+
t('mode.missingServiceHint', {
|
|
5363
|
+
profile,
|
|
5364
|
+
patch: rosterPatchPath(resolveDshHome(), profile),
|
|
5365
|
+
}),
|
|
5366
|
+
].join('\n'),
|
|
5367
|
+
});
|
|
5181
5368
|
this.markDirty();
|
|
5182
5369
|
return;
|
|
5183
5370
|
}
|
|
@@ -5187,12 +5374,16 @@ export class SshTui {
|
|
|
5187
5374
|
this.markDirty();
|
|
5188
5375
|
return;
|
|
5189
5376
|
}
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5377
|
+
// Shipped presets resolve through the TUI dictionary; a user-authored one
|
|
5378
|
+
// keeps the name its own preset.yml published.
|
|
5379
|
+
const labels = presets.map(preset => presetLabel(preset.id, preset.name, preset.trust));
|
|
5380
|
+
const matchesDirect = (preset, label) => preset.id.toLowerCase() === direct
|
|
5381
|
+
|| (preset.name ?? '').trim().toLowerCase() === direct
|
|
5382
|
+
|| label.toLowerCase() === direct;
|
|
5383
|
+
let index = direct === ''
|
|
5384
|
+
? -1
|
|
5385
|
+
: presets.findIndex((preset, at) => matchesDirect(preset, labels[at] ?? preset.id));
|
|
5386
|
+
if (index < 0 && direct !== '') {
|
|
5196
5387
|
this.pushRow({
|
|
5197
5388
|
kind: 'error',
|
|
5198
5389
|
text: t('mode.unknown', { id: arg.trim(), available: presets.map(preset => preset.id).join(', ') }),
|
|
@@ -5200,20 +5391,31 @@ export class SshTui {
|
|
|
5200
5391
|
this.markDirty();
|
|
5201
5392
|
return;
|
|
5202
5393
|
}
|
|
5203
|
-
if (
|
|
5394
|
+
if (index < 0 && direct === '') {
|
|
5204
5395
|
const answer = await this.askQuestion({
|
|
5205
5396
|
id: 'mode-pick',
|
|
5206
5397
|
question: t('mode.pick'),
|
|
5207
|
-
options: presets.map(preset => ({
|
|
5208
|
-
label:
|
|
5209
|
-
description: `${preset.id === this.presetId ? t('mode.currentPrefix') : ''}${preset.description ?? ''}`.trim(),
|
|
5398
|
+
options: presets.map((preset, at) => ({
|
|
5399
|
+
label: labels[at] ?? preset.id,
|
|
5400
|
+
description: `${preset.id === this.presetId ? t('mode.currentPrefix') : ''}${preset.broken === undefined ? preset.description ?? '' : t('mode.brokenSuffix', { reason: preset.broken })}`.trim(),
|
|
5210
5401
|
})),
|
|
5211
5402
|
});
|
|
5212
|
-
|
|
5403
|
+
index = labels.indexOf(answer.selected[0] ?? '');
|
|
5213
5404
|
}
|
|
5405
|
+
if (index < 0)
|
|
5406
|
+
return;
|
|
5407
|
+
const selected = presets[index];
|
|
5214
5408
|
if (selected === undefined)
|
|
5215
5409
|
return;
|
|
5216
|
-
|
|
5410
|
+
if (selected.broken !== undefined) {
|
|
5411
|
+
this.pushRow({
|
|
5412
|
+
kind: 'error',
|
|
5413
|
+
text: t('mode.broken', { name: labels[index] ?? selected.id, reason: selected.broken }),
|
|
5414
|
+
});
|
|
5415
|
+
this.markDirty();
|
|
5416
|
+
return;
|
|
5417
|
+
}
|
|
5418
|
+
const selectedName = labels[index] ?? selected.id;
|
|
5217
5419
|
const hasWork = sessionEvents(this.agent.session).some(event => event.type === 'turn/start');
|
|
5218
5420
|
if (!hasWork) {
|
|
5219
5421
|
await agentPresets.recompose(this.agent.ctx, selected.id);
|
|
@@ -5230,6 +5432,28 @@ export class SshTui {
|
|
|
5230
5432
|
await this.ctx.get('settings')?.update(settingsNamespace('agent-presets'), { default: selected.id });
|
|
5231
5433
|
this.markDirty();
|
|
5232
5434
|
}
|
|
5435
|
+
/**
|
|
5436
|
+
* `/mode fix`: write the roster row into this profile's user patch layer.
|
|
5437
|
+
*
|
|
5438
|
+
* The write only takes effect on the next launch — the loader composes the
|
|
5439
|
+
* patch tree once at boot — so the report says so instead of pretending the
|
|
5440
|
+
* running session gained a roster.
|
|
5441
|
+
*/
|
|
5442
|
+
async repairRoster() {
|
|
5443
|
+
const profile = profileFromArgv();
|
|
5444
|
+
const patch = rosterPatchPath(resolveDshHome(), profile);
|
|
5445
|
+
try {
|
|
5446
|
+
const result = await ensureRosterRows(resolveDshHome(), profile);
|
|
5447
|
+
this.pushRow({
|
|
5448
|
+
kind: 'system',
|
|
5449
|
+
text: result === 'present' ? t('mode.fixPresent', { patch }) : t('mode.fixWritten', { patch }),
|
|
5450
|
+
});
|
|
5451
|
+
}
|
|
5452
|
+
catch (error) {
|
|
5453
|
+
this.pushRow({ kind: 'error', text: t('mode.fixFailed', { patch, error: errorChain(error) }) });
|
|
5454
|
+
}
|
|
5455
|
+
this.markDirty();
|
|
5456
|
+
}
|
|
5233
5457
|
/** Current provider route selected for the running agent. */
|
|
5234
5458
|
currentProvider() {
|
|
5235
5459
|
// `agent.options` is authoritative for the launched agent; the selection
|
|
@@ -5281,6 +5505,37 @@ export class SshTui {
|
|
|
5281
5505
|
}
|
|
5282
5506
|
return payload;
|
|
5283
5507
|
}
|
|
5508
|
+
/**
|
|
5509
|
+
* Command Code's quota: the rolling 5h/weekly windows and credit pool off
|
|
5510
|
+
* `/alpha/billing/credits`, enriched by the subscription period and spend.
|
|
5511
|
+
* Only the credits read is required; the two enrichment reads are
|
|
5512
|
+
* best-effort so one failing endpoint cannot hide the windows.
|
|
5513
|
+
* @param source - the recognized Command Code route.
|
|
5514
|
+
*/
|
|
5515
|
+
async fetchCommandCodeQuota(source) {
|
|
5516
|
+
const apiKey = await this.resolveCredential(source.apiKeyEnv);
|
|
5517
|
+
if (apiKey === undefined)
|
|
5518
|
+
throw new Error(t('usage.noCred', { env: source.apiKeyEnv }));
|
|
5519
|
+
const headers = { authorization: `Bearer ${apiKey}`, accept: 'application/json' };
|
|
5520
|
+
const credits = await this.fetchJson(COMMAND_CODE_CREDITS_URL, headers, source.label);
|
|
5521
|
+
const subscription = await this.tryFetchJson(COMMAND_CODE_SUBSCRIPTIONS_URL, headers, source.label);
|
|
5522
|
+
const since = commandCodePeriodStart(subscription);
|
|
5523
|
+
const summaryUrl = since === undefined
|
|
5524
|
+
? COMMAND_CODE_USAGE_URL
|
|
5525
|
+
: `${COMMAND_CODE_USAGE_URL}?since=${encodeURIComponent(since)}`;
|
|
5526
|
+
const summary = await this.tryFetchJson(summaryUrl, headers, source.label);
|
|
5527
|
+
return parseCommandCodeQuota({ credits, subscription, summary }, source.provider);
|
|
5528
|
+
}
|
|
5529
|
+
/** Best-effort JSON read for quota enrichment; a failure leaves the field out. */
|
|
5530
|
+
async tryFetchJson(url, headers, label) {
|
|
5531
|
+
try {
|
|
5532
|
+
return await this.fetchJson(url, headers, label);
|
|
5533
|
+
}
|
|
5534
|
+
catch {
|
|
5535
|
+
// Enrichment only: the required billing read already decided the outcome.
|
|
5536
|
+
return undefined;
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5284
5539
|
/** Explain Zen metered billing instead of pretending it has a quota. */
|
|
5285
5540
|
zenUsageText(source) {
|
|
5286
5541
|
const usage = this.stats.usage;
|
|
@@ -5465,6 +5720,9 @@ export class SshTui {
|
|
|
5465
5720
|
}
|
|
5466
5721
|
}
|
|
5467
5722
|
const llmPiAi = this.ctx.get('settings')?.get(settingsNamespace('llm-pi-ai'));
|
|
5723
|
+
const commandCode = commandCodeSourceFor(provider, llmPiAi);
|
|
5724
|
+
if (commandCode !== null)
|
|
5725
|
+
return this.fetchCommandCodeQuota(commandCode);
|
|
5468
5726
|
const source = openCodeSourceFor(provider, llmPiAi);
|
|
5469
5727
|
if (source === null || source.flavor !== 'go')
|
|
5470
5728
|
return undefined;
|
|
@@ -5910,7 +6168,8 @@ export class SshTui {
|
|
|
5910
6168
|
if (text === '\r' || text === '\n') {
|
|
5911
6169
|
const submit = questionSubmit(dialog, this.input);
|
|
5912
6170
|
if (submit.kind === 'reject') {
|
|
5913
|
-
//
|
|
6171
|
+
// A list with options always answers with its highlight; this guards
|
|
6172
|
+
// only a malformed question whose cursor names no option.
|
|
5914
6173
|
dialog.reject(new UserQuestionError('ask_user_question was cancelled', 'ASK_ABORTED'));
|
|
5915
6174
|
return;
|
|
5916
6175
|
}
|
|
@@ -5947,6 +6206,7 @@ export class SshTui {
|
|
|
5947
6206
|
const templateEntries = [
|
|
5948
6207
|
{ key: 'template:official', label: templates.official.label, detail: 'api.deepseek.com' },
|
|
5949
6208
|
{ key: 'template:opencode-go', label: templates['opencode-go'].label, detail: 'opencode.ai/zen/go · Responses' },
|
|
6209
|
+
{ key: 'template:command-code', label: templates['command-code'].label, detail: 'api.commandcode.ai · Completions' },
|
|
5950
6210
|
{ key: 'template:openai-completions', label: templates['openai-completions'].label, detail: 'openai-completions' },
|
|
5951
6211
|
{ key: 'template:openai-responses', label: templates['openai-responses'].label, detail: 'openai-responses' },
|
|
5952
6212
|
{ key: 'template:anthropic-messages', label: templates['anthropic-messages'].label, detail: 'anthropic-messages' },
|
|
@@ -5964,6 +6224,35 @@ export class SshTui {
|
|
|
5964
6224
|
this.markDirty();
|
|
5965
6225
|
return true;
|
|
5966
6226
|
}
|
|
6227
|
+
/** The installed catalog's id→window index, built once per catalog load. */
|
|
6228
|
+
catalogWindowIndexSource() {
|
|
6229
|
+
this.catalogWindows ??= catalogWindowIndex(this.catalogPresets ?? []);
|
|
6230
|
+
return this.catalogWindows;
|
|
6231
|
+
}
|
|
6232
|
+
/**
|
|
6233
|
+
* Size the wizard's picked models from the installed catalog, filling in the
|
|
6234
|
+
* capacities the endpoint's listing did not disclose and recording them on
|
|
6235
|
+
* the wizard state so the saved entries carry them.
|
|
6236
|
+
* @param state - the wizard state whose `models` are being sized.
|
|
6237
|
+
* @returns the windows discovered for this pick, by model id.
|
|
6238
|
+
*/
|
|
6239
|
+
sizeOnboardingModels(state) {
|
|
6240
|
+
const index = this.catalogWindowIndexSource();
|
|
6241
|
+
const sized = new Map();
|
|
6242
|
+
for (const id of state.models) {
|
|
6243
|
+
const listed = state.modelCapacity?.get(id)?.contextWindow;
|
|
6244
|
+
const window = listed ?? (index.size === 0 ? undefined : catalogContextWindow(id, index));
|
|
6245
|
+
if (window === undefined)
|
|
6246
|
+
continue;
|
|
6247
|
+
sized.set(id, window);
|
|
6248
|
+
if (listed === undefined) {
|
|
6249
|
+
const capacity = state.modelCapacity?.get(id) ?? {};
|
|
6250
|
+
state.modelCapacity ??= new Map();
|
|
6251
|
+
state.modelCapacity.set(id, { ...capacity, contextWindow: window });
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
return sized;
|
|
6255
|
+
}
|
|
5967
6256
|
handleOnboardingChar(text) {
|
|
5968
6257
|
const state = this.onboarding;
|
|
5969
6258
|
if (state === undefined)
|
|
@@ -5982,6 +6271,7 @@ export class SshTui {
|
|
|
5982
6271
|
state.baseUrl = '';
|
|
5983
6272
|
state.key = '';
|
|
5984
6273
|
state.models = [];
|
|
6274
|
+
state.modelCapacity = new Map();
|
|
5985
6275
|
this.input = '';
|
|
5986
6276
|
this.cursor = 0;
|
|
5987
6277
|
if (entry.catalog !== undefined) {
|
|
@@ -6021,7 +6311,8 @@ export class SshTui {
|
|
|
6021
6311
|
case 'id':
|
|
6022
6312
|
case 'base-url':
|
|
6023
6313
|
case 'key':
|
|
6024
|
-
case 'models':
|
|
6314
|
+
case 'models':
|
|
6315
|
+
case 'context': {
|
|
6025
6316
|
if (state.step === 'models' && text === '\x06') {
|
|
6026
6317
|
void this.fetchOnboardingModels();
|
|
6027
6318
|
return;
|
|
@@ -6048,8 +6339,11 @@ export class SshTui {
|
|
|
6048
6339
|
}
|
|
6049
6340
|
else if (state.step === 'models') {
|
|
6050
6341
|
const template = onboardTemplate(state);
|
|
6342
|
+
// Ctrl+F has already filled state.models; Enter accepts that
|
|
6343
|
+
// listing instead of discarding it for the template's placeholder
|
|
6344
|
+
// ids, which a generic gateway does not serve.
|
|
6051
6345
|
const parsed = value === ''
|
|
6052
|
-
? template.defaultModels
|
|
6346
|
+
? (state.models.length > 0 ? state.models : template.defaultModels)
|
|
6053
6347
|
: value.split(/[\s,,]+/u).filter(Boolean);
|
|
6054
6348
|
if (parsed.length === 0) {
|
|
6055
6349
|
this.pushRow({ kind: 'error', text: t('onboard.needModel') });
|
|
@@ -6058,6 +6352,18 @@ export class SshTui {
|
|
|
6058
6352
|
}
|
|
6059
6353
|
state.models = parsed;
|
|
6060
6354
|
}
|
|
6355
|
+
else if (state.step === 'context') {
|
|
6356
|
+
// Enter keeps the pre-filled value; a typed number overrides it.
|
|
6357
|
+
if (value !== '') {
|
|
6358
|
+
const parsed = Number.parseInt(value.replace(/[_,\s]/gu, ''), 10);
|
|
6359
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
6360
|
+
this.pushRow({ kind: 'error', text: t('onboard.contextInvalid', { value }) });
|
|
6361
|
+
this.markDirty();
|
|
6362
|
+
return;
|
|
6363
|
+
}
|
|
6364
|
+
state.routeContextWindow = parsed;
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6061
6367
|
else {
|
|
6062
6368
|
state.baseUrl = value;
|
|
6063
6369
|
}
|
|
@@ -6091,6 +6397,7 @@ export class SshTui {
|
|
|
6091
6397
|
state.baseUrl = '';
|
|
6092
6398
|
state.key = '';
|
|
6093
6399
|
state.models = [];
|
|
6400
|
+
state.modelCapacity = new Map();
|
|
6094
6401
|
this.input = '';
|
|
6095
6402
|
this.cursor = 0;
|
|
6096
6403
|
this.markDirty();
|
|
@@ -6115,6 +6422,22 @@ export class SshTui {
|
|
|
6115
6422
|
state.step = 'models';
|
|
6116
6423
|
}
|
|
6117
6424
|
else if (state.step === 'models') {
|
|
6425
|
+
// Size the picks before the confirm step so the route default the wizard
|
|
6426
|
+
// persists is derived, not guessed: the listing first, the installed
|
|
6427
|
+
// catalog second. Only when some pick stayed unsized does the route
|
|
6428
|
+
// default matter, and only then is a step shown for it.
|
|
6429
|
+
const sized = this.sizeOnboardingModels(state);
|
|
6430
|
+
const unsized = state.models.filter(id => !sized.has(id));
|
|
6431
|
+
if (unsized.length === 0) {
|
|
6432
|
+
state.step = 'confirm';
|
|
6433
|
+
}
|
|
6434
|
+
else {
|
|
6435
|
+
state.routeContextWindow = suggestedRouteContextWindow(sized.values())
|
|
6436
|
+
?? HARNESS_DEFAULT_CONTEXT_WINDOW;
|
|
6437
|
+
state.step = 'context';
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
6440
|
+
else if (state.step === 'context') {
|
|
6118
6441
|
state.step = 'confirm';
|
|
6119
6442
|
}
|
|
6120
6443
|
this.input = '';
|
|
@@ -6166,6 +6489,13 @@ export class SshTui {
|
|
|
6166
6489
|
}
|
|
6167
6490
|
else {
|
|
6168
6491
|
state.models = ids;
|
|
6492
|
+
state.modelCapacity = new Map(discovered.flatMap(model => {
|
|
6493
|
+
const capacity = {
|
|
6494
|
+
...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
|
|
6495
|
+
...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
|
|
6496
|
+
};
|
|
6497
|
+
return Object.keys(capacity).length === 0 ? [] : [[model.id, capacity]];
|
|
6498
|
+
}));
|
|
6169
6499
|
this.input = '';
|
|
6170
6500
|
this.cursor = 0;
|
|
6171
6501
|
this.pushRow({ kind: 'system', text: t('onboard.fetchedModels', { count: ids.length, list: formatModelList(ids, 6) }) });
|
|
@@ -6219,14 +6549,20 @@ export class SshTui {
|
|
|
6219
6549
|
// to a supported level (if any) and persist it in both the profile
|
|
6220
6550
|
// and the default-model selection.
|
|
6221
6551
|
const llm = this.ctx.get('llm');
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
const
|
|
6552
|
+
// A hand-declared OpenAI-compatible gateway has no pi-ai catalog entry,
|
|
6553
|
+
// so nothing resolves its models before this profile is saved. Skip the
|
|
6554
|
+
// live lookup there and declare the vocabulary the TUI offers instead.
|
|
6555
|
+
const handDeclared = declaresOfferedReasoning(state.providerType);
|
|
6556
|
+
const defaultEffort = handDeclared || model === undefined || llm === undefined
|
|
6226
6557
|
? undefined
|
|
6227
|
-
:
|
|
6558
|
+
: await defaultReasoningEffort(llm, state.providerId, model);
|
|
6559
|
+
const reasoningEfforts = onboardingReasoningEfforts(state.providerType, defaultEffort === undefined ? undefined : String(defaultEffort));
|
|
6228
6560
|
const existing = this.piAiProviderProfile(state.providerId);
|
|
6229
6561
|
const existingModels = Array.isArray(existing?.models) ? existing.models : [];
|
|
6562
|
+
// The confirm step can be reached without walking the models step (and
|
|
6563
|
+
// a saved catalog answer may have landed after it), so size the picks
|
|
6564
|
+
// here too: the call is idempotent and fills only missing windows.
|
|
6565
|
+
this.sizeOnboardingModels(state);
|
|
6230
6566
|
const mergedIds = [];
|
|
6231
6567
|
const seen = new Set();
|
|
6232
6568
|
for (const id of state.models) {
|
|
@@ -6266,8 +6602,10 @@ export class SshTui {
|
|
|
6266
6602
|
}),
|
|
6267
6603
|
models: mergedIds.map(id => ({
|
|
6268
6604
|
id,
|
|
6605
|
+
...(state.modelCapacity?.get(id) ?? template.defaultModelCapacity?.[id] ?? {}),
|
|
6269
6606
|
...(reasoningEfforts === undefined ? {} : { reasoningEfforts }),
|
|
6270
6607
|
})),
|
|
6608
|
+
...(state.routeContextWindow === undefined ? {} : { defaultContextWindow: state.routeContextWindow }),
|
|
6271
6609
|
...(defaultEffort === undefined ? {} : { reasoning: defaultEffort }),
|
|
6272
6610
|
};
|
|
6273
6611
|
if (settings === undefined) {
|