kimaki 0.4.31 → 0.4.33
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/dist/discord-utils.js +2 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/message-formatting.js +3 -3
- package/dist/message-formatting.test.js +3 -3
- package/dist/session-handler.js +30 -0
- package/package.json +12 -11
- package/src/cli.ts +0 -0
- package/src/discord-utils.ts +2 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/message-formatting.test.ts +3 -3
- package/src/message-formatting.ts +3 -3
- package/src/session-handler.ts +31 -0
- package/LICENSE +0 -21
package/dist/discord-utils.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ChannelType, } from 'discord.js';
|
|
|
5
5
|
import { Lexer } from 'marked';
|
|
6
6
|
import { extractTagsArrays } from './xml.js';
|
|
7
7
|
import { formatMarkdownTables } from './format-tables.js';
|
|
8
|
+
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
8
9
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
|
|
9
10
|
import { createLogger } from './logger.js';
|
|
10
11
|
const discordLogger = createLogger('DISCORD');
|
|
@@ -159,6 +160,7 @@ export async function sendThreadMessage(thread, content, options) {
|
|
|
159
160
|
const MAX_LENGTH = 2000;
|
|
160
161
|
content = formatMarkdownTables(content);
|
|
161
162
|
content = unnestCodeBlocksFromLists(content);
|
|
163
|
+
content = limitHeadingDepth(content);
|
|
162
164
|
content = escapeBackticksInCodeBlocks(content);
|
|
163
165
|
// If custom flags provided, send as single message (no chunking)
|
|
164
166
|
if (options?.flags !== undefined) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Limit heading depth for Discord.
|
|
2
|
+
// Discord only supports headings up to ### (h3), so this converts
|
|
3
|
+
// ####, #####, etc. to ### to maintain consistent rendering.
|
|
4
|
+
import { Lexer } from 'marked';
|
|
5
|
+
export function limitHeadingDepth(markdown, maxDepth = 3) {
|
|
6
|
+
const lexer = new Lexer();
|
|
7
|
+
const tokens = lexer.lex(markdown);
|
|
8
|
+
let result = '';
|
|
9
|
+
for (const token of tokens) {
|
|
10
|
+
if (token.type === 'heading') {
|
|
11
|
+
const heading = token;
|
|
12
|
+
if (heading.depth > maxDepth) {
|
|
13
|
+
const hashes = '#'.repeat(maxDepth);
|
|
14
|
+
result += hashes + ' ' + heading.text + '\n';
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
result += token.raw;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
result += token.raw;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
3
|
+
test('converts h4 to h3', () => {
|
|
4
|
+
const input = '#### Fourth level heading';
|
|
5
|
+
const result = limitHeadingDepth(input);
|
|
6
|
+
expect(result).toMatchInlineSnapshot(`
|
|
7
|
+
"### Fourth level heading
|
|
8
|
+
"
|
|
9
|
+
`);
|
|
10
|
+
});
|
|
11
|
+
test('converts h5 to h3', () => {
|
|
12
|
+
const input = '##### Fifth level heading';
|
|
13
|
+
const result = limitHeadingDepth(input);
|
|
14
|
+
expect(result).toMatchInlineSnapshot(`
|
|
15
|
+
"### Fifth level heading
|
|
16
|
+
"
|
|
17
|
+
`);
|
|
18
|
+
});
|
|
19
|
+
test('converts h6 to h3', () => {
|
|
20
|
+
const input = '###### Sixth level heading';
|
|
21
|
+
const result = limitHeadingDepth(input);
|
|
22
|
+
expect(result).toMatchInlineSnapshot(`
|
|
23
|
+
"### Sixth level heading
|
|
24
|
+
"
|
|
25
|
+
`);
|
|
26
|
+
});
|
|
27
|
+
test('preserves h3 unchanged', () => {
|
|
28
|
+
const input = '### Third level heading';
|
|
29
|
+
const result = limitHeadingDepth(input);
|
|
30
|
+
expect(result).toMatchInlineSnapshot(`"### Third level heading"`);
|
|
31
|
+
});
|
|
32
|
+
test('preserves h2 unchanged', () => {
|
|
33
|
+
const input = '## Second level heading';
|
|
34
|
+
const result = limitHeadingDepth(input);
|
|
35
|
+
expect(result).toMatchInlineSnapshot(`"## Second level heading"`);
|
|
36
|
+
});
|
|
37
|
+
test('preserves h1 unchanged', () => {
|
|
38
|
+
const input = '# First level heading';
|
|
39
|
+
const result = limitHeadingDepth(input);
|
|
40
|
+
expect(result).toMatchInlineSnapshot(`"# First level heading"`);
|
|
41
|
+
});
|
|
42
|
+
test('handles multiple headings in document', () => {
|
|
43
|
+
const input = `# Title
|
|
44
|
+
|
|
45
|
+
Some text
|
|
46
|
+
|
|
47
|
+
## Section
|
|
48
|
+
|
|
49
|
+
### Subsection
|
|
50
|
+
|
|
51
|
+
#### Too deep
|
|
52
|
+
|
|
53
|
+
##### Even deeper
|
|
54
|
+
|
|
55
|
+
Regular paragraph
|
|
56
|
+
|
|
57
|
+
### Back to normal
|
|
58
|
+
`;
|
|
59
|
+
const result = limitHeadingDepth(input);
|
|
60
|
+
expect(result).toMatchInlineSnapshot(`
|
|
61
|
+
"# Title
|
|
62
|
+
|
|
63
|
+
Some text
|
|
64
|
+
|
|
65
|
+
## Section
|
|
66
|
+
|
|
67
|
+
### Subsection
|
|
68
|
+
|
|
69
|
+
### Too deep
|
|
70
|
+
### Even deeper
|
|
71
|
+
Regular paragraph
|
|
72
|
+
|
|
73
|
+
### Back to normal
|
|
74
|
+
"
|
|
75
|
+
`);
|
|
76
|
+
});
|
|
77
|
+
test('preserves heading with inline formatting', () => {
|
|
78
|
+
const input = '#### Heading with **bold** and `code`';
|
|
79
|
+
const result = limitHeadingDepth(input);
|
|
80
|
+
expect(result).toMatchInlineSnapshot(`
|
|
81
|
+
"### Heading with **bold** and \`code\`
|
|
82
|
+
"
|
|
83
|
+
`);
|
|
84
|
+
});
|
|
85
|
+
test('handles empty markdown', () => {
|
|
86
|
+
const result = limitHeadingDepth('');
|
|
87
|
+
expect(result).toMatchInlineSnapshot(`""`);
|
|
88
|
+
});
|
|
89
|
+
test('handles markdown with no headings', () => {
|
|
90
|
+
const input = 'Just some text\n\nAnd more text';
|
|
91
|
+
const result = limitHeadingDepth(input);
|
|
92
|
+
expect(result).toMatchInlineSnapshot(`
|
|
93
|
+
"Just some text
|
|
94
|
+
|
|
95
|
+
And more text"
|
|
96
|
+
`);
|
|
97
|
+
});
|
|
98
|
+
test('allows custom maxDepth', () => {
|
|
99
|
+
const input = '### Third level';
|
|
100
|
+
const result = limitHeadingDepth(input, 2);
|
|
101
|
+
expect(result).toMatchInlineSnapshot(`
|
|
102
|
+
"## Third level
|
|
103
|
+
"
|
|
104
|
+
`);
|
|
105
|
+
});
|
|
@@ -186,10 +186,10 @@ export function formatTodoList(part) {
|
|
|
186
186
|
const activeTodo = todos[activeIndex];
|
|
187
187
|
if (activeIndex === -1 || !activeTodo)
|
|
188
188
|
return '';
|
|
189
|
-
//
|
|
190
|
-
const
|
|
189
|
+
// digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
|
|
190
|
+
const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛';
|
|
191
191
|
const todoNumber = activeIndex + 1;
|
|
192
|
-
const num = todoNumber <= 20 ?
|
|
192
|
+
const num = todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`;
|
|
193
193
|
const content = activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1);
|
|
194
194
|
return `${num} **${escapeInlineMarkdown(content)}**`;
|
|
195
195
|
}
|
|
@@ -24,7 +24,7 @@ describe('formatTodoList', () => {
|
|
|
24
24
|
time: { start: 0, end: 0 },
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
27
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒉ **second task**"`);
|
|
28
28
|
});
|
|
29
29
|
test('formats double digit todo numbers', () => {
|
|
30
30
|
const todos = Array.from({ length: 12 }, (_, i) => ({
|
|
@@ -47,7 +47,7 @@ describe('formatTodoList', () => {
|
|
|
47
47
|
time: { start: 0, end: 0 },
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
50
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒓ **task 12**"`);
|
|
51
51
|
});
|
|
52
52
|
test('lowercases first letter of content', () => {
|
|
53
53
|
const part = {
|
|
@@ -68,6 +68,6 @@ describe('formatTodoList', () => {
|
|
|
68
68
|
time: { start: 0, end: 0 },
|
|
69
69
|
},
|
|
70
70
|
};
|
|
71
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
71
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒈ **fix the bug**"`);
|
|
72
72
|
});
|
|
73
73
|
});
|
package/dist/session-handler.js
CHANGED
|
@@ -313,8 +313,38 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
313
313
|
stopTyping = startTyping();
|
|
314
314
|
}
|
|
315
315
|
if (part.type === 'tool' && part.state.status === 'running') {
|
|
316
|
+
// Flush any pending text/reasoning parts before showing the tool
|
|
317
|
+
// This ensures text the LLM generated before the tool call is shown first
|
|
318
|
+
for (const p of currentParts) {
|
|
319
|
+
if (p.type !== 'step-start' && p.type !== 'step-finish' && p.id !== part.id) {
|
|
320
|
+
await sendPartMessage(p);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
316
323
|
await sendPartMessage(part);
|
|
317
324
|
}
|
|
325
|
+
// Show token usage for completed tools with large output (>5k tokens)
|
|
326
|
+
if (part.type === 'tool' && part.state.status === 'completed') {
|
|
327
|
+
const output = part.state.output || '';
|
|
328
|
+
const outputTokens = Math.ceil(output.length / 4);
|
|
329
|
+
const LARGE_OUTPUT_THRESHOLD = 3000;
|
|
330
|
+
if (outputTokens >= LARGE_OUTPUT_THRESHOLD) {
|
|
331
|
+
const formattedTokens = outputTokens >= 1000
|
|
332
|
+
? `${(outputTokens / 1000).toFixed(1)}k`
|
|
333
|
+
: String(outputTokens);
|
|
334
|
+
const percentageSuffix = (() => {
|
|
335
|
+
if (!modelContextLimit) {
|
|
336
|
+
return '';
|
|
337
|
+
}
|
|
338
|
+
const pct = (outputTokens / modelContextLimit) * 100;
|
|
339
|
+
if (pct < 1) {
|
|
340
|
+
return '';
|
|
341
|
+
}
|
|
342
|
+
return ` (${pct.toFixed(1)}%)`;
|
|
343
|
+
})();
|
|
344
|
+
const chunk = `⬦ ${part.tool} returned ${formattedTokens} tokens${percentageSuffix}`;
|
|
345
|
+
await thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
318
348
|
if (part.type === 'reasoning') {
|
|
319
349
|
await sendPartMessage(part);
|
|
320
350
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.33",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx --env-file .env src/cli.ts",
|
|
8
|
+
"prepublishOnly": "pnpm tsc",
|
|
9
|
+
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
10
|
+
"watch": "tsx scripts/watch-session.ts",
|
|
11
|
+
"test:events": "tsx test-events.ts",
|
|
12
|
+
"pcm-to-mp3": "bun scripts/pcm-to-mp3",
|
|
13
|
+
"test:send": "tsx send-test-message.ts",
|
|
14
|
+
"register-commands": "tsx scripts/register-commands.ts"
|
|
15
|
+
},
|
|
6
16
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
17
|
"bin": "bin.js",
|
|
8
18
|
"files": [
|
|
@@ -45,14 +55,5 @@
|
|
|
45
55
|
"string-dedent": "^3.0.2",
|
|
46
56
|
"undici": "^7.16.0",
|
|
47
57
|
"zod": "^4.2.1"
|
|
48
|
-
},
|
|
49
|
-
"scripts": {
|
|
50
|
-
"dev": "tsx --env-file .env src/cli.ts",
|
|
51
|
-
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
52
|
-
"watch": "tsx scripts/watch-session.ts",
|
|
53
|
-
"test:events": "tsx test-events.ts",
|
|
54
|
-
"pcm-to-mp3": "bun scripts/pcm-to-mp3",
|
|
55
|
-
"test:send": "tsx send-test-message.ts",
|
|
56
|
-
"register-commands": "tsx scripts/register-commands.ts"
|
|
57
58
|
}
|
|
58
|
-
}
|
|
59
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/discord-utils.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { Lexer } from 'marked'
|
|
12
12
|
import { extractTagsArrays } from './xml.js'
|
|
13
13
|
import { formatMarkdownTables } from './format-tables.js'
|
|
14
|
+
import { limitHeadingDepth } from './limit-heading-depth.js'
|
|
14
15
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js'
|
|
15
16
|
import { createLogger } from './logger.js'
|
|
16
17
|
|
|
@@ -201,6 +202,7 @@ export async function sendThreadMessage(
|
|
|
201
202
|
|
|
202
203
|
content = formatMarkdownTables(content)
|
|
203
204
|
content = unnestCodeBlocksFromLists(content)
|
|
205
|
+
content = limitHeadingDepth(content)
|
|
204
206
|
content = escapeBackticksInCodeBlocks(content)
|
|
205
207
|
|
|
206
208
|
// If custom flags provided, send as single message (no chunking)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
import { limitHeadingDepth } from './limit-heading-depth.js'
|
|
3
|
+
|
|
4
|
+
test('converts h4 to h3', () => {
|
|
5
|
+
const input = '#### Fourth level heading'
|
|
6
|
+
const result = limitHeadingDepth(input)
|
|
7
|
+
expect(result).toMatchInlineSnapshot(`
|
|
8
|
+
"### Fourth level heading
|
|
9
|
+
"
|
|
10
|
+
`)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('converts h5 to h3', () => {
|
|
14
|
+
const input = '##### Fifth level heading'
|
|
15
|
+
const result = limitHeadingDepth(input)
|
|
16
|
+
expect(result).toMatchInlineSnapshot(`
|
|
17
|
+
"### Fifth level heading
|
|
18
|
+
"
|
|
19
|
+
`)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('converts h6 to h3', () => {
|
|
23
|
+
const input = '###### Sixth level heading'
|
|
24
|
+
const result = limitHeadingDepth(input)
|
|
25
|
+
expect(result).toMatchInlineSnapshot(`
|
|
26
|
+
"### Sixth level heading
|
|
27
|
+
"
|
|
28
|
+
`)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('preserves h3 unchanged', () => {
|
|
32
|
+
const input = '### Third level heading'
|
|
33
|
+
const result = limitHeadingDepth(input)
|
|
34
|
+
expect(result).toMatchInlineSnapshot(`"### Third level heading"`)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('preserves h2 unchanged', () => {
|
|
38
|
+
const input = '## Second level heading'
|
|
39
|
+
const result = limitHeadingDepth(input)
|
|
40
|
+
expect(result).toMatchInlineSnapshot(`"## Second level heading"`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('preserves h1 unchanged', () => {
|
|
44
|
+
const input = '# First level heading'
|
|
45
|
+
const result = limitHeadingDepth(input)
|
|
46
|
+
expect(result).toMatchInlineSnapshot(`"# First level heading"`)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('handles multiple headings in document', () => {
|
|
50
|
+
const input = `# Title
|
|
51
|
+
|
|
52
|
+
Some text
|
|
53
|
+
|
|
54
|
+
## Section
|
|
55
|
+
|
|
56
|
+
### Subsection
|
|
57
|
+
|
|
58
|
+
#### Too deep
|
|
59
|
+
|
|
60
|
+
##### Even deeper
|
|
61
|
+
|
|
62
|
+
Regular paragraph
|
|
63
|
+
|
|
64
|
+
### Back to normal
|
|
65
|
+
`
|
|
66
|
+
const result = limitHeadingDepth(input)
|
|
67
|
+
expect(result).toMatchInlineSnapshot(`
|
|
68
|
+
"# Title
|
|
69
|
+
|
|
70
|
+
Some text
|
|
71
|
+
|
|
72
|
+
## Section
|
|
73
|
+
|
|
74
|
+
### Subsection
|
|
75
|
+
|
|
76
|
+
### Too deep
|
|
77
|
+
### Even deeper
|
|
78
|
+
Regular paragraph
|
|
79
|
+
|
|
80
|
+
### Back to normal
|
|
81
|
+
"
|
|
82
|
+
`)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('preserves heading with inline formatting', () => {
|
|
86
|
+
const input = '#### Heading with **bold** and `code`'
|
|
87
|
+
const result = limitHeadingDepth(input)
|
|
88
|
+
expect(result).toMatchInlineSnapshot(`
|
|
89
|
+
"### Heading with **bold** and \`code\`
|
|
90
|
+
"
|
|
91
|
+
`)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('handles empty markdown', () => {
|
|
95
|
+
const result = limitHeadingDepth('')
|
|
96
|
+
expect(result).toMatchInlineSnapshot(`""`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('handles markdown with no headings', () => {
|
|
100
|
+
const input = 'Just some text\n\nAnd more text'
|
|
101
|
+
const result = limitHeadingDepth(input)
|
|
102
|
+
expect(result).toMatchInlineSnapshot(`
|
|
103
|
+
"Just some text
|
|
104
|
+
|
|
105
|
+
And more text"
|
|
106
|
+
`)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('allows custom maxDepth', () => {
|
|
110
|
+
const input = '### Third level'
|
|
111
|
+
const result = limitHeadingDepth(input, 2)
|
|
112
|
+
expect(result).toMatchInlineSnapshot(`
|
|
113
|
+
"## Third level
|
|
114
|
+
"
|
|
115
|
+
`)
|
|
116
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Limit heading depth for Discord.
|
|
2
|
+
// Discord only supports headings up to ### (h3), so this converts
|
|
3
|
+
// ####, #####, etc. to ### to maintain consistent rendering.
|
|
4
|
+
|
|
5
|
+
import { Lexer, type Tokens } from 'marked'
|
|
6
|
+
|
|
7
|
+
export function limitHeadingDepth(markdown: string, maxDepth = 3): string {
|
|
8
|
+
const lexer = new Lexer()
|
|
9
|
+
const tokens = lexer.lex(markdown)
|
|
10
|
+
|
|
11
|
+
let result = ''
|
|
12
|
+
for (const token of tokens) {
|
|
13
|
+
if (token.type === 'heading') {
|
|
14
|
+
const heading = token as Tokens.Heading
|
|
15
|
+
if (heading.depth > maxDepth) {
|
|
16
|
+
const hashes = '#'.repeat(maxDepth)
|
|
17
|
+
result += hashes + ' ' + heading.text + '\n'
|
|
18
|
+
} else {
|
|
19
|
+
result += token.raw
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
result += token.raw
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
@@ -27,7 +27,7 @@ describe('formatTodoList', () => {
|
|
|
27
27
|
},
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
30
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒉ **second task**"`)
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
test('formats double digit todo numbers', () => {
|
|
@@ -53,7 +53,7 @@ describe('formatTodoList', () => {
|
|
|
53
53
|
},
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
56
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒓ **task 12**"`)
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
test('lowercases first letter of content', () => {
|
|
@@ -76,6 +76,6 @@ describe('formatTodoList', () => {
|
|
|
76
76
|
},
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
79
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒈ **fix the bug**"`)
|
|
80
80
|
})
|
|
81
81
|
})
|
|
@@ -245,10 +245,10 @@ export function formatTodoList(part: Part): string {
|
|
|
245
245
|
})
|
|
246
246
|
const activeTodo = todos[activeIndex]
|
|
247
247
|
if (activeIndex === -1 || !activeTodo) return ''
|
|
248
|
-
//
|
|
249
|
-
const
|
|
248
|
+
// digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
|
|
249
|
+
const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛'
|
|
250
250
|
const todoNumber = activeIndex + 1
|
|
251
|
-
const num = todoNumber <= 20 ?
|
|
251
|
+
const num = todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`
|
|
252
252
|
const content = activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1)
|
|
253
253
|
return `${num} **${escapeInlineMarkdown(content)}**`
|
|
254
254
|
}
|
package/src/session-handler.ts
CHANGED
|
@@ -437,9 +437,40 @@ export async function handleOpencodeSession({
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
if (part.type === 'tool' && part.state.status === 'running') {
|
|
440
|
+
// Flush any pending text/reasoning parts before showing the tool
|
|
441
|
+
// This ensures text the LLM generated before the tool call is shown first
|
|
442
|
+
for (const p of currentParts) {
|
|
443
|
+
if (p.type !== 'step-start' && p.type !== 'step-finish' && p.id !== part.id) {
|
|
444
|
+
await sendPartMessage(p)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
440
447
|
await sendPartMessage(part)
|
|
441
448
|
}
|
|
442
449
|
|
|
450
|
+
// Show token usage for completed tools with large output (>5k tokens)
|
|
451
|
+
if (part.type === 'tool' && part.state.status === 'completed') {
|
|
452
|
+
const output = part.state.output || ''
|
|
453
|
+
const outputTokens = Math.ceil(output.length / 4)
|
|
454
|
+
const LARGE_OUTPUT_THRESHOLD = 3000
|
|
455
|
+
if (outputTokens >= LARGE_OUTPUT_THRESHOLD) {
|
|
456
|
+
const formattedTokens = outputTokens >= 1000
|
|
457
|
+
? `${(outputTokens / 1000).toFixed(1)}k`
|
|
458
|
+
: String(outputTokens)
|
|
459
|
+
const percentageSuffix = (() => {
|
|
460
|
+
if (!modelContextLimit) {
|
|
461
|
+
return ''
|
|
462
|
+
}
|
|
463
|
+
const pct = (outputTokens / modelContextLimit) * 100
|
|
464
|
+
if (pct < 1) {
|
|
465
|
+
return ''
|
|
466
|
+
}
|
|
467
|
+
return ` (${pct.toFixed(1)}%)`
|
|
468
|
+
})()
|
|
469
|
+
const chunk = `⬦ ${part.tool} returned ${formattedTokens} tokens${percentageSuffix}`
|
|
470
|
+
await thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
443
474
|
if (part.type === 'reasoning') {
|
|
444
475
|
await sendPartMessage(part)
|
|
445
476
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Kimaki
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|