lsd-pi 1.1.4 → 1.1.6

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 (175) hide show
  1. package/README.md +2 -1
  2. package/dist/headless-ui.js +2 -0
  3. package/dist/onboarding.js +11 -8
  4. package/dist/resources/extensions/async-jobs/async-bash-tool.js +14 -0
  5. package/dist/resources/extensions/async-jobs/await-tool.js +14 -0
  6. package/dist/resources/extensions/async-jobs/cancel-job-tool.js +7 -0
  7. package/dist/resources/extensions/cache-timer/index.js +5 -0
  8. package/dist/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  9. package/dist/resources/extensions/codex-rotate/README.md +9 -3
  10. package/dist/resources/extensions/codex-rotate/commands.js +15 -8
  11. package/dist/resources/extensions/codex-rotate/index.js +17 -8
  12. package/dist/resources/extensions/memory/auto-extract.js +196 -80
  13. package/dist/resources/extensions/memory/dream.js +86 -19
  14. package/dist/resources/extensions/shared/rtk.js +89 -87
  15. package/dist/resources/extensions/subagent/index.js +33 -7
  16. package/dist/startup-model-validation.js +12 -2
  17. package/dist/update-check.js +2 -2
  18. package/dist/update-cmd.js +3 -3
  19. package/dist/welcome-screen.js +43 -14
  20. package/package.json +3 -2
  21. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts +2 -0
  22. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts.map +1 -0
  23. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js +46 -0
  24. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js.map +1 -0
  25. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +8 -0
  26. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/core/agent-session.js +43 -4
  28. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +3 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  31. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  33. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  35. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts +48 -0
  37. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts.map +1 -0
  38. package/packages/pi-coding-agent/dist/core/pty-executor.js +173 -0
  39. package/packages/pi-coding-agent/dist/core/pty-executor.js.map +1 -0
  40. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/sdk.js +16 -3
  42. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  44. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/settings-manager.js +18 -0
  46. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/tool-approval.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/tool-approval.js +2 -2
  49. package/packages/pi-coding-agent/dist/core/tool-approval.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +7 -0
  51. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/tools/index.js +23 -2
  53. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +50 -0
  55. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -0
  56. package/packages/pi-coding-agent/dist/core/tools/pty.js +289 -0
  57. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +36 -22
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +3 -5
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +23 -62
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -4
  68. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -4
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts +39 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +182 -0
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +6 -0
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +36 -0
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -4
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -2
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +106 -77
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +2 -5
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +4 -13
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +11 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +49 -13
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +2 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +3 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +27 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +251 -39
  107. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +2 -2
  109. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts +10 -0
  111. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/utils/terminal-screen.js +67 -0
  113. package/packages/pi-coding-agent/dist/utils/terminal-screen.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts +7 -0
  115. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js +67 -0
  117. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js.map +1 -0
  118. package/packages/pi-coding-agent/package.json +9 -4
  119. package/packages/pi-coding-agent/src/core/agent-session.clear-queue.test.ts +50 -0
  120. package/packages/pi-coding-agent/src/core/agent-session.ts +50 -4
  121. package/packages/pi-coding-agent/src/core/extensions/types.ts +1 -1
  122. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  123. package/packages/pi-coding-agent/src/core/pty-executor.ts +229 -0
  124. package/packages/pi-coding-agent/src/core/sdk.ts +16 -3
  125. package/packages/pi-coding-agent/src/core/settings-manager.ts +27 -0
  126. package/packages/pi-coding-agent/src/core/tool-approval.ts +2 -2
  127. package/packages/pi-coding-agent/src/core/tools/index.ts +35 -2
  128. package/packages/pi-coding-agent/src/core/tools/pty.ts +354 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +37 -24
  130. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +22 -70
  131. package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -3
  132. package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -3
  133. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +224 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +45 -0
  135. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -3
  136. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +104 -81
  137. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +5 -19
  138. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +55 -13
  139. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
  140. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +2 -0
  141. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +3 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +296 -48
  143. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +2 -2
  144. package/packages/pi-coding-agent/src/utils/terminal-screen.ts +77 -0
  145. package/packages/pi-coding-agent/src/utils/terminal-serializer.ts +72 -0
  146. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts +2 -0
  147. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts.map +1 -0
  148. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js +105 -0
  149. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js.map +1 -0
  150. package/packages/pi-tui/dist/components/editor.d.ts +4 -0
  151. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  152. package/packages/pi-tui/dist/components/editor.js +57 -3
  153. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  154. package/packages/pi-tui/dist/components/loader.d.ts +26 -6
  155. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  156. package/packages/pi-tui/dist/components/loader.js +178 -18
  157. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  158. package/packages/pi-tui/src/components/editor.ts +65 -3
  159. package/packages/pi-tui/src/components/loader.ts +196 -19
  160. package/pkg/dist/modes/interactive/theme/themes.js +2 -2
  161. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  162. package/pkg/package.json +1 -1
  163. package/src/resources/extensions/async-jobs/async-bash-tool.ts +13 -0
  164. package/src/resources/extensions/async-jobs/await-tool.ts +13 -0
  165. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +8 -0
  166. package/src/resources/extensions/cache-timer/index.ts +102 -96
  167. package/src/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  168. package/src/resources/extensions/codex-rotate/README.md +9 -3
  169. package/src/resources/extensions/codex-rotate/commands.ts +335 -329
  170. package/src/resources/extensions/codex-rotate/index.ts +85 -75
  171. package/src/resources/extensions/memory/auto-extract.ts +330 -204
  172. package/src/resources/extensions/memory/dream.ts +88 -21
  173. package/src/resources/extensions/memory/tests/auto-extract.test.ts +200 -144
  174. package/src/resources/extensions/shared/rtk.js +112 -0
  175. package/src/resources/extensions/subagent/index.ts +35 -6
