@oh-my-pi/pi-coding-agent 15.12.3 → 15.12.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/CHANGELOG.md +43 -1
- package/dist/cli.js +1120 -870
- package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
- package/dist/types/cli/args.d.ts +0 -1
- package/dist/types/cli/models-cli.d.ts +49 -0
- package/dist/types/commands/launch.d.ts +0 -3
- package/dist/types/commands/models.d.ts +33 -0
- package/dist/types/commands/token.d.ts +25 -0
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/models-config-schema.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/edit/hashline/params.d.ts +1 -1
- package/dist/types/edit/modes/apply-patch.d.ts +1 -1
- package/dist/types/edit/modes/patch.d.ts +1 -1
- package/dist/types/edit/modes/replace.d.ts +1 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +2 -2
- package/dist/types/extensibility/hooks/types.d.ts +2 -2
- package/dist/types/goals/tools/goal-tool.d.ts +1 -1
- package/dist/types/lsp/types.d.ts +1 -1
- package/dist/types/mcp/manager.d.ts +8 -0
- package/dist/types/mnemopi/config.d.ts +28 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -2
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
- package/dist/types/modes/components/status-line/component.d.ts +9 -5
- package/dist/types/modes/components/status-line/types.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +0 -17
- package/dist/types/modes/interactive-mode.d.ts +0 -3
- package/dist/types/modes/types.d.ts +0 -5
- package/dist/types/session/agent-session.d.ts +14 -33
- package/dist/types/session/agent-storage.d.ts +2 -1
- package/dist/types/session/indexed-session-storage.d.ts +1 -0
- package/dist/types/session/messages.d.ts +8 -10
- package/dist/types/session/session-manager.d.ts +15 -0
- package/dist/types/session/session-storage.d.ts +5 -0
- package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
- package/dist/types/task/types.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +1 -1
- package/dist/types/tools/ast-edit.d.ts +1 -1
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
- package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
- package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
- package/dist/types/tools/browser/registry.d.ts +16 -3
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/checkpoint.d.ts +1 -1
- package/dist/types/tools/debug.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/gh.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/inspect-image.d.ts +1 -1
- package/dist/types/tools/irc.d.ts +1 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/dist/types/tools/memory-recall.d.ts +1 -1
- package/dist/types/tools/memory-reflect.d.ts +1 -1
- package/dist/types/tools/memory-retain.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/render-mermaid.d.ts +1 -1
- package/dist/types/tools/resolve.d.ts +1 -1
- package/dist/types/tools/review.d.ts +1 -1
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +1 -1
- package/dist/types/tools/tts.d.ts +1 -1
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/clipboard.d.ts +4 -3
- package/dist/types/utils/image-loading.d.ts +18 -1
- package/dist/types/utils/thinking-display.d.ts +17 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/package.json +14 -14
- package/src/autoresearch/storage.ts +2 -1
- package/src/autoresearch/tools/init-experiment.ts +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +1 -1
- package/src/cli/args.ts +0 -8
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/models-cli.ts +427 -0
- package/src/cli-commands.ts +2 -0
- package/src/collab/host.ts +9 -12
- package/src/commands/launch.ts +0 -3
- package/src/commands/models.ts +61 -0
- package/src/commands/token.ts +89 -0
- package/src/commit/agentic/tools/analyze-file.ts +1 -1
- package/src/commit/agentic/tools/git-file-diff.ts +1 -1
- package/src/commit/agentic/tools/git-hunk.ts +1 -1
- package/src/commit/agentic/tools/git-overview.ts +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +1 -1
- package/src/commit/agentic/tools/propose-commit.ts +1 -1
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -1
- package/src/commit/agentic/tools/split-commit.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/commit/shared-llm.ts +1 -1
- package/src/config/model-registry.ts +15 -12
- package/src/config/model-resolver.ts +2 -2
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +18 -0
- package/src/edit/hashline/params.ts +1 -1
- package/src/edit/modes/apply-patch.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/modes/replace.ts +1 -1
- package/src/eval/agent-bridge.ts +1 -1
- package/src/eval/completion-bridge.ts +1 -1
- package/src/export/html/template.js +24 -2
- package/src/export/html/tool-views.generated.js +2 -2
- package/src/extensibility/custom-commands/loader.ts +1 -1
- package/src/extensibility/custom-commands/types.ts +2 -2
- package/src/extensibility/custom-tools/loader.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +2 -2
- package/src/extensibility/extensions/types.ts +2 -2
- package/src/extensibility/hooks/loader.ts +1 -1
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/skills.ts +18 -3
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +5 -2
- package/src/lsp/types.ts +1 -1
- package/src/main.ts +0 -25
- package/src/mcp/config-writer.ts +7 -3
- package/src/mcp/manager.ts +11 -0
- package/src/memories/index.ts +3 -1
- package/src/memories/storage.ts +2 -1
- package/src/mnemopi/config.ts +95 -11
- package/src/modes/acp/acp-agent.ts +5 -48
- package/src/modes/acp/acp-event-mapper.ts +5 -1
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/assistant-message.ts +8 -7
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/logout-account-selector.ts +130 -0
- package/src/modes/components/mcp-add-wizard.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +54 -157
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +2 -1
- package/src/modes/controllers/command-controller.ts +0 -12
- package/src/modes/controllers/event-controller.ts +23 -62
- package/src/modes/controllers/input-controller.ts +53 -30
- package/src/modes/controllers/mcp-command-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +56 -10
- package/src/modes/controllers/streaming-reveal.ts +4 -3
- package/src/modes/interactive-mode.ts +2 -8
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +0 -5
- package/src/modes/utils/ui-helpers.ts +2 -1
- package/src/prompts/system/empty-stop-retry.md +4 -6
- package/src/sdk.ts +15 -19
- package/src/session/agent-session.ts +125 -234
- package/src/session/agent-storage.ts +18 -9
- package/src/session/history-storage.ts +2 -1
- package/src/session/indexed-session-storage.ts +7 -0
- package/src/session/messages.ts +9 -11
- package/src/session/session-dump-format.ts +4 -2
- package/src/session/session-manager.ts +116 -0
- package/src/session/session-storage.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +15 -1
- package/src/slash-commands/helpers/logout.ts +88 -0
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +1 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
- package/src/tools/browser/cmux/rpc.ts +156 -0
- package/src/tools/browser/cmux/socket-client.ts +309 -0
- package/src/tools/browser/registry.ts +37 -3
- package/src/tools/browser/render.ts +6 -1
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +189 -18
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +16 -1
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +11 -6
- package/src/tools/fetch.ts +13 -2
- package/src/tools/find.ts +1 -1
- package/src/tools/gh.ts +1 -1
- package/src/tools/github-cache.ts +2 -1
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/inspect-image.ts +3 -1
- package/src/tools/irc.ts +1 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/memory-retain.ts +1 -1
- package/src/tools/read.ts +8 -2
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/search-tool-bm25.ts +1 -1
- package/src/tools/search.ts +1 -1
- package/src/tools/ssh.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/tools/tts.ts +1 -1
- package/src/tools/write.ts +1 -1
- package/src/utils/clipboard.ts +35 -18
- package/src/utils/image-loading.ts +35 -4
- package/src/utils/thinking-display.ts +37 -0
- package/src/web/search/index.ts +1 -1
- package/dist/types/cli/list-models.d.ts +0 -30
- package/src/cli/list-models.ts +0 -194
|
@@ -120,10 +120,38 @@ export class InputController {
|
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
this.ctx.editor.onEscape = () => {
|
|
123
|
+
// Active context maintenance owns Esc: auto/manual compaction,
|
|
124
|
+
// handoff generation, and auto-retry backoff all advertise
|
|
125
|
+
// "(esc to cancel)". Dispatch on live session state instead of
|
|
126
|
+
// swapping onEscape handlers — interleaved start/end events used
|
|
127
|
+
// to clobber the single saved-handler slot (auto-compaction start
|
|
128
|
+
// → /compact → auto end → manual finally), leaving Esc wired to a
|
|
129
|
+
// stale no-op closure until restart.
|
|
130
|
+
const viewSession = this.ctx.viewSession;
|
|
131
|
+
let aborted = false;
|
|
132
|
+
if (viewSession.isCompacting) {
|
|
133
|
+
try {
|
|
134
|
+
viewSession.abortCompaction();
|
|
135
|
+
} catch {}
|
|
136
|
+
aborted = true;
|
|
137
|
+
}
|
|
138
|
+
if (viewSession.isGeneratingHandoff) {
|
|
139
|
+
try {
|
|
140
|
+
viewSession.abortHandoff();
|
|
141
|
+
} catch {}
|
|
142
|
+
aborted = true;
|
|
143
|
+
}
|
|
144
|
+
if (viewSession.isRetrying) {
|
|
145
|
+
try {
|
|
146
|
+
viewSession.abortRetry();
|
|
147
|
+
} catch {}
|
|
148
|
+
aborted = true;
|
|
149
|
+
}
|
|
150
|
+
if (aborted) return;
|
|
151
|
+
|
|
123
152
|
if (this.ctx.loopModeEnabled) {
|
|
124
153
|
this.ctx.pauseLoop();
|
|
125
154
|
if (this.ctx.session.isStreaming) {
|
|
126
|
-
this.ctx.notifyInterrupting();
|
|
127
155
|
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
128
156
|
} else {
|
|
129
157
|
this.ctx.cancelPendingSubmission();
|
|
@@ -153,7 +181,6 @@ export class InputController {
|
|
|
153
181
|
// session is never streaming, so the native abort path below would
|
|
154
182
|
// no-op.
|
|
155
183
|
if (this.ctx.collabGuest.state?.isStreaming || this.ctx.loadingAnimation) {
|
|
156
|
-
if (!this.ctx.collabGuest.readOnly) this.ctx.notifyInterrupting();
|
|
157
184
|
this.ctx.collabGuest.sendAbort();
|
|
158
185
|
}
|
|
159
186
|
return;
|
|
@@ -176,7 +203,6 @@ export class InputController {
|
|
|
176
203
|
this.ctx.isPythonMode = false;
|
|
177
204
|
this.ctx.updateEditorBorderColor();
|
|
178
205
|
} else if (this.ctx.session.isStreaming) {
|
|
179
|
-
this.ctx.notifyInterrupting();
|
|
180
206
|
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
181
207
|
} else if (this.ctx.editor.getText().trim()) {
|
|
182
208
|
// Esc with typed text clears the draft instead of (or before) any double-Esc action
|
|
@@ -376,22 +402,16 @@ export class InputController {
|
|
|
376
402
|
return;
|
|
377
403
|
}
|
|
378
404
|
|
|
379
|
-
// Empty submit while streaming with queued
|
|
380
|
-
//
|
|
381
|
-
// waiting for the current tool/model boundary.
|
|
405
|
+
// Empty submit while streaming with queued messages: abort the active
|
|
406
|
+
// turn and let the post-unwind drain deliver the agent-core queue.
|
|
382
407
|
if (!text && this.ctx.session.isStreaming) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
await
|
|
408
|
+
if (this.ctx.session.queuedMessageCount > 0) {
|
|
409
|
+
const aborting = this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
410
|
+
await aborting;
|
|
386
411
|
this.ctx.updatePendingMessagesDisplay();
|
|
387
412
|
this.ctx.ui.requestRender();
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
if (this.ctx.session.queuedMessageCount > 0) {
|
|
391
|
-
// Preserve the existing empty-submit flush for non-steer queues.
|
|
392
|
-
await this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
393
|
-
return;
|
|
394
413
|
}
|
|
414
|
+
return;
|
|
395
415
|
}
|
|
396
416
|
|
|
397
417
|
if (!text) return;
|
|
@@ -663,9 +683,9 @@ export class InputController {
|
|
|
663
683
|
async #submitToFocusedSession(text: string, streamingBehavior: "steer" | "followUp"): Promise<void> {
|
|
664
684
|
const target = this.ctx.viewSession;
|
|
665
685
|
if (!text) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
await
|
|
686
|
+
if (target.isStreaming && target.queuedMessageCount > 0) {
|
|
687
|
+
const aborting = target.abort({ reason: USER_INTERRUPT_LABEL });
|
|
688
|
+
await aborting;
|
|
669
689
|
this.ctx.updatePendingMessagesDisplay();
|
|
670
690
|
this.ctx.ui.requestRender();
|
|
671
691
|
}
|
|
@@ -702,6 +722,18 @@ export class InputController {
|
|
|
702
722
|
this.ctx.clearEditor();
|
|
703
723
|
this.ctx.lastSigintTime = now;
|
|
704
724
|
}
|
|
725
|
+
// Sync-flush the session JSONL so in-flight writes survive a hard exit.
|
|
726
|
+
// The TUI consumes Ctrl+C as a key event in raw mode, so postmortem's
|
|
727
|
+
// process-level SIGINT handler never fires. The second press still
|
|
728
|
+
// funnels through shutdown() which awaits its own async flush — the
|
|
729
|
+
// sync flush here is a superset that also covers the first-press case.
|
|
730
|
+
try {
|
|
731
|
+
this.ctx.sessionManager.flushSync();
|
|
732
|
+
} catch (err) {
|
|
733
|
+
logger.warn("session-manager sync flush on Ctrl+C failed", {
|
|
734
|
+
error: err instanceof Error ? err.message : String(err),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
705
737
|
}
|
|
706
738
|
|
|
707
739
|
handleCtrlD(): void {
|
|
@@ -798,15 +830,6 @@ export class InputController {
|
|
|
798
830
|
args: args || undefined,
|
|
799
831
|
lineCount: body ? body.split("\n").length : 0,
|
|
800
832
|
};
|
|
801
|
-
// When the agent is streaming, register the compact slash-form text as
|
|
802
|
-
// the pending-display twin BEFORE dispatching the CustomMessage. The
|
|
803
|
-
// returned tag is embedded in details so AgentSession.#handleAgentEvent
|
|
804
|
-
// can remove the matching display entry when the agent consumes this
|
|
805
|
-
// message (mirrors the user-message dequeue path).
|
|
806
|
-
if (this.ctx.session.isStreaming) {
|
|
807
|
-
const tag = this.ctx.session.enqueueCustomMessageDisplay(text, streamingBehavior);
|
|
808
|
-
details.__pendingDisplayTag = tag;
|
|
809
|
-
}
|
|
810
833
|
await this.ctx.session.promptCustomMessage(
|
|
811
834
|
{
|
|
812
835
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
@@ -815,7 +838,7 @@ export class InputController {
|
|
|
815
838
|
details,
|
|
816
839
|
attribution: "user",
|
|
817
840
|
},
|
|
818
|
-
{ streamingBehavior },
|
|
841
|
+
{ streamingBehavior, queueChipText: text },
|
|
819
842
|
);
|
|
820
843
|
if (this.ctx.session.isStreaming) {
|
|
821
844
|
this.ctx.updatePendingMessagesDisplay();
|
|
@@ -905,7 +928,7 @@ export class InputController {
|
|
|
905
928
|
if (allQueued.length === 0) {
|
|
906
929
|
this.ctx.updatePendingMessagesDisplay();
|
|
907
930
|
if (options?.abort) {
|
|
908
|
-
this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
931
|
+
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
909
932
|
}
|
|
910
933
|
return 0;
|
|
911
934
|
}
|
|
@@ -924,7 +947,7 @@ export class InputController {
|
|
|
924
947
|
}
|
|
925
948
|
this.ctx.updatePendingMessagesDisplay();
|
|
926
949
|
if (options?.abort) {
|
|
927
|
-
this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
950
|
+
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
928
951
|
}
|
|
929
952
|
return allQueued.length;
|
|
930
953
|
}
|
|
@@ -813,6 +813,43 @@ export class MCPCommandController {
|
|
|
813
813
|
return null;
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
+
/**
|
|
817
|
+
* Resolve a server for an auth/test operation.
|
|
818
|
+
*
|
|
819
|
+
* Unlike {@link #findConfiguredServer} (which only reads writable OMP config
|
|
820
|
+
* files), this also recognizes runtime-discovered servers that `/mcp list`
|
|
821
|
+
* surfaces but that live in no writable config — e.g. servers from a Claude
|
|
822
|
+
* Code marketplace plugin (`cloudflare:cloudflare-api`), `.cursor/mcp.json`,
|
|
823
|
+
* etc. Without this, `/mcp reauth|test|unauth` reports "not found" for a
|
|
824
|
+
* server the list just showed.
|
|
825
|
+
*
|
|
826
|
+
* For a discovered server, any persisted change is written into the *user*
|
|
827
|
+
* config under the same (namespaced) name; the native provider (priority 100)
|
|
828
|
+
* shadows the discovered entry on the next reload, so an OAuth `auth` block
|
|
829
|
+
* persisted by `/mcp reauth` takes effect. `discovered` lets callers tailor
|
|
830
|
+
* messaging and skip pointless writes when there is nothing to persist.
|
|
831
|
+
*/
|
|
832
|
+
async #resolveServerForAuth(name: string): Promise<{
|
|
833
|
+
filePath: string;
|
|
834
|
+
scope: "user" | "project";
|
|
835
|
+
config: MCPServerConfig;
|
|
836
|
+
discovered: boolean;
|
|
837
|
+
} | null> {
|
|
838
|
+
const found = await this.#findConfiguredServer(name);
|
|
839
|
+
if (found) return { ...found, discovered: false };
|
|
840
|
+
|
|
841
|
+
const config = this.ctx.mcpManager?.getServerConfig(name);
|
|
842
|
+
const source = this.ctx.mcpManager?.getSource(name);
|
|
843
|
+
if (!config || !source) return null;
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
filePath: getMCPConfigPath("user", getProjectDir()),
|
|
847
|
+
scope: "user",
|
|
848
|
+
config,
|
|
849
|
+
discovered: true,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
816
853
|
async #removeManagedOAuthCredential(credentialId: string | undefined): Promise<void> {
|
|
817
854
|
if (!credentialId?.startsWith("mcp_oauth_")) return;
|
|
818
855
|
await this.ctx.session.modelRegistry.authStorage.remove(credentialId);
|
|
@@ -1199,7 +1236,7 @@ export class MCPCommandController {
|
|
|
1199
1236
|
|
|
1200
1237
|
let connection: MCPServerConnection | undefined;
|
|
1201
1238
|
try {
|
|
1202
|
-
const found = await this.#
|
|
1239
|
+
const found = await this.#resolveServerForAuth(name);
|
|
1203
1240
|
|
|
1204
1241
|
if (!found) {
|
|
1205
1242
|
this.ctx.showError(
|
|
@@ -1389,13 +1426,17 @@ export class MCPCommandController {
|
|
|
1389
1426
|
}
|
|
1390
1427
|
|
|
1391
1428
|
try {
|
|
1392
|
-
const found = await this.#
|
|
1429
|
+
const found = await this.#resolveServerForAuth(name);
|
|
1393
1430
|
if (!found) {
|
|
1394
1431
|
this.ctx.showError(`Server "${name}" not found.`);
|
|
1395
1432
|
return;
|
|
1396
1433
|
}
|
|
1397
1434
|
|
|
1398
1435
|
const currentAuth = (found.config as MCPServerConfig & { auth?: MCPAuthConfig }).auth;
|
|
1436
|
+
if (found.discovered && currentAuth?.type !== "oauth") {
|
|
1437
|
+
this.#showMessage(["", theme.fg("muted", `No stored OAuth auth to remove for "${name}".`), ""].join("\n"));
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1399
1440
|
if (currentAuth?.type === "oauth") {
|
|
1400
1441
|
await this.#removeManagedOAuthCredential(currentAuth.credentialId);
|
|
1401
1442
|
}
|
|
@@ -1419,7 +1460,7 @@ export class MCPCommandController {
|
|
|
1419
1460
|
}
|
|
1420
1461
|
|
|
1421
1462
|
try {
|
|
1422
|
-
const found = await this.#
|
|
1463
|
+
const found = await this.#resolveServerForAuth(name);
|
|
1423
1464
|
if (!found) {
|
|
1424
1465
|
this.ctx.showError(`Server "${name}" not found.`);
|
|
1425
1466
|
return;
|
|
@@ -30,6 +30,7 @@ import type { InteractiveModeContext } from "../../modes/types";
|
|
|
30
30
|
import type { ResetCreditRedeemOutcome } from "../../session/auth-storage";
|
|
31
31
|
import { type SessionInfo, SessionManager } from "../../session/session-manager";
|
|
32
32
|
import { FileSessionStorage } from "../../session/session-storage";
|
|
33
|
+
import { type LogoutAccount, toLogoutAccounts } from "../../slash-commands/helpers/logout";
|
|
33
34
|
import {
|
|
34
35
|
describeRedeemOutcome,
|
|
35
36
|
type ResetUsageAccount,
|
|
@@ -51,6 +52,7 @@ import { AssistantMessageComponent } from "../components/assistant-message";
|
|
|
51
52
|
import { CopySelectorComponent } from "../components/copy-selector";
|
|
52
53
|
import { ExtensionDashboard } from "../components/extensions";
|
|
53
54
|
import { HistorySearchComponent } from "../components/history-search";
|
|
55
|
+
import { LogoutAccountSelectorComponent } from "../components/logout-account-selector";
|
|
54
56
|
import { ModelSelectorComponent } from "../components/model-selector";
|
|
55
57
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
56
58
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
@@ -1000,23 +1002,28 @@ export class SelectorController {
|
|
|
1000
1002
|
}
|
|
1001
1003
|
}
|
|
1002
1004
|
|
|
1003
|
-
async #
|
|
1005
|
+
async #handleCredentialLogout(providerId: string, account: LogoutAccount): Promise<void> {
|
|
1004
1006
|
try {
|
|
1005
1007
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
this.ctx.showError(`Logout skipped: no stored credentials for ${providerId}.${suffix}`);
|
|
1008
|
+
const removed = await authStorage.removeCredential(providerId, account.credentialId);
|
|
1009
|
+
if (!removed) {
|
|
1010
|
+
this.ctx.showError(`Logout skipped: ${account.label} is no longer stored for ${providerId}.`);
|
|
1010
1011
|
return;
|
|
1011
1012
|
}
|
|
1012
1013
|
|
|
1013
|
-
await authStorage.logout(providerId);
|
|
1014
1014
|
await this.ctx.session.modelRegistry.refresh();
|
|
1015
1015
|
const block = new TranscriptBlock();
|
|
1016
1016
|
block.addChild(
|
|
1017
|
-
new Text(
|
|
1017
|
+
new Text(
|
|
1018
|
+
theme.fg(
|
|
1019
|
+
"success",
|
|
1020
|
+
`${theme.status.success} Successfully logged out ${account.label} from ${providerId}`,
|
|
1021
|
+
),
|
|
1022
|
+
1,
|
|
1023
|
+
0,
|
|
1024
|
+
),
|
|
1018
1025
|
);
|
|
1019
|
-
block.addChild(new Text(theme.fg("dim", `
|
|
1026
|
+
block.addChild(new Text(theme.fg("dim", `Credential removed from ${getAgentDbPath()}`), 1, 0));
|
|
1020
1027
|
const remainingSource = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
|
|
1021
1028
|
if (remainingSource) {
|
|
1022
1029
|
block.addChild(
|
|
@@ -1029,12 +1036,51 @@ export class SelectorController {
|
|
|
1029
1036
|
}
|
|
1030
1037
|
}
|
|
1031
1038
|
|
|
1039
|
+
async #showOAuthLogoutAccountSelector(providerId: string): Promise<void> {
|
|
1040
|
+
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
1041
|
+
try {
|
|
1042
|
+
await authStorage.reload();
|
|
1043
|
+
} catch (error: unknown) {
|
|
1044
|
+
this.ctx.showError(
|
|
1045
|
+
`Could not load stored credentials: ${error instanceof Error ? error.message : String(error)}`,
|
|
1046
|
+
);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const provider = getOAuthProviders().find(candidate => candidate.id === providerId);
|
|
1050
|
+
const accounts = toLogoutAccounts(providerId, authStorage.listStoredCredentials(providerId), {
|
|
1051
|
+
activeIdentity: authStorage.getOAuthAccountIdentity(providerId, this.ctx.session.sessionId),
|
|
1052
|
+
activeApiKey: authStorage.getCredentialOrigin(providerId)?.kind === "api_key",
|
|
1053
|
+
});
|
|
1054
|
+
if (accounts.length === 0) {
|
|
1055
|
+
const source = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
|
|
1056
|
+
const suffix = source ? ` Current auth comes from ${source}; remove that source to log out.` : "";
|
|
1057
|
+
this.ctx.showError(`Logout skipped: no stored credentials for ${providerId}.${suffix}`);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
this.showSelector(done => {
|
|
1062
|
+
const selector = new LogoutAccountSelectorComponent(
|
|
1063
|
+
provider?.name ?? providerId,
|
|
1064
|
+
accounts,
|
|
1065
|
+
account => {
|
|
1066
|
+
done();
|
|
1067
|
+
void this.#handleCredentialLogout(providerId, account);
|
|
1068
|
+
},
|
|
1069
|
+
() => {
|
|
1070
|
+
done();
|
|
1071
|
+
this.ctx.ui.requestRender();
|
|
1072
|
+
},
|
|
1073
|
+
);
|
|
1074
|
+
return { component: selector, focus: selector };
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1032
1078
|
async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
|
|
1033
1079
|
if (providerId) {
|
|
1034
1080
|
if (mode === "login") {
|
|
1035
1081
|
await this.#handleOAuthLogin(providerId);
|
|
1036
1082
|
} else {
|
|
1037
|
-
await this.#
|
|
1083
|
+
await this.#showOAuthLogoutAccountSelector(providerId);
|
|
1038
1084
|
}
|
|
1039
1085
|
return;
|
|
1040
1086
|
}
|
|
@@ -1062,7 +1108,7 @@ export class SelectorController {
|
|
|
1062
1108
|
if (mode === "login") {
|
|
1063
1109
|
await this.#handleOAuthLogin(selectedProviderId);
|
|
1064
1110
|
} else {
|
|
1065
|
-
await this.#
|
|
1111
|
+
await this.#showOAuthLogoutAccountSelector(selectedProviderId);
|
|
1066
1112
|
}
|
|
1067
1113
|
},
|
|
1068
1114
|
() => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { getSegmenter } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { LRUCache } from "lru-cache/raw";
|
|
4
|
+
import { hasVisibleThinking } from "../../utils/thinking-display";
|
|
4
5
|
import type { AssistantMessageComponent } from "../components/assistant-message";
|
|
5
6
|
|
|
6
7
|
export const STREAMING_REVEAL_FRAME_MS = 1000 / 30;
|
|
@@ -87,7 +88,7 @@ export function visibleUnits(message: AssistantMessage, hideThinking: boolean):
|
|
|
87
88
|
for (const block of message.content) {
|
|
88
89
|
if (block.type === "text") {
|
|
89
90
|
total += countGraphemes(block.text);
|
|
90
|
-
} else if (block.type === "thinking" && !hideThinking) {
|
|
91
|
+
} else if (block.type === "thinking" && !hideThinking && hasVisibleThinking(block)) {
|
|
91
92
|
total += countGraphemes(block.thinking);
|
|
92
93
|
}
|
|
93
94
|
}
|
|
@@ -128,7 +129,7 @@ export function buildDisplayMessage(
|
|
|
128
129
|
const units = countOf(i, block.text);
|
|
129
130
|
content.push(revealTextBlock(block, remaining, units));
|
|
130
131
|
remaining = Math.max(0, remaining - units);
|
|
131
|
-
} else if (block.type === "thinking" && !hideThinking) {
|
|
132
|
+
} else if (block.type === "thinking" && !hideThinking && hasVisibleThinking(block)) {
|
|
132
133
|
const units = countOf(i, block.thinking);
|
|
133
134
|
content.push(revealThinkingBlock(block, remaining, units));
|
|
134
135
|
remaining = Math.max(0, remaining - units);
|
|
@@ -230,7 +231,7 @@ export class StreamingRevealController {
|
|
|
230
231
|
const block = message.content[i]!;
|
|
231
232
|
if (block.type === "text") {
|
|
232
233
|
total += this.#unitCounter.count(i, block.text);
|
|
233
|
-
} else if (block.type === "thinking" && !this.#hideThinkingBlock) {
|
|
234
|
+
} else if (block.type === "thinking" && !this.#hideThinkingBlock && hasVisibleThinking(block)) {
|
|
234
235
|
total += this.#unitCounter.count(i, block.thinking);
|
|
235
236
|
}
|
|
236
237
|
}
|
|
@@ -383,8 +383,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
383
383
|
get #defaultWorkingMessage(): string {
|
|
384
384
|
return `Working…${interruptHint()}`;
|
|
385
385
|
}
|
|
386
|
-
autoCompactionEscapeHandler?: () => void;
|
|
387
|
-
retryEscapeHandler?: () => void;
|
|
388
386
|
unsubscribe?: () => void;
|
|
389
387
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
390
388
|
optimisticUserMessageSignature: string | undefined = undefined;
|
|
@@ -2437,7 +2435,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2437
2435
|
async #startGoalFromObjective(objective: string): Promise<void> {
|
|
2438
2436
|
await this.#enterGoalMode({ objective, silent: true });
|
|
2439
2437
|
this.#resetGoalContinuationSuppression();
|
|
2440
|
-
if (this.onInputCallback) {
|
|
2438
|
+
if (!this.session.isStreaming && this.onInputCallback) {
|
|
2441
2439
|
this.onInputCallback(this.startPendingSubmission({ text: objective }));
|
|
2442
2440
|
}
|
|
2443
2441
|
}
|
|
@@ -2452,7 +2450,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2452
2450
|
if (this.session.isStreaming) {
|
|
2453
2451
|
await this.session.sendGoalModeContext({ deliverAs: "steer" });
|
|
2454
2452
|
}
|
|
2455
|
-
if (this.onInputCallback) {
|
|
2453
|
+
if (!this.session.isStreaming && this.onInputCallback) {
|
|
2456
2454
|
this.onInputCallback(this.startPendingSubmission({ text: objective }));
|
|
2457
2455
|
}
|
|
2458
2456
|
}
|
|
@@ -3025,10 +3023,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3025
3023
|
this.setWorkingMessage(message);
|
|
3026
3024
|
}
|
|
3027
3025
|
|
|
3028
|
-
notifyInterrupting(): void {
|
|
3029
|
-
this.#eventController.notifyInterrupting();
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
3026
|
showNewVersionNotification(newVersion: string): void {
|
|
3033
3027
|
this.#uiHelpers.showNewVersionNotification(newVersion);
|
|
3034
3028
|
}
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { EditorTheme, MarkdownTheme, SelectListTheme, SettingsListTheme, Sy
|
|
|
13
13
|
import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import { LRUCache } from "lru-cache/raw";
|
|
16
|
-
import
|
|
16
|
+
import { z } from "zod/v4";
|
|
17
17
|
// Embed theme JSON files at build time
|
|
18
18
|
import darkThemeJson from "./dark.json" with { type: "json" };
|
|
19
19
|
import { defaultThemes } from "./defaults";
|
package/src/modes/types.ts
CHANGED
|
@@ -149,8 +149,6 @@ export interface InteractiveModeContext {
|
|
|
149
149
|
loadingAnimation: Loader | undefined;
|
|
150
150
|
autoCompactionLoader: Loader | undefined;
|
|
151
151
|
retryLoader: Loader | undefined;
|
|
152
|
-
autoCompactionEscapeHandler?: () => void;
|
|
153
|
-
retryEscapeHandler?: () => void;
|
|
154
152
|
unsubscribe?: () => void;
|
|
155
153
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
156
154
|
optimisticUserMessageSignature: string | undefined;
|
|
@@ -211,9 +209,6 @@ export interface InteractiveModeContext {
|
|
|
211
209
|
flushPendingModelSwitch(): Promise<void>;
|
|
212
210
|
setWorkingMessage(message?: string): void;
|
|
213
211
|
applyPendingWorkingMessage(): void;
|
|
214
|
-
/** Acknowledge a user interrupt (Esc) by switching the loader to an
|
|
215
|
-
* "Interrupting…" label until the agent turn unwinds. */
|
|
216
|
-
notifyInterrupting(): void;
|
|
217
212
|
ensureLoadingAnimation(): void;
|
|
218
213
|
startPendingSubmission(input: {
|
|
219
214
|
text: string;
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import type { SessionContext } from "../../session/session-manager";
|
|
40
40
|
import { createIrcMessageCard } from "../../tools/irc";
|
|
41
41
|
import { formatBytes, formatDuration } from "../../tools/render-utils";
|
|
42
|
+
import { hasVisibleThinking } from "../../utils/thinking-display";
|
|
42
43
|
|
|
43
44
|
type TextBlock = { type: "text"; text: string };
|
|
44
45
|
interface RenderInitialMessagesOptions {
|
|
@@ -367,7 +368,7 @@ export class UiHelpers {
|
|
|
367
368
|
const hasVisibleAssistantContent = message.content.some(
|
|
368
369
|
content =>
|
|
369
370
|
(content.type === "text" && content.text.trim().length > 0) ||
|
|
370
|
-
(content.type === "thinking" && content
|
|
371
|
+
(content.type === "thinking" && hasVisibleThinking(content)),
|
|
371
372
|
);
|
|
372
373
|
if (hasVisibleAssistantContent) {
|
|
373
374
|
// Rebuild reconstructs immutable history; seal (not finalize) so the
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
<system-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
(Empty response retry {{retryCount}}/{{maxRetries}})
|
|
6
|
-
</system-reminder>
|
|
1
|
+
<system-injection>
|
|
2
|
+
You stopped without completing the task. Continue.
|
|
3
|
+
Attempt #{{retryCount}}/{{maxRetries}}
|
|
4
|
+
</system-injection>
|
package/src/sdk.ts
CHANGED
|
@@ -1166,20 +1166,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1166
1166
|
SessionManager.create(cwd, SessionManager.getDefaultSessionDir(cwd, agentDir)),
|
|
1167
1167
|
);
|
|
1168
1168
|
const providerSessionId = options.providerSessionId ?? sessionManager.getSessionId();
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const hasKey = !!(await modelRegistry.getApiKey(candidate, providerSessionId));
|
|
1180
|
-
modelApiKeyAvailability.set(availabilityKey, hasKey);
|
|
1181
|
-
return hasKey;
|
|
1182
|
-
};
|
|
1169
|
+
// Startup model *selection* only needs to know whether auth is configured for
|
|
1170
|
+
// a candidate's provider — never the resolved key bytes. Use the synchronous,
|
|
1171
|
+
// side-effect-free probe (`hasConfiguredAuth`): it refreshes no OAuth tokens,
|
|
1172
|
+
// executes no `!command` keys, and issues no auth-broker requests. Resolving the
|
|
1173
|
+
// real key here (`getApiKey`) blocks resume on those network paths — a slow or
|
|
1174
|
+
// unreachable OAuth/broker endpoint stalls startup for the full ~10s refresh
|
|
1175
|
+
// timeout per candidate (observed as a hang in `restoreSessionModel`). The real
|
|
1176
|
+
// key is resolved lazily per request via ModelRegistry.resolver.
|
|
1177
|
+
const hasModelAuth = (candidate: Model): boolean => modelRegistry.hasConfiguredAuth(candidate);
|
|
1183
1178
|
|
|
1184
1179
|
// Load and create secret obfuscator early so resumed session state and prompt warnings
|
|
1185
1180
|
// reflect actual loaded secrets, not just the setting toggle.
|
|
@@ -1228,7 +1223,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1228
1223
|
: [];
|
|
1229
1224
|
let restoredSessionModelIndex = -1;
|
|
1230
1225
|
if (!hasExplicitModel && !model && sessionModelStrings.length > 0) {
|
|
1231
|
-
|
|
1226
|
+
logger.time("restoreSessionModel", () => {
|
|
1232
1227
|
let failedSessionModel: string | undefined;
|
|
1233
1228
|
for (let i = 0; i < sessionModelStrings.length; i++) {
|
|
1234
1229
|
const sessionModelStr = sessionModelStrings[i];
|
|
@@ -1239,7 +1234,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1239
1234
|
}
|
|
1240
1235
|
|
|
1241
1236
|
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
1242
|
-
if (restoredModel && (
|
|
1237
|
+
if (restoredModel && hasModelAuth(restoredModel)) {
|
|
1243
1238
|
model = restoredModel;
|
|
1244
1239
|
restoredSessionModelIndex = i;
|
|
1245
1240
|
break;
|
|
@@ -1492,6 +1487,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1492
1487
|
getSessionSpawns: () => options.spawns ?? "*",
|
|
1493
1488
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
1494
1489
|
getActiveModelString,
|
|
1490
|
+
getActiveModel: () => agent?.state.model ?? model,
|
|
1495
1491
|
getPlanModeState: () => session?.getPlanModeState(),
|
|
1496
1492
|
getPlanReferencePath: () => session?.getPlanReferencePath() ?? "local://PLAN.md",
|
|
1497
1493
|
getGoalModeState: () => session?.getGoalModeState(),
|
|
@@ -1866,7 +1862,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1866
1862
|
const parsedModel = parseModelString(sessionModelStr);
|
|
1867
1863
|
if (!parsedModel) continue;
|
|
1868
1864
|
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
1869
|
-
if (restoredModel && (
|
|
1865
|
+
if (restoredModel && hasModelAuth(restoredModel)) {
|
|
1870
1866
|
model = restoredModel;
|
|
1871
1867
|
modelFallbackMessage = undefined;
|
|
1872
1868
|
restoredSessionModelIndex = i;
|
|
@@ -1918,7 +1914,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1918
1914
|
const preferred = fallbackCandidates.find(
|
|
1919
1915
|
candidate => candidate.provider === provider && candidate.id === defaultId,
|
|
1920
1916
|
);
|
|
1921
|
-
if (preferred && (
|
|
1917
|
+
if (preferred && hasModelAuth(preferred)) {
|
|
1922
1918
|
model = preferred;
|
|
1923
1919
|
break;
|
|
1924
1920
|
}
|
|
@@ -1926,7 +1922,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1926
1922
|
// Otherwise, first available model with a valid API key.
|
|
1927
1923
|
if (!model) {
|
|
1928
1924
|
for (const candidate of fallbackCandidates) {
|
|
1929
|
-
if (
|
|
1925
|
+
if (hasModelAuth(candidate)) {
|
|
1930
1926
|
model = candidate;
|
|
1931
1927
|
break;
|
|
1932
1928
|
}
|