opencode-miniterm 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,8 +1,9 @@
1
- # OpenCode MiniTerm - Agent Guidelines
1
+ # OpenCode Miniterm - Agent Guidelines
2
2
 
3
3
  ## Build & Development Commands
4
4
 
5
5
  ### Run the application
6
+
6
7
  ```bash
7
8
  bun run src/index.ts
8
9
  # Or just:
@@ -10,46 +11,55 @@ bun src/index.ts
10
11
  ```
11
12
 
12
13
  ### Build (when bundler is added)
14
+
13
15
  ```bash
14
16
  bun build src/index.ts --outdir dist
15
17
  ```
16
18
 
17
19
  ### Testing
20
+
18
21
  No test framework is currently configured. Add one of these to package.json:
22
+
19
23
  - **Bun Test**: `bun test` (recommended - built-in, fast)
20
24
  - **Jest**: `npm test` or `bun run test`
21
25
  - **Vitest**: `vitest`
22
26
 
23
27
  To run a single test (once configured):
28
+
24
29
  - Bun Test: `bun test --test-name-pattern "testName"`
25
30
  - Jest: `npm test -- testName`
26
31
  - Vitest: `vitest run testName`
27
32
 
28
33
  ### Linting & Formatting (recommended additions)
34
+
29
35
  Install and configure these tools:
36
+
30
37
  ```bash
31
38
  bun add -d eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier
32
39
  ```
33
40
 
34
41
  Commands to add to package.json:
42
+
35
43
  ```json
36
44
  {
37
- "lint": "eslint src --ext .ts",
38
- "lint:fix": "eslint src --ext .ts --fix",
39
- "format": "prettier --write \"src/**/*.ts\"",
40
- "format:check": "prettier --check \"src/**/*.ts\"",
41
- "typecheck": "tsc --noEmit"
45
+ "lint": "eslint src --ext .ts",
46
+ "lint:fix": "eslint src --ext .ts --fix",
47
+ "format": "prettier --write \"src/**/*.ts\"",
48
+ "format:check": "prettier --check \"src/**/*.ts\"",
49
+ "typecheck": "tsc --noEmit"
42
50
  }
43
51
  ```
44
52
 
45
53
  ## Code Style Guidelines
46
54
 
47
55
  ### TypeScript Configuration
56
+
48
57
  - Use strict mode: `"strict": true` in tsconfig.json
49
58
  - Target ES2022+ for modern Node/Bun features
50
59
  - Use `moduleResolution: "bundler"` for Bun compatibility
51
60
 
52
61
  ### Imports
62
+
53
63
  - Use ES6 imports (ESM): `import { something } from 'module'`
54
64
  - Group imports in this order:
55
65
  1. Node/Bun built-ins
@@ -59,6 +69,7 @@ Commands to add to package.json:
59
69
  - Avoid default exports; prefer named exports for better tree-shaking
60
70
 
61
71
  ### Formatting
72
+
62
73
  - Use 2 spaces for indentation
63
74
  - Use single quotes for strings
64
75
  - Use semicolons at end of statements
@@ -67,6 +78,7 @@ Commands to add to package.json:
67
78
  - Spaces around operators: `a = b + c` not `a=b+c`
68
79
 
69
80
  ### Types & Type Safety
81
+
70
82
  - Always provide explicit return types for functions
71
83
  - Use `interface` for object shapes, `type` for unions/primitives
72
84
  - Avoid `any`; use `unknown` when type is truly unknown
@@ -74,6 +86,7 @@ Commands to add to package.json:
74
86
  - Leverage Bun's built-in type definitions (from `bun-types`)
75
87
 
76
88
  ### Naming Conventions
89
+
77
90
  - **Files**: kebab-case: `my-service.ts`
78
91
  - **Variables/Functions**: camelCase: `myFunction`
79
92
  - **Classes**: PascalCase: `MyService`
@@ -82,14 +95,18 @@ Commands to add to package.json:
82
95
  - **Types/Interfaces**: PascalCase, often with suffixes: `UserService`, `ConfigOptions`
83
96
 
84
97
  ### Error Handling
98
+
85
99
  - Use try/catch for async operations
86
100
  - Create custom error classes for domain-specific errors:
87
101
  ```ts
88
102
  class TerminalError extends Error {
89
- constructor(message: string, public code: string) {
90
- super(message);
91
- this.name = 'TerminalError';
92
- }
103
+ constructor(
104
+ message: string,
105
+ public code: string,
106
+ ) {
107
+ super(message);
108
+ this.name = "TerminalError";
109
+ }
93
110
  }
94
111
  ```
95
112
  - Always include error context in error messages
@@ -97,24 +114,28 @@ Commands to add to package.json:
97
114
  - Never swallow errors silently
98
115
 
99
116
  ### Async/Promise Handling
117
+
100
118
  - Use async/await over .then()/.catch()
101
119
  - Handle promise rejections: `process.on('unhandledRejection')`
102
120
  - Use Bun's optimized APIs where available (e.g., `Bun.file()`)
103
121
  - Implement timeouts for network requests
104
122
 
105
123
  ### Code Organization
124
+
106
125
  - Structure by feature/domain, not by file type
107
126
  - Keep files focused: one responsibility per file
108
127
  - Export at file end; avoid export分散
109
128
  - Use barrel files (`index.ts`) for cleaner imports
110
129
 
111
130
  ### Comments
131
+
112
132
  - Use JSDoc for public APIs: `/** @description ... */`
113
133
  - Comment WHY, not WHAT
114
134
  - Keep comments current with code changes
115
135
  - Avoid inline comments for obvious logic
116
136
 
117
137
  ### Performance (Bun-Specific)
138
+
118
139
  - Leverage Bun's fast I/O: `Bun.write()`, `Bun.file()`
119
140
  - Use `TextEncoder`/`TextDecoder` for encoding
120
141
  - Prefer native over polyfills
@@ -123,6 +144,7 @@ Commands to add to package.json:
123
144
  ## Project Context
124
145
 
125
146
  This is an alternative terminal UI for OpenCode. Focus on:
147
+
126
148
  - Fast, responsive terminal rendering
127
149
  - Clean CLI UX with good error messages
128
150
  - Efficient resource usage (memory/CPU)
