maka-agent 0.2.0-dev.14.20260902 → 0.2.0-dev.16.20260903

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.
Files changed (160) hide show
  1. package/dist/cli-core.js +0 -1
  2. package/dist/pi-transcript.js +48 -0
  3. package/dist/pi-tui-form-interaction.js +497 -0
  4. package/dist/pi-tui-pickers.js +256 -55
  5. package/dist/pi-tui-runner.js +215 -12
  6. package/dist/runtime-host-cli.js +17 -42
  7. package/dist/runtime-host-lifecycle-transaction.js +31 -13
  8. package/dist/runtime-host-managed-lifecycle-manager.js +1 -1
  9. package/dist/runtime-host-onboarding.js +3 -1
  10. package/dist/runtime-host-openrc-service.js +469 -0
  11. package/dist/runtime-host-run-command.js +6 -2
  12. package/dist/runtime-host-service-command.js +3 -3
  13. package/dist/runtime-host-service-management-command.js +43 -15
  14. package/dist/runtime-host-session-channel.js +3 -0
  15. package/dist/runtime-host-session-driver.js +21 -6
  16. package/dist/runtime-host-setup-command.js +30 -31
  17. package/dist/tui-context-refresh.js +103 -0
  18. package/dist/tui-copy-catalog.js +73 -0
  19. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  20. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  21. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  22. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  23. package/node_modules/@maka/core/dist/chat-model-choice.js +4 -0
  24. package/node_modules/@maka/core/dist/computer-use.js +27 -92
  25. package/node_modules/@maka/core/dist/interaction.js +577 -3
  26. package/node_modules/@maka/core/dist/llm-connections.js +4 -0
  27. package/node_modules/@maka/core/dist/local-memory.js +2 -2
  28. package/node_modules/@maka/core/dist/model-call-usage-projection.js +3 -0
  29. package/node_modules/@maka/core/dist/model-metadata.js +26 -7
  30. package/node_modules/@maka/core/dist/model-thinking.js +17 -0
  31. package/node_modules/@maka/core/dist/provider-registry.js +1 -0
  32. package/node_modules/@maka/core/dist/runtime-event.js +23 -1
  33. package/node_modules/@maka/core/dist/scheduled-task.js +4 -0
  34. package/node_modules/@maka/core/dist/session.js +95 -2
  35. package/node_modules/@maka/core/dist/text-sanitize.js +19 -0
  36. package/node_modules/@maka/core/dist/usage-ledger-merge.js +3 -0
  37. package/node_modules/@maka/core/dist/usage-record-schema.js +11 -5
  38. package/node_modules/@maka/core/dist/workhub-creation-intent.js +75 -9
  39. package/node_modules/@maka/core/dist/workhub-session-resolver.js +50 -0
  40. package/node_modules/@maka/core/package.json +2 -0
  41. package/node_modules/@maka/eval/dist/harbor-external-subject.js +2 -1
  42. package/node_modules/@maka/eval/dist/harness-executor.js +3 -2
  43. package/node_modules/@maka/eval/dist/install-preflight.js +39 -10
  44. package/node_modules/@maka/eval/dist/process-termination.js +83 -0
  45. package/node_modules/@maka/mcp/dist/sep-2243.js +3 -3
  46. package/node_modules/@maka/runtime/dist/agent-run.js +6 -3
  47. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +332 -118
  48. package/node_modules/@maka/runtime/dist/ai-sdk-compaction-contract.js +6 -1
  49. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +171 -341
  50. package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +3 -26
  51. package/node_modules/@maka/runtime/dist/bundled-skill-catalog.generated.js +1 -1
  52. package/node_modules/@maka/runtime/dist/computer-use-codec.js +1 -141
  53. package/node_modules/@maka/runtime/dist/computer-use-tools.js +23 -105
  54. package/node_modules/@maka/runtime/dist/context-budget-policy.js +20 -26
  55. package/node_modules/@maka/runtime/dist/context-budget.js +18 -20
  56. package/node_modules/@maka/runtime/dist/conversation-copy.js +23 -19
  57. package/node_modules/@maka/runtime/dist/cua-frame-state.js +1 -69
  58. package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +267 -0
  59. package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +6 -9
  60. package/node_modules/@maka/runtime/dist/history-compact-summarizer.js +67 -23
  61. package/node_modules/@maka/runtime/dist/history-compact-summary-validation.js +11 -18
  62. package/node_modules/@maka/runtime/dist/history-compaction.js +66 -73
  63. package/node_modules/@maka/runtime/dist/interaction-authority.js +69 -0
  64. package/node_modules/@maka/runtime/dist/mcp-tools.js +13 -1
  65. package/node_modules/@maka/runtime/dist/memory-extraction.js +11 -11
  66. package/node_modules/@maka/runtime/dist/model-adapter.js +24 -6
  67. package/node_modules/@maka/runtime/dist/model-factory.js +14 -5
  68. package/node_modules/@maka/runtime/dist/oauth-provider-contracts.js +28 -1
  69. package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +7 -6
  70. package/node_modules/@maka/runtime/dist/opencode-session-header.js +29 -0
  71. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +11 -1
  72. package/node_modules/@maka/runtime/dist/runtime-kernel.js +12 -11
  73. package/node_modules/@maka/runtime/dist/runtime-read-model.js +51 -13
  74. package/node_modules/@maka/runtime/dist/scheduled-task-tools.js +3 -0
  75. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +26 -0
  76. package/node_modules/@maka/runtime/dist/session-manager.js +1 -0
  77. package/node_modules/@maka/runtime/dist/session-projection-helpers.js +16 -1
  78. package/node_modules/@maka/runtime/dist/stream-graph-projection.js +6 -0
  79. package/node_modules/@maka/runtime/dist/stream-graph-read-model.js +5 -0
  80. package/node_modules/@maka/runtime/dist/stream-usage-fallback-fetch.js +74 -0
  81. package/node_modules/@maka/runtime/dist/subscription-credentials.js +51 -4
  82. package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +47 -3
  83. package/node_modules/@maka/runtime/dist/system-prompt/main-session-prompt.js +20 -1
  84. package/node_modules/@maka/runtime/dist/test-connection.js +18 -11
  85. package/node_modules/@maka/runtime/dist/tool-runtime.js +183 -0
  86. package/node_modules/@maka/runtime/package.json +1 -0
  87. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +14 -0
  88. package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +84 -1
  89. package/node_modules/@maka/runtime-host/dist/client/host-profile.js +68 -51
  90. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  91. package/node_modules/@maka/runtime-host/dist/client/owner-connection-code.js +8 -5
  92. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +342 -36
  93. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +60 -4
  94. package/node_modules/@maka/runtime-host/dist/peer-mesh/index.js +3 -3
  95. package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +1 -1
  96. package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +48 -60
  97. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +847 -431
  98. package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +33 -74
  99. package/node_modules/@maka/runtime-host/dist/peer-mesh/store.js +82 -69
  100. package/node_modules/@maka/runtime-host/dist/peer-reachability/index.js +21 -0
  101. package/node_modules/@maka/runtime-host/dist/peer-reachability/model.js +202 -0
  102. package/node_modules/@maka/runtime-host/dist/peer-reachability/owner.js +119 -0
  103. package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +245 -0
  104. package/node_modules/@maka/runtime-host/dist/protocol/agent-graph.js +4 -1
  105. package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +68 -0
  106. package/node_modules/@maka/runtime-host/dist/protocol/connection-effects.js +15 -8
  107. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +7 -23
  108. package/node_modules/@maka/runtime-host/dist/protocol/index.js +47 -1
  109. package/node_modules/@maka/runtime-host/dist/protocol/oauth.js +25 -1
  110. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  111. package/node_modules/@maka/runtime-host/dist/protocol/peer-mesh.js +14 -31
  112. package/node_modules/@maka/runtime-host/dist/protocol/scheduled-task.js +19 -6
  113. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +36 -1
  114. package/node_modules/@maka/runtime-host/dist/protocol/session-revision.js +11 -2
  115. package/node_modules/@maka/runtime-host/dist/protocol/usage-pricing.js +16 -2
  116. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +51 -2
  117. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +35 -2
  118. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +1 -0
  119. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +12 -10
  120. package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +163 -30
  121. package/node_modules/@maka/runtime-host/dist/server/connection-effect-coordinator.js +38 -14
  122. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +45 -22
  123. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +78 -20
  124. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +4 -2
  125. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +2 -1
  126. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +49 -28
  127. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +3 -6
  128. package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +130 -27
  129. package/node_modules/@maka/runtime-host/dist/server/interaction-projection.js +30 -1
  130. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +2 -4
  131. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +16 -3
  132. package/node_modules/@maka/runtime-host/dist/server/oauth-coordinator.js +66 -3
  133. package/node_modules/@maka/runtime-host/dist/server/oauth-execution-authority.js +20 -3
  134. package/node_modules/@maka/runtime-host/dist/server/peer-listener.js +8 -21
  135. package/node_modules/@maka/runtime-host/dist/server/peer-mesh-authority.js +1 -1
  136. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +6 -3
  137. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +72 -12
  138. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +48 -18
  139. package/node_modules/@maka/runtime-host/dist/server/session-turn-access-request-coordinator.js +43 -26
  140. package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +15 -1
  141. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +229 -4
  142. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +146 -8
  143. package/node_modules/@maka/runtime-host/dist/transport/peer-native.js +32 -6
  144. package/node_modules/@maka/runtime-host/package.json +1 -0
  145. package/node_modules/@maka/storage/dist/agent-run-store.js +18 -5
  146. package/node_modules/@maka/storage/dist/codex-session-adapter.js +14 -0
  147. package/node_modules/@maka/storage/dist/execution-stores.js +5 -0
  148. package/node_modules/@maka/storage/dist/interaction-store.js +13 -1
  149. package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +9 -3
  150. package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +49 -19
  151. package/node_modules/@maka/storage/dist/runtime-policy/oauth-login-receipt-document.js +3 -1
  152. package/node_modules/@maka/storage/dist/runtime-policy/onboarding-transaction.js +13 -5
  153. package/node_modules/@maka/storage/dist/session-copy-cleanup.js +4 -2
  154. package/node_modules/@maka/storage/dist/session-store.js +39 -6
  155. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +16 -9
  156. package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +30 -3
  157. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +117 -0
  158. package/node_modules/@maka/storage/dist/sqlite-usage-store.js +29 -6
  159. package/node_modules/@maka/storage/dist/usage-stores.js +3 -1
  160. package/package.json +1 -1
