dsh-ssh-tui 0.6.3 → 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 +16 -2
- package/README.md +13 -1
- 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 +54 -0
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +54 -0
- package/lib/i18n/zh.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/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/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 +282 -10
- 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/provider-catalog.d.ts +2 -0
- package/lib/types/quota.d.ts +49 -0
- package/lib/types/term-text.d.ts +33 -4
- package/lib/types/tool-present.d.ts +1 -0
- package/lib/types/tui.d.ts +62 -1
- package/package.json +1 -1
package/lib/tui.js
CHANGED
|
@@ -29,6 +29,7 @@ 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';
|
|
34
35
|
import { presetLabel, profileFromArgv } from './preset-label.js';
|
|
@@ -51,13 +52,13 @@ import { clipAnsiToWidth, hrefAtColumn, osc52Clipboard, paintedLinkHits, cursorV
|
|
|
51
52
|
import { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, HANGUP_CANCEL_TIMEOUT_MS, ignoreFurtherHangupSignals, isEscapePrefix, isHangupErrno, parseCursorPositionReply, PICKER_WINDOW, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, } from './paint.js';
|
|
52
53
|
import { TerminalInputGuard } from './terminal-input.js';
|
|
53
54
|
import { contextPressureAlertText, contextPressureRingColor, contextPressureView, formatContextPressureRing, describeProviderRoute, fitFooterStatsLine, fitFooterStatusLine, footerActivity, footerIdentityParts, footerStatsGroups, formatContextPressureChip, formatStatusReport, formatTokens, parseContextPressure, promptPressureTokens, providerUsesLocalOAuth, shouldIdleAutoCompact, } from './footer.js';
|
|
54
|
-
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';
|
|
55
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';
|
|
56
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';
|
|
57
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';
|
|
58
59
|
export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, findCursorPositionReply, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, } from './paint.js';
|
|
59
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';
|
|
60
|
-
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';
|
|
61
62
|
export { commandAcceptsAttachments, forEachSessionEvent, isAssistantStreamEvent, isTokenDeltaChunk, listPersistenceHeaders, inspectPersistenceSession, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
|
|
62
63
|
export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoProgressLabel, todoSummary, } from './plan.js';
|
|
63
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';
|
|
@@ -81,6 +82,12 @@ function installUserQuestionAnswerer(ctx, questions, ask) {
|
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
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;
|
|
84
91
|
function providerTemplates() {
|
|
85
92
|
return {
|
|
86
93
|
official: {
|
|
@@ -96,6 +103,16 @@ function providerTemplates() {
|
|
|
96
103
|
api: 'openai-responses',
|
|
97
104
|
defaultModels: ['deepseek-v4-flash', 'deepseek-v4-pro'],
|
|
98
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
|
+
},
|
|
99
116
|
'openai-completions': {
|
|
100
117
|
label: t('onboard.providerCompletions'),
|
|
101
118
|
defaultId: 'my-gateway',
|
|
@@ -329,6 +346,62 @@ function effortChoices(ids) {
|
|
|
329
346
|
function undeclaredEffortChoices() {
|
|
330
347
|
return effortChoices(UNDECLARED_EFFORT_IDS);
|
|
331
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
|
+
}
|
|
332
405
|
function localOAuthEffortChoices(modelId) {
|
|
333
406
|
return effortChoices(modelId === 'grok-4.6'
|
|
334
407
|
? ['off', 'low', 'medium', 'high', 'xhigh']
|
|
@@ -371,6 +444,8 @@ export class SshTui {
|
|
|
371
444
|
/** Web-aligned provider presets from the host's pi-ai catalog (undefined until loaded / when unreachable). */
|
|
372
445
|
catalogPresets;
|
|
373
446
|
catalogLoad;
|
|
447
|
+
/** Memoized id→window index over {@link catalogPresets}. */
|
|
448
|
+
catalogWindows;
|
|
374
449
|
input = '';
|
|
375
450
|
cursor = 0;
|
|
376
451
|
inputFolded = false;
|
|
@@ -1037,6 +1112,7 @@ export class SshTui {
|
|
|
1037
1112
|
baseUrl: '',
|
|
1038
1113
|
key: '',
|
|
1039
1114
|
models: [],
|
|
1115
|
+
modelCapacity: new Map(),
|
|
1040
1116
|
catalogPresets: undefined,
|
|
1041
1117
|
catalog: undefined,
|
|
1042
1118
|
providerCursor: 0,
|
|
@@ -2456,6 +2532,15 @@ export class SshTui {
|
|
|
2456
2532
|
addDialog(t('onboard.modelsCatalogHint'));
|
|
2457
2533
|
addDialog(t('onboard.enterEsc'));
|
|
2458
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;
|
|
2459
2544
|
case 'confirm':
|
|
2460
2545
|
addDialog(t('onboard.confirmTitle'));
|
|
2461
2546
|
addDialog(t('onboard.confirmProvider', { label: providerLabel }));
|
|
@@ -2465,6 +2550,9 @@ export class SshTui {
|
|
|
2465
2550
|
api: template.api ?? (ob.providerType === 'catalog' ? t('onboard.apiCatalog') : 'deepseek-official'),
|
|
2466
2551
|
}));
|
|
2467
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
|
+
}
|
|
2468
2556
|
addDialog(t('onboard.confirmKey', {
|
|
2469
2557
|
head: sliceCodePoints(ob.key, 6),
|
|
2470
2558
|
tail: lastCodePoints(ob.key, 4),
|
|
@@ -4440,6 +4528,14 @@ export class SshTui {
|
|
|
4440
4528
|
if (ids.has(modelId))
|
|
4441
4529
|
return true;
|
|
4442
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
|
+
}
|
|
4443
4539
|
const reasoningEfforts = reasoningEffortsForDefault(profile.reasoning);
|
|
4444
4540
|
if (reasoningEfforts !== undefined)
|
|
4445
4541
|
modelEntry.reasoningEfforts = reasoningEfforts;
|
|
@@ -4691,6 +4787,8 @@ export class SshTui {
|
|
|
4691
4787
|
})),
|
|
4692
4788
|
}, 0, 1, currentIndex);
|
|
4693
4789
|
const effort = choices.find(option => option.label === effortAnswer.selected[0])?.id;
|
|
4790
|
+
if (isUndeclared && effort !== undefined)
|
|
4791
|
+
await this.declareReasoningEffort(provider, modelId, effort);
|
|
4694
4792
|
const next = {
|
|
4695
4793
|
provider,
|
|
4696
4794
|
model: modelId,
|
|
@@ -4986,7 +5084,61 @@ export class SshTui {
|
|
|
4986
5084
|
return;
|
|
4987
5085
|
await this.setReasoningEffort(provider, modelId, picked.id, isUndeclared);
|
|
4988
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
|
+
}
|
|
4989
5139
|
async setReasoningEffort(provider, modelId, effort, isUndeclared) {
|
|
5140
|
+
if (isUndeclared && effort !== undefined)
|
|
5141
|
+
await this.declareReasoningEffort(provider, modelId, effort);
|
|
4990
5142
|
const next = {
|
|
4991
5143
|
provider,
|
|
4992
5144
|
model: modelId,
|
|
@@ -5034,6 +5186,9 @@ export class SshTui {
|
|
|
5034
5186
|
return;
|
|
5035
5187
|
}
|
|
5036
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
|
+
}
|
|
5037
5192
|
const next = {
|
|
5038
5193
|
...current,
|
|
5039
5194
|
...(targetEffort === undefined ? { reasoningEffort: undefined } : { reasoningEffort: ReasoningEffortId(targetEffort) }),
|
|
@@ -5073,6 +5228,9 @@ export class SshTui {
|
|
|
5073
5228
|
const picked = choices.find(option => option.label === answer.selected[0]);
|
|
5074
5229
|
if (picked === undefined)
|
|
5075
5230
|
return;
|
|
5231
|
+
if (effortOptions.length === 0 && picked.id !== undefined) {
|
|
5232
|
+
await this.declareReasoningEffort(provider, current.model, picked.id);
|
|
5233
|
+
}
|
|
5076
5234
|
const next = {
|
|
5077
5235
|
...current,
|
|
5078
5236
|
...(picked.id === undefined
|
|
@@ -5347,6 +5505,37 @@ export class SshTui {
|
|
|
5347
5505
|
}
|
|
5348
5506
|
return payload;
|
|
5349
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
|
+
}
|
|
5350
5539
|
/** Explain Zen metered billing instead of pretending it has a quota. */
|
|
5351
5540
|
zenUsageText(source) {
|
|
5352
5541
|
const usage = this.stats.usage;
|
|
@@ -5531,6 +5720,9 @@ export class SshTui {
|
|
|
5531
5720
|
}
|
|
5532
5721
|
}
|
|
5533
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);
|
|
5534
5726
|
const source = openCodeSourceFor(provider, llmPiAi);
|
|
5535
5727
|
if (source === null || source.flavor !== 'go')
|
|
5536
5728
|
return undefined;
|
|
@@ -5976,7 +6168,8 @@ export class SshTui {
|
|
|
5976
6168
|
if (text === '\r' || text === '\n') {
|
|
5977
6169
|
const submit = questionSubmit(dialog, this.input);
|
|
5978
6170
|
if (submit.kind === 'reject') {
|
|
5979
|
-
//
|
|
6171
|
+
// A list with options always answers with its highlight; this guards
|
|
6172
|
+
// only a malformed question whose cursor names no option.
|
|
5980
6173
|
dialog.reject(new UserQuestionError('ask_user_question was cancelled', 'ASK_ABORTED'));
|
|
5981
6174
|
return;
|
|
5982
6175
|
}
|
|
@@ -6013,6 +6206,7 @@ export class SshTui {
|
|
|
6013
6206
|
const templateEntries = [
|
|
6014
6207
|
{ key: 'template:official', label: templates.official.label, detail: 'api.deepseek.com' },
|
|
6015
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' },
|
|
6016
6210
|
{ key: 'template:openai-completions', label: templates['openai-completions'].label, detail: 'openai-completions' },
|
|
6017
6211
|
{ key: 'template:openai-responses', label: templates['openai-responses'].label, detail: 'openai-responses' },
|
|
6018
6212
|
{ key: 'template:anthropic-messages', label: templates['anthropic-messages'].label, detail: 'anthropic-messages' },
|
|
@@ -6030,6 +6224,35 @@ export class SshTui {
|
|
|
6030
6224
|
this.markDirty();
|
|
6031
6225
|
return true;
|
|
6032
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
|
+
}
|
|
6033
6256
|
handleOnboardingChar(text) {
|
|
6034
6257
|
const state = this.onboarding;
|
|
6035
6258
|
if (state === undefined)
|
|
@@ -6048,6 +6271,7 @@ export class SshTui {
|
|
|
6048
6271
|
state.baseUrl = '';
|
|
6049
6272
|
state.key = '';
|
|
6050
6273
|
state.models = [];
|
|
6274
|
+
state.modelCapacity = new Map();
|
|
6051
6275
|
this.input = '';
|
|
6052
6276
|
this.cursor = 0;
|
|
6053
6277
|
if (entry.catalog !== undefined) {
|
|
@@ -6087,7 +6311,8 @@ export class SshTui {
|
|
|
6087
6311
|
case 'id':
|
|
6088
6312
|
case 'base-url':
|
|
6089
6313
|
case 'key':
|
|
6090
|
-
case 'models':
|
|
6314
|
+
case 'models':
|
|
6315
|
+
case 'context': {
|
|
6091
6316
|
if (state.step === 'models' && text === '\x06') {
|
|
6092
6317
|
void this.fetchOnboardingModels();
|
|
6093
6318
|
return;
|
|
@@ -6114,8 +6339,11 @@ export class SshTui {
|
|
|
6114
6339
|
}
|
|
6115
6340
|
else if (state.step === 'models') {
|
|
6116
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.
|
|
6117
6345
|
const parsed = value === ''
|
|
6118
|
-
? template.defaultModels
|
|
6346
|
+
? (state.models.length > 0 ? state.models : template.defaultModels)
|
|
6119
6347
|
: value.split(/[\s,,]+/u).filter(Boolean);
|
|
6120
6348
|
if (parsed.length === 0) {
|
|
6121
6349
|
this.pushRow({ kind: 'error', text: t('onboard.needModel') });
|
|
@@ -6124,6 +6352,18 @@ export class SshTui {
|
|
|
6124
6352
|
}
|
|
6125
6353
|
state.models = parsed;
|
|
6126
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
|
+
}
|
|
6127
6367
|
else {
|
|
6128
6368
|
state.baseUrl = value;
|
|
6129
6369
|
}
|
|
@@ -6157,6 +6397,7 @@ export class SshTui {
|
|
|
6157
6397
|
state.baseUrl = '';
|
|
6158
6398
|
state.key = '';
|
|
6159
6399
|
state.models = [];
|
|
6400
|
+
state.modelCapacity = new Map();
|
|
6160
6401
|
this.input = '';
|
|
6161
6402
|
this.cursor = 0;
|
|
6162
6403
|
this.markDirty();
|
|
@@ -6181,6 +6422,22 @@ export class SshTui {
|
|
|
6181
6422
|
state.step = 'models';
|
|
6182
6423
|
}
|
|
6183
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') {
|
|
6184
6441
|
state.step = 'confirm';
|
|
6185
6442
|
}
|
|
6186
6443
|
this.input = '';
|
|
@@ -6232,6 +6489,13 @@ export class SshTui {
|
|
|
6232
6489
|
}
|
|
6233
6490
|
else {
|
|
6234
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
|
+
}));
|
|
6235
6499
|
this.input = '';
|
|
6236
6500
|
this.cursor = 0;
|
|
6237
6501
|
this.pushRow({ kind: 'system', text: t('onboard.fetchedModels', { count: ids.length, list: formatModelList(ids, 6) }) });
|
|
@@ -6285,14 +6549,20 @@ export class SshTui {
|
|
|
6285
6549
|
// to a supported level (if any) and persist it in both the profile
|
|
6286
6550
|
// and the default-model selection.
|
|
6287
6551
|
const llm = this.ctx.get('llm');
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
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
|
|
6292
6557
|
? undefined
|
|
6293
|
-
:
|
|
6558
|
+
: await defaultReasoningEffort(llm, state.providerId, model);
|
|
6559
|
+
const reasoningEfforts = onboardingReasoningEfforts(state.providerType, defaultEffort === undefined ? undefined : String(defaultEffort));
|
|
6294
6560
|
const existing = this.piAiProviderProfile(state.providerId);
|
|
6295
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);
|
|
6296
6566
|
const mergedIds = [];
|
|
6297
6567
|
const seen = new Set();
|
|
6298
6568
|
for (const id of state.models) {
|
|
@@ -6332,8 +6602,10 @@ export class SshTui {
|
|
|
6332
6602
|
}),
|
|
6333
6603
|
models: mergedIds.map(id => ({
|
|
6334
6604
|
id,
|
|
6605
|
+
...(state.modelCapacity?.get(id) ?? template.defaultModelCapacity?.[id] ?? {}),
|
|
6335
6606
|
...(reasoningEfforts === undefined ? {} : { reasoningEfforts }),
|
|
6336
6607
|
})),
|
|
6608
|
+
...(state.routeContextWindow === undefined ? {} : { defaultContextWindow: state.routeContextWindow }),
|
|
6337
6609
|
...(defaultEffort === undefined ? {} : { reasoning: defaultEffort }),
|
|
6338
6610
|
};
|
|
6339
6611
|
if (settings === undefined) {
|