@@ -138,6 +160,7 @@ This is an alternative terminal UI for OpenCode. Focus on:
138
160
  ## OpenCode Server Integration
139
161
 
140
162
  ### Starting the Server
163
+
141
164
  - Use `opencode serve` to start a headless HTTP server (not `opencode server`)
142
165
  - Default URL: `http://127.0.0.1:4096` (port may vary, can be 0/random)
143
166
  - Server requires 2-3 seconds to initialize before accepting requests
@@ -145,6 +168,7 @@ This is an alternative terminal UI for OpenCode. Focus on:
145
168
  - Always handle SIGINT to properly shut down the server process
146
169
 
147
170
  ### Authentication
171
+
148
172
  - Server may require HTTP Basic Auth if `OPENCODE_SERVER_PASSWORD` is set
149
173
  - Username: `OPENCODE_SERVER_USERNAME` env var (default: 'opencode')
150
174
  - Password: `OPENCODE_SERVER_PASSWORD` env var (required if server has password set)
@@ -153,6 +177,7 @@ This is an alternative terminal UI for OpenCode. Focus on:
153
177
  - Check env vars at startup: `echo $OPENCODE_SERVER_PASSWORD` to verify it's set
154
178
 
155
179
  ### Creating Sessions
180
+
156
181
  ```ts
157
182
  POST /session
158
183
  Headers: { "Content-Type": "application/json", "Authorization": "Basic <creds>" }
@@ -161,14 +186,17 @@ Response: { id: string, title?: string, ... }
161
186
  ```
162
187
 
163
188
  ### Getting Available Models
189
+
164
190
  ```ts
165
191
  GET /config/providers
166
192
  Headers: { "Authorization": "Basic <creds>" }
167
193
  Response: { providers: Provider[], default: { [key: string]: string } }
168
194
  ```
195
+
169
196
  Note: `/models` endpoint returns HTML documentation, not JSON. Use `/config/providers` for programmatic access.
170
197
 
171
198
  ### Sending Messages
199
+
172
200
  ```ts
173
201
  POST /session/:id/message
174
202
  Headers: { "Content-Type": "application/json", "Authorization": "Basic <creds>" }
@@ -183,6 +211,7 @@ Response: { info: Message, parts: Part[] }
183
211
  ```
184
212
 
185
213
  ### Getting Session Messages
214
+
186
215
  ```ts
187
216
  GET /session/:id/message
188
217
  Headers: { "Authorization": "Basic <creds>" }
@@ -190,20 +219,24 @@ Response: { info: Message, parts: Part[] }[]
190
219
  ```
191
220
 
192
221
  ### Undoing Messages (Revert)
222
+
193
223
  ```ts
194
224
  POST /session/:id/revert
195
225
  Headers: { "Content-Type": "application/json", "Authorization": "Basic <creds>" }
196
226
  Body: { messageID: string, partID?: string }
197
227
  Response: { id: string, revert: { messageID, snapshot, diff } }
198
228
  ```
229
+
199
230
  Typically used to undo the last assistant message by fetching messages first, then reverting the last one.
200
231
 
201
232
  **IMPORTANT**: The `model` field is required when sending messages. Without it, the request will hang indefinitely. Get available models from `GET /config/providers` or `GET /models`. Common models:
233
+
202
234
  - `big-pickle` (opencode provider) - default, high quality
203
235
  - `glm-5-free` (opencode provider) - free GLM model
204
236
  - `gpt-5-nano` (opencode provider) - fast GPT model
205
237
 
206
238
  ### Response Format
239
+
207
240
  - Response has `{ info, parts }` structure
208
241
  - Parts can be: `step-start`, `reasoning`, `text`, `step-finish`, `tool_use`, `tool_result`
209
242
  - `step-start` - Indicates beginning of a thinking/processing step
@@ -215,6 +248,7 @@ Typically used to undo the last assistant message by fetching messages first, th
215
248
  - Display reasoning and text parts to the user for transparency
216
249
 
217
250
  ### Server-Sent Events (SSE)
251
+
218
252
  - Connect to event stream at `/event` for real-time updates
219
253
  - Events include: `message.part.updated`, `session.status`, `session.updated`, `message.updated`, `session.diff`, `session.idle`
220
254
  - Event structure: `{ type: string, properties: {...} }`
@@ -224,6 +258,7 @@ Typically used to undo the last assistant message by fetching messages first, th
224
258
  - Delta updates allow streaming reasoning and text for better UX
225
259
 
226
260
  ### Error Handling
261
+
227
262
  - Server returns 401 Unauthorized when authentication is missing/invalid
228
263
  - Handle connection errors (server may not be ready yet)
229
264
  - Always parse error text from response for debugging
@@ -240,4 +275,4 @@ Typically used to undo the last assistant message by fetching messages first, th
240
275
  - Never commit API keys, tokens, or secrets
241
276
  - Validate all user inputs
242
277
  - Sanitize terminal output to prevent injection
243
- - Use environment variables for configuration
278
+ - Use environment variables for configuration
package/bun.lock CHANGED
@@ -47,7 +47,7 @@
47
47
 
48
48
  "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
49
49
 
50
- "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.14", "", {}, "sha512-nPkWAmzgPJYyfCJAV4NG7HTfN/iuO3B6fv8sT26NhPiR+EqD9i8sh4X1LwI7wEbbMOwWOX1PhrssW6gXQOOQZQ=="],
50
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.15", "", {}, "sha512-NUJNlyBCdZ4R0EBLjJziEQOp2XbRPJosaMcTcWSWO5XJPKGUpz0u8ql+5cR8K+v2RJ+hp2NobtNwpjEYfe6BRQ=="],
51
51
 
52
52
  "@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@6.0.2", "", { "dependencies": { "@babel/generator": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "javascript-natural-sort": "^0.7.1", "lodash-es": "^4.17.21", "minimatch": "^9.0.0", "parse-imports-exports": "^0.2.4" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-ember-template-tag": ">= 2.0.0", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-ember-template-tag", "prettier-plugin-svelte", "svelte"] }, "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA=="],
53
53
 
@@ -71,7 +71,7 @@
71
71
 
