opencode-miniterm 1.0.1 → 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 +46 -11
- package/README.md +164 -1
- package/bun.lock +3 -3
- package/package.json +3 -3
- package/src/ansi.ts +4 -0
- package/src/commands/diff.ts +3 -2
- package/src/commands/init.ts +3 -2
- package/src/commands/new.ts +1 -1
- package/src/commands/page.ts +9 -6
- package/src/commands/sessions.ts +4 -4
- package/src/commands/undo.ts +4 -3
- package/src/config.ts +2 -1
- package/src/index.ts +108 -39
- package/src/render.ts +57 -35
- package/test/render.test.ts +54 -41
- package/src/commands/kill.ts +0 -33
package/test/render.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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("
|
|
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: "
|
|
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).
|
|
252
|
-
expect(output).toContain(
|
|
253
|
-
|
|
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", "
|
|
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(["
|
|
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([
|
|
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
|
|
371
|
+
it("should wrap thinking output with indicator", () => {
|
|
359
372
|
const result = wrapText(
|
|
360
|
-
"
|
|
361
|
-
|
|
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("
|
|
377
|
+
expect(result[0]).toBe(" Let me analyze this problem step");
|
|
365
378
|
});
|
|
366
379
|
|
|
367
|
-
it("should wrap response output with
|
|
380
|
+
it("should wrap response output with indicator", () => {
|
|
368
381
|
const result = wrapText(
|
|
369
|
-
"
|
|
370
|
-
|
|
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
|
});
|
package/src/commands/kill.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
-
import type { State } from "../index";
|
|
3
|
-
import type { Command } from "../types";
|
|
4
|
-
|
|
5
|
-
let command: Command = {
|
|
6
|
-
name: "/kill",
|
|
7
|
-
description: "Abort a session (e.g. `/kill ses_123`)",
|
|
8
|
-
run,
|
|
9
|
-
running: false,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export default command;
|
|
13
|
-
|
|
14
|
-
async function run(client: OpencodeClient, _state: State, input?: string): Promise<void> {
|
|
15
|
-
if (!input) {
|
|
16
|
-
console.log("Usage: /kill <session_id>");
|
|
17
|
-
console.log();
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const result = await client.session.abort({
|
|
22
|
-
path: { id: input },
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
if (result.error) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`Failed to abort session (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
console.log(`Session aborted successfully.`);
|
|
32
|
-
console.log();
|
|
33
|
-
}
|