@@ -0,0 +1,354 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { AgentTool } from "@gsd/pi-agent-core";
3
+ import { type Static, Type } from "@sinclair/typebox";
4
+ import { executeBashInPty, type PtyExecutionSession } from "../pty-executor.js";
5
+ import {
6
+ createHeadlessTerminal,
7
+ snapshotTerminalBufferText,
8
+ snapshotTerminalViewportText,
9
+ type HeadlessTerminal,
10
+ } from "../../utils/terminal-screen.js";
11
+
12
+ const ptyStartSchema = Type.Object({
13
+ command: Type.String({ description: "Shell command to start inside the PTY" }),
14
+ cols: Type.Optional(Type.Number({ description: "Terminal width in columns (default 80)" })),
15
+ rows: Type.Optional(Type.Number({ description: "Terminal height in rows (default 24)" })),
16
+ loginShell: Type.Optional(Type.Boolean({ description: "Whether to run the command in a login shell" })),
17
+ });
18
+
19
+ const ptySendSchema = Type.Object({
20
+ sessionId: Type.String({ description: "PTY session id from pty_start" }),
21
+ input: Type.String({ description: "Text or control sequence to send, e.g. \"y\\r\" or \"\\u001b[A\"" }),
22
+ });
23
+
24
+ const ptyReadSchema = Type.Object({
25
+ sessionId: Type.String({ description: "PTY session id from pty_start" }),
26
+ view: Type.Optional(Type.Union([
27
+ Type.Literal("viewport"),
28
+ Type.Literal("buffer"),
29
+ ], { description: "Read the visible screen (viewport) or the full logical buffer" })),
30
+ });
31
+
32
+ const ptyWaitSchema = Type.Object({
33
+ sessionId: Type.String({ description: "PTY session id from pty_start" }),
34
+ text: Type.Optional(Type.String({ description: "Wait until the viewport or buffer contains this text" })),
35
+ view: Type.Optional(Type.Union([
36
+ Type.Literal("viewport"),
37
+ Type.Literal("buffer"),
38
+ ], { description: "Where to search for the text (default viewport)" })),
39
+ timeoutMs: Type.Optional(Type.Number({ description: "How long to wait before timing out (default 30000)" })),
40
+ stableMs: Type.Optional(Type.Number({ description: "If text is omitted, wait until the screen stops changing for this long (default 800)" })),
41
+ });
42
+
43
+ const ptyResizeSchema = Type.Object({
44
+ sessionId: Type.String({ description: "PTY session id from pty_start" }),
45
+ cols: Type.Number({ description: "Terminal width in columns" }),
46
+ rows: Type.Number({ description: "Terminal height in rows" }),
47
+ });
48
+
49
+ const ptyKillSchema = Type.Object({
50
+ sessionId: Type.String({ description: "PTY session id from pty_start" }),
51
+ });
52
+
53
+ export type PtyStartInput = Static<typeof ptyStartSchema>;
54
+ export type PtySendInput = Static<typeof ptySendSchema>;
55
+ export type PtyReadInput = Static<typeof ptyReadSchema>;
56
+ export type PtyWaitInput = Static<typeof ptyWaitSchema>;
57
+ export type PtyResizeInput = Static<typeof ptyResizeSchema>;
58
+ export type PtyKillInput = Static<typeof ptyKillSchema>;
59
+
60
+ interface ManagedPtySession {
61
+ id: string;
62
+ command: string;
63
+ pty: PtyExecutionSession;
64
+ terminal: HeadlessTerminal;
65
+ writeChain: Promise<void>;
66
+ completed: boolean;
67
+ cancelled: boolean;
68
+ exitCode?: number;
69
+ createdAt: number;
70
+ lastUpdateAt: number;
71
+ }
72
+
73
+ export interface PtyToolDetails {
74
+ sessionId: string;
75
+ pid?: number;
76
+ completed?: boolean;
77
+ cancelled?: boolean;
78
+ exitCode?: number;
79
+ view?: "viewport" | "buffer";
80
+ screenText?: string;
81
+ }
82
+
83
+ class PtySessionManager {
84
+ private sessions = new Map<string, ManagedPtySession>();
85
+
86
+ constructor(private cwd: string) {}
87
+
88
+ async start(command: string, options?: { cols?: number; rows?: number; loginShell?: boolean }): Promise<ManagedPtySession> {
89
+ const id = `pty_${randomUUID().slice(0, 8)}`;
90
+ const terminal = createHeadlessTerminal(options?.cols ?? 80, options?.rows ?? 24, 10000);
91
+ const session: ManagedPtySession = {
92
+ id,
93
+ command,
94
+ pty: undefined as unknown as PtyExecutionSession,
95
+ terminal,
96
+ writeChain: Promise.resolve(),
97
+ completed: false,
98
+ cancelled: false,
99
+ createdAt: Date.now(),
100
+ lastUpdateAt: Date.now(),
101
+ };
102
+ this.sessions.set(id, session);
103
+
104
+ let pty: PtyExecutionSession;
105
+ try {
106
+ pty = await executeBashInPty(command, {
107
+ cols: options?.cols,
108
+ rows: options?.rows,
109
+ cwd: this.cwd,
110
+ loginShell: options?.loginShell,
111
+ onChunk: (chunk) => {
112
+ const current = this.sessions.get(id);
113
+ if (!current) return;
114
+ current.lastUpdateAt = Date.now();
115
+ current.writeChain = current.writeChain.then(
116
+ () =>
117
+ new Promise<void>((resolve) => {
118
+ current.terminal.write(chunk, () => resolve());
119
+ }),
120
+ );
121
+ },
122
+ });
123
+ } catch (error) {
124
+ this.sessions.delete(id);
125
+ throw error;
126
+ }
127
+ session.pty = pty;
128
+
129
+ pty.result
130
+ .then((result) => {
131
+ const current = this.sessions.get(id);
132
+ if (!current) return;
133
+ current.completed = true;
134
+ current.cancelled = result.cancelled;
135
+ current.exitCode = result.exitCode;
136
+ current.lastUpdateAt = Date.now();
137
+ })
138
+ .catch(() => {
139
+ const current = this.sessions.get(id);
140
+ if (!current) return;
141
+ current.completed = true;
142
+ current.lastUpdateAt = Date.now();
143
+ });
144
+
145
+ return session;
146
+ }
147
+
148
+ get(sessionId: string): ManagedPtySession {
149
+ const session = this.sessions.get(sessionId);
150
+ if (!session) {
151
+ throw new Error(`PTY session not found: ${sessionId}`);
152
+ }
153
+ return session;
154
+ }
155
+
156
+ async send(sessionId: string, input: string): Promise<ManagedPtySession> {
157
+ const session = this.get(sessionId);
158
+ if (session.completed || !session.pty.handle.isActive()) {
159
+ throw new Error(`PTY session is no longer active: ${sessionId}`);
160
+ }
161
+ session.pty.handle.write(input);
162
+ session.lastUpdateAt = Date.now();
163
+ await new Promise((resolve) => setTimeout(resolve, 50));
164
+ return session;
165
+ }
166
+
167
+ async resize(sessionId: string, cols: number, rows: number): Promise<ManagedPtySession> {
168
+ const session = this.get(sessionId);
169
+ const safeCols = Math.max(20, cols);
170
+ const safeRows = Math.max(5, rows);
171
+ session.pty.handle.resize(safeCols, safeRows);
172
+ session.terminal.resize(safeCols, safeRows);
173
+ session.lastUpdateAt = Date.now();
174
+ return session;
175
+ }
176
+
177
+ async kill(sessionId: string): Promise<ManagedPtySession> {
178
+ const session = this.get(sessionId);
179
+ if (session.pty.handle.isActive()) {
180
+ session.pty.handle.kill();
181
+ }
182
+ session.cancelled = true;
183
+ session.completed = true;
184
+ session.lastUpdateAt = Date.now();
185
+ return session;
186
+ }
187
+
188
+ async read(sessionId: string, view: "viewport" | "buffer" = "viewport"): Promise<{ session: ManagedPtySession; text: string }> {
189
+ const session = this.get(sessionId);
190
+ await session.writeChain;
191
+ const text = view === "buffer"
192
+ ? snapshotTerminalBufferText(session.terminal)
193
+ : snapshotTerminalViewportText(session.terminal);
194
+ return { session, text };
195
+ }
196
+
197
+ async wait(
198
+ sessionId: string,
199
+ options?: { text?: string; view?: "viewport" | "buffer"; timeoutMs?: number; stableMs?: number },
200
+ ): Promise<{ session: ManagedPtySession; text: string }> {
201
+ const timeoutMs = Math.max(50, options?.timeoutMs ?? 30000);
202
+ const stableMs = Math.max(50, options?.stableMs ?? 800);
203
+ const view = options?.view ?? "viewport";
204
+ const start = Date.now();
205
+ let lastSnapshot = "";
206
+ let stableSince = Date.now();
207
+
208
+ while (Date.now() - start < timeoutMs) {
209
+ const { text } = await this.read(sessionId, view);
210
+ if (options?.text) {
211
+ if (text.includes(options.text)) {
212
+ return { session: this.get(sessionId), text };
213
+ }
214
+ if (this.get(sessionId).completed) {
215
+ return { session: this.get(sessionId), text };
216
+ }
217
+ } else {
218
+ if (text !== lastSnapshot) {
219
+ lastSnapshot = text;
220
+ stableSince = Date.now();
221
+ } else if (Date.now() - stableSince >= stableMs || this.get(sessionId).completed) {
222
+ return { session: this.get(sessionId), text };
223
+ }
224
+ }
225
+ await new Promise((resolve) => setTimeout(resolve, 100));
226
+ }
227
+
228
+ await this.read(sessionId, view);
229
+ throw new Error(
230
+ options?.text
231
+ ? `Timed out waiting for text in PTY session ${sessionId}: ${options.text}`
232
+ : `Timed out waiting for PTY session ${sessionId} to stabilize`,
233
+ );
234
+ }
235
+ }
236
+
237
+ function formatSessionSummary(session: ManagedPtySession): string {
238
+ const bits = [`session ${session.id}`, `pid ${session.pty.handle.pid}`];
239
+ if (session.completed) {
240
+ bits.push(session.cancelled ? "cancelled" : `completed${session.exitCode !== undefined ? ` exit ${session.exitCode}` : ""}`);
241
+ } else {
242
+ bits.push("running");
243
+ }
244
+ return bits.join(" · ");
245
+ }
246
+
247
+ function buildReadText(session: ManagedPtySession, view: "viewport" | "buffer", text: string): string {
248
+ const header = `[${formatSessionSummary(session)} · ${view}]`;
249
+ return text ? `${header}\n${text}` : `${header}\n(no visible text)`;
250
+ }
251
+
252
+ export function createPtyTools(cwd: string): Record<string, AgentTool<any, PtyToolDetails>> {
253
+ const manager = new PtySessionManager(cwd);
254
+
255
+ const ptyStartTool: AgentTool<typeof ptyStartSchema, PtyToolDetails> = {
256
+ name: "pty_start",
257
+ label: "pty_start",
258
+ description: "Start an agent-controlled interactive PTY session. Use this when a command requires terminal interaction, prompts, or a full-screen TUI. Returns a sessionId for follow-up pty_send/pty_read/pty_wait/pty_kill calls.",
259
+ parameters: ptyStartSchema,
260
+ execute: async (_toolCallId, { command, cols, rows, loginShell }) => {
261
+ const session = await manager.start(command, { cols, rows, loginShell });
262
+ const { text } = await manager.read(session.id, "viewport");
263
+ return {
264
+ content: [{ type: "text", text: `Started PTY session ${session.id} for: ${command}\n${buildReadText(session, "viewport", text)}` }],
265
+ details: { sessionId: session.id, pid: session.pty.handle.pid, completed: session.completed, cancelled: session.cancelled, exitCode: session.exitCode, view: "viewport", screenText: text },
266
+ };
267
+ },
268
+ };
269
+
270
+ const ptySendTool: AgentTool<typeof ptySendSchema, PtyToolDetails> = {
271
+ name: "pty_send",
272
+ label: "pty_send",
273
+ description: "Send text or control sequences to an existing PTY session, e.g. \"y\\r\" or \"\\u001b[A\".",
274
+ parameters: ptySendSchema,
275
+ execute: async (_toolCallId, { sessionId, input }) => {
276
+ const session = await manager.send(sessionId, input);
277
+ const { text } = await manager.read(sessionId, "viewport");
278
+ return {
279
+ content: [{ type: "text", text: `Sent input to ${sessionId}: ${JSON.stringify(input)}\n${buildReadText(session, "viewport", text)}` }],
280
+ details: { sessionId, pid: session.pty.handle.pid, completed: session.completed, cancelled: session.cancelled, exitCode: session.exitCode, view: "viewport", screenText: text },
281
+ };
282
+ },
283
+ };
284
+
285
+ const ptyReadTool: AgentTool<typeof ptyReadSchema, PtyToolDetails> = {
286
+ name: "pty_read",
287
+ label: "pty_read",
288
+ description: "Read the current PTY screen state. Use view='viewport' for the visible screen or view='buffer' for the full logical buffer.",
289
+ parameters: ptyReadSchema,
290
+ execute: async (_toolCallId, { sessionId, view }) => {
291
+ const effectiveView = view ?? "viewport";
292
+ const { session, text } = await manager.read(sessionId, effectiveView);
293
+ return {
294
+ content: [{ type: "text", text: buildReadText(session, effectiveView, text) }],
295
+ details: { sessionId, pid: session.pty.handle.pid, completed: session.completed, cancelled: session.cancelled, exitCode: session.exitCode, view: effectiveView, screenText: text },
296
+ };
297
+ },
298
+ };
299
+
300
+ const ptyWaitTool: AgentTool<typeof ptyWaitSchema, PtyToolDetails> = {
301
+ name: "pty_wait",
302
+ label: "pty_wait",
303
+ description: "Wait until text appears in a PTY session or until the PTY screen stops changing. Useful between pty_send calls.",
304
+ parameters: ptyWaitSchema,
305
+ execute: async (_toolCallId, { sessionId, text, view, timeoutMs, stableMs }) => {
306
+ const effectiveView = view ?? "viewport";
307
+ const result = await manager.wait(sessionId, { text, view: effectiveView, timeoutMs, stableMs });
308
+ return {
309
+ content: [{ type: "text", text: buildReadText(result.session, effectiveView, result.text) }],
310
+ details: { sessionId, pid: result.session.pty.handle.pid, completed: result.session.completed, cancelled: result.session.cancelled, exitCode: result.session.exitCode, view: effectiveView, screenText: result.text },
311
+ };
312
+ },
313
+ };
314
+
315
+ const ptyResizeTool: AgentTool<typeof ptyResizeSchema, PtyToolDetails> = {
316
+ name: "pty_resize",
317
+ label: "pty_resize",
318
+ description: "Resize an active PTY session to the given terminal dimensions.",
319
+ parameters: ptyResizeSchema,
320
+ execute: async (_toolCallId, { sessionId, cols, rows }) => {
321
+ const session = await manager.resize(sessionId, cols, rows);
322
+ const { text } = await manager.read(sessionId, "viewport");
323
+ return {
324
+ content: [{ type: "text", text: `Resized ${sessionId} to ${cols}x${rows}\n${buildReadText(session, "viewport", text)}` }],
325
+ details: { sessionId, pid: session.pty.handle.pid, completed: session.completed, cancelled: session.cancelled, exitCode: session.exitCode, view: "viewport", screenText: text },
326
+ };
327
+ },
328
+ };
329
+
330
+ const ptyKillTool: AgentTool<typeof ptyKillSchema, PtyToolDetails> = {
331
+ name: "pty_kill",
332
+ label: "pty_kill",
333
+ description: "Terminate an agent-controlled PTY session.",
334
+ parameters: ptyKillSchema,
335
+ execute: async (_toolCallId, { sessionId }) => {
336
+ const session = await manager.kill(sessionId);
337
+ return {
338
+ content: [{ type: "text", text: `Terminated PTY session ${sessionId}. ${formatSessionSummary(session)}` }],
339
+ details: { sessionId, pid: session.pty.handle.pid, completed: session.completed, cancelled: session.cancelled, exitCode: session.exitCode },
340
+ };
341
+ },
342
+ };
343
+
344
+ return {
345
+ pty_start: ptyStartTool,
346
+ pty_send: ptySendTool,
347
+ pty_read: ptyReadTool,
348
+ pty_wait: ptyWaitTool,
349
+ pty_resize: ptyResizeTool,
350
+ pty_kill: ptyKillTool,
351
+ };
352
+ }
353
+
354
+ export const ptyToolNames = ["pty_start", "pty_send", "pty_read", "pty_wait", "pty_resize", "pty_kill"] as const;
@@ -9,6 +9,7 @@ import { formatTimestamp, type TimestampFormat } from "./timestamp.js";
9
9
  export class AssistantMessageComponent extends Container {
10
10
  private contentContainer: Container;
11
11
  private hideThinkingBlock: boolean;
12
+ private thinkingLevel: string;
12
13
  private markdownTheme: MarkdownTheme;
13
14
  private lastMessage?: AssistantMessage;
14
15
  private timestampFormat: TimestampFormat;
@@ -18,10 +19,12 @@ export class AssistantMessageComponent extends Container {
18
19
  hideThinkingBlock = false,
19
20
  markdownTheme: MarkdownTheme = getMarkdownTheme(),
20
21
  timestampFormat: TimestampFormat = "date-time-iso",
22
+ thinkingLevel = "off",
21
23
  ) {
22
24
  super();
23
25
 
24
26
  this.hideThinkingBlock = hideThinkingBlock;
27
+ this.thinkingLevel = thinkingLevel;
25
28
  this.markdownTheme = markdownTheme;
26
29
  this.timestampFormat = timestampFormat;
27
30
 
@@ -45,51 +48,61 @@ export class AssistantMessageComponent extends Container {
45
48
  this.hideThinkingBlock = hide;
46
49
  }
47
50
 
51
+ setThinkingLevel(level: string): void {
52
+ this.thinkingLevel = level;
53
+ }
54
+
48
55
  updateContent(message: AssistantMessage): void {
49
56
  this.lastMessage = message;
50
57
 
51
58
  // Clear content container
52
59
  this.contentContainer.clear();
53
60
 
54
- const hasVisibleContent = message.content.some(
55
- (c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
56
- );
61
+ const hasVisibleContent = message.content.some((c) => {
62
+ if (c.type === "text") return Boolean(c.text.trim());
63
+ if (c.type === "thinking") return !this.hideThinkingBlock && Boolean(c.thinking.trim());
64
+ return false;
65
+ });
57
66
 
58
67
  if (hasVisibleContent) {
59
68
  this.contentContainer.addChild(new Spacer(1));
60
69
  }
61
70
 
62
71
  // Render content in order
72
+ let markerAdded = false;
73
+ const responseMarker = `${theme.fg("accent", "●")} `;
63
74
  for (let i = 0; i < message.content.length; i++) {
64
75
  const content = message.content[i];
65
76
  if (content.type === "text" && content.text.trim()) {
66
77
  // Assistant text messages with no background - trim the text
67
78
  // Set paddingY=0 to avoid extra spacing before tool executions
68
- this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, this.markdownTheme));
79
+ const text = content.text.trim();
80
+ const withMarker = markerAdded ? text : `${responseMarker}${text}`;
81
+ this.contentContainer.addChild(new Markdown(withMarker, 1, 0, this.markdownTheme));
82
+ markerAdded = true;
69
83
  } else if (content.type === "thinking" && content.thinking.trim()) {
84
+ if (this.hideThinkingBlock) {
85
+ // Hide thinking content entirely when hide-thinking is enabled.
86
+ continue;
87
+ }
88
+
70
89
  // Add spacing only when another visible assistant content block follows.
71
90
  // This avoids a superfluous blank line before separately-rendered tool execution blocks.
72
- const hasVisibleContentAfter = message.content
73
- .slice(i + 1)
74
- .some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
91
+ const hasVisibleContentAfter = message.content.slice(i + 1).some((c) => {
92
+ if (c.type === "text") return Boolean(c.text.trim());
93
+ if (c.type === "thinking") return !this.hideThinkingBlock && Boolean(c.thinking.trim());
94
+ return false;
95
+ });
75
96
 
76
- if (this.hideThinkingBlock) {
77
- // Show static "Thinking..." label when hidden
78
- this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0));
79
- if (hasVisibleContentAfter) {
80
- this.contentContainer.addChild(new Spacer(1));
81
- }
82
- } else {
83
- // Thinking traces in thinkingText color, italic
84
- this.contentContainer.addChild(
85
- new Markdown(content.thinking.trim(), 1, 0, this.markdownTheme, {
86
- color: (text: string) => theme.fg("thinkingText", text),
87
- italic: true,
88
- }),
89
- );
90
- if (hasVisibleContentAfter) {
91
- this.contentContainer.addChild(new Spacer(1));
92
- }
97
+ // Thinking traces in thinkingText color, italic
98
+ this.contentContainer.addChild(
99
+ new Markdown(content.thinking.trim(), 1, 0, this.markdownTheme, {
100
+ color: (text: string) => theme.fg("thinkingText", text),
101
+ italic: true,
102
+ }),
103
+ );
104
+ if (hasVisibleContentAfter) {
105
+ this.contentContainer.addChild(new Spacer(1));
93
106
  }
94
107
  }
95
108
  }
@@ -3,21 +3,17 @@
3
3
  */
4
4
 
5
5
  import { Container, Loader, Text, type TUI } from "@gsd/pi-tui";
6
- import stripAnsi from "strip-ansi";
7
6
  import {
8
7
  DEFAULT_MAX_BYTES,
9
8
  DEFAULT_MAX_LINES,
10
9
  type TruncationResult,
11
10
  truncateTail,
12
11
  } from "../../../core/tools/truncate.js";
12
+ import { renderTerminalLines } from "../../../utils/terminal-serializer.js";
13
13
  import { theme, type ThemeColor } from "../theme/theme.js";
14
- import { DynamicBorder } from "./dynamic-border.js";
15
14
  import { editorKey, keyHint } from "./keybinding-hints.js";
16
15
  import { truncateToVisualLines } from "./visual-truncate.js";
17
16
 
18
- // Flash interval for RTK badge animation (ms)
19
- const RTK_FLASH_INTERVAL_MS = 400;
20
-
21
17
  // Preview line limit when not expanded (matches tool execution behavior)
22
18
  const PREVIEW_LINES = 20;
23
19
 
@@ -26,6 +22,7 @@ type ToolOutputMode = "minimal" | "normal";
26
22
  export class BashExecutionComponent extends Container {
27
23
  private command: string;
28
24
  private outputLines: string[] = [];
25
+ private rawOutput = "";
29
26
  private status: "running" | "complete" | "cancelled" | "error" = "running";
30
27
  private exitCode: number | undefined = undefined;
31
28
  private loader: Loader;
@@ -37,10 +34,7 @@ export class BashExecutionComponent extends Container {
37
34
  private ui: TUI;
38
35
  private colorKey: ThemeColor;
39
36
  private sandboxed: boolean;
40
- private rtkActive: boolean;
41
- private rtkFlashOn = true;
42
- private rtkFlashTimer: NodeJS.Timeout | null = null;
43
- // Dedicated header node — updated in-place to avoid full container rebuild on flash tick
37
+ // Dedicated header node
44
38
  private headerText: Text;
45
39
 
46
40
  constructor(
@@ -48,28 +42,23 @@ export class BashExecutionComponent extends Container {
48
42
  ui: TUI,
49
43
  excludeFromContext = false,
50
44
  renderMode: ToolOutputMode = "normal",
51
- rtkActive = false,
45
+ _rtkActive = false,
52
46
  sandboxed = false,
53
47
  ) {
54
48
  super();
55
49
  this.command = command;
56
50
  this.ui = ui;
57
51
  this.renderMode = renderMode;
58
- this.rtkActive = rtkActive;
59
52
  this.sandboxed = sandboxed;
60
53
 
61
- // Use dim border for excluded-from-context commands (!! prefix)
54
+ // Use dim tone for excluded-from-context commands (!! prefix)
62
55
  this.colorKey = (excludeFromContext ? "dim" : "bashMode") as ThemeColor;
63
- const borderColor = (str: string) => theme.fg(this.colorKey, str);
64
-
65
- // Top border
66
- this.addChild(new DynamicBorder(borderColor));
67
56
 
68
- // Content container (holds dynamic content between borders)
57
+ // Content container
69
58
  this.contentContainer = new Container();
70
59
  this.addChild(this.contentContainer);
71
60
 
72
- // Dedicated header Text node — updated directly for flash without full rebuild
61
+ // Header Text node
73
62
  this.headerText = new Text(this.buildHeaderText(), 1, 0);
74
63
  this.contentContainer.addChild(this.headerText);
75
64
 
@@ -81,33 +70,14 @@ export class BashExecutionComponent extends Container {
81
70
  `Running... (${editorKey("selectCancel")} to cancel)`, // Plain text for loader
82
71
  );
83
72
  this.contentContainer.addChild(this.loader);
84
-
85
- // Bottom border
86
- this.addChild(new DynamicBorder(borderColor));
87
-
88
- // Start RTK flash animation if active
89
- if (this.rtkActive) {
90
- this.rtkFlashTimer = setInterval(() => {
91
- this.rtkFlashOn = !this.rtkFlashOn;
92
- // Only update the header node — no full container rebuild
93
- this.headerText.setText(this.buildHeaderText());
94
- this.ui.requestRender();
95
- }, RTK_FLASH_INTERVAL_MS);
96
- }
97
73
  }
98
74
 
99
- /** Build the header line text including the RTK badge when active. */
75
+ /** Build the header line text. */
100
76
  private buildHeaderText(): string {
101
- let text = theme.fg(this.colorKey, theme.bold(`$ ${this.command}`));
77
+ let text = `${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg(this.colorKey, theme.bold(`$ ${this.command}`))}`;
102
78
  if (this.sandboxed) {
103
79
  text += ` ${theme.fg("success", "[sandboxed]")}`;
104
80
  }
105
- if (this.rtkActive) {
106
- const badge = this.rtkFlashOn
107
- ? theme.fg("accent", "$ RTK")
108
- : theme.fg("dim", "$ RTK");
109
- text = `${text} ${badge}`;
110
- }
111
81
  return text;
112
82
  }
113
83
 
@@ -116,10 +86,6 @@ export class BashExecutionComponent extends Container {
116
86
  * from the tree before setComplete() has been called (e.g. on clear/cancel).
117
87
  */
118
88
  dispose(): void {
119
- if (this.rtkFlashTimer) {
120
- clearInterval(this.rtkFlashTimer);
121
- this.rtkFlashTimer = null;
122
- }
123
89
  this.loader.dispose();
124
90
  }
125
91
 
@@ -144,20 +110,9 @@ export class BashExecutionComponent extends Container {
144
110
  }
145
111
 
146
112
  appendOutput(chunk: string): void {
147
- // Strip ANSI codes and normalize line endings
148
- // Note: binary data is already sanitized in tui-renderer.ts executeBashCommand
149
- const clean = stripAnsi(chunk).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
150
-
151
- // Append to output lines
152
- const newLines = clean.split("\n");
153
- if (this.outputLines.length > 0 && newLines.length > 0) {
154
- // Append first chunk to last line (incomplete line continuation)
155
- this.outputLines[this.outputLines.length - 1] += newLines[0];
156
- this.outputLines.push(...newLines.slice(1));
157
- } else {
158
- this.outputLines.push(...newLines);
159
- }
160
-
113
+ // Strip ANSI codes and preserve carriage-return semantics for display.
114
+ this.rawOutput += chunk;
115
+ this.outputLines = renderTerminalLines(this.rawOutput);
161
116
  this.updateDisplay();
162
117
  }
163
118
 
@@ -183,15 +138,6 @@ export class BashExecutionComponent extends Container {
183
138
  // Stop loader
184
139
  this.loader.stop();
185
140
 
186
- // Stop RTK flash — settle to steady dim state
187
- if (this.rtkFlashTimer) {
188
- clearInterval(this.rtkFlashTimer);
189
- this.rtkFlashTimer = null;
190
- this.rtkFlashOn = false;
191
- // Final header update to ensure dim badge is shown
192
- this.headerText.setText(this.buildHeaderText());
193
- }
194
-
195
141
  this.updateDisplay();
196
142
  }
197
143
 
@@ -223,7 +169,7 @@ export class BashExecutionComponent extends Container {
223
169
  const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n");
224
170
  this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
225
171
  } else if (this.renderMode === "minimal") {
226
- this.contentContainer.addChild(new Text(`\n${theme.fg("muted", `(${keyHint("expandTools", "to expand")})`)}`, 1, 0));
172
+ // collapsed — no inline hint needed (shown in editor bottom border)
227
173
  } else {
228
174
  // Use shared visual truncation utility
229
175
  const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n");
@@ -243,14 +189,20 @@ export class BashExecutionComponent extends Container {
243
189
  } else {
244
190
  const statusParts: string[] = [];
245
191
 
246
- // Show how many lines are hidden (collapsed preview)
247
- if (hiddenLineCount > 0) {
192
+ // Show expand/collapse hint whenever there is output
193
+ if (availableLines.length > 0) {
248
194
  if (this.expanded) {
249
195
  statusParts.push(`(${keyHint("expandTools", "to collapse")})`);
250
- } else {
196
+ } else if (this.renderMode === "minimal") {
197
+ statusParts.push(`(${keyHint("expandTools", "to expand")})`);
198
+ } else if (hiddenLineCount > 0) {
199
+ // Normal mode: show line count + hint
251
200
  statusParts.push(
252
201
  `${theme.fg("muted", `... ${hiddenLineCount} more lines`)} (${keyHint("expandTools", "to expand")})`,
253
202
  );
203
+ } else {
204
+ // Normal mode: all preview lines visible, still offer expand
205
+ statusParts.push(`(${keyHint("expandTools", "to expand")})`);
254
206
  }
255
207
  }
256
208
 
@@ -46,9 +46,7 @@ export class BranchSummaryMessageComponent extends Box {
46
46
  } else {
47
47
  this.addChild(
48
48
  new Text(
49
- theme.fg("customMessageText", "Branch summary (") +
50
- theme.fg("dim", editorKey("expandTools")) +
51
- theme.fg("customMessageText", " to expand)"),
49
+ theme.fg("customMessageText", "Branch summary"),
52
50
  0,
53
51
  0,
54
52
  ),
@@ -47,9 +47,7 @@ export class CompactionSummaryMessageComponent extends Box {
47
47
  } else {
48
48
  this.addChild(
49
49
  new Text(
50
- theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (`) +
51
- theme.fg("dim", editorKey("expandTools")) +
52
- theme.fg("customMessageText", " to expand)"),
50
+ theme.fg("customMessageText", `Compacted from ${tokenStr} tokens`),
53
51
  0,
54
52
  0,
55
53
  ),