@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
package/src/lsp/types.ts
CHANGED
package/src/main.ts
CHANGED
|
@@ -25,7 +25,6 @@ import type { Args } from "./cli/args";
|
|
|
25
25
|
import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags";
|
|
26
26
|
import { processFileArguments } from "./cli/file-processor";
|
|
27
27
|
import { buildInitialMessage } from "./cli/initial-message";
|
|
28
|
-
import { runListModelsCommand } from "./cli/list-models";
|
|
29
28
|
import { selectSession } from "./cli/session-picker";
|
|
30
29
|
import { applyStartupCwd } from "./cli/startup-cwd";
|
|
31
30
|
import { findConfigFile } from "./config";
|
|
@@ -920,30 +919,6 @@ export async function runRootCommand(
|
|
|
920
919
|
process.exit(0);
|
|
921
920
|
}
|
|
922
921
|
|
|
923
|
-
if (parsedArgs.listModels !== undefined) {
|
|
924
|
-
const settingsInstance = await logger.time("settings:init:list-models", Settings.init, {
|
|
925
|
-
cwd: getProjectDir(),
|
|
926
|
-
configFiles: parsedArgs.config,
|
|
927
|
-
});
|
|
928
|
-
await modelRegistry.refresh("online");
|
|
929
|
-
const cliExtensionPaths = parsedArgs.noExtensions
|
|
930
|
-
? []
|
|
931
|
-
: [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
|
|
932
|
-
const settingsExtensions = settingsInstance.get("extensions") ?? [];
|
|
933
|
-
const disabledExtensionIds = settingsInstance.get("disabledExtensions") ?? [];
|
|
934
|
-
const searchPattern = typeof parsedArgs.listModels === "string" ? parsedArgs.listModels : undefined;
|
|
935
|
-
await runListModelsCommand({
|
|
936
|
-
modelRegistry,
|
|
937
|
-
cwd: getProjectDir(),
|
|
938
|
-
additionalExtensionPaths: cliExtensionPaths,
|
|
939
|
-
settingsExtensions,
|
|
940
|
-
disabledExtensionIds,
|
|
941
|
-
disableExtensionDiscovery: Boolean(parsedArgs.noExtensions),
|
|
942
|
-
searchPattern,
|
|
943
|
-
});
|
|
944
|
-
process.exit(0);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
922
|
if (parsedArgs.export) {
|
|
948
923
|
let result: string;
|
|
949
924
|
try {
|
package/src/mcp/config-writer.ts
CHANGED
|
@@ -67,9 +67,13 @@ export function validateServerName(name: string): string | undefined {
|
|
|
67
67
|
if (name.length > 100) {
|
|
68
68
|
return "Server name is too long (max 100 characters)";
|
|
69
69
|
}
|
|
70
|
-
// Check for invalid characters
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// Check for invalid characters. Colon is allowed so namespaced plugin servers
|
|
71
|
+
// (e.g. "cloudflare:cloudflare-api" from a Claude Code marketplace plugin) can
|
|
72
|
+
// be persisted: the runtime already accepts colons in server names (tool names
|
|
73
|
+
// sanitize them via createMCPToolName) and `/mcp reauth` writes such names back
|
|
74
|
+
// as a user-config override that shadows the discovered entry.
|
|
75
|
+
if (!/^[a-zA-Z0-9_.:-]+$/.test(name)) {
|
|
76
|
+
return "Server name can only contain letters, numbers, dash, underscore, dot, and colon";
|
|
73
77
|
}
|
|
74
78
|
return undefined;
|
|
75
79
|
}
|
package/src/mcp/manager.ts
CHANGED
|
@@ -643,6 +643,17 @@ export class MCPManager {
|
|
|
643
643
|
return this.#sources.get(name) ?? this.#connections.get(name)?._source;
|
|
644
644
|
}
|
|
645
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Get the preserved (pre-auth) config for a known server — whether currently
|
|
648
|
+
* connected or merely discovered (a connect was attempted but may have failed,
|
|
649
|
+
* e.g. an OAuth server that has not been authorized yet). Mirrors the
|
|
650
|
+
* reconnect lookup at {@link reconnectServer} so callers like `/mcp reauth`
|
|
651
|
+
* can recover a discovered server's config without re-reading config files.
|
|
652
|
+
*/
|
|
653
|
+
getServerConfig(name: string): MCPServerConfig | undefined {
|
|
654
|
+
return this.#connections.get(name)?.config ?? this.#serverConfigs.get(name);
|
|
655
|
+
}
|
|
656
|
+
|
|
646
657
|
/**
|
|
647
658
|
* Wait for a connection to complete (or fail).
|
|
648
659
|
*/
|
package/src/memories/index.ts
CHANGED
|
@@ -1071,7 +1071,9 @@ function truncateByApproxTokens(text: string, tokenLimit: number): string {
|
|
|
1071
1071
|
|
|
1072
1072
|
function computeModelTokenBudget(model: Model, config: MemoryRuntimeConfig): number {
|
|
1073
1073
|
const maxTokens =
|
|
1074
|
-
Number.isFinite(model.contextWindow) && model.contextWindow > 0
|
|
1074
|
+
model.contextWindow !== null && Number.isFinite(model.contextWindow) && model.contextWindow > 0
|
|
1075
|
+
? model.contextWindow
|
|
1076
|
+
: config.fallbackTokenLimit;
|
|
1075
1077
|
return Math.max(2048, Math.floor(maxTokens));
|
|
1076
1078
|
}
|
|
1077
1079
|
|
package/src/memories/storage.ts
CHANGED
|
@@ -46,10 +46,11 @@ function globalJobKey(cwd: string): string {
|
|
|
46
46
|
|
|
47
47
|
export function openMemoryDb(dbPath: string): Database {
|
|
48
48
|
const db = new Database(dbPath);
|
|
49
|
+
// Install the busy handler BEFORE any lock-taking statement. See #2421.
|
|
50
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
49
51
|
db.exec(`
|
|
50
52
|
PRAGMA journal_mode=WAL;
|
|
51
53
|
PRAGMA synchronous=NORMAL;
|
|
52
|
-
PRAGMA busy_timeout=5000;
|
|
53
54
|
|
|
54
55
|
CREATE TABLE IF NOT EXISTS threads (
|
|
55
56
|
id TEXT PRIMARY KEY,
|
package/src/mnemopi/config.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import * as fs from "node:fs";
|
|
1
3
|
import * as path from "node:path";
|
|
2
4
|
import type { MnemopiOptions } from "@oh-my-pi/pi-mnemopi";
|
|
3
|
-
import { getMemoriesDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { getMemoriesDir, logger } from "@oh-my-pi/pi-utils";
|
|
4
6
|
import type { Settings } from "../config/settings";
|
|
5
|
-
import * as git from "../utils/git";
|
|
6
7
|
|
|
7
8
|
export type MnemopiLlmMode = "none" | "smol" | "remote";
|
|
8
9
|
|
|
@@ -42,15 +43,18 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
|
|
|
42
43
|
const configuredDbPath = settings.get("mnemopi.dbPath");
|
|
43
44
|
const cwd = settings.getCwd();
|
|
44
45
|
const scoping = settings.get("mnemopi.scoping");
|
|
45
|
-
const
|
|
46
|
+
const dbPath = configuredDbPath ?? path.join(getMemoriesDir(agentDir), "mnemopi", "mnemopi.db");
|
|
47
|
+
const scope = computeMnemopiBankScope(settings.get("mnemopi.bank"), cwd, scoping);
|
|
48
|
+
const recallBanks =
|
|
49
|
+
scoping === "global" ? scope.recallBanks : extendRecallWithLegacyBanks(scope.recallBanks, dbPath, cwd);
|
|
46
50
|
const llmMode = settings.get("mnemopi.llmMode");
|
|
47
51
|
return {
|
|
48
|
-
dbPath
|
|
52
|
+
dbPath,
|
|
49
53
|
baseBank: scope.baseBank,
|
|
50
54
|
bank: scope.bank,
|
|
51
55
|
globalBank: scope.globalBank,
|
|
52
56
|
retainBank: scope.retainBank,
|
|
53
|
-
recallBanks
|
|
57
|
+
recallBanks,
|
|
54
58
|
scoping,
|
|
55
59
|
autoRecall: settings.get("mnemopi.autoRecall"),
|
|
56
60
|
autoRetain: settings.get("mnemopi.autoRetain"),
|
|
@@ -86,7 +90,11 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
|
|
|
86
90
|
|
|
87
91
|
const DEFAULT_SHARED_BANK = "default";
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
// Cap legacy-bank scanning at session start so a pathological banks/
|
|
94
|
+
// directory cannot dominate startup latency.
|
|
95
|
+
const LEGACY_BANK_SCAN_LIMIT = 64;
|
|
96
|
+
|
|
97
|
+
export interface MnemopiBankScope {
|
|
90
98
|
baseBank: string;
|
|
91
99
|
bank: string;
|
|
92
100
|
globalBank: string;
|
|
@@ -94,9 +102,19 @@ interface MnemopiBankScope {
|
|
|
94
102
|
recallBanks: readonly string[];
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Resolve write/recall banks for a session.
|
|
107
|
+
*
|
|
108
|
+
* Mnemopi has no tag-filtered recall, so `per-project-tagged` maps to a
|
|
109
|
+
* project-local write bank plus a shared recall-visible bank. The project
|
|
110
|
+
* bank is derived purely from {@link cwd} — see {@link projectBank} for the
|
|
111
|
+
* stability contract.
|
|
112
|
+
*/
|
|
113
|
+
export function computeMnemopiBankScope(
|
|
114
|
+
configured: string | undefined,
|
|
115
|
+
cwd: string,
|
|
116
|
+
scoping: MnemopiScoping,
|
|
117
|
+
): MnemopiBankScope {
|
|
100
118
|
const project = projectBank(configured, cwd);
|
|
101
119
|
const globalBank = sharedBank(configured);
|
|
102
120
|
switch (scoping) {
|
|
@@ -131,8 +149,17 @@ function sharedBank(configured: string | undefined): string {
|
|
|
131
149
|
return sanitizeBankName(configured) ?? DEFAULT_SHARED_BANK;
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Derive the per-project bank id from `cwd` alone.
|
|
154
|
+
*
|
|
155
|
+
* Earlier versions resolved the enclosing git root before hashing, which
|
|
156
|
+
* made the bank id unstable: removing or adding a `.git` anywhere above the
|
|
157
|
+
* cwd repointed the same conversation directory to a different bank and
|
|
158
|
+
* fragmented memories (#2412). The git lookup is gone here; the rescue path
|
|
159
|
+
* for already-fragmented installs lives in {@link extendRecallWithLegacyBanks}.
|
|
160
|
+
*/
|
|
134
161
|
function projectBank(configured: string | undefined, cwd: string): string {
|
|
135
|
-
const projectRoot =
|
|
162
|
+
const projectRoot = path.resolve(cwd || ".");
|
|
136
163
|
const project = projectBankSegment(projectRoot);
|
|
137
164
|
const base = sanitizeBankName(configured);
|
|
138
165
|
return limitBankName(base ? `${base}-${project}` : project);
|
|
@@ -140,7 +167,64 @@ function projectBank(configured: string | undefined, cwd: string): string {
|
|
|
140
167
|
|
|
141
168
|
function projectBankSegment(projectRoot: string): string {
|
|
142
169
|
const project = sanitizeBankName(path.basename(projectRoot)) ?? "default";
|
|
143
|
-
return limitBankName(`${project}-${Bun.hash(
|
|
170
|
+
return limitBankName(`${project}-${Bun.hash(projectRoot).toString(36)}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Discover sibling banks under `<dbDir>/banks/` whose `working_memory` rows
|
|
175
|
+
* already carry the active `cwd` in `metadata_json.$.cwd`, and add them to
|
|
176
|
+
* the recall set. This rescues memories stranded by a previous, less-stable
|
|
177
|
+
* bank derivation (#2412) without changing the write target — only recall is
|
|
178
|
+
* widened.
|
|
179
|
+
*
|
|
180
|
+
* Robust by design: a missing banks directory, unreadable bank dir, or
|
|
181
|
+
* corrupt SQLite file is silently skipped. Scanning is capped at
|
|
182
|
+
* {@link LEGACY_BANK_SCAN_LIMIT} to bound startup cost.
|
|
183
|
+
*/
|
|
184
|
+
export function extendRecallWithLegacyBanks(
|
|
185
|
+
resolved: readonly string[],
|
|
186
|
+
dbPath: string,
|
|
187
|
+
cwd: string,
|
|
188
|
+
): readonly string[] {
|
|
189
|
+
const banksDir = path.join(path.dirname(dbPath), "banks");
|
|
190
|
+
const cwdAbs = path.resolve(cwd || ".");
|
|
191
|
+
let entries: fs.Dirent[];
|
|
192
|
+
try {
|
|
193
|
+
entries = fs.readdirSync(banksDir, { withFileTypes: true });
|
|
194
|
+
} catch {
|
|
195
|
+
return resolved;
|
|
196
|
+
}
|
|
197
|
+
const have = new Set(resolved);
|
|
198
|
+
const extras: string[] = [];
|
|
199
|
+
let scanned = 0;
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
if (!entry.isDirectory() || have.has(entry.name)) continue;
|
|
202
|
+
if (scanned >= LEGACY_BANK_SCAN_LIMIT) break;
|
|
203
|
+
scanned++;
|
|
204
|
+
const candidate = path.join(banksDir, entry.name, "mnemopi.db");
|
|
205
|
+
if (bankHasCwd(candidate, cwdAbs)) extras.push(entry.name);
|
|
206
|
+
}
|
|
207
|
+
return extras.length === 0 ? resolved : [...resolved, ...extras];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function bankHasCwd(dbPath: string, cwd: string): boolean {
|
|
211
|
+
let db: Database | undefined;
|
|
212
|
+
try {
|
|
213
|
+
db = new Database(dbPath, { readonly: true });
|
|
214
|
+
const row = db
|
|
215
|
+
.query("SELECT 1 FROM working_memory WHERE json_extract(metadata_json, '$.cwd') = ? LIMIT 1")
|
|
216
|
+
.get(cwd);
|
|
217
|
+
return row !== null;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger.debug("Mnemopi: legacy bank probe failed", { dbPath, error: String(error) });
|
|
220
|
+
return false;
|
|
221
|
+
} finally {
|
|
222
|
+
try {
|
|
223
|
+
db?.close();
|
|
224
|
+
} catch {
|
|
225
|
+
// nothing to do — read-only handle.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
144
228
|
}
|
|
145
229
|
|
|
146
230
|
function sanitizeBankName(value: string | undefined): string | undefined {
|
|
@@ -31,14 +31,11 @@ import {
|
|
|
31
31
|
type ResumeSessionResponse,
|
|
32
32
|
type SessionConfigOption,
|
|
33
33
|
type SessionInfo,
|
|
34
|
-
type SessionModelState,
|
|
35
34
|
type SessionModeState,
|
|
36
35
|
type SessionNotification,
|
|
37
36
|
type SessionUpdate,
|
|
38
37
|
type SetSessionConfigOptionRequest,
|
|
39
38
|
type SetSessionConfigOptionResponse,
|
|
40
|
-
type SetSessionModelRequest,
|
|
41
|
-
type SetSessionModelResponse,
|
|
42
39
|
type SetSessionModeRequest,
|
|
43
40
|
type SetSessionModeResponse,
|
|
44
41
|
type Usage,
|
|
@@ -77,6 +74,7 @@ import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
|
|
|
77
74
|
import { normalizeLocalScheme } from "../../tools/path-utils";
|
|
78
75
|
import { runResolveInvocation } from "../../tools/resolve";
|
|
79
76
|
import { ToolError } from "../../tools/tool-errors";
|
|
77
|
+
import { getVisibleThinkingText } from "../../utils/thinking-display";
|
|
80
78
|
import { createAcpClientBridge } from "./acp-client-bridge";
|
|
81
79
|
import {
|
|
82
80
|
buildToolCallStartUpdate,
|
|
@@ -121,7 +119,6 @@ type PromptQueueState = {
|
|
|
121
119
|
type PromptLifecycleError = Error & { readonly code: "ACP_SESSION_CLOSED" };
|
|
122
120
|
|
|
123
121
|
type PromptTurnState = {
|
|
124
|
-
userMessageId: string;
|
|
125
122
|
cancelRequested: boolean;
|
|
126
123
|
settled: boolean;
|
|
127
124
|
/**
|
|
@@ -465,7 +462,6 @@ export class AcpAgent implements Agent {
|
|
|
465
462
|
const response: NewSessionResponse = {
|
|
466
463
|
sessionId: record.session.sessionId,
|
|
467
464
|
configOptions: this.#buildConfigOptions(record.session),
|
|
468
|
-
models: this.#buildModelState(record.session),
|
|
469
465
|
modes: this.#buildModeState(record.session),
|
|
470
466
|
};
|
|
471
467
|
this.#scheduleBootstrapUpdates(record.session.sessionId);
|
|
@@ -478,7 +474,6 @@ export class AcpAgent implements Agent {
|
|
|
478
474
|
await this.#replaySessionHistory(record);
|
|
479
475
|
const response: LoadSessionResponse = {
|
|
480
476
|
configOptions: this.#buildConfigOptions(record.session),
|
|
481
|
-
models: this.#buildModelState(record.session),
|
|
482
477
|
modes: this.#buildModeState(record.session),
|
|
483
478
|
};
|
|
484
479
|
this.#scheduleBootstrapUpdates(record.session.sessionId);
|
|
@@ -507,7 +502,6 @@ export class AcpAgent implements Agent {
|
|
|
507
502
|
const record = await this.#resumeManagedSession(params.sessionId, params.cwd, params.mcpServers ?? []);
|
|
508
503
|
const response: ResumeSessionResponse = {
|
|
509
504
|
configOptions: this.#buildConfigOptions(record.session),
|
|
510
|
-
models: this.#buildModelState(record.session),
|
|
511
505
|
modes: this.#buildModeState(record.session),
|
|
512
506
|
};
|
|
513
507
|
this.#scheduleBootstrapUpdates(record.session.sessionId);
|
|
@@ -520,7 +514,6 @@ export class AcpAgent implements Agent {
|
|
|
520
514
|
const response: ForkSessionResponse = {
|
|
521
515
|
sessionId: record.session.sessionId,
|
|
522
516
|
configOptions: this.#buildConfigOptions(record.session),
|
|
523
|
-
models: this.#buildModelState(record.session),
|
|
524
517
|
modes: this.#buildModeState(record.session),
|
|
525
518
|
};
|
|
526
519
|
this.#scheduleBootstrapUpdates(record.session.sessionId);
|
|
@@ -588,13 +581,6 @@ export class AcpAgent implements Agent {
|
|
|
588
581
|
return { configOptions: this.#buildConfigOptions(record.session) };
|
|
589
582
|
}
|
|
590
583
|
|
|
591
|
-
async unstable_setSessionModel(params: SetSessionModelRequest): Promise<SetSessionModelResponse> {
|
|
592
|
-
const record = this.#getSessionRecord(params.sessionId);
|
|
593
|
-
await this.#setModelById(record.session, params.modelId);
|
|
594
|
-
await this.#pushConfigOptionUpdate(record);
|
|
595
|
-
return {};
|
|
596
|
-
}
|
|
597
|
-
|
|
598
584
|
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
599
585
|
const record = this.#getSessionRecord(params.sessionId);
|
|
600
586
|
const activeTurn = record.promptTurn;
|
|
@@ -633,7 +619,6 @@ export class AcpAgent implements Agent {
|
|
|
633
619
|
const converted = this.#convertPromptBlocks(params.prompt);
|
|
634
620
|
const pendingPrompt = Promise.withResolvers<PromptResponse>();
|
|
635
621
|
record.promptTurn = {
|
|
636
|
-
userMessageId: params.messageId ?? crypto.randomUUID(),
|
|
637
622
|
cancelRequested: false,
|
|
638
623
|
settled: false,
|
|
639
624
|
cleanup: undefined,
|
|
@@ -766,7 +751,6 @@ export class AcpAgent implements Agent {
|
|
|
766
751
|
this.#cloneUsageStatistics(record.session.sessionManager.getUsageStatistics()),
|
|
767
752
|
record.session.sessionManager.getUsageStatistics(),
|
|
768
753
|
),
|
|
769
|
-
userMessageId: promptTurn?.userMessageId,
|
|
770
754
|
});
|
|
771
755
|
return;
|
|
772
756
|
}
|
|
@@ -844,7 +828,6 @@ export class AcpAgent implements Agent {
|
|
|
844
828
|
this.#finishPrompt(record, {
|
|
845
829
|
stopReason: "cancelled",
|
|
846
830
|
usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
|
|
847
|
-
userMessageId: promptTurn.userMessageId,
|
|
848
831
|
});
|
|
849
832
|
return cleanup;
|
|
850
833
|
}
|
|
@@ -1162,7 +1145,6 @@ export class AcpAgent implements Agent {
|
|
|
1162
1145
|
this.#finishPrompt(record, {
|
|
1163
1146
|
stopReason: this.#resolveStopReason(event, promptTurn.cancelRequested),
|
|
1164
1147
|
usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
|
|
1165
|
-
userMessageId: promptTurn.userMessageId,
|
|
1166
1148
|
});
|
|
1167
1149
|
}
|
|
1168
1150
|
}
|
|
@@ -1385,28 +1367,6 @@ export class AcpAgent implements Agent {
|
|
|
1385
1367
|
return configOptions;
|
|
1386
1368
|
}
|
|
1387
1369
|
|
|
1388
|
-
#buildModelState(session: AgentSession): SessionModelState | undefined {
|
|
1389
|
-
const models = session.getAvailableModels();
|
|
1390
|
-
if (models.length === 0) {
|
|
1391
|
-
return undefined;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
const availableModels = models.map(model => ({
|
|
1395
|
-
modelId: this.#toModelId(model),
|
|
1396
|
-
name: model.name,
|
|
1397
|
-
description: `${model.provider}/${model.id}`,
|
|
1398
|
-
}));
|
|
1399
|
-
const currentModelId = session.model ? this.#toModelId(session.model) : availableModels[0]?.modelId;
|
|
1400
|
-
if (!currentModelId) {
|
|
1401
|
-
return undefined;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
return {
|
|
1405
|
-
availableModels,
|
|
1406
|
-
currentModelId,
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
1370
|
#buildThinkingOptions(session: AgentSession): Array<{ value: string; name: string; description?: string }> {
|
|
1411
1371
|
return [
|
|
1412
1372
|
{ value: THINKING_OFF, name: "Off" },
|
|
@@ -1948,17 +1908,14 @@ export class AcpAgent implements Agent {
|
|
|
1948
1908
|
});
|
|
1949
1909
|
continue;
|
|
1950
1910
|
}
|
|
1951
|
-
if (
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
typeof item.thinking === "string" &&
|
|
1955
|
-
item.thinking.length > 0
|
|
1956
|
-
) {
|
|
1911
|
+
if (item.type === "thinking" && "thinking" in item && typeof item.thinking === "string") {
|
|
1912
|
+
const thinking = getVisibleThinkingText(item);
|
|
1913
|
+
if (thinking.length === 0) continue;
|
|
1957
1914
|
notifications.push({
|
|
1958
1915
|
sessionId,
|
|
1959
1916
|
update: {
|
|
1960
1917
|
sessionUpdate: "agent_thought_chunk",
|
|
1961
|
-
content: { type: "text", text:
|
|
1918
|
+
content: { type: "text", text: thinking },
|
|
1962
1919
|
messageId,
|
|
1963
1920
|
},
|
|
1964
1921
|
});
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
10
10
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
11
11
|
import type { TodoStatus } from "../../tools/todo";
|
|
12
|
+
import { hasVisibleThinking } from "../../utils/thinking-display";
|
|
12
13
|
|
|
13
14
|
interface MessageProgress {
|
|
14
15
|
textEmitted: boolean;
|
|
@@ -256,13 +257,16 @@ function mapAssistantMessageUpdate(
|
|
|
256
257
|
progress.textEmitted = true;
|
|
257
258
|
}
|
|
258
259
|
break;
|
|
259
|
-
case "thinking_delta":
|
|
260
|
+
case "thinking_delta": {
|
|
261
|
+
const block = event.assistantMessageEvent.partial?.content?.[event.assistantMessageEvent.contentIndex];
|
|
262
|
+
if (block?.type === "thinking" && !hasVisibleThinking(block)) return [];
|
|
260
263
|
sessionUpdate = "agent_thought_chunk";
|
|
261
264
|
text = event.assistantMessageEvent.delta;
|
|
262
265
|
if (text.length > 0 && progress) {
|
|
263
266
|
progress.thoughtEmitted = true;
|
|
264
267
|
}
|
|
265
268
|
break;
|
|
269
|
+
}
|
|
266
270
|
case "done":
|
|
267
271
|
if (progress?.textEmitted) {
|
|
268
272
|
return [];
|
|
@@ -39,6 +39,7 @@ import type { SessionMessageEntry } from "../../session/session-manager";
|
|
|
39
39
|
import { parseSessionEntries } from "../../session/session-manager";
|
|
40
40
|
import { createIrcMessageCard } from "../../tools/irc";
|
|
41
41
|
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
42
|
+
import { hasVisibleThinking } from "../../utils/thinking-display";
|
|
42
43
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
43
44
|
import { getEditorTheme, theme } from "../theme/theme";
|
|
44
45
|
import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
@@ -992,7 +993,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
992
993
|
const hasVisibleAssistantContent = message.content.some(
|
|
993
994
|
content =>
|
|
994
995
|
(content.type === "text" && content.text.trim().length > 0) ||
|
|
995
|
-
(content.type === "thinking" && content
|
|
996
|
+
(content.type === "thinking" && hasVisibleThinking(content)),
|
|
996
997
|
);
|
|
997
998
|
if (hasVisibleAssistantContent) {
|
|
998
999
|
// New visible turn content closes the current read run (mirrors rebuild).
|
|
@@ -6,6 +6,7 @@ import type { AssistantThinkingRenderer } from "../../extensibility/extensions/t
|
|
|
6
6
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
7
7
|
import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
|
|
8
8
|
import { getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
9
|
+
import { getVisibleThinkingText, hasVisibleThinking } from "../../utils/thinking-display";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Max lines of a turn-ending provider error rendered inline in the transcript.
|
|
@@ -245,7 +246,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
245
246
|
if (content.type === "text") {
|
|
246
247
|
parts.push(content.text.trim() ? "T1" : "T0");
|
|
247
248
|
} else if (content.type === "thinking") {
|
|
248
|
-
if (!content
|
|
249
|
+
if (!hasVisibleThinking(content)) parts.push("K0");
|
|
249
250
|
else if (this.hideThinkingBlock) parts.push("KH");
|
|
250
251
|
else parts.push("KV");
|
|
251
252
|
} else {
|
|
@@ -284,7 +285,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
284
285
|
for (const item of this.#fastPathItems) {
|
|
285
286
|
if (item.blockType === "thinking") {
|
|
286
287
|
const content = message.content[item.contentIndex];
|
|
287
|
-
if (content?.type === "thinking" && content
|
|
288
|
+
if (content?.type === "thinking" && getVisibleThinkingText(content) !== item.lastText) return false;
|
|
288
289
|
}
|
|
289
290
|
}
|
|
290
291
|
}
|
|
@@ -312,7 +313,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
312
313
|
if (item.blockType === "text" && content?.type === "text") {
|
|
313
314
|
newText = content.text.trim();
|
|
314
315
|
} else if (item.blockType === "thinking" && content?.type === "thinking") {
|
|
315
|
-
newText = content
|
|
316
|
+
newText = getVisibleThinkingText(content);
|
|
316
317
|
} else {
|
|
317
318
|
// Block at this index is gone or changed type (index shift) — fail closed.
|
|
318
319
|
this.#fastPathKey = undefined;
|
|
@@ -347,7 +348,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
347
348
|
const hasVisibleContent = message.content.some(
|
|
348
349
|
c =>
|
|
349
350
|
(c.type === "text" && c.text.trim()) ||
|
|
350
|
-
(!this.hideThinkingBlock && c.type === "thinking" && c
|
|
351
|
+
(!this.hideThinkingBlock && c.type === "thinking" && hasVisibleThinking(c)),
|
|
351
352
|
);
|
|
352
353
|
|
|
353
354
|
// Render content in order
|
|
@@ -362,7 +363,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
362
363
|
md.transientRenderCache = this.#lastUpdateTransient;
|
|
363
364
|
this.#contentContainer.addChild(md);
|
|
364
365
|
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
365
|
-
} else if (content.type === "thinking" && content
|
|
366
|
+
} else if (content.type === "thinking" && hasVisibleThinking(content)) {
|
|
367
|
+
const thinkingText = getVisibleThinkingText(content);
|
|
366
368
|
if (this.hideThinkingBlock) {
|
|
367
369
|
thinkingIndex += 1;
|
|
368
370
|
continue;
|
|
@@ -371,9 +373,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
371
373
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
372
374
|
const hasVisibleContentAfter = message.content
|
|
373
375
|
.slice(i + 1)
|
|
374
|
-
.some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c
|
|
376
|
+
.some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && hasVisibleThinking(c)));
|
|
375
377
|
|
|
376
|
-
const thinkingText = content.thinking.trim();
|
|
377
378
|
// Thinking traces in thinkingText color, italic
|
|
378
379
|
const md = new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
|
|
379
380
|
color: (text: string) => theme.fg("thinkingText", text),
|
|
@@ -16,6 +16,7 @@ export * from "./hook-message";
|
|
|
16
16
|
export * from "./hook-selector";
|
|
17
17
|
export * from "./keybinding-hints";
|
|
18
18
|
export * from "./login-dialog";
|
|
19
|
+
export * from "./logout-account-selector";
|
|
19
20
|
export * from "./model-selector";
|
|
20
21
|
export * from "./oauth-selector";
|
|
21
22
|
export * from "./queue-mode-selector";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Container, matchesKey, ScrollView, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { theme } from "../../modes/theme/theme";
|
|
3
|
+
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
4
|
+
import type { LogoutAccount } from "../../slash-commands/helpers/logout";
|
|
5
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
6
|
+
|
|
7
|
+
const LOGOUT_SELECTOR_MAX_VISIBLE = 10;
|
|
8
|
+
|
|
9
|
+
/** Account picker for `/logout` after the provider has been selected. */
|
|
10
|
+
export class LogoutAccountSelectorComponent extends Container {
|
|
11
|
+
#listContainer: Container;
|
|
12
|
+
#accounts: LogoutAccount[];
|
|
13
|
+
#selectedIndex = 0;
|
|
14
|
+
#statusMessage: string | undefined;
|
|
15
|
+
#onSelectCallback: (account: LogoutAccount) => void;
|
|
16
|
+
#onCancelCallback: () => void;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
providerName: string,
|
|
20
|
+
accounts: LogoutAccount[],
|
|
21
|
+
onSelect: (account: LogoutAccount) => void,
|
|
22
|
+
onCancel: () => void,
|
|
23
|
+
) {
|
|
24
|
+
super();
|
|
25
|
+
this.#accounts = accounts;
|
|
26
|
+
this.#onSelectCallback = onSelect;
|
|
27
|
+
this.#onCancelCallback = onCancel;
|
|
28
|
+
const activeIndex = accounts.findIndex(account => account.active);
|
|
29
|
+
this.#selectedIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
30
|
+
|
|
31
|
+
this.addChild(new DynamicBorder());
|
|
32
|
+
this.addChild(new Spacer(1));
|
|
33
|
+
this.addChild(new TruncatedText(theme.bold(`Select ${providerName} account to log out:`)));
|
|
34
|
+
this.addChild(new Spacer(1));
|
|
35
|
+
this.#listContainer = new Container();
|
|
36
|
+
this.addChild(this.#listContainer);
|
|
37
|
+
this.addChild(new Spacer(1));
|
|
38
|
+
this.addChild(new DynamicBorder());
|
|
39
|
+
this.#updateList();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#updateList(): void {
|
|
43
|
+
this.#listContainer.clear();
|
|
44
|
+
|
|
45
|
+
const total = this.#accounts.length;
|
|
46
|
+
const maxVisible = LOGOUT_SELECTOR_MAX_VISIBLE;
|
|
47
|
+
const startIndex =
|
|
48
|
+
total <= maxVisible
|
|
49
|
+
? 0
|
|
50
|
+
: Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
|
|
51
|
+
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
52
|
+
|
|
53
|
+
const rows: string[] = [];
|
|
54
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
55
|
+
const account = this.#accounts[i];
|
|
56
|
+
if (!account) continue;
|
|
57
|
+
const activeTag = account.active ? theme.fg("muted", " (active)") : "";
|
|
58
|
+
const detail = account.detail ? theme.fg("dim", ` ${account.detail}`) : "";
|
|
59
|
+
if (i === this.#selectedIndex) {
|
|
60
|
+
rows.push(`${theme.fg("accent", `${theme.nav.cursor} ${account.label}`)}${activeTag}${detail}`);
|
|
61
|
+
} else {
|
|
62
|
+
rows.push(` ${account.label}${activeTag}${detail}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (rows.length > 0) {
|
|
67
|
+
const sv = new ScrollView(rows, {
|
|
68
|
+
height: rows.length,
|
|
69
|
+
scrollbar: "auto",
|
|
70
|
+
totalRows: total,
|
|
71
|
+
theme: { track: text => theme.fg("muted", text), thumb: text => theme.fg("accent", text) },
|
|
72
|
+
});
|
|
73
|
+
sv.setScrollOffset(startIndex);
|
|
74
|
+
this.#listContainer.addChild(sv);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (total === 0) {
|
|
78
|
+
this.#listContainer.addChild(new TruncatedText(theme.fg("muted", " No stored accounts to log out"), 0, 0));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.#listContainer.addChild(
|
|
82
|
+
new TruncatedText(theme.fg("muted", " ↑/↓ select · ↵ log out account · Esc cancel"), 0, 0),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (this.#statusMessage) {
|
|
86
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
87
|
+
this.#listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.#statusMessage}`), 0, 0));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
handleInput(keyData: string): void {
|
|
92
|
+
if (matchesSelectCancel(keyData)) {
|
|
93
|
+
this.#onCancelCallback();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (matchesSelectUp(keyData)) {
|
|
98
|
+
if (this.#accounts.length > 0) {
|
|
99
|
+
this.#selectedIndex = this.#selectedIndex === 0 ? this.#accounts.length - 1 : this.#selectedIndex - 1;
|
|
100
|
+
}
|
|
101
|
+
this.#statusMessage = undefined;
|
|
102
|
+
this.#updateList();
|
|
103
|
+
} else if (matchesSelectDown(keyData)) {
|
|
104
|
+
if (this.#accounts.length > 0) {
|
|
105
|
+
this.#selectedIndex = this.#selectedIndex === this.#accounts.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
106
|
+
}
|
|
107
|
+
this.#statusMessage = undefined;
|
|
108
|
+
this.#updateList();
|
|
109
|
+
} else if (matchesKey(keyData, "pageUp")) {
|
|
110
|
+
if (this.#accounts.length > 0) {
|
|
111
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex - LOGOUT_SELECTOR_MAX_VISIBLE);
|
|
112
|
+
}
|
|
113
|
+
this.#statusMessage = undefined;
|
|
114
|
+
this.#updateList();
|
|
115
|
+
} else if (matchesKey(keyData, "pageDown")) {
|
|
116
|
+
if (this.#accounts.length > 0) {
|
|
117
|
+
this.#selectedIndex = Math.min(
|
|
118
|
+
this.#accounts.length - 1,
|
|
119
|
+
this.#selectedIndex + LOGOUT_SELECTOR_MAX_VISIBLE,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
this.#statusMessage = undefined;
|
|
123
|
+
this.#updateList();
|
|
124
|
+
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
125
|
+
const account = this.#accounts[this.#selectedIndex];
|
|
126
|
+
if (!account) return;
|
|
127
|
+
this.#onSelectCallback(account);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|