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,77 @@
1
+ import { Container, 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
+ const THEME_LAYOUT_OPTIONS = {
6
+ minPrimaryColumnWidth: 12,
7
+ maxPrimaryColumnWidth: 28,
8
+ };
9
+ export class ThemeSelectorComponent extends Container {
10
+ currentTheme;
11
+ availableThemes;
12
+ onSelect;
13
+ onCancel;
14
+ onPreview;
15
+ selectList;
16
+ _focused = false;
17
+ get focused() {
18
+ return this._focused;
19
+ }
20
+ set focused(value) {
21
+ this._focused = value;
22
+ }
23
+ constructor(currentTheme, availableThemes = ["dark", "light"], onSelect, onCancel, onPreview) {
24
+ super();
25
+ this.currentTheme = currentTheme;
26
+ this.availableThemes = availableThemes;
27
+ this.onSelect = onSelect;
28
+ this.onCancel = onCancel;
29
+ this.onPreview = onPreview;
30
+ const items = availableThemes.map((name) => ({
31
+ value: name,
32
+ label: name,
33
+ description: name === currentTheme ? "(current)" : undefined,
34
+ }));
35
+ const listTheme = {
36
+ selectedPrefix: (s) => theme.fg("accent", s),
37
+ selectedText: (s) => theme.bold(theme.fg("accent", s)),
38
+ description: (s) => theme.fg("muted", s),
39
+ scrollInfo: (s) => theme.dim(s),
40
+ noMatch: (_s) => theme.dim("无匹配主题"),
41
+ };
42
+ this.selectList = new SelectList(items, Math.max(1, items.length), listTheme, THEME_LAYOUT_OPTIONS);
43
+ const curIdx = availableThemes.indexOf(currentTheme);
44
+ if (curIdx !== -1) {
45
+ this.selectList.setSelectedIndex(curIdx);
46
+ }
47
+ this.selectList.onSelect = (item) => onSelect(item.value);
48
+ this.selectList.onCancel = () => onCancel();
49
+ this.selectList.onSelectionChange = (item) => onPreview?.(item.value);
50
+ this.addChild(new DynamicBorder());
51
+ this.addChild(new Spacer(1));
52
+ this.addChild(new Text(theme.bold("Theme Selector"), 0, 0));
53
+ this.addChild(new Text(theme.fg("muted", "Enter: select · Up/Down: live preview · Esc: revert & exit"), 0, 0));
54
+ this.addChild(new Spacer(1));
55
+ this.addChild(this.selectList);
56
+ this.addChild(new Spacer(1));
57
+ this.addChild(new Text(theme.fg("dim", " Changes persist to settings.json · Escape to cancel"), 0, 0));
58
+ this.addChild(new DynamicBorder());
59
+ }
60
+ handleInput(data) {
61
+ if (matchesKey(data, "ctrl+c")) {
62
+ this.onCancel();
63
+ return;
64
+ }
65
+ if (matchesKey(data, "up") ||
66
+ matchesKey(data, "down") ||
67
+ isEnterKey(data) ||
68
+ matchesKey(data, "escape")) {
69
+ if (isEnterKey(data)) {
70
+ this.selectList.handleInput("\r");
71
+ }
72
+ else {
73
+ this.selectList.handleInput(data);
74
+ }
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,21 @@
1
+ import { Container, Input, SelectList } from "@earendil-works/pi-tui";
2
+ export declare const THINKING_LEVEL_DESCRIPTIONS: Record<string, string>;
3
+ export declare class ThinkingSelectorComponent extends Container {
4
+ readonly currentLevel: string;
5
+ readonly availableLevels: string[];
6
+ readonly onSelect: (level: string) => void;
7
+ readonly onCancel: () => void;
8
+ readonly onSelectAsDefault?: ((level: string) => void) | undefined;
9
+ readonly defaultLevel?: string | undefined;
10
+ searchInput: Input;
11
+ selectList: SelectList;
12
+ private selectListChildIndex;
13
+ private allItems;
14
+ private _focused;
15
+ get focused(): boolean;
16
+ set focused(value: boolean);
17
+ constructor(currentLevel: string, availableLevels: string[], onSelect: (level: string) => void, onCancel: () => void, onSelectAsDefault?: ((level: string) => void) | undefined, defaultLevel?: string | undefined);
18
+ private buildSelectList;
19
+ private applyFilter;
20
+ handleInput(data: string): void;
21
+ }
@@ -0,0 +1,128 @@
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 THINKING_LEVEL_DESCRIPTIONS = {
6
+ off: "No reasoning",
7
+ minimal: "Very brief reasoning (~1k tokens)",
8
+ low: "Light reasoning (~2k tokens)",
9
+ medium: "Moderate reasoning (~8k tokens)",
10
+ high: "Deep reasoning (~16k tokens)",
11
+ xhigh: "Extra-high reasoning (~32k tokens)",
12
+ max: "Maximum reasoning",
13
+ };
14
+ export class ThinkingSelectorComponent extends Container {
15
+ currentLevel;
16
+ availableLevels;
17
+ onSelect;
18
+ onCancel;
19
+ onSelectAsDefault;
20
+ defaultLevel;
21
+ searchInput;
22
+ selectList;
23
+ selectListChildIndex;
24
+ allItems;
25
+ _focused = false;
26
+ get focused() {
27
+ return this._focused;
28
+ }
29
+ set focused(value) {
30
+ this._focused = value;
31
+ this.searchInput.focused = value;
32
+ }
33
+ constructor(currentLevel, availableLevels, onSelect, onCancel, onSelectAsDefault, defaultLevel) {
34
+ super();
35
+ this.currentLevel = currentLevel;
36
+ this.availableLevels = availableLevels;
37
+ this.onSelect = onSelect;
38
+ this.onCancel = onCancel;
39
+ this.onSelectAsDefault = onSelectAsDefault;
40
+ this.defaultLevel = defaultLevel;
41
+ this.allItems = availableLevels.map((level) => {
42
+ const isCurrent = level.toLowerCase() === currentLevel.toLowerCase();
43
+ const isDefault = level.toLowerCase() === (defaultLevel || "").toLowerCase();
44
+ const desc = THINKING_LEVEL_DESCRIPTIONS[level] || "Reasoning level";
45
+ return {
46
+ value: level,
47
+ label: `${isCurrent ? "✓ " : " "}${level}`,
48
+ description: isDefault ? `${desc} · default` : desc,
49
+ };
50
+ });
51
+ this.addChild(new DynamicBorder());
52
+ this.addChild(new Spacer(1));
53
+ this.addChild(new Text(theme.bold("Thinking Level"), 0, 0));
54
+ this.addChild(new Spacer(1));
55
+ this.addChild(new Text(theme.fg("muted", "Shift+Tab cycles thinking levels in-session"), 0, 0));
56
+ this.addChild(new Spacer(1));
57
+ this.searchInput = new Input();
58
+ this.searchInput.onSubmit = () => {
59
+ const item = this.selectList.getSelectedItem();
60
+ if (item) {
61
+ this.onSelect(item.value);
62
+ }
63
+ };
64
+ this.addChild(this.searchInput);
65
+ this.addChild(new Spacer(1));
66
+ this.selectList = this.buildSelectList(this.allItems, currentLevel);
67
+ this.selectListChildIndex = this.children.length;
68
+ this.addChild(this.selectList);
69
+ this.addChild(new Spacer(1));
70
+ this.addChild(new Text(theme.fg("dim", " Enter to select · Ctrl+S to set as default · Escape to cancel"), 0, 0));
71
+ this.addChild(new DynamicBorder());
72
+ }
73
+ buildSelectList(items, preselect) {
74
+ const listTheme = {
75
+ selectedPrefix: (s) => theme.fg("accent", s),
76
+ selectedText: (s) => theme.bold(theme.fg("accent", s)),
77
+ description: (s) => theme.fg("muted", s),
78
+ scrollInfo: (s) => theme.dim(s),
79
+ noMatch: (_s) => theme.dim("无匹配等级"),
80
+ };
81
+ const list = new SelectList(items, Math.max(1, items.length), listTheme, {
82
+ minPrimaryColumnWidth: 12,
83
+ maxPrimaryColumnWidth: 32,
84
+ });
85
+ const curIdx = items.findIndex((i) => i.value === preselect);
86
+ if (curIdx !== -1) {
87
+ list.setSelectedIndex(curIdx);
88
+ }
89
+ list.onSelect = (item) => this.onSelect(item.value);
90
+ list.onCancel = () => this.onCancel();
91
+ return list;
92
+ }
93
+ applyFilter(query) {
94
+ const filtered = query
95
+ ? fuzzyFilter(this.allItems, query, (item) => `${item.value} ${item.description ?? ""}`)
96
+ : this.allItems;
97
+ const selectedValue = this.selectList.getSelectedItem()?.value;
98
+ const newList = this.buildSelectList(filtered, selectedValue || "");
99
+ this.children[this.selectListChildIndex] = newList;
100
+ this.selectList = newList;
101
+ }
102
+ handleInput(data) {
103
+ if (matchesKey(data, "ctrl+c")) {
104
+ this.onCancel();
105
+ return;
106
+ }
107
+ if (matchesKey(data, "ctrl+s") && this.onSelectAsDefault) {
108
+ const item = this.selectList.getSelectedItem();
109
+ if (item)
110
+ this.onSelectAsDefault(item.value);
111
+ return;
112
+ }
113
+ if (matchesKey(data, "up") ||
114
+ matchesKey(data, "down") ||
115
+ isEnterKey(data) ||
116
+ matchesKey(data, "escape")) {
117
+ if (isEnterKey(data)) {
118
+ this.selectList.handleInput("\r");
119
+ }
120
+ else {
121
+ this.selectList.handleInput(data);
122
+ }
123
+ return;
124
+ }
125
+ this.searchInput.handleInput(data);
126
+ this.applyFilter(this.searchInput.getValue());
127
+ }
128
+ }
@@ -0,0 +1,31 @@
1
+ import { Container } from "@earendil-works/pi-tui";
2
+ export declare const SPINNER_FRAMES: string[];
3
+ export declare class ToolExecutionComponent extends Container {
4
+ readonly toolName: string;
5
+ readonly toolCallId: string;
6
+ args: Record<string, unknown>;
7
+ private readonly onRequestRender?;
8
+ private box;
9
+ private isFinished;
10
+ private isError;
11
+ private isExpanded;
12
+ private resultText;
13
+ private partialOutput;
14
+ private elapsedSeconds;
15
+ private startTime;
16
+ private spinnerFrame;
17
+ private animInterval;
18
+ constructor(toolName: string, toolCallId: string, args?: Record<string, unknown>, onRequestRender?: (() => void) | undefined);
19
+ private startAnimation;
20
+ private stopAnimation;
21
+ dispose(): void;
22
+ get finished(): boolean;
23
+ get elapsed(): number;
24
+ updateArgs(args: Record<string, unknown>): void;
25
+ updatePartialResult(partial: unknown): void;
26
+ updateResult(result: unknown, isError: boolean, elapsedSeconds?: number): void;
27
+ toggleExpanded(): void;
28
+ render(width: number): string[];
29
+ private formatArgs;
30
+ private updateDisplay;
31
+ }
@@ -0,0 +1,206 @@
1
+ import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
2
+ import { theme } from "../theme/theme.js";
3
+ export const SPINNER_FRAMES = [
4
+ "⠋",
5
+ "⠙",
6
+ "⠹",
7
+ "⠸",
8
+ "⠼",
9
+ "⠴",
10
+ "⠦",
11
+ "⠧",
12
+ "⠇",
13
+ "⠏",
14
+ ];
15
+ const SPINNER_INTERVAL_MS = 80;
16
+ const MAX_PREVIEW_LINES = 15;
17
+ export class ToolExecutionComponent extends Container {
18
+ toolName;
19
+ toolCallId;
20
+ args;
21
+ onRequestRender;
22
+ box;
23
+ isFinished = false;
24
+ isError = false;
25
+ isExpanded = false;
26
+ resultText = "";
27
+ partialOutput = "";
28
+ elapsedSeconds = 0;
29
+ startTime = Date.now();
30
+ spinnerFrame = 0;
31
+ animInterval = null;
32
+ constructor(toolName, toolCallId, args = {}, onRequestRender) {
33
+ super();
34
+ this.toolName = toolName;
35
+ this.toolCallId = toolCallId;
36
+ this.args = args;
37
+ this.onRequestRender = onRequestRender;
38
+ this.addChild(new Spacer(1));
39
+ this.box = new Box(1, 1, (t) => theme.bg("toolPendingBg", t));
40
+ this.addChild(this.box);
41
+ this.updateDisplay();
42
+ this.startAnimation();
43
+ }
44
+ startAnimation() {
45
+ if (this.isFinished || this.animInterval)
46
+ return;
47
+ this.animInterval = setInterval(() => {
48
+ if (this.isFinished) {
49
+ this.stopAnimation();
50
+ return;
51
+ }
52
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
53
+ this.updateDisplay();
54
+ if (this.onRequestRender) {
55
+ this.onRequestRender();
56
+ }
57
+ }, SPINNER_INTERVAL_MS);
58
+ if (typeof this.animInterval?.unref === "function") {
59
+ this.animInterval.unref();
60
+ }
61
+ }
62
+ stopAnimation() {
63
+ if (this.animInterval) {
64
+ clearInterval(this.animInterval);
65
+ this.animInterval = null;
66
+ }
67
+ }
68
+ dispose() {
69
+ this.stopAnimation();
70
+ }
71
+ get finished() {
72
+ return this.isFinished;
73
+ }
74
+ get elapsed() {
75
+ if (this.isFinished) {
76
+ return this.elapsedSeconds;
77
+ }
78
+ return Math.max(0, (Date.now() - this.startTime) / 1000);
79
+ }
80
+ updateArgs(args) {
81
+ this.args = args;
82
+ this.updateDisplay();
83
+ }
84
+ updatePartialResult(partial) {
85
+ if (this.isFinished)
86
+ return;
87
+ if (typeof partial === "string") {
88
+ this.partialOutput = partial;
89
+ }
90
+ else if (partial && typeof partial === "object") {
91
+ const data = partial.data;
92
+ this.partialOutput = String(data || JSON.stringify(partial));
93
+ }
94
+ else {
95
+ this.partialOutput = String(partial ?? "");
96
+ }
97
+ this.updateDisplay();
98
+ if (this.onRequestRender) {
99
+ this.onRequestRender();
100
+ }
101
+ }
102
+ updateResult(result, isError, elapsedSeconds) {
103
+ this.stopAnimation();
104
+ this.isFinished = true;
105
+ this.isError = isError;
106
+ this.partialOutput = "";
107
+ this.elapsedSeconds =
108
+ elapsedSeconds !== undefined && elapsedSeconds >= 0
109
+ ? elapsedSeconds
110
+ : Math.max(0, (Date.now() - this.startTime) / 1000);
111
+ if (typeof result === "string") {
112
+ this.resultText = result;
113
+ }
114
+ else if (result && typeof result === "object") {
115
+ const data = result.data;
116
+ const error = result.error;
117
+ this.resultText = String(data || error || JSON.stringify(result));
118
+ }
119
+ else {
120
+ this.resultText = String(result ?? "");
121
+ }
122
+ this.box.setBgFn((t) => theme.bg(this.isError ? "toolErrorBg" : "toolSuccessBg", t));
123
+ this.updateDisplay();
124
+ }
125
+ toggleExpanded() {
126
+ this.isExpanded = !this.isExpanded;
127
+ this.updateDisplay();
128
+ }
129
+ render(width) {
130
+ if (!this.isFinished) {
131
+ this.updateDisplay();
132
+ }
133
+ return super.render(width);
134
+ }
135
+ formatArgs() {
136
+ const keys = Object.keys(this.args);
137
+ if (keys.length === 0) {
138
+ return "";
139
+ }
140
+ const parts = keys.map((k) => {
141
+ const val = this.args[k];
142
+ let s = typeof val === "object" ? JSON.stringify(val) : String(val);
143
+ if (s.length > 50) {
144
+ s = s.slice(0, 47) + "...";
145
+ }
146
+ return `${k}=${s}`;
147
+ });
148
+ return `(${parts.join(", ")})`;
149
+ }
150
+ updateDisplay() {
151
+ this.box.clear();
152
+ // 1. Header
153
+ const spinnerChar = SPINNER_FRAMES[this.spinnerFrame] || "⠋";
154
+ let icon = theme.fg("warning", spinnerChar);
155
+ let statusSuffix = "";
156
+ if (this.isFinished) {
157
+ if (this.isError) {
158
+ icon = theme.fg("error", "✗");
159
+ statusSuffix = theme.fg("error", "(失败)");
160
+ }
161
+ else {
162
+ icon = theme.fg("success", "✓");
163
+ statusSuffix = theme.fg("dim", `(${this.elapsedSeconds.toFixed(1)}s)`);
164
+ }
165
+ }
166
+ else {
167
+ statusSuffix = theme.fg("dim", `Elapsed ${this.elapsed.toFixed(1)}s`);
168
+ }
169
+ const argsStr = this.formatArgs();
170
+ const parts = [icon, theme.bold(theme.fg("toolTitle", this.toolName))];
171
+ if (argsStr) {
172
+ parts.push(theme.fg("dim", argsStr));
173
+ }
174
+ if (statusSuffix) {
175
+ parts.push(statusSuffix);
176
+ }
177
+ const titleText = parts.join(" ");
178
+ this.box.addChild(new Text(titleText, 0, 0));
179
+ // 2. Result Output (if finished)
180
+ if (this.isFinished && this.resultText.trim()) {
181
+ this.box.addChild(new Spacer(1));
182
+ const lines = this.resultText.trim().split("\n");
183
+ let renderedText = "";
184
+ if (this.isExpanded || lines.length <= MAX_PREVIEW_LINES) {
185
+ renderedText = lines.map((l) => theme.fg("toolOutput", l)).join("\n");
186
+ }
187
+ else {
188
+ const preview = lines.slice(0, MAX_PREVIEW_LINES);
189
+ const remaining = lines.length - MAX_PREVIEW_LINES;
190
+ renderedText = preview.map((l) => theme.fg("toolOutput", l)).join("\n");
191
+ renderedText += `\n${theme.dim(`... (剩余 ${remaining} 行,按 Ctrl+O 展开查看)`)}`;
192
+ }
193
+ this.box.addChild(new Text(renderedText, 0, 0));
194
+ }
195
+ else if (!this.isFinished && this.partialOutput.trim()) {
196
+ // 正在运行中的实时流式输出预览 (对标 Pi bash renderer options.isPartial)
197
+ this.box.addChild(new Spacer(1));
198
+ const lines = this.partialOutput.trim().split("\n");
199
+ const previewLines = lines.slice(-MAX_PREVIEW_LINES);
200
+ const renderedText = previewLines
201
+ .map((l) => theme.fg("dim", l))
202
+ .join("\n");
203
+ this.box.addChild(new Text(renderedText, 0, 0));
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,40 @@
1
+ import { Container } from "@earendil-works/pi-tui";
2
+ export interface TreeNode {
3
+ id: string;
4
+ parent_id: string | null;
5
+ role: string;
6
+ type: string;
7
+ preview: string;
8
+ is_leaf: boolean;
9
+ is_active: boolean;
10
+ timestamp: number;
11
+ }
12
+ export interface FlattenedTreeNode {
13
+ node: TreeNode;
14
+ depth: number;
15
+ isLast: boolean;
16
+ ancestorContinues: boolean[];
17
+ }
18
+ export declare function buildDAGTree(nodes: TreeNode[]): FlattenedTreeNode[];
19
+ export declare function buildDAGTreePrefix(item: FlattenedTreeNode): string;
20
+ export declare class TreeSelectorComponent extends Container {
21
+ private loadNodes;
22
+ readonly onSelect: (node: TreeNode) => void;
23
+ readonly onCancel: () => void;
24
+ readonly activeLeafId?: string | undefined;
25
+ private requestRender?;
26
+ private listContainer;
27
+ private allNodes;
28
+ private displayNodes;
29
+ private selectedIndex;
30
+ private maxVisible;
31
+ private lastWidth;
32
+ private _focused;
33
+ get focused(): boolean;
34
+ set focused(value: boolean);
35
+ constructor(loadNodes: () => Promise<TreeNode[]>, onSelect: (node: TreeNode) => void, onCancel: () => void, activeLeafId?: string | undefined, requestRender?: (() => void) | undefined);
36
+ render(width: number): string[];
37
+ reload(): Promise<void>;
38
+ updateList(): void;
39
+ handleInput(data: string): void;
40
+ }
@@ -0,0 +1,173 @@
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 function buildDAGTree(nodes) {
6
+ const byId = new Map();
7
+ for (const n of nodes) {
8
+ byId.set(n.id, { node: n, children: [] });
9
+ }
10
+ const roots = [];
11
+ for (const n of nodes) {
12
+ const item = byId.get(n.id);
13
+ if (n.parent_id && byId.has(n.parent_id)) {
14
+ byId.get(n.parent_id).children.push(item);
15
+ }
16
+ else {
17
+ roots.push(item);
18
+ }
19
+ }
20
+ const result = [];
21
+ const walk = (item, depth, ancestorContinues, isLast) => {
22
+ result.push({ node: item.node, depth, isLast, ancestorContinues });
23
+ for (let i = 0; i < item.children.length; i++) {
24
+ const childIsLast = i === item.children.length - 1;
25
+ const continues = depth > 0 ? !isLast : false;
26
+ walk(item.children[i], depth + 1, [...ancestorContinues, continues], childIsLast);
27
+ }
28
+ };
29
+ for (let i = 0; i < roots.length; i++) {
30
+ walk(roots[i], 0, [], i === roots.length - 1);
31
+ }
32
+ return result;
33
+ }
34
+ export function buildDAGTreePrefix(item) {
35
+ if (item.depth === 0) {
36
+ return "■ ";
37
+ }
38
+ const parts = item.ancestorContinues.map((c) => (c ? "│ " : " "));
39
+ const branch = item.isLast ? "└─ " : "├─ ";
40
+ return parts.join("") + branch;
41
+ }
42
+ export class TreeSelectorComponent extends Container {
43
+ loadNodes;
44
+ onSelect;
45
+ onCancel;
46
+ activeLeafId;
47
+ requestRender;
48
+ listContainer;
49
+ allNodes = [];
50
+ displayNodes = [];
51
+ selectedIndex = 0;
52
+ maxVisible = 12;
53
+ lastWidth = 80;
54
+ _focused = false;
55
+ get focused() {
56
+ return this._focused;
57
+ }
58
+ set focused(value) {
59
+ this._focused = value;
60
+ }
61
+ constructor(loadNodes, onSelect, onCancel, activeLeafId, requestRender) {
62
+ super();
63
+ this.loadNodes = loadNodes;
64
+ this.onSelect = onSelect;
65
+ this.onCancel = onCancel;
66
+ this.activeLeafId = activeLeafId;
67
+ this.requestRender = requestRender;
68
+ this.listContainer = new Container();
69
+ this.addChild(new DynamicBorder());
70
+ this.addChild(new Spacer(1));
71
+ this.addChild(new Text(theme.bold("Session Tree (DAG Explorer)"), 0, 0));
72
+ this.addChild(new Text(theme.fg("muted", "Enter: switch to branch · Up/Down: navigate · Esc: exit"), 0, 0));
73
+ this.addChild(new Spacer(1));
74
+ this.addChild(this.listContainer);
75
+ this.addChild(new Spacer(1));
76
+ this.addChild(new Text(theme.fg("dim", " Enter to switch branch · Up/Down to navigate · Escape to cancel"), 0, 0));
77
+ this.addChild(new DynamicBorder());
78
+ void this.reload();
79
+ }
80
+ render(width) {
81
+ this.lastWidth = width;
82
+ return super.render(width);
83
+ }
84
+ async reload() {
85
+ try {
86
+ this.allNodes = await this.loadNodes();
87
+ }
88
+ catch {
89
+ this.allNodes = [];
90
+ }
91
+ this.displayNodes = buildDAGTree(this.allNodes);
92
+ // Default to currently active leaf or the last item
93
+ const initialIdx = this.displayNodes.findIndex((item) => item.node.id === this.activeLeafId ||
94
+ (item.node.is_active && item.node.is_leaf));
95
+ this.selectedIndex =
96
+ initialIdx >= 0 ? initialIdx : Math.max(0, this.displayNodes.length - 1);
97
+ this.updateList();
98
+ if (this.requestRender) {
99
+ this.requestRender();
100
+ }
101
+ }
102
+ updateList() {
103
+ this.listContainer.clear();
104
+ if (this.displayNodes.length === 0) {
105
+ this.listContainer.addChild(new Text(theme.fg("muted", " 当前会话暂无节点记录。"), 0, 0));
106
+ return;
107
+ }
108
+ const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.displayNodes.length - this.maxVisible));
109
+ const endIndex = Math.min(startIndex + this.maxVisible, this.displayNodes.length);
110
+ for (let i = startIndex; i < endIndex; i++) {
111
+ const item = this.displayNodes[i];
112
+ if (!item)
113
+ continue;
114
+ const node = item.node;
115
+ const isSelected = i === this.selectedIndex;
116
+ const cursor = isSelected ? theme.fg("accent", "› ") : " ";
117
+ const activeMarker = node.is_active ? theme.fg("accent", "* ") : " ";
118
+ const branchPrefix = buildDAGTreePrefix(item);
119
+ let roleBadge = `[${node.role}]`;
120
+ if (node.role === "user") {
121
+ roleBadge = theme.fg("accent", roleBadge);
122
+ }
123
+ else if (node.role === "assistant") {
124
+ roleBadge = theme.fg("success", roleBadge);
125
+ }
126
+ else {
127
+ roleBadge = theme.fg("muted", roleBadge);
128
+ }
129
+ let lineText = `${cursor}${activeMarker}${theme.fg("dim", branchPrefix)}${roleBadge} ${node.preview}`;
130
+ if (node.id === this.activeLeafId || (node.is_active && node.is_leaf)) {
131
+ lineText += theme.fg("accent", " [ACTIVE LEAF]");
132
+ }
133
+ if (isSelected) {
134
+ // Pad dynamically to viewport width
135
+ const pad = Math.max(0, this.lastWidth - 4 - visibleWidth(lineText));
136
+ lineText = theme.bg("selectedBg", lineText + " ".repeat(pad));
137
+ }
138
+ this.listContainer.addChild(new Text(lineText, 0, 0));
139
+ }
140
+ if (startIndex > 0 || endIndex < this.displayNodes.length) {
141
+ const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.displayNodes.length})`);
142
+ this.listContainer.addChild(new Text(scrollInfo, 0, 0));
143
+ }
144
+ }
145
+ handleInput(data) {
146
+ if (matchesKey(data, "up")) {
147
+ if (this.displayNodes.length === 0)
148
+ return;
149
+ this.selectedIndex =
150
+ this.selectedIndex === 0
151
+ ? this.displayNodes.length - 1
152
+ : this.selectedIndex - 1;
153
+ this.updateList();
154
+ }
155
+ else if (matchesKey(data, "down")) {
156
+ if (this.displayNodes.length === 0)
157
+ return;
158
+ this.selectedIndex =
159
+ this.selectedIndex === this.displayNodes.length - 1
160
+ ? 0
161
+ : this.selectedIndex + 1;
162
+ this.updateList();
163
+ }
164
+ else if (isEnterKey(data)) {
165
+ const selected = this.displayNodes[this.selectedIndex]?.node;
166
+ if (selected)
167
+ this.onSelect(selected);
168
+ }
169
+ else if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
170
+ this.onCancel();
171
+ }
172
+ }
173
+ }