@@ -16,9 +16,10 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { CombinedAutocompleteProvider, Editor, Key, SelectList, decodeKittyPrintable, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
19
+ import { CombinedAutocompleteProvider, Editor, Key, SelectList, decodeKittyPrintable, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
20
20
  import { defineUiMessageCatalog, formatUiMessage, resolveUiMessageCatalog, } from '@maka/core/ui-locale';
21
- import { providerDefaultsOf, } from '@maka/core/llm-connections';
21
+ import { providerDefaultsOf, validateSlug, } from '@maka/core/llm-connections';
22
+ import { CONNECTION_NAME_MAX_LENGTH } from '@maka/core/runtime-policy';
22
23
  import { ansi, editorTheme, selectListTheme, stripAnsi } from './tui-ansi.js';
23
24
  import { TUI_COPY_RESOURCES } from './tui-copy-catalog.js';
24
25
  const TUI_PICKER_COPY = resolveUiMessageCatalog(defineUiMessageCatalog()(TUI_COPY_RESOURCES.pickers));
@@ -399,24 +400,37 @@ export class UserQuestionOverlay {
399
400
  }
400
401
  render(width) {
401
402
  const safeWidth = Math.max(1, width);
402
- const lines = [
403
- padLine(`${this.input.title} ${ansi.accent(this.input.rightLabel)}`, safeWidth),
404
- padLine(ansi.dim(this.input.hint), safeWidth),
405
- padLine('', safeWidth),
403
+ // The title wraps like the option rows: a long question must not lose its
404
+ // tail to a hard cut at the terminal width (#4610).
405
+ const wrappedTitle = wrapTextWithAnsi(`${this.input.title} ${ansi.accent(this.input.rightLabel)}`, safeWidth);
406
+ const titleLines = (wrappedTitle.length > 0 ? wrappedTitle : ['']).map((line) => padLine(line, safeWidth));
407
+ const hint = padLine(ansi.dim(this.input.hint), safeWidth);
408
+ const blank = padLine('', safeWidth);
409
+ const divider = padLine(ansi.accent('-'.repeat(safeWidth)), safeWidth);
410
+ const inputRows = this.renderInputRow(safeWidth);
411
+ const optionRows = this.input.options.map((option, index) => formatUserQuestionOptionRow(option, index === this.activeIndex, safeWidth));
412
+ const assemble = (title, options) => [
413
+ ...title,
414
+ hint,
415
+ blank,
416
+ ...options.flat(),
417
+ ...inputRows,
418
+ divider,
406
419
  ];
407
- this.input.options.forEach((option, index) => {
408
- lines.push(this.renderOptionRow(option, index === this.activeIndex, safeWidth));
409
- });
410
- lines.push(...this.renderInputRow(safeWidth));
411
- lines.push(padLine(ansi.accent('-'.repeat(safeWidth)), safeWidth));
412
- return lines;
413
- }
414
- renderOptionRow(option, active, width) {
415
- const prefix = active ? '→ ' : ' ';
416
- const body = option.description
417
- ? `${option.label} ${active ? option.description : ansi.dim(option.description)}`
418
- : option.label;
419
- return formatPickerItemLine(`${prefix}${body}`, width);
420
+ const full = assemble(titleLines, optionRows);
421
+ const budget = this.input.maxRows?.() ?? Number.POSITIVE_INFINITY;
422
+ if (full.length <= budget)
423
+ return full;
424
+ // Over budget pi-tui would slice(0, maxHeight) — silently dropping the
425
+ // input row and divider. Degrade instead: cap the title at two lines and
426
+ // give every option an equal share of the remaining rows, each ending in
427
+ // a visible ellipsis when clamped. Only a terminal too short for one row
428
+ // per option still overflows, falling back to the pre-existing clip.
429
+ const cappedTitle = clampRowsWithEllipsis(titleLines, 2, safeWidth);
430
+ const fixedRows = cappedTitle.length + 2 + inputRows.length + 1;
431
+ const optionBudget = Math.max(this.input.options.length, budget - fixedRows);
432
+ const perOption = Math.max(1, Math.floor(optionBudget / Math.max(1, this.input.options.length)));
433
+ return assemble(cappedTitle, optionRows.map((rows) => clampRowsWithEllipsis(rows, perOption, safeWidth)));
420
434
  }
421
435
  renderInputRow(width) {
422
436
  const prefix = this.onInputRow ? '→ ' : ' ';
@@ -715,15 +729,57 @@ function formatPickerItemLine(line, width) {
715
729
  const padded = padLine(line, width);
716
730
  return stripAnsi(line).startsWith('→ ') ? ansi.reverse(padded) : padded;
717
731
  }
732
+ /**
733
+ * One AskUserQuestion option row, wrapped instead of truncated (#4610): the
734
+ * option body is the decision content, and options routinely carry long
735
+ * trade-off descriptions that a hard cut at the terminal width made
736
+ * unreadable. Continuation lines indent under the option body, aligned past
737
+ * the `→ `/` ` marker; the active row's highlight band covers every wrapped
738
+ * line, not just the first.
739
+ */
740
+ export function formatUserQuestionOptionRow(option, active, width) {
741
+ const safeWidth = Math.max(1, width);
742
+ const prefix = active ? '→ ' : ' ';
743
+ const body = option.description
744
+ ? `${option.label} ${active ? option.description : ansi.dim(option.description)}`
745
+ : option.label;
746
+ const wrapped = wrapTextWithAnsi(body, Math.max(1, safeWidth - USER_QUESTION_ROW_PREFIX_WIDTH));
747
+ const continuation = ' '.repeat(USER_QUESTION_ROW_PREFIX_WIDTH);
748
+ return (wrapped.length > 0 ? wrapped : ['']).map((line, index) => {
749
+ const padded = padLine(`${index === 0 ? prefix : continuation}${line}`, safeWidth);
750
+ return active ? ansi.reverse(padded) : padded;
751
+ });
752
+ }
753
+ /**
754
+ * Cap an already-formatted wrapped row group at `maxRows`, folding the last
755
+ * kept line into an ellipsis so the elision is visible. truncateToWidth is
756
+ * ANSI-aware, so an active (reversed) line keeps its closing SGR.
757
+ */
758
+ export function clampRowsWithEllipsis(rows, maxRows, width) {
759
+ if (rows.length <= maxRows)
760
+ return rows;
761
+ const keep = Math.max(1, maxRows);
762
+ const kept = rows.slice(0, keep);
763
+ // truncateToWidth only appends its marker when it actually cuts, and the
764
+ // kept row is already padded to full width — so cut one column short and
765
+ // add the ellipsis by hand to guarantee the elision stays visible.
766
+ const shortened = truncateToWidth(kept[kept.length - 1] ?? '', Math.max(1, width - 1), '');
767
+ kept[kept.length - 1] = padLine(`${shortened}…`, width);
768
+ return kept;
769
+ }
718
770
  function padLine(text, width) {
719
771
  const safeWidth = Math.max(1, width);
720
772
  const trimmed = visibleWidth(text) > safeWidth ? truncateToWidth(text, safeWidth, '') : text;
721
773
  return `${trimmed}${' '.repeat(Math.max(0, safeWidth - visibleWidth(trimmed)))}`;
722
774
  }
723
- function keyEntryHint(copy, hasConnection, returnsToBaseUrl) {
775
+ function keyEntryHint(copy, hasConnection, returnsTo) {
724
776
  const action = hasConnection ? 'reuse' : 'enter';
725
- const destination = returnsToBaseUrl ? 'baseUrl' : 'provider';
726
- return copy.keyHints[action][destination];
777
+ return copy.keyHints[action][returnsTo];
778
+ }
779
+ /** The union's discriminant sits on `target.kind`, one level down — TS will
780
+ * not narrow the entry from that check alone, so route through this guard. */
781
+ function isCreateEntry(entry) {
782
+ return entry.target.kind === 'create';
727
783
  }
728
784
  const ONBOARDING_MODELS_MAX_VISIBLE = 10;
729
785
  /**
@@ -744,7 +800,11 @@ export class OnboardingWizard {
744
800
  searchEditor;
745
801
  baseUrlEditor;
746
802
  keyEditor;
803
+ nameEditor;
804
+ slugEditor;
747
805
  modelsSearchEditor;
806
+ identityFocus = 'name';
807
+ identityNameDraft = '';
748
808
  filtered;
749
809
  list;
750
810
  // Models phase state. Selection seeds from the picked provider's enabled set
@@ -784,6 +844,19 @@ export class OnboardingWizard {
784
844
  if (this.picked)
785
845
  this.input.onSubmitKey(value);
786
846
  };
847
+ this.nameEditor = new Editor(tui, editorTheme(), { paddingX: 0 });
848
+ this.nameEditor.onSubmit = (value) => {
849
+ // Enter on the optional name advances to the slug field in-frame. The
850
+ // Editor clears itself on submit, so capture the value here and restore
851
+ // the visible text — Esc back from the slug field must not find a blank
852
+ // name, and the slug submit reads the draft, not the editor.
853
+ this.identityNameDraft = value;
854
+ this.nameEditor.setText(value);
855
+ this.identityFocus = 'slug';
856
+ this.status = { kind: 'prompt' };
857
+ };
858
+ this.slugEditor = new Editor(tui, editorTheme(), { paddingX: 0 });
859
+ this.slugEditor.onSubmit = (value) => this.submitIdentity(value);
787
860
  this.modelsSearchEditor = new Editor(tui, editorTheme(), { paddingX: 0 });
788
861
  this.modelsSearchEditor.onChange = (text) => this.applyModelQuery(text);
789
862
  }
@@ -799,9 +872,19 @@ export class OnboardingWizard {
799
872
  }
800
873
  enterKeyPhase(provider) {
801
874
  this.picked = provider;
802
- // A relay has no registry endpoint, so the wizard must collect one
875
+ // A create target stops at the identity step first: name and slug come
876
+ // prefilled with the Host-derived defaults, and accepting them verbatim
877
+ // sends the same bare target a build without this step would send.
878
+ // A relay has no registry endpoint, so the wizard must still collect one
803
879
  // before the key — the deferred phase-2 step from #1254 (#3405).
804
- this.phase = provider.requiresBaseUrl ? 'baseUrl' : 'key';
880
+ const create = isCreateEntry(provider);
881
+ this.phase = create ? 'identity' : provider.requiresBaseUrl ? 'baseUrl' : 'key';
882
+ if (create) {
883
+ this.identityFocus = 'name';
884
+ this.nameEditor.setText(provider.label);
885
+ this.identityNameDraft = provider.label;
886
+ this.slugEditor.setText(provider.suggestedSlug);
887
+ }
805
888
  this.status = { kind: 'prompt' };
806
889
  this.baseUrlEditor.setText('');
807
890
  this.keyEditor.setText('');
@@ -817,6 +900,41 @@ export class OnboardingWizard {
817
900
  this.modelsSearchEditor.setText('');
818
901
  this.input.onPickProvider(provider);
819
902
  }
903
+ /**
904
+ * Slug submit on the identity step. The name field needs no validation of
905
+ * its own beyond the catalog's length cap: empty or untouched means the
906
+ * provider label stays. An empty or untouched slug likewise keeps the
907
+ * Host-derived identity — only a genuinely edited slug crosses the wire,
908
+ * and it must pass the same rule the catalog decoder enforces.
909
+ */
910
+ submitIdentity(rawSlug) {
911
+ const picked = this.picked;
912
+ if (!picked || !isCreateEntry(picked) || this.phase !== 'identity')
913
+ return;
914
+ const slug = rawSlug.trim();
915
+ const name = this.identityNameDraft.trim();
916
+ if (name.length > CONNECTION_NAME_MAX_LENGTH) {
917
+ this.identityFocus = 'name';
918
+ this.status = { kind: 'error', text: this.copy.identityNameTooLong };
919
+ return;
920
+ }
921
+ const customSlug = slug.length > 0 && slug !== picked.suggestedSlug ? slug : null;
922
+ if (customSlug !== null) {
923
+ const error = validateSlug(customSlug);
924
+ if (error) {
925
+ this.identityFocus = 'slug';
926
+ this.status = { kind: 'error', text: this.copy.identitySlugInvalid };
927
+ return;
928
+ }
929
+ }
930
+ const defaultName = picked.label;
931
+ this.input.onSubmitIdentity({
932
+ slug: customSlug,
933
+ name: name.length > 0 && name !== defaultName ? name : null,
934
+ });
935
+ this.status = { kind: 'prompt' };
936
+ this.phase = picked.requiresBaseUrl ? 'baseUrl' : 'key';
937
+ }
820
938
  submitBaseUrl(value) {
821
939
  if (!this.picked || this.phase !== 'baseUrl')
822
940
  return;
@@ -877,6 +995,17 @@ export class OnboardingWizard {
877
995
  this.modelHighlight = 0;
878
996
  this.modelScroll = 0;
879
997
  }
998
+ /** Runner hook: the Host rejected the requested slug — bounce back to the
999
+ * identity step with the user's text intact so they can edit it. */
1000
+ setIdentityError(text) {
1001
+ if (this.picked?.target.kind !== 'create')
1002
+ return;
1003
+ this.phase = 'identity';
1004
+ this.identityFocus = 'slug';
1005
+ this.status = { kind: 'error', text };
1006
+ this.keyEditor.setText('');
1007
+ this.keyEditor.disableSubmit = false;
1008
+ }
880
1009
  /** Runner hook: verify is in flight. Lock the key field and show progress. */
881
1010
  setVerifying() {
882
1011
  if (this.phase !== 'key')
@@ -937,17 +1066,40 @@ export class OnboardingWizard {
937
1066
  this.searchEditor.invalidate();
938
1067
  this.baseUrlEditor.invalidate();
939
1068
  this.keyEditor.invalidate();
1069
+ this.nameEditor.invalidate();
1070
+ this.slugEditor.invalidate();
940
1071
  this.modelsSearchEditor.invalidate();
941
1072
  this.list.invalidate();
942
1073
  }
943
- /** Step label: relays have four steps (the base-URL one), the rest three. */
944
- step(position) {
945
- return `${position}/${this.picked?.requiresBaseUrl ? 4 : 3}`;
1074
+ /** Step label: relays add a base-URL step, create targets an identity one. */
1075
+ stepFor(phase) {
1076
+ const create = this.picked?.target.kind === 'create';
1077
+ const relay = this.picked?.requiresBaseUrl === true;
1078
+ const total = 3 + (create ? 1 : 0) + (relay ? 1 : 0);
1079
+ let position = 1;
1080
+ if (phase === 'search')
1081
+ return `1/${total}`;
1082
+ if (create) {
1083
+ position += 1;
1084
+ if (phase === 'identity')
1085
+ return `${position}/${total}`;
1086
+ }
1087
+ if (relay) {
1088
+ position += 1;
1089
+ if (phase === 'baseUrl')
1090
+ return `${position}/${total}`;
1091
+ }
1092
+ position += 1;
1093
+ if (phase === 'key')
1094
+ return `${position}/${total}`;
1095
+ return `${total}/${total}`;
946
1096
  }
947
1097
  handleInput(data) {
948
1098
  switch (this.phase) {
949
1099
  case 'search':
950
1100
  return this.handleSearchInput(data);
1101
+ case 'identity':
1102
+ return this.handleIdentityInput(data);
951
1103
  case 'baseUrl':
952
1104
  return this.handleBaseUrlInput(data);
953
1105
  case 'key':
@@ -958,15 +1110,41 @@ export class OnboardingWizard {
958
1110
  return this.handleSuccessInput(data);
959
1111
  }
960
1112
  }
961
- handleBaseUrlInput(data) {
1113
+ handleIdentityInput(data) {
962
1114
  if (matchesKey(data, Key.ctrl('c'))) {
963
1115
  this.input.onCancel();
964
1116
  return;
965
1117
  }
966
1118
  if (matchesKey(data, Key.escape)) {
1119
+ // One level back: slug field → name field → provider search.
1120
+ if (this.identityFocus === 'slug') {
1121
+ this.identityFocus = 'name';
1122
+ this.status = { kind: 'prompt' };
1123
+ return;
1124
+ }
967
1125
  this.phase = 'search';
968
1126
  this.picked = undefined;
969
1127
  this.status = { kind: 'prompt' };
1128
+ this.input.onBack();
1129
+ return;
1130
+ }
1131
+ (this.identityFocus === 'name' ? this.nameEditor : this.slugEditor).handleInput(data);
1132
+ }
1133
+ handleBaseUrlInput(data) {
1134
+ if (matchesKey(data, Key.ctrl('c'))) {
1135
+ this.input.onCancel();
1136
+ return;
1137
+ }
1138
+ if (matchesKey(data, Key.escape)) {
1139
+ // One level back: a create target returns to its identity step.
1140
+ if (this.picked?.target.kind === 'create') {
1141
+ this.phase = 'identity';
1142
+ }
1143
+ else {
1144
+ this.phase = 'search';
1145
+ this.picked = undefined;
1146
+ }
1147
+ this.status = { kind: 'prompt' };
970
1148
  this.baseUrlEditor.setText('');
971
1149
  this.input.onBack();
972
1150
  return;
@@ -1002,11 +1180,14 @@ export class OnboardingWizard {
1002
1180
  return;
1003
1181
  }
1004
1182
  if (matchesKey(data, Key.escape)) {
1005
- // One level back: a relay returns to its base-URL step, everything
1006
- // else to the provider search.
1183
+ // One level back: a relay returns to its base-URL step, a create target
1184
+ // to its identity step, everything else to the provider search.
1007
1185
  if (this.picked?.requiresBaseUrl) {
1008
1186
  this.phase = 'baseUrl';
1009
1187
  }
1188
+ else if (this.picked?.target.kind === 'create') {
1189
+ this.phase = 'identity';
1190
+ }
1010
1191
  else {
1011
1192
  this.phase = 'search';
1012
1193
  this.picked = undefined;
@@ -1108,6 +1289,8 @@ export class OnboardingWizard {
1108
1289
  switch (this.phase) {
1109
1290
  case 'search':
1110
1291
  return this.renderSearch(safeWidth);
1292
+ case 'identity':
1293
+ return this.renderIdentity(safeWidth);
1111
1294
  case 'baseUrl':
1112
1295
  return this.renderBaseUrl(safeWidth);
1113
1296
  case 'key':
@@ -1118,17 +1301,43 @@ export class OnboardingWizard {
1118
1301
  return this.renderSuccess(safeWidth);
1119
1302
  }
1120
1303
  }
1304
+ focusOnly(active) {
1305
+ for (const editor of [
1306
+ this.searchEditor,
1307
+ this.nameEditor,
1308
+ this.slugEditor,
1309
+ this.baseUrlEditor,
1310
+ this.keyEditor,
1311
+ this.modelsSearchEditor,
1312
+ ]) {
1313
+ editor.focused = editor === active;
1314
+ }
1315
+ }
1316
+ renderIdentity(width) {
1317
+ this.focusOnly(this.identityFocus === 'name' ? this.nameEditor : this.slugEditor);
1318
+ const label = this.picked?.label ?? '';
1319
+ return [
1320
+ padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.stepFor('identity')}`)} ${ansi.accent(label)}`, width),
1321
+ padLine(ansi.dim(this.copy.identityHint), width),
1322
+ padLine('', width),
1323
+ ...this.renderFieldRow(this.nameEditor, this.copy.connectionNameLabel, width),
1324
+ padLine('', width),
1325
+ ...this.renderFieldRow(this.slugEditor, this.copy.connectionSlugLabel, width),
1326
+ padLine('', width),
1327
+ padLine(this.status.kind === 'error'
1328
+ ? ansi.red(`✗ ${this.status.text}`)
1329
+ : ansi.dim(this.copy.continueAction), width),
1330
+ padLine(ansi.accent('-'.repeat(width)), width),
1331
+ ];
1332
+ }
1121
1333
  renderBaseUrl(width) {
1122
- this.searchEditor.focused = false;
1123
- this.baseUrlEditor.focused = true;
1124
- this.keyEditor.focused = false;
1125
- this.modelsSearchEditor.focused = false;
1334
+ this.focusOnly(this.baseUrlEditor);
1126
1335
  const label = this.picked?.label ?? '';
1127
1336
  const hint = this.picked?.target.kind === 'existing'
1128
1337
  ? this.copy.reuseBaseUrlHint
1129
1338
  : this.copy.enterBaseUrlHint;
1130
1339
  return [
1131
- padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.step(2)}`)} ${ansi.accent(label)}`, width),
1340
+ padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.stepFor('baseUrl')}`)} ${ansi.accent(label)}`, width),
1132
1341
  padLine(ansi.dim(hint), width),
1133
1342
  padLine('', width),
1134
1343
  ...this.renderFieldRow(this.baseUrlEditor, this.copy.baseUrlLabel, width),
@@ -1140,12 +1349,9 @@ export class OnboardingWizard {
1140
1349
  ];
1141
1350
  }
1142
1351
  renderSearch(width) {
1143
- this.searchEditor.focused = true;
1144
- this.baseUrlEditor.focused = false;
1145
- this.keyEditor.focused = false;
1146
- this.modelsSearchEditor.focused = false;
1352
+ this.focusOnly(this.searchEditor);
1147
1353
  return [
1148
- padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.step(1)}`)} ${ansi.accent(String(this.filtered.length))}`, width),
1354
+ padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.stepFor('search')}`)} ${ansi.accent(String(this.filtered.length))}`, width),
1149
1355
  padLine(ansi.dim(this.copy.providerSearchHint), width),
1150
1356
  padLine('', width),
1151
1357
  ...this.renderFieldRow(this.searchEditor, this.copy.searchLabel, width),
@@ -1157,14 +1363,15 @@ export class OnboardingWizard {
1157
1363
  ];
1158
1364
  }
1159
1365
  renderKey(width) {
1160
- this.searchEditor.focused = false;
1161
- this.baseUrlEditor.focused = false;
1162
- this.keyEditor.focused = this.status.kind === 'prompt' || this.status.kind === 'error';
1163
- this.modelsSearchEditor.focused = false;
1366
+ this.focusOnly(this.status.kind === 'prompt' || this.status.kind === 'error' ? this.keyEditor : null);
1164
1367
  const label = this.picked?.label ?? '';
1165
- const hint = keyEntryHint(this.copy, this.picked?.target.kind === 'existing', this.picked?.requiresBaseUrl === true);
1368
+ const hint = keyEntryHint(this.copy, this.picked?.target.kind === 'existing', this.picked?.requiresBaseUrl === true
1369
+ ? 'baseUrl'
1370
+ : this.picked?.target.kind === 'create'
1371
+ ? 'identity'
1372
+ : 'provider');
1166
1373
  return [
1167
- padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.step(this.picked?.requiresBaseUrl ? 3 : 2)}`)} ${ansi.accent(label)}`, width),
1374
+ padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.stepFor('key')}`)} ${ansi.accent(label)}`, width),
1168
1375
  padLine(ansi.dim(hint), width),
1169
1376
  padLine('', width),
1170
1377
  ...this.renderFieldRow(this.keyEditor, this.copy.apiKeyLabel, width),
@@ -1186,13 +1393,10 @@ export class OnboardingWizard {
1186
1393
  }
1187
1394
  }
1188
1395
  renderModels(width) {
1189
- this.searchEditor.focused = false;
1190
- this.baseUrlEditor.focused = false;
1191
- this.keyEditor.focused = false;
1192
- this.modelsSearchEditor.focused = this.status.kind !== 'saving';
1396
+ this.focusOnly(this.status.kind !== 'saving' ? this.modelsSearchEditor : null);
1193
1397
  const label = this.picked?.label ?? '';
1194
1398
  const lines = [
1195
- padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.step(this.picked?.requiresBaseUrl ? 4 : 3)}`)} ${ansi.accent(label)}`, width),
1399
+ padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.stepFor('models')}`)} ${ansi.accent(label)}`, width),
1196
1400
  padLine(ansi.dim(this.copy.modelSelectionHint), width),
1197
1401
  padLine('', width),
1198
1402
  ...this.renderFieldRow(this.modelsSearchEditor, this.copy.searchLabel, width),
@@ -1229,10 +1433,7 @@ export class OnboardingWizard {
1229
1433
  }
1230
1434
  }
1231
1435
  renderSuccess(width) {
1232
- this.searchEditor.focused = false;
1233
- this.baseUrlEditor.focused = false;
1234
- this.keyEditor.focused = false;
1235
- this.modelsSearchEditor.focused = false;
1436
+ this.focusOnly(null);
1236
1437
  const label = this.picked?.label ?? '';
1237
1438
  return [
1238
1439
  padLine(`${this.copy.setupTitle} ${ansi.dim(`· ${this.copy.complete}`)} ${ansi.accent(label)}`, width),