72
72
  "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260122.2", "", { "os": "win32", "cpu": "x64" }, "sha512-TJH+sSn7vxcGvaSm+fhcpW//dfSGBZGHrxoy0edAbE7UpFf8ub/3cILL5V1pCsF7w3aLNNgVKRpUkowdUgYcPQ=="],
73
73
 
74
- "allmark": ["allmark@1.0.0", "", { "bin": { "allmark": "dist/bin/index.mjs" } }, "sha512-Stsjcu2cLPBwPlbfba/iMaDGVhJcm6pYU8hNtyTIlYHhckBvCOtiHhyBujl6PltcFnpvG0VQpwyBXWjwY1e6ZA=="],
74
+ "allmark": ["allmark@1.0.1", "", { "bin": { "allmark": "dist/bin/index.mjs" } }, "sha512-r4eeGshglWb3G99D4anWSPpieZ9kw9zXHeGd6NXjBbsbq+aCq/7W9hkrqYq0ywlf43ync2KKfmjeTHJum0AHEQ=="],
75
75
 
76
76
  "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
77
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-miniterm",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "A small front-end terminal UI for OpenCode",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "typescript": "^5"
29
29
  },
30
30
  "dependencies": {
31
- "@opencode-ai/sdk": "^1.2.14",
32
- "allmark": "^1.0.0"
31
+ "@opencode-ai/sdk": "^1.2.15",
32
+ "allmark": "^1.0.1"
33
33
  }
34
34
  }
package/src/ansi.ts CHANGED
@@ -9,12 +9,16 @@ export const CURSOR_UP = (lines: number) => `\x1b[${lines}A`;
9
9
  export const RESET = "\x1b[0m";
10
10
  export const BRIGHT_WHITE = "\x1b[97m";
11
11
  export const BRIGHT_BLACK = "\x1b[90m";
12
+ export const BOLD_BRIGHT_BLACK = "\x1b[1;90m";
12
13
  export const RED = "\x1b[31m";
13
14
  export const GREEN = "\x1b[32m";
14
15
  export const BLUE = "\x1b[34m";
15
16
  export const CYAN = "\x1b[36m";
16
17
  export const BOLD_MAGENTA = "\x1b[1;35m";
17
18
  export const STRIKETHROUGH = "\x1b[9m";
