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/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
- // no selection: treat as cancel unless there are no options
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
- const defaultEffort = model !== undefined && llm !== undefined
6289
- ? await defaultReasoningEffort(llm, state.providerId, model)
6290
- : undefined;
6291
- const reasoningEfforts = defaultEffort === undefined
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
- : { off: null, [defaultEffort]: defaultEffort };
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) {