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,393 @@
1
+ import * as os from "node:os";
2
+ import { Container, fuzzyMatch, Input, matchesKey, Spacer, Text, visibleWidth, } from "@earendil-works/pi-tui";
3
+ import { theme } from "../theme/theme.js";
4
+ import { DynamicBorder } from "./dynamic-border.js";
5
+ import { isEnterKey } from "./keys.js";
6
+ function normalizeKey(k) {
7
+ if (!k)
8
+ return "";
9
+ return k.replace(/\\/g, "/").toLowerCase();
10
+ }
11
+ export function buildSessionTree(sessions) {
12
+ const byKey = new Map();
13
+ for (const s of sessions) {
14
+ const node = {
15
+ session: s,
16
+ children: [],
17
+ latestActivity: s.modified * 1000,
18
+ };
19
+ byKey.set(s.id, node);
20
+ byKey.set(s.id.toLowerCase(), node);
21
+ if (s.path) {
22
+ byKey.set(s.path, node);
23
+ byKey.set(normalizeKey(s.path), node);
24
+ const filename = s.path.split(/[/\\]/).pop() || "";
25
+ const stem = filename.replace(/\.jsonl$/i, "");
26
+ if (stem) {
27
+ byKey.set(stem, node);
28
+ byKey.set(stem.toLowerCase(), node);
29
+ }
30
+ }
31
+ }
32
+ const roots = [];
33
+ for (const s of sessions) {
34
+ const node = byKey.get(s.id);
35
+ const pKey = s.parent_session || s.parent_session_path;
36
+ let parentNode;
37
+ if (pKey) {
38
+ const pFilename = pKey.split(/[/\\]/).pop() || "";
39
+ const pStem = pFilename.replace(/\.jsonl$/i, "");
40
+ parentNode =
41
+ byKey.get(pKey) ||
42
+ byKey.get(normalizeKey(pKey)) ||
43
+ byKey.get(pKey.toLowerCase()) ||
44
+ (pStem
45
+ ? byKey.get(pStem) || byKey.get(pStem.toLowerCase())
46
+ : undefined);
47
+ }
48
+ if (parentNode && parentNode !== node) {
49
+ parentNode.children.push(node);
50
+ }
51
+ else {
52
+ roots.push(node);
53
+ }
54
+ }
55
+ const updateLatestActivity = (node) => {
56
+ let latest = node.latestActivity;
57
+ for (const child of node.children) {
58
+ latest = Math.max(latest, updateLatestActivity(child));
59
+ }
60
+ node.latestActivity = latest;
61
+ return latest;
62
+ };
63
+ for (const r of roots) {
64
+ updateLatestActivity(r);
65
+ }
66
+ const sortNodes = (nodes) => {
67
+ nodes.sort((a, b) => b.latestActivity - a.latestActivity);
68
+ for (const n of nodes) {
69
+ sortNodes(n.children);
70
+ }
71
+ };
72
+ sortNodes(roots);
73
+ return roots;
74
+ }
75
+ export function flattenSessionTree(roots) {
76
+ const result = [];
77
+ const walk = (node, depth, ancestorContinues, isLast) => {
78
+ result.push({ session: node.session, depth, isLast, ancestorContinues });
79
+ for (let i = 0; i < node.children.length; i++) {
80
+ const childIsLast = i === node.children.length - 1;
81
+ const continues = depth > 0 ? !isLast : false;
82
+ walk(node.children[i], depth + 1, [...ancestorContinues, continues], childIsLast);
83
+ }
84
+ };
85
+ for (let i = 0; i < roots.length; i++) {
86
+ walk(roots[i], 0, [], i === roots.length - 1);
87
+ }
88
+ return result;
89
+ }
90
+ export function buildTreePrefix(node) {
91
+ if (node.depth === 0) {
92
+ return "";
93
+ }
94
+ const parts = node.ancestorContinues.map((continues) => continues ? "│ " : " ");
95
+ const branch = node.isLast ? "└─ " : "├─ ";
96
+ return parts.join("") + branch;
97
+ }
98
+ function shortenPath(p) {
99
+ const home = os.homedir();
100
+ if (!p)
101
+ return p;
102
+ if (p.startsWith(home)) {
103
+ return `~${p.slice(home.length)}`;
104
+ }
105
+ return p;
106
+ }
107
+ function formatSessionDate(timestampSec) {
108
+ const now = Date.now();
109
+ const diffMs = now - timestampSec * 1000;
110
+ const diffMins = Math.floor(diffMs / 60000);
111
+ const diffHours = Math.floor(diffMs / 3600000);
112
+ const diffDays = Math.floor(diffMs / 86400000);
113
+ if (diffMins < 1)
114
+ return "now";
115
+ if (diffMins < 60)
116
+ return `${diffMins}m`;
117
+ if (diffHours < 24)
118
+ return `${diffHours}h`;
119
+ if (diffDays < 7)
120
+ return `${diffDays}d`;
121
+ if (diffDays < 30)
122
+ return `${Math.floor(diffDays / 7)}w`;
123
+ if (diffDays < 365)
124
+ return `${Math.floor(diffDays / 30)}mo`;
125
+ return `${Math.floor(diffDays / 365)}y`;
126
+ }
127
+ export class SessionSelectorComponent extends Container {
128
+ loadSessions;
129
+ onSelect;
130
+ onCancel;
131
+ requestRender;
132
+ activeSessionId;
133
+ onDelete;
134
+ searchInput;
135
+ allSessions = [];
136
+ filteredSessions = [];
137
+ displayNodes = [];
138
+ selectedIndex = 0;
139
+ maxVisible = 10;
140
+ scope = "current";
141
+ showPath = false;
142
+ confirmingDeleteId = null;
143
+ errorMessage = null;
144
+ errorTimeout = null;
145
+ _focused = false;
146
+ get focused() {
147
+ return this._focused;
148
+ }
149
+ set focused(value) {
150
+ this._focused = value;
151
+ this.searchInput.focused = value;
152
+ }
153
+ constructor(loadSessions, onSelect, onCancel, requestRender, activeSessionId, onDelete) {
154
+ super();
155
+ this.loadSessions = loadSessions;
156
+ this.onSelect = onSelect;
157
+ this.onCancel = onCancel;
158
+ this.requestRender = requestRender;
159
+ this.activeSessionId = activeSessionId;
160
+ this.onDelete = onDelete;
161
+ this.searchInput = new Input();
162
+ this.searchInput.onSubmit = () => {
163
+ const selected = this.displayNodes[this.selectedIndex]?.session;
164
+ if (selected)
165
+ this.onSelect(selected);
166
+ };
167
+ void this.reload();
168
+ }
169
+ async reload() {
170
+ try {
171
+ this.allSessions = await this.loadSessions(this.scope === "all");
172
+ }
173
+ catch {
174
+ this.allSessions = [];
175
+ }
176
+ this.applyFilter();
177
+ this.rebuildUI();
178
+ if (this.requestRender) {
179
+ this.requestRender();
180
+ }
181
+ }
182
+ applyFilter() {
183
+ const q = this.searchInput.getValue().trim().toLowerCase();
184
+ if (q) {
185
+ this.filteredSessions = this.allSessions.filter((s) => {
186
+ const text = `${s.id} ${s.name || ""} ${s.cwd || ""}`.toLowerCase();
187
+ return fuzzyMatch(q, text).matches || text.includes(q);
188
+ });
189
+ this.displayNodes = this.filteredSessions.map((s) => ({
190
+ session: s,
191
+ depth: 0,
192
+ isLast: false,
193
+ ancestorContinues: [],
194
+ }));
195
+ }
196
+ else {
197
+ this.filteredSessions = [...this.allSessions];
198
+ const roots = buildSessionTree(this.allSessions);
199
+ this.displayNodes = flattenSessionTree(roots);
200
+ }
201
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.displayNodes.length - 1));
202
+ }
203
+ rebuildUI() {
204
+ this.clear();
205
+ this.addChild(new DynamicBorder());
206
+ this.addChild(new Spacer(1));
207
+ // Header Info
208
+ const scopeLabel = this.scope === "current"
209
+ ? "◉ Current Folder | ○ All"
210
+ : "○ Current Folder | ◉ All";
211
+ this.addChild(new Text(`${theme.bold("Resume Session")} ${theme.fg("accent", scopeLabel)}`, 1, 0));
212
+ if (this.confirmingDeleteId !== null) {
213
+ this.addChild(new Text(theme.fg("error", "Delete session? Enter to confirm · Esc to cancel"), 1, 0));
214
+ }
215
+ else if (this.errorMessage === null) {
216
+ this.addChild(new Text(theme.fg("muted", "Tab: scope · Ctrl+D: delete · Ctrl+P: path · Enter: resume · Esc: cancel"), 1, 0));
217
+ }
218
+ else {
219
+ this.addChild(new Text(theme.fg("error", this.errorMessage), 1, 0));
220
+ }
221
+ this.addChild(new Spacer(1));
222
+ // Search Input
223
+ this.addChild(this.searchInput);
224
+ this.addChild(new Spacer(1));
225
+ // List rendering
226
+ if (this.displayNodes.length === 0) {
227
+ this.addChild(new Text(theme.fg("muted", " 未发现匹配的历史会话。"), 1, 0));
228
+ }
229
+ else {
230
+ const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.displayNodes.length - this.maxVisible));
231
+ const end = Math.min(start + this.maxVisible, this.displayNodes.length);
232
+ for (let i = start; i < end; i++) {
233
+ const node = this.displayNodes[i];
234
+ if (!node)
235
+ continue;
236
+ const s = node.session;
237
+ const isSelected = i === this.selectedIndex;
238
+ const isCurrent = s.id === this.activeSessionId;
239
+ const isConfirming = s.id === this.confirmingDeleteId;
240
+ const deletePrefix = isConfirming
241
+ ? theme.fg("error", "[delete?] ")
242
+ : "";
243
+ const cursor = isSelected ? theme.fg("accent", "› ") : " ";
244
+ const treePrefix = buildTreePrefix(node);
245
+ let titleText = s.name || s.id;
246
+ if (isConfirming) {
247
+ titleText = theme.fg("error", titleText);
248
+ }
249
+ else if (isCurrent) {
250
+ titleText = theme.fg("accent", titleText);
251
+ }
252
+ else if (s.name) {
253
+ titleText = theme.fg("warning", titleText);
254
+ }
255
+ const msgInfo = `${s.message_count || 0}`;
256
+ const timeInfo = formatSessionDate(s.modified);
257
+ let meta = `${msgInfo} ${timeInfo}`;
258
+ if (this.scope === "all" && s.cwd) {
259
+ meta = `${shortenPath(s.cwd)} ${meta}`;
260
+ }
261
+ if (this.showPath && s.path) {
262
+ meta = `${shortenPath(s.path)} ${meta}`;
263
+ }
264
+ const left = cursor +
265
+ deletePrefix +
266
+ theme.fg("dim", treePrefix) +
267
+ (isSelected ? theme.bold(titleText) : titleText);
268
+ const right = theme.fg(isConfirming ? "error" : "dim", meta);
269
+ const pad = Math.max(2, 75 - visibleWidth(left) - visibleWidth(right));
270
+ let rowStr = left + " ".repeat(pad) + right;
271
+ if (isSelected) {
272
+ rowStr = theme.bg("selectedBg", rowStr);
273
+ }
274
+ this.addChild(new Text(rowStr, 1, 0));
275
+ }
276
+ if (this.displayNodes.length > this.maxVisible) {
277
+ this.addChild(new Text(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.displayNodes.length})`), 1, 0));
278
+ }
279
+ }
280
+ this.addChild(new Spacer(1));
281
+ this.addChild(new DynamicBorder());
282
+ }
283
+ handleInput(data) {
284
+ // 处于删除二次确认拦截模式
285
+ if (this.confirmingDeleteId !== null) {
286
+ if (isEnterKey(data)) {
287
+ const toDelete = this.displayNodes.find((n) => n.session.id === this.confirmingDeleteId)?.session;
288
+ if (toDelete) {
289
+ this.executeDelete(toDelete);
290
+ }
291
+ return;
292
+ }
293
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
294
+ this.confirmingDeleteId = null;
295
+ this.rebuildUI();
296
+ if (this.requestRender)
297
+ this.requestRender();
298
+ return;
299
+ }
300
+ return;
301
+ }
302
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
303
+ this.onCancel();
304
+ return;
305
+ }
306
+ if (matchesKey(data, "ctrl+d")) {
307
+ const selected = this.displayNodes[this.selectedIndex]?.session;
308
+ if (selected) {
309
+ if (selected.id === this.activeSessionId) {
310
+ this.setErrorMessage("Cannot delete the currently active session");
311
+ return;
312
+ }
313
+ this.confirmingDeleteId = selected.id;
314
+ this.rebuildUI();
315
+ if (this.requestRender)
316
+ this.requestRender();
317
+ }
318
+ return;
319
+ }
320
+ if (matchesKey(data, "tab")) {
321
+ this.scope = this.scope === "current" ? "all" : "current";
322
+ void this.reload();
323
+ return;
324
+ }
325
+ if (matchesKey(data, "ctrl+p")) {
326
+ this.showPath = !this.showPath;
327
+ this.rebuildUI();
328
+ if (this.requestRender)
329
+ this.requestRender();
330
+ return;
331
+ }
332
+ if (matchesKey(data, "up")) {
333
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
334
+ this.rebuildUI();
335
+ if (this.requestRender)
336
+ this.requestRender();
337
+ return;
338
+ }
339
+ if (matchesKey(data, "down")) {
340
+ this.selectedIndex = Math.min(this.displayNodes.length - 1, this.selectedIndex + 1);
341
+ this.rebuildUI();
342
+ if (this.requestRender)
343
+ this.requestRender();
344
+ return;
345
+ }
346
+ if (isEnterKey(data)) {
347
+ const selected = this.displayNodes[this.selectedIndex]?.session;
348
+ if (selected) {
349
+ this.onSelect(selected);
350
+ return;
351
+ }
352
+ }
353
+ const prevQuery = this.searchInput.getValue();
354
+ this.searchInput.handleInput?.(data);
355
+ if (this.searchInput.getValue() !== prevQuery) {
356
+ this.applyFilter();
357
+ this.rebuildUI();
358
+ if (this.requestRender)
359
+ this.requestRender();
360
+ }
361
+ }
362
+ setErrorMessage(msg) {
363
+ if (this.errorTimeout) {
364
+ clearTimeout(this.errorTimeout);
365
+ }
366
+ this.errorMessage = msg;
367
+ this.rebuildUI();
368
+ if (this.requestRender)
369
+ this.requestRender();
370
+ this.errorTimeout = setTimeout(() => {
371
+ this.errorMessage = null;
372
+ this.errorTimeout = null;
373
+ this.rebuildUI();
374
+ if (this.requestRender)
375
+ this.requestRender();
376
+ }, 2500);
377
+ }
378
+ executeDelete(session) {
379
+ this.confirmingDeleteId = null;
380
+ if (this.onDelete) {
381
+ void Promise.resolve(this.onDelete(session)).then(() => {
382
+ void this.reload();
383
+ });
384
+ }
385
+ else {
386
+ this.allSessions = this.allSessions.filter((s) => s.id !== session.id);
387
+ this.applyFilter();
388
+ this.rebuildUI();
389
+ if (this.requestRender)
390
+ this.requestRender();
391
+ }
392
+ }
393
+ }
@@ -0,0 +1,24 @@
1
+ import { Container } from "@earendil-works/pi-tui";
2
+ export interface SettingItemDef {
3
+ key: string;
4
+ label: string;
5
+ type: "boolean" | "cycle" | "string";
6
+ options?: string[];
7
+ description: string;
8
+ }
9
+ export declare class SettingsSelectorComponent extends Container {
10
+ readonly onChange: (key: string, value: unknown) => void;
11
+ readonly onClose: () => void;
12
+ readonly definitions: SettingItemDef[];
13
+ private listContainer;
14
+ private selectedIndex;
15
+ private currentSettings;
16
+ private lastWidth;
17
+ private _focused;
18
+ get focused(): boolean;
19
+ set focused(value: boolean);
20
+ constructor(initialSettings: Record<string, unknown>, onChange: (key: string, value: unknown) => void, onClose: () => void, definitions?: SettingItemDef[]);
21
+ render(width: number): string[];
22
+ updateList(): void;
23
+ handleInput(data: string): void;
24
+ }
@@ -0,0 +1,146 @@
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
+ const SETTING_DEFINITIONS = [
6
+ {
7
+ key: "auto_compact",
8
+ label: "Auto Compaction",
9
+ type: "boolean",
10
+ description: "Automatically compact context when usage exceeds threshold",
11
+ },
12
+ {
13
+ key: "default_model",
14
+ label: "Default Model",
15
+ type: "string",
16
+ description: "Default LLM model identifier (switch with /model)",
17
+ },
18
+ {
19
+ key: "default_thinking_level",
20
+ label: "Thinking Level",
21
+ type: "cycle",
22
+ options: ["off", "minimal", "low", "medium", "high", "xhigh", "max"],
23
+ description: "Default reasoning thinking depth budget",
24
+ },
25
+ {
26
+ key: "default_permission_mode",
27
+ label: "Permission Mode",
28
+ type: "cycle",
29
+ options: ["review", "yolo", "strict"],
30
+ description: "Security permission mode for workspace mutations",
31
+ },
32
+ {
33
+ key: "theme",
34
+ label: "Theme",
35
+ type: "cycle",
36
+ options: ["dark", "light"],
37
+ description: "Active terminal color theme palette",
38
+ },
39
+ ];
40
+ export class SettingsSelectorComponent extends Container {
41
+ onChange;
42
+ onClose;
43
+ definitions;
44
+ listContainer;
45
+ selectedIndex = 0;
46
+ currentSettings;
47
+ lastWidth = 80;
48
+ _focused = false;
49
+ get focused() {
50
+ return this._focused;
51
+ }
52
+ set focused(value) {
53
+ this._focused = value;
54
+ }
55
+ constructor(initialSettings, onChange, onClose, definitions = SETTING_DEFINITIONS) {
56
+ super();
57
+ this.onChange = onChange;
58
+ this.onClose = onClose;
59
+ this.definitions = definitions;
60
+ this.currentSettings = { ...initialSettings };
61
+ this.listContainer = new Container();
62
+ this.addChild(new DynamicBorder());
63
+ this.addChild(new Spacer(1));
64
+ this.addChild(new Text(theme.bold("Settings"), 0, 0));
65
+ this.addChild(new Text(theme.fg("muted", "Enter: toggle / cycle value · Up/Down: navigate · Esc: close"), 0, 0));
66
+ this.addChild(new Spacer(1));
67
+ this.addChild(this.listContainer);
68
+ this.addChild(new Spacer(1));
69
+ this.addChild(new Text(theme.fg("dim", " Changes are saved to ~/.my-pi-agent/settings.json · Esc to close"), 0, 0));
70
+ this.addChild(new DynamicBorder());
71
+ this.updateList();
72
+ }
73
+ render(width) {
74
+ this.lastWidth = width;
75
+ return super.render(width);
76
+ }
77
+ updateList() {
78
+ this.listContainer.clear();
79
+ for (let i = 0; i < this.definitions.length; i++) {
80
+ const def = this.definitions[i];
81
+ const isSelected = i === this.selectedIndex;
82
+ const val = this.currentSettings[def.key];
83
+ let valueDisplay = "";
84
+ if (def.type === "boolean") {
85
+ valueDisplay = val
86
+ ? theme.fg("success", "[x]")
87
+ : theme.fg("muted", "[ ]");
88
+ }
89
+ else {
90
+ valueDisplay = theme.fg("accent", String(val ?? "not set"));
91
+ }
92
+ const cursor = isSelected ? theme.fg("accent", "› ") : " ";
93
+ const label = isSelected ? theme.bold(def.label) : def.label;
94
+ const left = `${cursor}${label}`;
95
+ const right = `${valueDisplay} ${theme.fg("dim", `(${def.description})`)}`;
96
+ const pad = Math.max(2, this.lastWidth - 4 - visibleWidth(left) - visibleWidth(right));
97
+ let lineText = left + " ".repeat(pad) + right;
98
+ if (isSelected) {
99
+ lineText = theme.bg("selectedBg", lineText);
100
+ }
101
+ this.listContainer.addChild(new Text(lineText, 0, 0));
102
+ }
103
+ }
104
+ handleInput(data) {
105
+ if (matchesKey(data, "up")) {
106
+ this.selectedIndex =
107
+ this.selectedIndex === 0
108
+ ? this.definitions.length - 1
109
+ : this.selectedIndex - 1;
110
+ this.updateList();
111
+ }
112
+ else if (matchesKey(data, "down")) {
113
+ this.selectedIndex =
114
+ this.selectedIndex === this.definitions.length - 1
115
+ ? 0
116
+ : this.selectedIndex + 1;
117
+ this.updateList();
118
+ }
119
+ else if (isEnterKey(data) || matchesKey(data, "space")) {
120
+ const def = this.definitions[this.selectedIndex];
121
+ if (!def)
122
+ return;
123
+ let nextVal;
124
+ if (def.type === "boolean") {
125
+ nextVal = !this.currentSettings[def.key];
126
+ }
127
+ else if (def.type === "cycle" &&
128
+ def.options &&
129
+ def.options.length > 0) {
130
+ const cur = String(this.currentSettings[def.key] ?? def.options[0]);
131
+ const curIdx = def.options.indexOf(cur);
132
+ const nextIdx = (curIdx + 1) % def.options.length;
133
+ nextVal = def.options[nextIdx];
134
+ }
135
+ else {
136
+ return;
137
+ }
138
+ this.currentSettings[def.key] = nextVal;
139
+ this.onChange(def.key, nextVal);
140
+ this.updateList();
141
+ }
142
+ else if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
143
+ this.onClose();
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,25 @@
1
+ import { Loader } from "@earendil-works/pi-tui";
2
+ /**
3
+ * 基础状态指示器(100% 对标 Pi 原厂 StatusIndicator):继承 Loader 带有 80ms 动态旋转帧
4
+ */
5
+ export declare class StatusIndicator extends Loader {
6
+ kind: string;
7
+ constructor(kind: string, ui: any, spinnerColorFn: (spinner: string) => string, messageColorFn: (msg: string) => string, message: string, indicator?: any);
8
+ start(): void;
9
+ renderInBorder(width: number): string;
10
+ renderSpinnerInBorder(width: number): string;
11
+ dispose(): void;
12
+ }
13
+ /**
14
+ * 运行中状态指示器(100% 对标 Pi 原厂 WorkingStatusIndicator):
15
+ * 可直接嵌入 CustomEditor 顶部边框,展示转圈动效:── ⠸ Working ───────────
16
+ */
17
+ export declare class WorkingStatusIndicator extends StatusIndicator {
18
+ constructor(ui: any, message?: string, indicator?: any, colorFn?: (text: string) => string);
19
+ }
20
+ /**
21
+ * 上下文压缩状态指示器(对标 Pi 原厂 CompactionStatusIndicator)
22
+ */
23
+ export declare class CompactionStatusIndicator extends StatusIndicator {
24
+ constructor(ui: any, reason?: "manual" | "overflow");
25
+ }
@@ -0,0 +1,60 @@
1
+ import { Loader, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { theme } from "../theme/theme.js";
3
+ /**
4
+ * 基础状态指示器(100% 对标 Pi 原厂 StatusIndicator):继承 Loader 带有 80ms 动态旋转帧
5
+ */
6
+ export class StatusIndicator extends Loader {
7
+ kind;
8
+ constructor(kind, ui, spinnerColorFn, messageColorFn, message, indicator) {
9
+ super(ui, spinnerColorFn, messageColorFn, message, indicator);
10
+ this.kind = kind;
11
+ // unref 内部定时器,防止挂死 Node 进程或测试退出
12
+ const timer = this.intervalId;
13
+ if (timer && typeof timer.unref === "function") {
14
+ timer.unref();
15
+ }
16
+ }
17
+ start() {
18
+ super.start();
19
+ const timer = this.intervalId;
20
+ if (timer && typeof timer.unref === "function") {
21
+ timer.unref();
22
+ }
23
+ }
24
+ renderInBorder(width) {
25
+ const lines = super.render(width + 2);
26
+ const line = lines[1] ?? lines[0] ?? "";
27
+ const clean = line.startsWith(" ")
28
+ ? line.slice(1).trimEnd()
29
+ : line.trimEnd();
30
+ return truncateToWidth(clean, width, "");
31
+ }
32
+ renderSpinnerInBorder(width) {
33
+ const ind = this.getRenderedIndicator?.() ?? "⠋";
34
+ return truncateToWidth(ind, width, "");
35
+ }
36
+ dispose() {
37
+ this.stop();
38
+ }
39
+ }
40
+ /**
41
+ * 运行中状态指示器(100% 对标 Pi 原厂 WorkingStatusIndicator):
42
+ * 可直接嵌入 CustomEditor 顶部边框,展示转圈动效:── ⠸ Working ───────────
43
+ */
44
+ export class WorkingStatusIndicator extends StatusIndicator {
45
+ constructor(ui, message = "Working", indicator, colorFn) {
46
+ super("working", ui, colorFn ?? ((text) => theme.fg("accent", text)), colorFn ?? ((text) => theme.fg("muted", text)), message, indicator);
47
+ }
48
+ }
49
+ /**
50
+ * 上下文压缩状态指示器(对标 Pi 原厂 CompactionStatusIndicator)
51
+ */
52
+ export class CompactionStatusIndicator extends StatusIndicator {
53
+ constructor(ui, reason = "manual") {
54
+ const cancelHint = "(Esc to cancel)";
55
+ const label = reason === "manual"
56
+ ? `Compacting context... ${cancelHint}`
57
+ : "Context overflow, auto-compacting... (Esc to cancel)";
58
+ super("compaction", ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
59
+ }
60
+ }
@@ -0,0 +1,14 @@
1
+ import { Container, SelectList } from "@earendil-works/pi-tui";
2
+ export declare class ThemeSelectorComponent extends Container {
3
+ readonly currentTheme: string;
4
+ readonly availableThemes: string[];
5
+ readonly onSelect: (themeName: string) => void;
6
+ readonly onCancel: () => void;
7
+ readonly onPreview?: ((themeName: string) => void) | undefined;
8
+ selectList: SelectList;
9
+ private _focused;
10
+ get focused(): boolean;
11
+ set focused(value: boolean);
12
+ constructor(currentTheme: string, availableThemes: string[] | undefined, onSelect: (themeName: string) => void, onCancel: () => void, onPreview?: ((themeName: string) => void) | undefined);
13
+ handleInput(data: string): void;
14
+ }