19
+ export const WHITE_BACKGROUND = "\x1b[47m";
20
+ export const BRIGHT_BLACK_BACKGROUND = "\x1b[0;100m";
21
+ export const BOLD_BLACK = "\x1b[1;30m";
18
22
  export const ANSI_CODE_PATTERN = /^\x1b\[[0-9;]*m/;
19
23
 
20
24
  export function stripAnsiCodes(str: string): string {
@@ -1,5 +1,6 @@
1
1
  import type { OpencodeClient } from "@opencode-ai/sdk";
2
2
  import type { Key } from "node:readline";
3
+ import * as ansi from "../ansi";
3
4
  import type { State } from "../index";
4
5
  import { wrapText } from "../render";
5
6
  import type { Command } from "../types";
@@ -24,18 +25,20 @@ function run(client: OpencodeClient, state: State): void {
24
25
  if (!part || !part.text.trim()) continue;
25
26
 
26
27
  if (part.title === "thinking") {
27
- pages.push(`💭 \x1b[90m${part.text.trimStart()}\x1b[0m`);
28
+ pages.push(
29
+ `${ansi.BOLD_BRIGHT_BLACK}~${ansi.RESET} ${ansi.BRIGHT_BLACK}${part.text.trimStart()}${ansi.RESET}`,
30
+ );
28
31
  } else if (part.title === "response") {
29
- pages.push(`💬 ${part.text.trimStart()}`);
30
- } else if (part.title === "tool") {
31
- pages.push(part.text);
32
- } else if (part.title === "files") {
32
+ pages.push(
33
+ `${ansi.WHITE_BACKGROUND}${ansi.BOLD_BLACK}*${ansi.RESET} ${part.text.trimStart()}`,
34
+ );
35
+ } else {
33
36
  pages.push(part.text);
34
37
  }
35
38
  }
36
39
 
37
40
  if (pages.length === 0) {
38
- console.log("\n\x1b[90mNo parts to display yet.\x1b[0m\n");
41
+ console.log(`${ansi.BRIGHT_BLACK}No parts to display yet.${ansi.RESET}\n`);
39
42
  return;
40
43
  }
41
44
 
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ let client: ReturnType<typeof createOpencodeClient>;
50
50
 
51
51
  let processing = true;
52
52
  let retryInterval: ReturnType<typeof setInterval> | null = null;
53
+ let isRequestActive = false;
53
54
 
54
55
  interface AccumulatedPart {
55
56
  key: string;
@@ -134,6 +135,8 @@ async function main() {
134
135
 
135
136
  await updateSessionTitle();
136
137
 
138
+ const sessionHistory = await loadSessionHistory();
139
+
137
140
  const activeDisplay = await getActiveDisplay(client);
138
141
 
139
142
  process.stdout.write(`${ansi.CLEAR_SCREEN_UP}${ansi.CLEAR_FROM_CURSOR}`);
@@ -158,11 +161,13 @@ async function main() {
158
161
  let inputBuffer = "";
159
162
  let cursorPosition = 0;
160
163
  let completions: string[] = [];
161
- let history: string[] = [];
162
- let historyIndex = -1;
164
+ let history: string[] = sessionHistory;
165
+ let historyIndex = history.length;
163
166
  let selectedCompletion = 0;
164
167
  let showCompletions = false;
165
168
  let completionCycling = false;
169
+ let lastSpaceTime = 0;
170
+ let currentInputBuffer: string | null = null;
166
171
 
167
172
  const getCompletions = async (text: string): Promise<string[]> => {
168
173
  if (text.startsWith("/")) {
@@ -249,6 +254,7 @@ async function main() {
249
254
  showCompletions = false;
250
255
  completionCycling = false;
251
256
  completions = [];
257
+ currentInputBuffer = null;
252
258
 
253
259
  if (input) {
254
260
  if (history[history.length - 1] !== input) {
@@ -281,13 +287,16 @@ async function main() {
281
287
  return;
282
288
  }
283
289
 
290
+ isRequestActive = true;
284
291
  process.stdout.write(ansi.CURSOR_HIDE);
285
292
  startAnimation();
286
293
  if (isLoggingEnabled()) {
287
294
  console.log(`📝 ${ansi.BRIGHT_BLACK}Logging to ${getLogDir()}\n${ansi.RESET}`);
288
295
  }
289
296
  await sendMessage(state.sessionID, input);
297
+ isRequestActive = false;
290
298
  } catch (error: any) {
299
+ isRequestActive = false;
291
300
  if (error.message !== "Request cancelled") {
292
301
  stopAnimation();
293
302
  console.error("Error:", error.message);
@@ -310,11 +319,17 @@ async function main() {
310
319
 
311
320
  switch (key.name) {
312
321
  case "up": {
322
+ if (historyIndex === history.length) {
323
+ currentInputBuffer = inputBuffer;
324
+ }
313
325
  if (history.length > 0) {
314
326
  if (historyIndex > 0) {
315
327
  historyIndex--;
328
+ inputBuffer = history[historyIndex]!;
329
+ } else {
330
+ historyIndex = Math.max(-1, historyIndex - 1);
331
+ inputBuffer = "";
316
332
  }
317
- inputBuffer = history[historyIndex]!;
318
333
  cursorPosition = inputBuffer.length;
319
334
  renderLine();
320
335
  }
@@ -324,9 +339,11 @@ async function main() {
324
339
  if (history.length > 0) {
325
340
  if (historyIndex < history.length - 1) {
326
341
  historyIndex++;
342
+ inputBuffer = history[historyIndex]!;
327
343
  } else {
328
344
  historyIndex = history.length;
329
- inputBuffer = "";
345
+ inputBuffer = currentInputBuffer || "";
346
+ currentInputBuffer = null;
330
347
  }
331
348
  cursorPosition = inputBuffer.length;
332
349
  renderLine();
@@ -343,13 +360,21 @@ async function main() {
343
360
  return;
344
361
  }
345
362
  case "escape": {
346
- if (state.sessionID) {
347
- client.session.abort({ path: { id: state.sessionID } }).catch(() => {});
363
+ if (isRequestActive) {
364
+ if (state.sessionID) {
365
+ client.session.abort({ path: { id: state.sessionID } }).catch(() => {});
366
+ }
367
+ stopAnimation();
368
+ process.stdout.write(ansi.CURSOR_SHOW);
369
+ process.stdout.write(`\r ${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
370
+ writePrompt();
371
+ isRequestActive = false;
372
+ } else {
373
+ inputBuffer = "";
374
+ cursorPosition = 0;
375
+ currentInputBuffer = null;
376
+ renderLine();
348
377
  }
349
- stopAnimation();
350
- process.stdout.write(ansi.CURSOR_SHOW);
351
- process.stdout.write(`\r${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
352
- writePrompt();
353
378
  return;
354
379
  }
355
380
  case "return": {
@@ -361,6 +386,7 @@ async function main() {
361
386
  inputBuffer =
362
387
  inputBuffer.slice(0, cursorPosition - 1) + inputBuffer.slice(cursorPosition);
363
388
  cursorPosition--;
389
+ currentInputBuffer = null;
364
390
  }
365
391
  break;
366
392
  }
@@ -368,6 +394,7 @@ async function main() {
368
394
  if (cursorPosition < inputBuffer.length) {
369
395
  inputBuffer =
370
396
  inputBuffer.slice(0, cursorPosition) + inputBuffer.slice(cursorPosition + 1);
397
+ currentInputBuffer = null;
371
398
  }
372
399
  break;
373
400
  }
@@ -389,9 +416,30 @@ async function main() {
389
416
  }
390
417
  default: {
391
418
  if (str) {
392
- inputBuffer =
393
- inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
394
- cursorPosition += str.length;
419
+ if (str === " ") {
420
+ const now = Date.now();
421
+ if (
422
+ now - lastSpaceTime < 500 &&
423
+ cursorPosition > 0 &&
424
+ inputBuffer[cursorPosition - 1] === " "
425
+ ) {
426
+ inputBuffer =
427
+ inputBuffer.slice(0, cursorPosition - 1) +
428
+ ". " +
429
+ inputBuffer.slice(cursorPosition);
430
+ cursorPosition += 1;
431
+ } else {
432
+ inputBuffer =
433
+ inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
434
+ cursorPosition += str.length;
435
+ }
436
+ lastSpaceTime = now;
437
+ } else {
438
+ inputBuffer =
439
+ inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
440
+ cursorPosition += str.length;
441
+ }
442
+ currentInputBuffer = null;
395
443
  }
396
444
  }
397
445
  }
@@ -459,6 +507,34 @@ async function updateSessionTitle(): Promise<void> {
459
507
  }
460
508
  }
461
509
 
510
+ async function loadSessionHistory(): Promise<string[]> {
511
+ try {
512
+ const result = await client.session.messages({
513
+ path: { id: state.sessionID },
514
+ });
515
+ if (result.error || !result.data) {
516
+ return [];
517
+ }
518
+
519
+ const history: string[] = [];
520
+ for (const msg of result.data) {
521
+ if (msg.info.role === "user") {
522
+ const textParts = msg.parts
523
+ .filter((p: Part) => p.type === "text")
524
+ .map((p: Part) => (p as any).text || "")
525
+ .filter(Boolean);
526
+ const text = textParts.join("").trim();
527
+ if (text && !text.startsWith("/")) {
528
+ history.push(text);
529
+ }
530
+ }
531
+ }
532
+ return history;
533
+ } catch {
534
+ return [];
535
+ }
536
+ }
537
+
462
538
  async function startEventListener(): Promise<void> {
463
539
  try {
464
540
  const { stream } = await client.event.subscribe({
@@ -525,7 +601,7 @@ async function sendMessage(sessionID: string, message: string) {
525
601
 
526
602
  const duration = Date.now() - requestStartTime;
527
603
  const durationText = formatDuration(duration);
528
- console.log(`${ansi.BRIGHT_BLACK}Completed in ${durationText}${ansi.RESET}\n`);
604
+ console.log(` ${ansi.BRIGHT_BLACK}Completed in ${durationText}${ansi.RESET}\n`);
529
605
 
530
606
  writePrompt();
531
607
 
@@ -589,6 +665,7 @@ async function processEvent(event: Event): Promise<void> {
589
665
  case "session.status":
590
666
  if (event.type === "session.status" && event.properties.status.type === "idle") {
591
667
  stopAnimation();
668
+ isRequestActive = false;
592
669
  process.stdout.write(ansi.CURSOR_SHOW);
593
670
  if (retryInterval) {
594
671
  clearInterval(retryInterval);
@@ -697,7 +774,7 @@ async function processReasoning(part: Part) {
697
774
 
698
775
  const text = (part as any).text || "";
699
776
  const cleanText = ansi.stripAnsiCodes(text.trimStart());
700
- await writeToLog(`💭 Thinking...\n\n${cleanText}\n\n`);
777
+ await writeToLog(`Thinking:\n\n${cleanText}\n\n`);
701
778
 
702
779
  render(state);
703
780
  }
@@ -713,7 +790,7 @@ async function processText(part: Part) {
713
790
 
714
791
  const text = (part as any).text || "";
715
792
  const cleanText = ansi.stripAnsiCodes(text.trimStart());
716
- await writeToLog(`💬 Response:\n\n${cleanText}\n\n`);
793
+ await writeToLog(`Response:\n\n${cleanText}\n\n`);
717
794
 
718
795
  render(state);
719
796
  }
@@ -722,7 +799,7 @@ async function processToolUse(part: Part) {
722
799
  const toolPart = part as ToolPart;
723
800
  const toolName = toolPart.tool || "unknown";
724
801
  const toolInput = toolPart.state.input["description"] || toolPart.state.input["filePath"] || {};
725
- const toolText = `🔧 ${toolName}: ${ansi.BRIGHT_BLACK}${toolInput}${ansi.RESET}`;
802
+ const toolText = `${ansi.BRIGHT_BLACK}$${ansi.RESET} ${toolName}: ${ansi.BRIGHT_BLACK}${toolInput}${ansi.RESET}`;
726
803
 
727
804
  if (state.accumulatedResponse[state.accumulatedResponse.length - 1]?.title === "tool") {
728
805
  state.accumulatedResponse[state.accumulatedResponse.length - 1]!.text = toolText;
@@ -731,7 +808,7 @@ async function processToolUse(part: Part) {
731
808
  }
732
809
 
733
810
  const cleanToolText = ansi.stripAnsiCodes(toolText);
734
- await writeToLog(`${cleanToolText}\n\n`);
811
+ await writeToLog(`$ ${cleanToolText}\n\n`);
735
812
 
736
813
  render(state);
737
814
  }
@@ -746,28 +823,26 @@ function processDelta(partID: string, delta: string) {
746
823
  }
747
824
 
748
825
  async function processDiff(diff: FileDiff[]) {
749
- let hasChanges = false;
750
826
  const parts: string[] = [];
751
827
  for (const file of diff) {
752
- const status = !file.before ? "added" : !file.after ? "deleted" : "modified";
753
- const statusIcon = status === "added" ? "A" : status === "modified" ? "M" : "D";
754
- const statusLabel =
755
- status === "added" ? "added" : status === "modified" ? "modified" : "deleted";
756
- const addStr = file.additions > 0 ? `${ansi.GREEN}+${file.additions}${ansi.RESET}` : "";
757
- const delStr = file.deletions > 0 ? `${ansi.RED}-${file.deletions}${ansi.RESET}` : "";
758
- const stats = [addStr, delStr].filter(Boolean).join(" ");
759
- const line = ` ${ansi.BLUE}${statusIcon}${ansi.RESET} ${file.file} (${statusLabel}) ${stats}`;
760
- parts.push(line);
761
-
762
828
  const newAfter = file.after ?? "";
763
829
  const oldAfter = state.lastFileAfter.get(file.file);
764
830
  if (newAfter !== oldAfter) {
765
- hasChanges = true;
831
+ const status = !file.before ? "added" : !file.after ? "deleted" : "modified";
832
+ const statusIcon = status === "added" ? "A" : status === "modified" ? "M" : "D";
833
+ const statusLabel =
834
+ status === "added" ? "added" : status === "modified" ? "modified" : "deleted";
835
+ const addStr = file.additions > 0 ? `${ansi.GREEN}+${file.additions}${ansi.RESET}` : "";
836
+ const delStr = file.deletions > 0 ? `${ansi.RED}-${file.deletions}${ansi.RESET}` : "";
837
+ const stats = [addStr, delStr].filter(Boolean).join(" ");
838
+ const line = `${ansi.BLUE}${statusIcon}${ansi.RESET} ${file.file} (${statusLabel}) ${stats}`;
839
+ parts.push(line);
840
+
766
841
  state.lastFileAfter.set(file.file, newAfter);
767
842
  }
768
843
  }
769
844
 
770
- if (hasChanges) {
845
+ if (parts.length > 0) {
771
846
  state.accumulatedResponse.push({ key: "diff", title: "files", text: parts.join("\n") });
772
847
 
773
848
  const diffText = ansi.stripAnsiCodes(parts.join("\n"));
@@ -783,15 +858,11 @@ async function processTodos(todos: Todo[]) {
783
858
  for (let todo of todos) {
784
859
  let todoText = "";
785
860
  if (todo.status === "completed") {
786
- todoText += ansi.STRIKETHROUGH;
787
861
  todoText += "- [✓] ";
788
862
  } else {
789
863
  todoText += "- [ ] ";
790
864
  }
791
865
  todoText += todo.content;
792
- if (todo.status === "completed") {
793
- todoText += ansi.RESET;
794
- }
795
866
  todoListText += todoText + "\n";
796
867
  }
797
868
 
package/src/render.ts CHANGED
@@ -12,11 +12,7 @@ export function render(state: State, details = false): void {
12
12
  }
13
13
 
14
14
  // Only show the last (i.e. active) thinking part
15
- // Only show the last (i.e. active) tool use
16
- // Only show the last files part between parts
17
15
  let foundPart = false;
18
- let foundFiles = false;
19
- let foundTodo = false;
20
16
  for (let i = state.accumulatedResponse.length - 1; i >= 0; i--) {
21
17
  const part = state.accumulatedResponse[i];
22
18
  if (!part) continue;
@@ -26,18 +22,15 @@ export function render(state: State, details = false): void {
26
22
  }
27
23
 
28
24
  if (part.title === "thinking") {
25
+ if (part.active === false) {
26
+ break;
27
+ }
29
28
  part.active = !foundPart;
30
29
  foundPart = true;
31
- } else if (part.title === "tool") {
32
- part.active = !foundPart;
33
- } else if (part.title === "files") {
34
- part.active = !foundFiles;
35
- foundFiles = true;
36
- } else if (part.title === "todo") {
37
- part.active = !foundTodo;
38
- foundTodo = true;
39
- } else {
30
+ } else if (part.title === "response") {
31
+ part.active = true;
40
32
  foundPart = true;
33
+ } else {
41
34
  part.active = true;
42
35
  }
43
36
  }
@@ -48,17 +41,19 @@ export function render(state: State, details = false): void {
48
41
  if (!part.text.trim()) continue;
49
42
 
50
43
  if (part.title === "thinking") {
44
+ // Show max 10 thinking lines
51
45
  const partText = details ? part.text.trimStart() : lastThinkingLines(part.text.trimStart());
52
- output += `💭 ${ansi.BRIGHT_BLACK}${partText}${ansi.RESET}\n\n`;
46
+ output += `${ansi.BOLD_BRIGHT_BLACK}~${ansi.RESET} ${ansi.BRIGHT_BLACK}${partText}${ansi.RESET}\n\n`;
53
47
  } else if (part.title === "response") {
48
+ // Show all response lines
54
49
  const doc = parse(part.text.trimStart(), gfm);
55
50
  const partText = renderToConsole(doc);
56
- output += `💬 ${partText}\n\n`;
57
- } else if (part.title === "tool") {
58
- output += part.text + "\n\n";
59
- } else if (part.title === "files") {
51
+ output += `${ansi.WHITE_BACKGROUND}${ansi.BOLD_BLACK}*${ansi.RESET} ${partText}\n\n`;
52
+ } else if (part.title === "tool" || part.title === "files") {
53
+ // TODO: Show max 10 tool/file lines?
60
54
  output += part.text + "\n\n";
61
55
  } else if (part.title === "todo") {
56
+ // Show the whole todo list
62
57
  output += part.text + "\n\n";
63
58
  }
64
59
  }
@@ -84,6 +79,9 @@ export function render(state: State, details = false): void {
84
79
  }
85
80
 
86
81
  state.renderedLines = lines;
82
+ } else if (state.renderedLines.length > 0) {
83
+ clearRenderedLines(state, state.renderedLines.length);
84
+ state.renderedLines = [];
87
85
  }
88
86
  }
89
87
 
@@ -130,15 +128,17 @@ function clearRenderedLines(state: State, linesToClear: number): void {
130
128
  }
131
129
 
132
130
  export function wrapText(text: string, width: number): string[] {
131
+ const INDENT = " ";
132
+ const indentLength = INDENT.length;
133
133
  const lines: string[] = [];
134
- let currentLine = "";
135
- let visibleLength = 0;
134
+ let currentLine = INDENT;
135
+ let visibleLength = indentLength;
136
136
  let i = 0;
137
137
 
138
138
  const pushLine = () => {
139
139
  lines.push(currentLine);
140
- currentLine = "";
141
- visibleLength = 0;
140
+ currentLine = INDENT;
141
+ visibleLength = indentLength;
142
142
  };
143
143
 
144
144
  const addWord = (word: string, wordVisibleLength: number) => {
@@ -150,21 +150,21 @@ export function wrapText(text: string, width: number): string[] {
150
150
  : visibleLength + 1 + wordVisibleLength <= width;
151
151
 
152
152
  if (wouldFit) {
153
- if (visibleLength > 0) {
153
+ if (visibleLength > indentLength) {
154
154
  currentLine += " ";
155
155
  visibleLength++;
156
156
  }
157
157
  currentLine += word;
158
158
  visibleLength += wordVisibleLength;
159
- } else if (visibleLength > 0) {
159
+ } else if (visibleLength > indentLength) {
160
160
  pushLine();
161
- currentLine = word;
162
- visibleLength = wordVisibleLength;
161
+ currentLine = INDENT + word;
162
+ visibleLength = indentLength + wordVisibleLength;
163
163
  } else if (wordVisibleLength <= width) {
164
- currentLine = word;
165
- visibleLength = wordVisibleLength;
164
+ currentLine = INDENT + word;
165
+ visibleLength = indentLength + wordVisibleLength;
166
166
  } else {
167
- const wordWidth = width;
167
+ const wordWidth = width - indentLength;
168
168
  for (let w = 0; w < word.length; ) {
169
169
  let segment = "";
170
170
  let segmentVisible = 0;
@@ -192,8 +192,8 @@ export function wrapText(text: string, width: number): string[] {
192
192
  if (currentLine) {
193
193
  pushLine();
194
194
  }
195
- currentLine = segment;
196
- visibleLength = segmentVisible;
195
+ currentLine = INDENT + segment;
196
+ visibleLength = indentLength + segmentVisible;
197
197
  }
198
198
  }
199
199
  }
@@ -237,7 +237,7 @@ export function wrapText(text: string, width: number): string[] {
237
237
  }
238
238
  }
239
239
 
240
- if (currentLine || lines.length === 0) {
240
+ if (currentLine.trim() || lines.length === 0) {
241
241
  pushLine();
242
242
  }
243
243
 
@@ -252,18 +252,40 @@ export function writePrompt(): void {
252
252
 
253
253
  const ANIMATION_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇"];
254
254
  let animationInterval: ReturnType<typeof setInterval> | null = null;
255
+ let requestStartTime: number | null = null;
255
256
 
256
- export function startAnimation(): void {
257
+ export function startAnimation(startTime?: number): void {
257
258
  if (animationInterval) return;
258
259
 
260
+ requestStartTime = startTime || Date.now();
261
+
259
262
  let index = 0;
260
263
  animationInterval = setInterval(() => {
261
- process.stdout.write(`\r${ansi.BOLD_MAGENTA}`);
262
- process.stdout.write(`${ANIMATION_CHARS[index]}${ansi.RESET}`);
264
+ const elapsed = Date.now() - requestStartTime!;
265
+ const elapsedText = formatDuration(elapsed);
266
+
267
+ process.stdout.write(
268
+ `\r${ansi.BOLD_MAGENTA}${ANIMATION_CHARS[index]} ${ansi.RESET}${ansi.BRIGHT_BLACK}Running for ${elapsedText}${ansi.RESET}`,
269
+ );
263
270
  index = (index + 1) % ANIMATION_CHARS.length;
264
271
  }, 100);
265
272
  }
266
273
 
274
+ function formatDuration(ms: number): string {
275
+ const seconds = ms / 1000;
276
+ if (seconds < 60) {
277
+ return `${Math.round(seconds)}s`;
278
+ }
279
+ const minutes = Math.floor(seconds / 60);
280
+ const remainingSeconds = Math.round(seconds % 60);
281
+ if (minutes < 60) {
282
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
283
+ }
284
+ const hours = Math.floor(minutes / 60);
285
+ const remainingMinutes = minutes % 60;
286
+ return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
287
+ }
288
+
267
289
  export function stopAnimation(): void {
268
290
  if (animationInterval) {
269
291
  clearInterval(animationInterval);
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it, vi } from "bun:test";
2
2
  import { type State } from "../src";
3
+ import * as ansi from "../src/ansi";
3
4
  import { render, wrapText } from "../src/render";
4
5
 
5
6
  describe("render", () => {
@@ -61,7 +62,7 @@ describe("render", () => {
61
62
  const calls = write.mock.calls.map((c) => c[0]);
62
63
  expect(calls.some((c) => c.includes("\u001B[2A"))).toBe(true);
63
64
  const outputCall = calls.find((c) => c.includes("i've done it"));
64
- expect(outputCall).toContain("💬");
65
+ expect(outputCall).toContain(`${ansi.WHITE_BACKGROUND}${ansi.BOLD_BLACK}*${ansi.RESET}`);
65
66
  });
66
67
  });
67
68
 
@@ -76,8 +77,9 @@ describe("render", () => {
76
77
  render(state);
77
78
 
78
79
  const output = write.mock.calls.map((c) => c[0]).join("");
79
- expect(output).toContain("💭");
80
- expect(output).toContain("分析问题");
80
+ expect(output).toContain(
81
+ `${ansi.BOLD_BRIGHT_BLACK}~${ansi.RESET} ${ansi.BRIGHT_BLACK}分析问题${ansi.RESET}`,
82
+ );
81
83
  });
82
84
 
83
85
  it("should only show thinking indicator for last thinking part", () => {
@@ -93,9 +95,10 @@ describe("render", () => {
93
95
  render(state);
94
96
 
95
97
  const output = write.mock.calls.map((c) => c[0]).join("");
96
- expect(output).toContain("💭");
97
- expect(output).toContain("second");
98
- expect(output).not.toMatch(/first.*Thinking/);
98
+ expect(output).toContain(
99
+ `${ansi.BOLD_BRIGHT_BLACK}~${ansi.RESET} ${ansi.BRIGHT_BLACK}second${ansi.RESET}`,
100
+ );
101
+ expect(output).not.toContain("first");
99
102
  });
100
103
 
101
104
  it("should skip parts with empty text", () => {
@@ -134,23 +137,24 @@ describe("render", () => {
134
137
  render(state);
135
138
 
136
139
  const output = write.mock.calls.map((c) => c[0]).join("");
137
- expect(output).toContain("💬");
138
- expect(output).toContain("Hello world");
140
+ expect(output).toContain(
141
+ `${ansi.WHITE_BACKGROUND}${ansi.BOLD_BLACK}*${ansi.RESET} Hello world`,
142
+ );
139
143
  });
140
144
  });
141
145
 
142
146
  describe("tool parts", () => {
143
- it("should render tool part without indicator", () => {
147
+ it("should render tool part with indicator", () => {
144
148
  const write = vi.fn();
145
149
  const state = createMockState({
146
- accumulatedResponse: [{ key: "xxx", title: "tool", text: "🔧 bash: ls -la" }],
150
+ accumulatedResponse: [{ key: "xxx", title: "tool", text: "bash: ls -la" }],
147
151
  write,
148
152
  });
149
153
 
150
154
  render(state);
151
155
 
152
156
  const output = write.mock.calls.map((c) => c[0]).join("");
153
- expect(output).toContain("🔧 bash: ls -la");
157
+ expect(output).toContain("bash: ls -la");
154
158
  });
155
159
  });
156
160
 
@@ -238,7 +242,7 @@ describe("render", () => {
238
242
  const state = createMockState({
239
243
  accumulatedResponse: [
240
244
  { key: "xxx", title: "thinking", text: "分析中" },
241
- { key: "xxx", title: "tool", text: "🔧 bash: npm test" },
245
+ { key: "xxx", title: "tool", text: "bash: npm test" },
242
246
  { key: "xxx", title: "response", text: "Test results: 5 passed" },
243
247
  ],
244
248
  write,
@@ -247,10 +251,11 @@ describe("render", () => {
247
251
  render(state);
248
252
 
249
253
  const output = write.mock.calls.map((c) => c[0]).join("");
250
- expect(output).not.toContain("💭");
251
- expect(output).not.toContain("🔧 bash: npm test");
252
- expect(output).toContain("💬");
253
- expect(output).toContain("Test results: 5 passed");
254
+ expect(output).not.toContain(`分析中`);
255
+ expect(output).toContain(`bash: npm test`);
256
+ expect(output).toContain(
257
+ `${ansi.WHITE_BACKGROUND}${ansi.BOLD_BLACK}*${ansi.RESET} Test results: 5 passed`,
258
+ );
254
259
  });
255
260
  });
256
261
  });
@@ -259,115 +264,123 @@ describe("wrapText", () => {
259
264
  describe("basic wrapping", () => {
260
265
  it("should return single line for text shorter than width", () => {
261
266
  const result = wrapText("hello", 20);
262
- expect(result).toEqual(["hello"]);
267
+ expect(result).toEqual([" hello"]);
263
268
  });
264
269
 
265
270
  it("should wrap text longer than width", () => {
266
271
  const result = wrapText("hello world this is a long text", 10);
267
- expect(result).toEqual(["hello", "world this", "is a long", "text"]);
272
+ expect(result).toEqual([" hello", " world", " this is", " a long", " text"]);
268
273
  });
269
274
 
270
275
  it("should handle text exactly at width", () => {
271
276
  const result = wrapText("1234567890", 10);
272
- expect(result).toEqual(["1234567890"]);
277
+ expect(result).toEqual([" 1234567890"]);
273
278
  });
274
279
 
275
280
  it("should break long word that exceeds width", () => {
276
281
  const result = wrapText("12345678901", 10);
277
- expect(result).toEqual(["1234567890", "1"]);
282
+ expect(result).toEqual([" ", " 12345678", " 901"]);
278
283
  });
279
284
  });
280
285
 
281
286
  describe("multiple lines", () => {
282
287
  it("should preserve existing newlines", () => {
283
288
  const result = wrapText("line1\nline2\nline3", 20);
284
- expect(result).toEqual(["line1", "line2", "line3"]);
289
+ expect(result).toEqual([" line1", " line2", " line3"]);
285
290
  });
286
291
 
287
292
  it("should wrap lines that are too long", () => {
288
293
  const result = wrapText("very long line1\nshort\nvery long line2", 10);
289
- expect(result).toEqual(["very long", "line1", "short", "very long", "line2"]);
294
+ expect(result).toEqual([
295
+ " very",
296
+ " long",
297
+ " line1",
298
+ " short",
299
+ " very",
300
+ " long",
301
+ " line2",
302
+ ]);
290
303
  });
291
304
 
292
305
  it("should handle empty lines", () => {
293
306
  const result = wrapText("line1\n\nline3", 20);
294
- expect(result).toEqual(["line1", "", "line3"]);
307
+ expect(result).toEqual([" line1", " ", " line3"]);
295
308
  });
296
309
  });
297
310
 
298
311
  describe("ANSI codes", () => {
299
312
  it("should preserve ANSI codes in output", () => {
300
313
  const result = wrapText("\x1b[31mred\x1b[0m text", 20);
301
- expect(result).toEqual(["\x1b[31mred\x1b[0m text"]);
314
+ expect(result).toEqual([" \x1b[31mred\x1b[0m text"]);
302
315
  });
303
316
 
304
317
  it("should not count ANSI codes toward visible width", () => {
305
318
  const result = wrapText("\x1b[31mred\x1b[0m text", 8);
306
- expect(result).toEqual(["\x1b[31mred\x1b[0m text"]);
319
+ expect(result).toEqual([" \x1b[31mred\x1b[0m", " text"]);
307
320
  });
308
321
 
309
322
  it("should handle multiple ANSI codes", () => {
310
323
  const result = wrapText("\x1b[31m\x1b[1mbold red\x1b[0m\x1b[32m green\x1b[0m", 10);
311
- expect(result).toEqual(["\x1b[31m\x1b[1mbold red\x1b[0m\x1b[32m", "green\x1b[0m"]);
324
+ expect(result).toEqual([" \x1b[31m\x1b[1mbold red\x1b[0m\x1b[32m", " green\x1b[0m"]);
312
325
  });
313
326
 
314
327
  it("should handle ANSI codes at wrap boundary", () => {
315
328
  const result = wrapText("12345\x1b[31m67890\x1b[0m", 10);
316
- expect(result).toEqual(["12345\x1b[31m67890\x1b[0m"]);
329
+ expect(result).toEqual([" 12345\x1b[31m67890\x1b[0m"]);
317
330
  });
318
331
  });
319
332
 
320
333
  describe("edge cases", () => {
321
334
  it("should handle empty string", () => {
322
335
  const result = wrapText("", 20);
323
- expect(result).toEqual([""]);
336
+ expect(result).toEqual([" "]);
324
337
  });
325
338
 
326
339
  it("should handle single character", () => {
327
340
  const result = wrapText("a", 20);
328
- expect(result).toEqual(["a"]);
341
+ expect(result).toEqual([" a"]);
329
342
  });
330
343
 
331
344
  it("should handle width of 1", () => {
332
345
  const result = wrapText("a b c", 1);
333
- expect(result).toEqual(["a", "b", "c"]);
346
+ expect(result).toEqual([" a", " b", " c"]);
334
347
  });
335
348
 
336
349
  it("should handle carriage return characters", () => {
337
350
  const result = wrapText("hello\r\nworld", 20);
338
- expect(result).toEqual(["hello", "world"]);
351
+ expect(result).toEqual([" hello", " world"]);
339
352
  });
340
353
 
341
354
  it("should handle trailing newline", () => {
342
355
  const result = wrapText("hello\n", 20);
343
- expect(result).toEqual(["hello"]);
356
+ expect(result).toEqual([" hello"]);
344
357
  });
345
358
 
346
359
  it("should handle multiple trailing newlines", () => {
347
360
  const result = wrapText("hello\n\n", 20);
348
- expect(result).toEqual(["hello", ""]);
361
+ expect(result).toEqual([" hello", " "]);
349
362
  });
350
363
 
351
364
  it("should handle leading newline", () => {
352
365
  const result = wrapText("\nhello", 20);
353
- expect(result).toEqual(["", "hello"]);
366
+ expect(result).toEqual([" ", " hello"]);
354
367
  });
355
368
  });
356
369
 
357
370
  describe("real-world scenarios", () => {
358
- it("should wrap thinking output with emoji", () => {
371
+ it("should wrap thinking output with indicator", () => {
359
372
  const result = wrapText(
360
- "💭 Let me analyze this problem step by step to find the best solution",
361
- 40,
373
+ "Let me analyze this problem step by step to find the best solution",
374
+ 36,
362
375
  );
363
376
  expect(result.length).toBeGreaterThan(1);
364
- expect(result[0]).toBe("💭 Let me analyze this problem step by");
377
+ expect(result[0]).toBe(" Let me analyze this problem step");
365
378
  });
366
379
 
367
- it("should wrap response output with emoji", () => {
380
+ it("should wrap response output with indicator", () => {
368
381
  const result = wrapText(
369
- "💬 Here is the solution:\nWe need to implement the fix by updating the wrapText function",
370
- 30,
382
+ "Here is the solution:\nWe need to implement the fix by updating the wrapText function",
383
+ 26,
371
384
  );
372
385
  expect(result.length).toBeGreaterThan(1);
373
386
  });