my-pi-agent 0.1.0

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 (141) hide show
  1. package/README.md +318 -0
  2. package/package.json +45 -0
  3. package/pyproject.toml +50 -0
  4. package/src/my_agent_core/__init__.py +123 -0
  5. package/src/my_agent_core/agent.py +441 -0
  6. package/src/my_agent_core/background.py +121 -0
  7. package/src/my_agent_core/context.py +505 -0
  8. package/src/my_agent_core/events.py +153 -0
  9. package/src/my_agent_core/extensions/__init__.py +9 -0
  10. package/src/my_agent_core/extensions/core.py +197 -0
  11. package/src/my_agent_core/hooks.py +130 -0
  12. package/src/my_agent_core/loop.py +709 -0
  13. package/src/my_agent_core/main.py +134 -0
  14. package/src/my_agent_core/memory.py +241 -0
  15. package/src/my_agent_core/message_queue.py +110 -0
  16. package/src/my_agent_core/plugins.py +212 -0
  17. package/src/my_agent_core/registry.py +186 -0
  18. package/src/my_agent_core/session/__init__.py +79 -0
  19. package/src/my_agent_core/session/entries.py +197 -0
  20. package/src/my_agent_core/session/jsonl.py +60 -0
  21. package/src/my_agent_core/session/memory.py +137 -0
  22. package/src/my_agent_core/session/session.py +400 -0
  23. package/src/my_agent_core/session/storage.py +245 -0
  24. package/src/my_agent_core/session/store.py +131 -0
  25. package/src/my_agent_core/session/tree.py +86 -0
  26. package/src/my_agent_core/skills.py +149 -0
  27. package/src/my_agent_core/subagent_tasks.py +170 -0
  28. package/src/my_agent_core/subagents.py +148 -0
  29. package/src/my_agent_core/task_store.py +248 -0
  30. package/src/my_agent_core/tool_history.py +189 -0
  31. package/src/my_agent_core/tools/__init__.py +5 -0
  32. package/src/my_agent_core/tools/builtin/__init__.py +5 -0
  33. package/src/my_agent_core/tools/builtin/task.py +30 -0
  34. package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
  35. package/src/my_agent_core/tools/core.py +239 -0
  36. package/src/my_agent_llm/__init__.py +45 -0
  37. package/src/my_agent_llm/auth/__init__.py +46 -0
  38. package/src/my_agent_llm/auth/antigravity.py +209 -0
  39. package/src/my_agent_llm/auth/manager.py +259 -0
  40. package/src/my_agent_llm/auth/quota.py +56 -0
  41. package/src/my_agent_llm/auth/schema.py +94 -0
  42. package/src/my_agent_llm/client.py +116 -0
  43. package/src/my_agent_llm/config.py +17 -0
  44. package/src/my_agent_llm/events.py +84 -0
  45. package/src/my_agent_llm/models.py +195 -0
  46. package/src/my_agent_llm/providers/__init__.py +4 -0
  47. package/src/my_agent_llm/providers/_base.py +94 -0
  48. package/src/my_agent_llm/providers/anthropic.py +298 -0
  49. package/src/my_agent_llm/providers/antigravity.py +480 -0
  50. package/src/my_agent_llm/providers/deepseek.py +196 -0
  51. package/src/my_agent_llm/providers/openai.py +364 -0
  52. package/src/my_agent_llm/providers/registry.py +16 -0
  53. package/src/my_agent_llm/stream.py +218 -0
  54. package/src/my_coding_agent/__init__.py +66 -0
  55. package/src/my_coding_agent/agent.py +208 -0
  56. package/src/my_coding_agent/cli.py +78 -0
  57. package/src/my_coding_agent/file_reference.py +80 -0
  58. package/src/my_coding_agent/macro.py +408 -0
  59. package/src/my_coding_agent/mcp.py +243 -0
  60. package/src/my_coding_agent/mutation_queue.py +37 -0
  61. package/src/my_coding_agent/paths.py +119 -0
  62. package/src/my_coding_agent/permissions.py +84 -0
  63. package/src/my_coding_agent/prompt.py +54 -0
  64. package/src/my_coding_agent/rpc_server.py +2817 -0
  65. package/src/my_coding_agent/settings.py +126 -0
  66. package/src/my_coding_agent/tools/__init__.py +55 -0
  67. package/src/my_coding_agent/tools/base.py +58 -0
  68. package/src/my_coding_agent/tools/bash.py +206 -0
  69. package/src/my_coding_agent/tools/edit.py +226 -0
  70. package/src/my_coding_agent/tools/find.py +118 -0
  71. package/src/my_coding_agent/tools/grep.py +177 -0
  72. package/src/my_coding_agent/tools/ls.py +112 -0
  73. package/src/my_coding_agent/tools/read.py +113 -0
  74. package/src/my_coding_agent/tools/write.py +72 -0
  75. package/tui/README.md +27 -0
  76. package/tui/bin/my-agent.js +98 -0
  77. package/tui/dist/app.d.ts +41 -0
  78. package/tui/dist/app.js +110 -0
  79. package/tui/dist/bridge/event-translator.d.ts +92 -0
  80. package/tui/dist/bridge/event-translator.js +216 -0
  81. package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
  82. package/tui/dist/bridge/kernel-bridge.js +132 -0
  83. package/tui/dist/client.d.ts +63 -0
  84. package/tui/dist/client.js +239 -0
  85. package/tui/dist/components/assistant-message.d.ts +19 -0
  86. package/tui/dist/components/assistant-message.js +90 -0
  87. package/tui/dist/components/compaction-summary-message.d.ts +19 -0
  88. package/tui/dist/components/compaction-summary-message.js +46 -0
  89. package/tui/dist/components/custom-editor.d.ts +18 -0
  90. package/tui/dist/components/custom-editor.js +56 -0
  91. package/tui/dist/components/dynamic-border.d.ts +9 -0
  92. package/tui/dist/components/dynamic-border.js +14 -0
  93. package/tui/dist/components/footer.d.ts +39 -0
  94. package/tui/dist/components/footer.js +199 -0
  95. package/tui/dist/components/header.d.ts +4 -0
  96. package/tui/dist/components/header.js +21 -0
  97. package/tui/dist/components/keys.d.ts +5 -0
  98. package/tui/dist/components/keys.js +12 -0
  99. package/tui/dist/components/login-selector.d.ts +26 -0
  100. package/tui/dist/components/login-selector.js +181 -0
  101. package/tui/dist/components/logout-selector.d.ts +19 -0
  102. package/tui/dist/components/logout-selector.js +88 -0
  103. package/tui/dist/components/model-selector.d.ts +40 -0
  104. package/tui/dist/components/model-selector.js +268 -0
  105. package/tui/dist/components/session-selector.d.ts +54 -0
  106. package/tui/dist/components/session-selector.js +393 -0
  107. package/tui/dist/components/settings-selector.d.ts +24 -0
  108. package/tui/dist/components/settings-selector.js +146 -0
  109. package/tui/dist/components/status-indicator.d.ts +25 -0
  110. package/tui/dist/components/status-indicator.js +60 -0
  111. package/tui/dist/components/theme-selector.d.ts +14 -0
  112. package/tui/dist/components/theme-selector.js +77 -0
  113. package/tui/dist/components/thinking-selector.d.ts +21 -0
  114. package/tui/dist/components/thinking-selector.js +128 -0
  115. package/tui/dist/components/tool-execution.d.ts +31 -0
  116. package/tui/dist/components/tool-execution.js +206 -0
  117. package/tui/dist/components/tree-selector.d.ts +40 -0
  118. package/tui/dist/components/tree-selector.js +173 -0
  119. package/tui/dist/components/user-message-selector.d.ts +21 -0
  120. package/tui/dist/components/user-message-selector.js +103 -0
  121. package/tui/dist/components/user-message.d.ts +5 -0
  122. package/tui/dist/components/user-message.js +15 -0
  123. package/tui/dist/index.d.ts +11 -0
  124. package/tui/dist/index.js +11 -0
  125. package/tui/dist/interactive/chat-viewport.d.ts +19 -0
  126. package/tui/dist/interactive/chat-viewport.js +41 -0
  127. package/tui/dist/interactive/components.d.ts +1 -0
  128. package/tui/dist/interactive/components.js +1 -0
  129. package/tui/dist/interactive/interactive-mode.d.ts +89 -0
  130. package/tui/dist/interactive/interactive-mode.js +1625 -0
  131. package/tui/dist/interactive/theme.d.ts +1 -0
  132. package/tui/dist/interactive/theme.js +1 -0
  133. package/tui/dist/interactive/tui-renderer.d.ts +8 -0
  134. package/tui/dist/interactive/tui-renderer.js +10 -0
  135. package/tui/dist/protocol.d.ts +78 -0
  136. package/tui/dist/protocol.js +1 -0
  137. package/tui/dist/theme/dark.json +54 -0
  138. package/tui/dist/theme/light.json +71 -0
  139. package/tui/dist/theme/theme.d.ts +20 -0
  140. package/tui/dist/theme/theme.js +86 -0
  141. package/tui/package.json +25 -0
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 宽度自适应横向边界线组件 (100% 对标 Pi DynamicBorder)
3
+ */
4
+ export declare class DynamicBorder {
5
+ color: (str: string) => string;
6
+ constructor(color?: (str: string) => string);
7
+ invalidate(): void;
8
+ render(width: number): string[];
9
+ }
@@ -0,0 +1,14 @@
1
+ import { theme } from "../theme/theme.js";
2
+ /**
3
+ * 宽度自适应横向边界线组件 (100% 对标 Pi DynamicBorder)
4
+ */
5
+ export class DynamicBorder {
6
+ color;
7
+ constructor(color = (str) => theme.fg("borderMuted", str)) {
8
+ this.color = color;
9
+ }
10
+ invalidate() { }
11
+ render(width) {
12
+ return [this.color("─".repeat(Math.max(1, width)))];
13
+ }
14
+ }
@@ -0,0 +1,39 @@
1
+ import { Container } from "@earendil-works/pi-tui";
2
+ export interface FooterData {
3
+ workspace: string;
4
+ gitBranch?: string;
5
+ sessionName?: string;
6
+ providerName?: string;
7
+ modelName?: string;
8
+ thinkingLevel?: string;
9
+ inputTokens?: number;
10
+ outputTokens?: number;
11
+ cacheReadTokens?: number;
12
+ cacheWriteTokens?: number;
13
+ cacheHitRate?: number;
14
+ totalTokens?: number;
15
+ tokensUsed?: number;
16
+ contextTokens?: number;
17
+ contextWindow?: number;
18
+ autoCompactEnabled?: boolean;
19
+ costUsd?: number;
20
+ elapsedSeconds?: number;
21
+ isBusy?: boolean;
22
+ }
23
+ export declare function formatTokens(count: number): string;
24
+ export declare class FooterComponent extends Container {
25
+ private readonly onRequestRender?;
26
+ private data;
27
+ private spinnerFrame;
28
+ private busyInterval;
29
+ constructor(initialData?: Partial<FooterData>, onRequestRender?: (() => void) | undefined);
30
+ update(partial: Partial<FooterData>): void;
31
+ private startAnimation;
32
+ private stopAnimation;
33
+ dispose(): void;
34
+ getContextWindow(): number;
35
+ getSessionName(): string | undefined;
36
+ render(width: number): string[];
37
+ private formatCwd;
38
+ formatTokens(count: number): string;
39
+ }
@@ -0,0 +1,199 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import { Container, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
4
+ import { theme } from "../theme/theme.js";
5
+ import { SPINNER_FRAMES } from "./tool-execution.js";
6
+ export function formatTokens(count) {
7
+ if (count < 1000)
8
+ return count.toString();
9
+ if (count < 10000)
10
+ return `${(count / 1000).toFixed(1)}k`;
11
+ if (count < 1000000)
12
+ return `${Math.round(count / 1000)}k`;
13
+ if (count < 10000000)
14
+ return `${(count / 1000000).toFixed(1)}M`;
15
+ return `${Math.round(count / 1000000)}M`;
16
+ }
17
+ export class FooterComponent extends Container {
18
+ onRequestRender;
19
+ data;
20
+ spinnerFrame = 0;
21
+ busyInterval = null;
22
+ constructor(initialData, onRequestRender) {
23
+ super();
24
+ this.onRequestRender = onRequestRender;
25
+ this.data = {
26
+ workspace: process.cwd(),
27
+ modelName: "default",
28
+ thinkingLevel: "off",
29
+ inputTokens: 0,
30
+ outputTokens: 0,
31
+ totalTokens: 0,
32
+ contextWindow: 128000,
33
+ costUsd: 0,
34
+ ...initialData,
35
+ };
36
+ if (this.data.isBusy) {
37
+ this.startAnimation();
38
+ }
39
+ }
40
+ update(partial) {
41
+ const wasBusy = Boolean(this.data.isBusy);
42
+ this.data = { ...this.data, ...partial };
43
+ const nowBusy = Boolean(this.data.isBusy);
44
+ if (nowBusy && !wasBusy) {
45
+ this.startAnimation();
46
+ }
47
+ else if (!nowBusy && wasBusy) {
48
+ this.stopAnimation();
49
+ }
50
+ }
51
+ startAnimation() {
52
+ if (this.busyInterval)
53
+ return;
54
+ this.busyInterval = setInterval(() => {
55
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
56
+ if (this.onRequestRender) {
57
+ this.onRequestRender();
58
+ }
59
+ }, 80);
60
+ if (typeof this.busyInterval?.unref === "function") {
61
+ this.busyInterval.unref();
62
+ }
63
+ }
64
+ stopAnimation() {
65
+ if (this.busyInterval) {
66
+ clearInterval(this.busyInterval);
67
+ this.busyInterval = null;
68
+ }
69
+ }
70
+ dispose() {
71
+ this.stopAnimation();
72
+ }
73
+ getContextWindow() {
74
+ return this.data.contextWindow || 128000;
75
+ }
76
+ getSessionName() {
77
+ return this.data.sessionName;
78
+ }
79
+ render(width) {
80
+ // 1. 第一行:工作区路径 (~ 折叠) + 分支 + 会话名
81
+ let pwd = this.formatCwd(this.data.workspace);
82
+ if (this.data.gitBranch) {
83
+ pwd += ` (${this.data.gitBranch})`;
84
+ }
85
+ if (this.data.sessionName) {
86
+ pwd += ` • ${this.data.sessionName}`;
87
+ }
88
+ const line1 = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
89
+ // 2. 第二行左侧:Token 指标与实时状态 (完全对齐 Pi 原厂格式)
90
+ const statsParts = [];
91
+ if (this.data.inputTokens && this.data.inputTokens > 0) {
92
+ statsParts.push(`↑${this.formatTokens(this.data.inputTokens)}`);
93
+ }
94
+ if (this.data.outputTokens && this.data.outputTokens > 0) {
95
+ statsParts.push(`↓${this.formatTokens(this.data.outputTokens)}`);
96
+ }
97
+ if (this.data.cacheReadTokens && this.data.cacheReadTokens > 0) {
98
+ statsParts.push(`R${this.formatTokens(this.data.cacheReadTokens)}`);
99
+ }
100
+ if (this.data.cacheWriteTokens && this.data.cacheWriteTokens > 0) {
101
+ statsParts.push(`W${this.formatTokens(this.data.cacheWriteTokens)}`);
102
+ }
103
+ let chRate = this.data.cacheHitRate;
104
+ if (chRate === undefined &&
105
+ ((this.data.cacheReadTokens && this.data.cacheReadTokens > 0) ||
106
+ (this.data.cacheWriteTokens && this.data.cacheWriteTokens > 0))) {
107
+ const promptSum = (this.data.inputTokens || 0) +
108
+ (this.data.cacheReadTokens || 0) +
109
+ (this.data.cacheWriteTokens || 0);
110
+ if (promptSum > 0) {
111
+ chRate = ((this.data.cacheReadTokens || 0) / promptSum) * 100;
112
+ }
113
+ }
114
+ if (chRate !== undefined &&
115
+ ((this.data.cacheReadTokens && this.data.cacheReadTokens > 0) ||
116
+ (this.data.cacheWriteTokens && this.data.cacheWriteTokens > 0))) {
117
+ statsParts.push(`CH${chRate.toFixed(1)}%`);
118
+ }
119
+ if (this.data.tokensUsed &&
120
+ this.data.tokensUsed > 0 &&
121
+ !this.data.inputTokens &&
122
+ !this.data.outputTokens) {
123
+ statsParts.push(this.formatTokens(this.data.tokensUsed));
124
+ }
125
+ if (this.data.costUsd && this.data.costUsd > 0) {
126
+ statsParts.push(`$${this.data.costUsd.toFixed(3)}`);
127
+ }
128
+ const contextWin = this.data.contextWindow || 128000;
129
+ const autoIndicator = this.data.autoCompactEnabled === false ? "" : " (auto)";
130
+ const contextTok = this.data.contextTokens ?? this.data.totalTokens ?? 0;
131
+ const percent = contextWin > 0 ? (contextTok / contextWin) * 100 : 0;
132
+ const percentStr = `${percent.toFixed(1)}%/${this.formatTokens(contextWin)}${autoIndicator}`;
133
+ const contextColor = percent > 90 ? "error" : percent > 70 ? "warning" : "dim";
134
+ statsParts.push(theme.fg(contextColor, percentStr));
135
+ if (this.data.elapsedSeconds && this.data.elapsedSeconds > 0) {
136
+ statsParts.push(theme.fg("dim", `${this.data.elapsedSeconds.toFixed(1)}s`));
137
+ }
138
+ if (this.data.isBusy) {
139
+ const char = SPINNER_FRAMES[this.spinnerFrame] || "⠋";
140
+ statsParts.push(theme.fg("warning", char));
141
+ }
142
+ const statsLeft = statsParts.join(" ");
143
+ // 3. 第二行右侧:模型与思考深度 (provider) model • thinking
144
+ const prov = this.data.providerName ? `(${this.data.providerName}) ` : "";
145
+ const model = this.data.modelName || "default";
146
+ const thinking = this.data.thinkingLevel && this.data.thinkingLevel !== "off"
147
+ ? ` • ${this.data.thinkingLevel}`
148
+ : "";
149
+ const rightSide = `${prov}${model}${thinking}`;
150
+ // 4. 动态计算填充间距并右对齐,应用 ANSI 独立染色保护
151
+ let statsLeftFormatted = statsLeft;
152
+ let statsLeftWidth = visibleWidth(statsLeftFormatted);
153
+ if (statsLeftWidth > width) {
154
+ statsLeftFormatted = truncateToWidth(statsLeftFormatted, width, "...");
155
+ statsLeftWidth = visibleWidth(statsLeftFormatted);
156
+ }
157
+ const minPadding = 2;
158
+ const rightWidth = visibleWidth(rightSide);
159
+ let line2;
160
+ if (statsLeftWidth + minPadding + rightWidth <= width) {
161
+ const padLen = width - statsLeftWidth - rightWidth;
162
+ const remainder = " ".repeat(padLen) + rightSide;
163
+ line2 = theme.fg("dim", statsLeftFormatted) + theme.fg("dim", remainder);
164
+ }
165
+ else {
166
+ const availableForRight = width - statsLeftWidth - minPadding;
167
+ if (availableForRight > 0) {
168
+ const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
169
+ const padLen = Math.max(0, width - statsLeftWidth - visibleWidth(truncatedRight));
170
+ const remainder = " ".repeat(padLen) + truncatedRight;
171
+ line2 =
172
+ theme.fg("dim", statsLeftFormatted) + theme.fg("dim", remainder);
173
+ }
174
+ else {
175
+ line2 = truncateToWidth(theme.fg("dim", statsLeftFormatted), width, "...");
176
+ }
177
+ }
178
+ return [line1, line2];
179
+ }
180
+ formatCwd(dir) {
181
+ if (!dir)
182
+ return "";
183
+ const home = os.homedir();
184
+ const resolved = path.resolve(dir);
185
+ const isWindows = process.platform === "win32";
186
+ const normResolved = isWindows ? resolved.toLowerCase() : resolved;
187
+ const normHome = isWindows ? home.toLowerCase() : home;
188
+ if (normResolved === normHome)
189
+ return "~";
190
+ if (normResolved.startsWith(normHome + path.sep) ||
191
+ normResolved.startsWith(normHome + "/")) {
192
+ return "~" + resolved.slice(home.length).replace(/\\/g, "/");
193
+ }
194
+ return resolved.replace(/\\/g, "/");
195
+ }
196
+ formatTokens(count) {
197
+ return formatTokens(count);
198
+ }
199
+ }
@@ -0,0 +1,4 @@
1
+ import { Container } from "@earendil-works/pi-tui";
2
+ export declare class HeaderComponent extends Container {
3
+ constructor(version?: string);
4
+ }
@@ -0,0 +1,21 @@
1
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
2
+ import { theme } from "../theme/theme.js";
3
+ export class HeaderComponent extends Container {
4
+ constructor(version = "0.1.0") {
5
+ super();
6
+ this.addChild(new Spacer(1));
7
+ const logo = theme.bold(theme.fg("accent", "my-pi-agent")) +
8
+ theme.fg("dim", ` v${version}`);
9
+ const hints = [
10
+ theme.fg("dim", "Esc") + theme.fg("muted", " interrupt"),
11
+ theme.fg("dim", "Ctrl+C") + theme.fg("muted", " clear/exit"),
12
+ theme.fg("dim", "/") + theme.fg("muted", " commands"),
13
+ theme.fg("dim", "!") + theme.fg("muted", " bash"),
14
+ theme.fg("dim", "Ctrl+O") + theme.fg("muted", " expand"),
15
+ ].join(theme.fg("muted", " · "));
16
+ const onboarding = theme.fg("dim", "欢迎使用 my-pi-agent!输入需求或按 / 开启命令菜单。");
17
+ const content = `${logo}\n${hints}\n\n${onboarding}`;
18
+ this.addChild(new Text(content, 1, 0));
19
+ this.addChild(new Spacer(1));
20
+ }
21
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 统一回车判断函数,兼容跨平台回车按键:
3
+ * 包括 VT100/ANSI 单字符 \r, \n,以及 Windows ConPTY / SSH 的 \r\n,和 pi-tui 的 return/enter
4
+ */
5
+ export declare function isEnterKey(data: string): boolean;
@@ -0,0 +1,12 @@
1
+ import { matchesKey } from "@earendil-works/pi-tui";
2
+ /**
3
+ * 统一回车判断函数,兼容跨平台回车按键:
4
+ * 包括 VT100/ANSI 单字符 \r, \n,以及 Windows ConPTY / SSH 的 \r\n,和 pi-tui 的 return/enter
5
+ */
6
+ export function isEnterKey(data) {
7
+ return (matchesKey(data, "return") ||
8
+ matchesKey(data, "enter") ||
9
+ data === "\r" ||
10
+ data === "\n" ||
11
+ data === "\r\n");
12
+ }
@@ -0,0 +1,26 @@
1
+ import { Container, Input, SelectList } from "@earendil-works/pi-tui";
2
+ export interface ProviderOption {
3
+ id: string;
4
+ label: string;
5
+ description: string;
6
+ }
7
+ export declare const SUPPORTED_LOGIN_PROVIDERS: ProviderOption[];
8
+ export declare class LoginSelectorComponent extends Container {
9
+ readonly onSubmit: (provider: string, key: string) => void;
10
+ readonly onCancel: () => void;
11
+ readonly providers: ProviderOption[];
12
+ searchInput: Input;
13
+ keyInput: Input;
14
+ selectList: SelectList;
15
+ private phase;
16
+ private selectedProvider;
17
+ private allItems;
18
+ private _focused;
19
+ get focused(): boolean;
20
+ set focused(value: boolean);
21
+ constructor(onSubmit: (provider: string, key: string) => void, onCancel: () => void, providers?: ProviderOption[]);
22
+ private buildSelectList;
23
+ private applyFilter;
24
+ private rebuildUI;
25
+ handleInput(data: string): void;
26
+ }
@@ -0,0 +1,181 @@
1
+ import { Container, fuzzyFilter, Input, matchesKey, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
2
+ import { theme } from "../theme/theme.js";
3
+ import { DynamicBorder } from "./dynamic-border.js";
4
+ import { isEnterKey } from "./keys.js";
5
+ export const SUPPORTED_LOGIN_PROVIDERS = [
6
+ {
7
+ id: "deepseek",
8
+ label: "DeepSeek",
9
+ description: "API Key (deepseek-chat, deepseek-reasoner)",
10
+ },
11
+ {
12
+ id: "openai",
13
+ label: "OpenAI",
14
+ description: "API Key (gpt-4o, gpt-4o-mini, o1, o3)",
15
+ },
16
+ {
17
+ id: "anthropic",
18
+ label: "Anthropic",
19
+ description: "API Key (claude-3-5-sonnet, claude-3-5-haiku)",
20
+ },
21
+ {
22
+ id: "antigravity",
23
+ label: "Antigravity",
24
+ description: "读取 auth.json 认证凭据 (Google Cloud Code Assist)",
25
+ },
26
+ ];
27
+ export class LoginSelectorComponent extends Container {
28
+ onSubmit;
29
+ onCancel;
30
+ providers;
31
+ searchInput;
32
+ keyInput;
33
+ selectList;
34
+ phase = "select_provider";
35
+ selectedProvider = SUPPORTED_LOGIN_PROVIDERS[0];
36
+ allItems;
37
+ _focused = false;
38
+ get focused() {
39
+ return this._focused;
40
+ }
41
+ set focused(value) {
42
+ this._focused = value;
43
+ if (this.phase === "select_provider") {
44
+ this.searchInput.focused = value;
45
+ }
46
+ else {
47
+ this.keyInput.focused = value;
48
+ }
49
+ }
50
+ constructor(onSubmit, onCancel, providers = SUPPORTED_LOGIN_PROVIDERS) {
51
+ super();
52
+ this.onSubmit = onSubmit;
53
+ this.onCancel = onCancel;
54
+ this.providers = providers;
55
+ this.allItems = providers.map((p) => ({
56
+ value: p.id,
57
+ label: p.label,
58
+ description: p.description,
59
+ }));
60
+ this.searchInput = new Input();
61
+ this.keyInput = new Input();
62
+ this.selectList = this.buildSelectList(this.allItems);
63
+ this.rebuildUI();
64
+ }
65
+ buildSelectList(items) {
66
+ const listTheme = {
67
+ selectedPrefix: (s) => theme.fg("accent", s),
68
+ selectedText: (s) => theme.bold(theme.fg("accent", s)),
69
+ description: (s) => theme.fg("muted", s),
70
+ scrollInfo: (s) => theme.dim(s),
71
+ noMatch: (_s) => theme.dim("无匹配 Provider"),
72
+ };
73
+ const list = new SelectList(items, Math.max(1, items.length), listTheme, {
74
+ minPrimaryColumnWidth: 14,
75
+ maxPrimaryColumnWidth: 28,
76
+ });
77
+ list.onSelect = (item) => {
78
+ const found = this.providers.find((p) => p.id === item.value);
79
+ if (found) {
80
+ this.selectedProvider = found;
81
+ this.phase = "enter_key";
82
+ this.rebuildUI();
83
+ }
84
+ };
85
+ list.onCancel = () => this.onCancel();
86
+ return list;
87
+ }
88
+ applyFilter(query) {
89
+ const filtered = query
90
+ ? fuzzyFilter(this.allItems, query, (item) => `${item.value} ${item.label} ${item.description ?? ""}`)
91
+ : this.allItems;
92
+ this.selectList = this.buildSelectList(filtered);
93
+ this.rebuildUI();
94
+ }
95
+ rebuildUI() {
96
+ this.clear();
97
+ this.addChild(new DynamicBorder());
98
+ this.addChild(new Spacer(1));
99
+ if (this.phase === "select_provider") {
100
+ this.searchInput.focused = this._focused;
101
+ this.keyInput.focused = false;
102
+ this.addChild(new Text(theme.bold("Login / Bind Provider Credentials"), 0, 0));
103
+ this.addChild(new Spacer(1));
104
+ this.addChild(new Text(theme.fg("muted", "Credentials will be securely saved to ~/.my-pi-agent/auth.json"), 0, 0));
105
+ this.addChild(new Spacer(1));
106
+ this.addChild(this.searchInput);
107
+ this.addChild(new Spacer(1));
108
+ this.addChild(this.selectList);
109
+ this.addChild(new Spacer(1));
110
+ this.addChild(new Text(theme.fg("dim", " Enter to select · Escape to cancel"), 0, 0));
111
+ }
112
+ else {
113
+ this.keyInput.focused = this._focused;
114
+ this.searchInput.focused = false;
115
+ if (this.selectedProvider.id === "antigravity") {
116
+ this.addChild(new Text(theme.bold("Antigravity 认证凭据配置 (auth.json)"), 0, 0));
117
+ this.addChild(new Spacer(1));
118
+ this.addChild(new Text(theme.fg("muted", "Antigravity 依赖 Google OAuth 凭据,系统将自动扫描并读取下列路径:"), 0, 0));
119
+ this.addChild(new Text(theme.fg("accent", " 1. ~/.my-pi-agent/auth.json (推荐:当前 Agent 专属凭据路径)\n 2. ~/.pi/agent/auth.json (Pi 官方扩展认证凭据路径)"), 0, 0));
120
+ this.addChild(new Spacer(1));
121
+ this.addChild(new Text(theme.fg("dim", "• 若上述路径已放置包含 antigravity 字段的 auth.json,直接按 Enter 即可自动读取绑定。\n• 若需手动输入,请在下方粘贴 Access Token,或按 Escape 返回:"), 0, 0));
122
+ this.addChild(new Spacer(1));
123
+ this.addChild(this.keyInput);
124
+ this.addChild(new Spacer(1));
125
+ this.addChild(new Text(theme.fg("dim", " Enter 直接读取 auth.json / 提交 Token · Escape 返回"), 0, 0));
126
+ }
127
+ else {
128
+ this.addChild(new Text(theme.bold(`Enter API Key for ${this.selectedProvider.label}`), 0, 0));
129
+ this.addChild(new Spacer(1));
130
+ this.addChild(new Text(theme.fg("muted", `Paste your key below and press Enter (saved to ~/.my-pi-agent/auth.json):`), 0, 0));
131
+ this.addChild(new Spacer(1));
132
+ this.addChild(this.keyInput);
133
+ this.addChild(new Spacer(1));
134
+ this.addChild(new Text(theme.fg("dim", " Enter to confirm & save · Escape to back"), 0, 0));
135
+ }
136
+ }
137
+ this.addChild(new DynamicBorder());
138
+ }
139
+ handleInput(data) {
140
+ if (matchesKey(data, "ctrl+c")) {
141
+ this.onCancel();
142
+ return;
143
+ }
144
+ if (this.phase === "select_provider") {
145
+ if (matchesKey(data, "up") ||
146
+ matchesKey(data, "down") ||
147
+ isEnterKey(data) ||
148
+ matchesKey(data, "escape")) {
149
+ if (isEnterKey(data)) {
150
+ this.selectList.handleInput("\r");
151
+ }
152
+ else {
153
+ this.selectList.handleInput(data);
154
+ }
155
+ return;
156
+ }
157
+ this.searchInput.handleInput(data);
158
+ this.applyFilter(this.searchInput.getValue());
159
+ }
160
+ else {
161
+ if (matchesKey(data, "escape")) {
162
+ this.phase = "select_provider";
163
+ this.rebuildUI();
164
+ return;
165
+ }
166
+ if (isEnterKey(data)) {
167
+ const key = this.keyInput.getValue().trim();
168
+ if (this.selectedProvider.id === "antigravity") {
169
+ // Antigravity 支持直接按 Enter 自动读取 auth.json,或提交手动粘贴的 token
170
+ this.onSubmit("antigravity", key);
171
+ return;
172
+ }
173
+ if (key) {
174
+ this.onSubmit(this.selectedProvider.id, key);
175
+ }
176
+ return;
177
+ }
178
+ this.keyInput.handleInput(data);
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,19 @@
1
+ import { Container } from "@earendil-works/pi-tui";
2
+ export interface LogoutProviderItem {
3
+ id: string;
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ export declare class LogoutSelectorComponent extends Container {
8
+ readonly providers: LogoutProviderItem[];
9
+ readonly onSelect: (providerId: string) => void;
10
+ readonly onCancel: () => void;
11
+ private listContainer;
12
+ private selectedIndex;
13
+ private _focused;
14
+ get focused(): boolean;
15
+ set focused(value: boolean);
16
+ constructor(providers: LogoutProviderItem[], onSelect: (providerId: string) => void, onCancel: () => void);
17
+ updateList(): void;
18
+ handleInput(data: string): void;
19
+ }
@@ -0,0 +1,88 @@
1
+ import { Container, matchesKey, Spacer, Text, visibleWidth, } from "@earendil-works/pi-tui";
2
+ import { theme } from "../theme/theme.js";
3
+ import { DynamicBorder } from "./dynamic-border.js";
4
+ import { isEnterKey } from "./keys.js";
5
+ export class LogoutSelectorComponent extends Container {
6
+ providers;
7
+ onSelect;
8
+ onCancel;
9
+ listContainer;
10
+ selectedIndex = 0;
11
+ _focused = false;
12
+ get focused() {
13
+ return this._focused;
14
+ }
15
+ set focused(value) {
16
+ this._focused = value;
17
+ }
18
+ constructor(providers, onSelect, onCancel) {
19
+ super();
20
+ this.providers = providers;
21
+ this.onSelect = onSelect;
22
+ this.onCancel = onCancel;
23
+ this.listContainer = new Container();
24
+ this.addChild(new DynamicBorder());
25
+ this.addChild(new Spacer(1));
26
+ this.addChild(new Text(theme.bold("Logout / Remove Stored Credentials"), 0, 0));
27
+ this.addChild(new Text(theme.fg("muted", "Select a provider to remove its credentials from ~/.my-pi-agent/auth.json:"), 0, 0));
28
+ this.addChild(new Spacer(1));
29
+ this.addChild(this.listContainer);
30
+ this.addChild(new Spacer(1));
31
+ this.addChild(new Text(theme.fg("dim", " Enter to remove credentials · Escape to cancel"), 0, 0));
32
+ this.addChild(new DynamicBorder());
33
+ this.updateList();
34
+ }
35
+ updateList() {
36
+ this.listContainer.clear();
37
+ if (this.providers.length === 0) {
38
+ this.listContainer.addChild(new Text(theme.fg("muted", " 未发现任何已保存的凭据 (No stored credentials)。"), 0, 0));
39
+ return;
40
+ }
41
+ for (let i = 0; i < this.providers.length; i++) {
42
+ const item = this.providers[i];
43
+ if (!item)
44
+ continue;
45
+ const isSelected = i === this.selectedIndex;
46
+ const cursor = isSelected ? theme.fg("accent", "› ") : " ";
47
+ const label = isSelected ? theme.bold(item.label) : item.label;
48
+ const desc = item.description
49
+ ? theme.fg("dim", ` (${item.description})`)
50
+ : "";
51
+ const left = `${cursor}${label}${desc}`;
52
+ const pad = Math.max(2, 78 - visibleWidth(left));
53
+ let lineText = left + " ".repeat(pad);
54
+ if (isSelected) {
55
+ lineText = theme.bg("selectedBg", lineText);
56
+ }
57
+ this.listContainer.addChild(new Text(lineText, 0, 0));
58
+ }
59
+ }
60
+ handleInput(data) {
61
+ if (matchesKey(data, "up")) {
62
+ if (this.providers.length === 0)
63
+ return;
64
+ this.selectedIndex =
65
+ this.selectedIndex === 0
66
+ ? this.providers.length - 1
67
+ : this.selectedIndex - 1;
68
+ this.updateList();
69
+ }
70
+ else if (matchesKey(data, "down")) {
71
+ if (this.providers.length === 0)
72
+ return;
73
+ this.selectedIndex =
74
+ this.selectedIndex === this.providers.length - 1
75
+ ? 0
76
+ : this.selectedIndex + 1;
77
+ this.updateList();
78
+ }
79
+ else if (isEnterKey(data)) {
80
+ const selected = this.providers[this.selectedIndex];
81
+ if (selected)
82
+ this.onSelect(selected.id);
83
+ }
84
+ else if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
85
+ this.onCancel();
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,40 @@
1
+ import { Container, Input } from "@earendil-works/pi-tui";
2
+ export interface ModelItem {
3
+ id: string;
4
+ provider: string;
5
+ name?: string;
6
+ contextWindow?: number;
7
+ is_configured?: boolean;
8
+ }
9
+ export type ModelListLoader = () => Promise<ModelItem[]>;
10
+ export declare class ModelSelectorComponent extends Container {
11
+ readonly currentModel: string;
12
+ readonly onSelect: (model: ModelItem) => void;
13
+ readonly onCancel: () => void;
14
+ readonly onSelectAsDefault?: ((model: ModelItem) => void) | undefined;
15
+ readonly defaultModelId?: string | undefined;
16
+ private readonly requestRender?;
17
+ searchInput: Input;
18
+ private headerContainer;
19
+ private listContainer;
20
+ private allModels;
21
+ private scopedModelItems;
22
+ private activeModels;
23
+ private filteredModels;
24
+ private selectedIndex;
25
+ private scope;
26
+ private loader?;
27
+ private _focused;
28
+ get focused(): boolean;
29
+ set focused(value: boolean);
30
+ constructor(currentModel: string, modelsOrLoader: ModelItem[] | ModelListLoader, onSelect: (model: ModelItem) => void, onCancel: () => void, initialSearch?: string, onSelectAsDefault?: ((model: ModelItem) => void) | undefined, defaultModelId?: string | undefined, requestRender?: (() => void) | undefined, scopedModels?: ModelItem[]);
31
+ private rebuildStaticLayout;
32
+ private getScopeText;
33
+ private getScopeHintText;
34
+ private updateHeader;
35
+ reload(initialSearch?: string): Promise<void>;
36
+ private sortModels;
37
+ filterModels(query: string): void;
38
+ updateList(): void;
39
+ handleInput(data: string): void;
40
+ }