kimaki 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-model.e2e.test.js +33 -33
- package/dist/anthropic-account-identity.js +1 -1
- package/dist/anthropic-auth-plugin.js +5 -5
- package/dist/anthropic-auth-plugin.test.js +30 -2
- package/dist/anthropic-auth-state.js +1 -1
- package/dist/bash-tool-schema-plugin.js +18 -0
- package/dist/bash-tool-schema-plugin.test.js +20 -1
- package/dist/bin.js +7 -3
- package/dist/bot-lifecycle.test.js +206 -0
- package/dist/cache-drift-plugin.js +25 -18
- package/dist/cache-rewrite.js +39 -0
- package/dist/cache-rewrite.test.js +41 -0
- package/dist/channel-management.js +113 -51
- package/dist/cli-commands/misc.js +4 -1
- package/dist/cli-commands/project.js +7 -4
- package/dist/cli-commands/session.js +118 -36
- package/dist/cli-commands/thread.js +63 -0
- package/dist/cli-parsing.test.js +3 -3
- package/dist/cli-runner.js +11 -1
- package/dist/cli-send-thread.e2e.test.js +2 -2
- package/dist/cli.js +6 -4
- package/dist/commands/ask-question.js +72 -23
- package/dist/commands/queue.js +84 -36
- package/dist/commands/queue.test.js +15 -0
- package/dist/commands/restart-opencode-server.js +7 -1
- package/dist/commands/session-id.js +5 -5
- package/dist/database.js +149 -6
- package/dist/db.js +34 -0
- package/dist/db.test.js +199 -1
- package/dist/default-channel-provisioning.test.js +2 -1
- package/dist/discord-bot.js +220 -146
- package/dist/discord-command-registration.js +1 -1
- package/dist/external-directory-permissions.e2e.test.js +3 -3
- package/dist/external-directory-project-deny.e2e.test.js +1 -1
- package/dist/forum-sync/watchers.js +15 -0
- package/dist/gateway-proxy.e2e.test.js +9 -9
- package/dist/guild-categories.test.js +225 -0
- package/dist/hrana-server.js +68 -14
- package/dist/interaction-handler.js +5 -1
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/kimaki-opencode-plugin.test.js +28 -0
- package/dist/list-channel-threads.js +163 -0
- package/dist/list-channel-threads.test.js +80 -0
- package/dist/markdown.js +164 -102
- package/dist/markdown.test.js +88 -20
- package/dist/message-formatting.js +29 -18
- package/dist/message-formatting.test.js +111 -3
- package/dist/opencode.js +27 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-resume.e2e.test.js +100 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +1 -1
- package/dist/question-text-before-dropdown.e2e.test.js +1 -1
- package/dist/queue-advanced-abort.e2e.test.js +3 -3
- package/dist/queue-advanced-action-buttons.e2e.test.js +5 -5
- package/dist/queue-advanced-e2e-setup.js +63 -0
- package/dist/queue-advanced-footer.e2e.test.js +18 -18
- package/dist/queue-advanced-model-switch.e2e.test.js +3 -4
- package/dist/queue-advanced-permissions-typing.e2e.test.js +2 -2
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +3 -3
- package/dist/queue-delete-message.e2e.test.js +4 -4
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +3 -3
- package/dist/queue-interrupt-drain.e2e.test.js +10 -10
- package/dist/queue-question-select-drain.e2e.test.js +5 -5
- package/dist/runtime-lifecycle.e2e.test.js +95 -11
- package/dist/schema.js +21 -0
- package/dist/session-handler/event-stream-state.js +240 -6
- package/dist/session-handler/event-stream-state.test.js +1116 -130
- package/dist/session-handler/thread-runtime-state.js +4 -0
- package/dist/session-handler/thread-session-runtime.js +499 -89
- package/dist/sleep.e2e.test.js +7 -7
- package/dist/store.js +1 -1
- package/dist/system-message.js +64 -23
- package/dist/system-message.test.js +69 -23
- package/dist/task-id-plugin.js +16 -0
- package/dist/task-runner.js +7 -0
- package/dist/task-schedule.test.js +1 -1
- package/dist/test-utils.js +5 -3
- package/dist/thread-message-queue.e2e.test.js +30 -31
- package/dist/undo-redo.e2e.test.js +3 -3
- package/dist/voice-message.e2e.test.js +64 -26
- package/dist/voice-routing-input.e2e.test.js +4 -4
- package/dist/voice-routing-question.e2e.test.js +4 -4
- package/dist/voice.js +107 -21
- package/dist/voice.test.js +52 -1
- package/dist/wait-session.js +24 -2
- package/dist/worktree-lifecycle.e2e.test.js +8 -8
- package/package.json +7 -7
- package/skills/egaki/SKILL.md +14 -0
- package/skills/holocron/SKILL.md +125 -42
- package/skills/opensrc/SKILL.md +20 -3
- package/skills/spiceflow/SKILL.md +53 -172
- package/skills/strada/SKILL.md +61 -232
- package/src/agent-model.e2e.test.ts +33 -33
- package/src/anthropic-account-identity.ts +1 -1
- package/src/anthropic-auth-plugin.test.ts +37 -2
- package/src/anthropic-auth-plugin.ts +9 -7
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +29 -0
- package/src/bash-tool-schema-plugin.ts +31 -0
- package/src/bin.ts +7 -3
- package/src/bot-lifecycle.test.ts +252 -0
- package/src/cache-drift-plugin.ts +34 -24
- package/src/cache-rewrite.test.ts +49 -0
- package/src/cache-rewrite.ts +96 -0
- package/src/channel-management.ts +144 -65
- package/src/cli-commands/misc.ts +4 -1
- package/src/cli-commands/project.ts +9 -4
- package/src/cli-commands/session.ts +167 -38
- package/src/cli-commands/thread.ts +86 -0
- package/src/cli-parsing.test.ts +3 -3
- package/src/cli-runner.ts +11 -0
- package/src/cli-send-thread.e2e.test.ts +2 -2
- package/src/cli.ts +8 -6
- package/src/commands/ask-question.ts +88 -25
- package/src/commands/queue.test.ts +19 -0
- package/src/commands/queue.ts +111 -45
- package/src/commands/restart-opencode-server.ts +12 -1
- package/src/commands/session-id.ts +5 -5
- package/src/database.ts +219 -6
- package/src/db.test.ts +236 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +2 -1
- package/src/discord-bot.ts +272 -185
- package/src/discord-command-registration.ts +3 -1
- package/src/external-directory-permissions.e2e.test.ts +3 -3
- package/src/external-directory-project-deny.e2e.test.ts +1 -1
- package/src/forum-sync/watchers.ts +20 -0
- package/src/gateway-proxy.e2e.test.ts +9 -9
- package/src/guild-categories.test.ts +260 -0
- package/src/hrana-server.ts +96 -16
- package/src/interaction-handler.ts +7 -0
- package/src/kimaki-opencode-plugin.test.ts +34 -0
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/list-channel-threads.test.ts +90 -0
- package/src/list-channel-threads.ts +252 -0
- package/src/markdown.test.ts +108 -22
- package/src/markdown.ts +216 -103
- package/src/message-formatting.test.ts +129 -2
- package/src/message-formatting.ts +28 -14
- package/src/opencode.ts +28 -13
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-resume.e2e.test.ts +121 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +1 -1
- package/src/question-text-before-dropdown.e2e.test.ts +1 -1
- package/src/queue-advanced-abort.e2e.test.ts +3 -3
- package/src/queue-advanced-action-buttons.e2e.test.ts +5 -5
- package/src/queue-advanced-e2e-setup.ts +65 -0
- package/src/queue-advanced-footer.e2e.test.ts +18 -18
- package/src/queue-advanced-model-switch.e2e.test.ts +3 -4
- package/src/queue-advanced-permissions-typing.e2e.test.ts +2 -2
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +3 -3
- package/src/queue-delete-message.e2e.test.ts +4 -4
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +3 -3
- package/src/queue-interrupt-drain.e2e.test.ts +10 -10
- package/src/queue-question-select-drain.e2e.test.ts +5 -5
- package/src/runtime-lifecycle.e2e.test.ts +104 -10
- package/src/schema.sql +16 -0
- package/src/schema.ts +23 -0
- package/src/session-handler/event-stream-state.test.ts +1184 -133
- package/src/session-handler/event-stream-state.ts +322 -8
- package/src/session-handler/thread-runtime-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +597 -95
- package/src/sleep.e2e.test.ts +7 -7
- package/src/store.ts +2 -2
- package/src/system-message.test.ts +70 -23
- package/src/system-message.ts +67 -22
- package/src/task-id-plugin.ts +17 -0
- package/src/task-runner.ts +8 -0
- package/src/task-schedule.test.ts +1 -1
- package/src/test-utils.ts +5 -3
- package/src/thread-message-queue.e2e.test.ts +30 -31
- package/src/undo-redo.e2e.test.ts +3 -3
- package/src/voice-message.e2e.test.ts +70 -26
- package/src/voice-routing-input.e2e.test.ts +4 -4
- package/src/voice-routing-question.e2e.test.ts +4 -4
- package/src/voice.test.ts +56 -0
- package/src/voice.ts +135 -25
- package/src/wait-session.ts +25 -2
- package/src/worktree-lifecycle.e2e.test.ts +8 -8
package/dist/markdown.js
CHANGED
|
@@ -15,6 +15,112 @@ class UnexpectedError extends errore.createTaggedError({
|
|
|
15
15
|
}
|
|
16
16
|
const markdownLogger = createLogger(LogPrefix.MARKDOWN);
|
|
17
17
|
const TOOL_OUTPUT_MAX_CHARS = 30_000;
|
|
18
|
+
export const DEFAULT_TOOL_INPUT_MAX_CHARS = 80;
|
|
19
|
+
function stringifyToolValue(value) {
|
|
20
|
+
if (typeof value === 'string')
|
|
21
|
+
return value;
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
export function truncateChars(value, maxChars) {
|
|
25
|
+
const collapsed = value.replace(/\s+/g, ' ').trim();
|
|
26
|
+
if (maxChars <= 0)
|
|
27
|
+
return '';
|
|
28
|
+
if (collapsed.length <= maxChars)
|
|
29
|
+
return collapsed;
|
|
30
|
+
if (maxChars === 1)
|
|
31
|
+
return '…';
|
|
32
|
+
return `${collapsed.slice(0, maxChars - 1)}…`;
|
|
33
|
+
}
|
|
34
|
+
export function formatDuration(ms) {
|
|
35
|
+
if (ms < 1000)
|
|
36
|
+
return `${ms}ms`;
|
|
37
|
+
if (ms < 60_000)
|
|
38
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
39
|
+
const minutes = Math.floor(ms / 60_000);
|
|
40
|
+
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
41
|
+
return `${minutes}m ${seconds}s`;
|
|
42
|
+
}
|
|
43
|
+
function messageCreatedAt(message) {
|
|
44
|
+
const created = message.time?.created;
|
|
45
|
+
return typeof created === 'number' ? created : undefined;
|
|
46
|
+
}
|
|
47
|
+
function messageEndedAt(message) {
|
|
48
|
+
const completed = message.time?.completed;
|
|
49
|
+
if (typeof completed === 'number')
|
|
50
|
+
return completed;
|
|
51
|
+
return messageCreatedAt(message);
|
|
52
|
+
}
|
|
53
|
+
/** User sent → last assistant finished, until the next user message. */
|
|
54
|
+
export function userPromptDurationMs({ messages, userIndex, }) {
|
|
55
|
+
const userCreated = messageCreatedAt(messages[userIndex]?.info ?? {});
|
|
56
|
+
if (userCreated === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
let end = userCreated;
|
|
59
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
60
|
+
const info = messages[i]?.info;
|
|
61
|
+
if (info?.role === 'user')
|
|
62
|
+
break;
|
|
63
|
+
if (info?.role !== 'assistant')
|
|
64
|
+
continue;
|
|
65
|
+
const ended = messageEndedAt(info);
|
|
66
|
+
if (ended !== undefined && ended > end)
|
|
67
|
+
end = ended;
|
|
68
|
+
}
|
|
69
|
+
const ms = end - userCreated;
|
|
70
|
+
return ms > 0 ? ms : undefined;
|
|
71
|
+
}
|
|
72
|
+
export function fileBaseName(filePath) {
|
|
73
|
+
const normalized = filePath.replaceAll('\\', '/');
|
|
74
|
+
const segments = normalized.split('/');
|
|
75
|
+
return segments[segments.length - 1] || filePath;
|
|
76
|
+
}
|
|
77
|
+
function readTaskSessionId(metadata) {
|
|
78
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
79
|
+
return undefined;
|
|
80
|
+
const sessionId = metadata.sessionId;
|
|
81
|
+
return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : undefined;
|
|
82
|
+
}
|
|
83
|
+
function taskChildSessionId({ input, metadata, }) {
|
|
84
|
+
const sessionId = readTaskSessionId(metadata);
|
|
85
|
+
if (sessionId)
|
|
86
|
+
return sessionId;
|
|
87
|
+
const taskId = input?.task_id;
|
|
88
|
+
if (typeof taskId === 'string' && taskId.startsWith('ses'))
|
|
89
|
+
return taskId;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/** Compact tool input for session markdown. Keep this greppable and short. */
|
|
93
|
+
export function formatCompactToolSummary({ tool, input, maxChars = DEFAULT_TOOL_INPUT_MAX_CHARS, metadata, }) {
|
|
94
|
+
const record = input ?? {};
|
|
95
|
+
if (tool === 'read') {
|
|
96
|
+
const path = record.filePath ?? record.path;
|
|
97
|
+
return typeof path === 'string' && path.length > 0 ? fileBaseName(path) : '';
|
|
98
|
+
}
|
|
99
|
+
if (tool === 'task') {
|
|
100
|
+
const description = typeof record.description === 'string' ? record.description : '';
|
|
101
|
+
const sessionId = taskChildSessionId({ input: record, metadata });
|
|
102
|
+
if (!sessionId)
|
|
103
|
+
return truncateChars(description, maxChars);
|
|
104
|
+
const separator = description.trim() ? 1 : 0;
|
|
105
|
+
const descBudget = Math.max(0, maxChars - sessionId.length - separator);
|
|
106
|
+
const desc = truncateChars(description, descBudget);
|
|
107
|
+
return [desc, sessionId].filter(Boolean).join(' ');
|
|
108
|
+
}
|
|
109
|
+
if (tool === 'bash') {
|
|
110
|
+
const command = typeof record.command === 'string' ? record.command : '';
|
|
111
|
+
const collapsedCommand = command.replace(/\s+/g, ' ').trim();
|
|
112
|
+
if (collapsedCommand.length > 0 && collapsedCommand.length <= maxChars) {
|
|
113
|
+
return collapsedCommand;
|
|
114
|
+
}
|
|
115
|
+
const description = typeof record.description === 'string' ? record.description : '';
|
|
116
|
+
return truncateChars(description || collapsedCommand, maxChars);
|
|
117
|
+
}
|
|
118
|
+
const parts = [];
|
|
119
|
+
for (const [key, value] of Object.entries(record)) {
|
|
120
|
+
parts.push(`${key}=${stringifyToolValue(value)}`);
|
|
121
|
+
}
|
|
122
|
+
return truncateChars(parts.join(' '), maxChars);
|
|
123
|
+
}
|
|
18
124
|
export class ShareMarkdown {
|
|
19
125
|
client;
|
|
20
126
|
constructor(client) {
|
|
@@ -26,7 +132,7 @@ export class ShareMarkdown {
|
|
|
26
132
|
* @returns Error or markdown string
|
|
27
133
|
*/
|
|
28
134
|
async generate(options) {
|
|
29
|
-
const { sessionID, includeSystemInfo, lastAssistantOnly, compactTools = true } = options;
|
|
135
|
+
const { sessionID, includeSystemInfo, lastAssistantOnly, compactTools = true, includeThinking = false, toolInputMaxChars = DEFAULT_TOOL_INPUT_MAX_CHARS, } = options;
|
|
30
136
|
// Get session info
|
|
31
137
|
const sessionResponse = await this.client.session.get({
|
|
32
138
|
sessionID,
|
|
@@ -45,12 +151,7 @@ export class ShareMarkdown {
|
|
|
45
151
|
const messages = messagesResponse.data;
|
|
46
152
|
// If lastAssistantOnly, filter to only the last assistant message
|
|
47
153
|
const messagesToRender = lastAssistantOnly
|
|
48
|
-
? (() =>
|
|
49
|
-
const assistantMessages = messages.filter((m) => m.info.role === 'assistant');
|
|
50
|
-
return assistantMessages.length > 0
|
|
51
|
-
? [assistantMessages[assistantMessages.length - 1]]
|
|
52
|
-
: [];
|
|
53
|
-
})()
|
|
154
|
+
? messages.filter((m) => m.info.role === 'assistant').slice(-1)
|
|
54
155
|
: messages;
|
|
55
156
|
// Build markdown
|
|
56
157
|
const lines = [];
|
|
@@ -74,17 +175,37 @@ export class ShareMarkdown {
|
|
|
74
175
|
lines.push('## Conversation');
|
|
75
176
|
lines.push('');
|
|
76
177
|
}
|
|
77
|
-
for (const message of messagesToRender) {
|
|
78
|
-
const messageLines = this.renderMessage(message.info, message.parts, {
|
|
178
|
+
for (const [index, message] of messagesToRender.entries()) {
|
|
179
|
+
const messageLines = this.renderMessage(message.info, message.parts, {
|
|
180
|
+
compactTools,
|
|
181
|
+
includeThinking,
|
|
182
|
+
toolInputMaxChars,
|
|
183
|
+
});
|
|
79
184
|
lines.push(...messageLines);
|
|
80
185
|
lines.push('');
|
|
186
|
+
const nextRole = messagesToRender[index + 1]?.info.role;
|
|
187
|
+
if (message.info.role !== 'assistant' || nextRole === 'assistant')
|
|
188
|
+
continue;
|
|
189
|
+
for (let userIndex = index - 1; userIndex >= 0; userIndex--) {
|
|
190
|
+
if (messagesToRender[userIndex]?.info.role !== 'user')
|
|
191
|
+
continue;
|
|
192
|
+
const durationMs = userPromptDurationMs({
|
|
193
|
+
messages: messagesToRender,
|
|
194
|
+
userIndex,
|
|
195
|
+
});
|
|
196
|
+
if (durationMs !== undefined) {
|
|
197
|
+
lines.push(`duration: ${formatDuration(durationMs)}`);
|
|
198
|
+
lines.push('');
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
81
202
|
}
|
|
82
203
|
return lines.join('\n');
|
|
83
204
|
}
|
|
84
205
|
renderMessage(message, parts, opts) {
|
|
85
206
|
const lines = [];
|
|
86
207
|
if (message.role === 'user') {
|
|
87
|
-
lines.push('###
|
|
208
|
+
lines.push('### user');
|
|
88
209
|
lines.push('');
|
|
89
210
|
for (const part of parts) {
|
|
90
211
|
if (part.type === 'text' && part.text) {
|
|
@@ -95,7 +216,7 @@ export class ShareMarkdown {
|
|
|
95
216
|
}
|
|
96
217
|
}
|
|
97
218
|
else if (part.type === 'file') {
|
|
98
|
-
lines.push(
|
|
219
|
+
lines.push(`file: ${part.filename || 'unnamed file'}`);
|
|
99
220
|
if (part.url) {
|
|
100
221
|
lines.push(` - URL: ${part.url}`);
|
|
101
222
|
}
|
|
@@ -104,47 +225,38 @@ export class ShareMarkdown {
|
|
|
104
225
|
}
|
|
105
226
|
}
|
|
106
227
|
else if (message.role === 'assistant') {
|
|
107
|
-
const modelId = [message.providerID, message.modelID].filter(Boolean).join('/') ||
|
|
108
|
-
'unknown model';
|
|
109
|
-
lines.push(`### 🤖 Assistant (${modelId})`);
|
|
110
|
-
lines.push('');
|
|
111
|
-
// Filter and process parts
|
|
112
228
|
const filteredParts = parts.filter((part) => {
|
|
113
|
-
if (part.type === 'step-start'
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (part.type === 'patch')
|
|
229
|
+
if (part.type === 'step-start' ||
|
|
230
|
+
part.type === 'step-finish' ||
|
|
231
|
+
part.type === 'snapshot' ||
|
|
232
|
+
part.type === 'patch')
|
|
118
233
|
return false;
|
|
119
|
-
if (part.type === '
|
|
120
|
-
return false;
|
|
121
|
-
if (part.type === 'text' && part.synthetic === true)
|
|
234
|
+
if (part.type === 'text' && (part.synthetic === true || !part.text))
|
|
122
235
|
return false;
|
|
123
236
|
if (part.type === 'tool' && part.tool === 'todoread')
|
|
124
237
|
return false;
|
|
125
|
-
if (part.type === 'text' && !part.text)
|
|
126
|
-
return false;
|
|
127
238
|
if (part.type === 'tool' &&
|
|
128
239
|
(part.state.status === 'pending' || part.state.status === 'running'))
|
|
129
240
|
return false;
|
|
130
241
|
return true;
|
|
131
242
|
});
|
|
243
|
+
const body = [];
|
|
132
244
|
for (const part of filteredParts) {
|
|
133
|
-
|
|
134
|
-
? this.renderPartCompact(part,
|
|
135
|
-
: this.renderPart(part,
|
|
136
|
-
lines.push(...partLines);
|
|
137
|
-
}
|
|
138
|
-
// Add completion time if available
|
|
139
|
-
if (message.time?.completed) {
|
|
140
|
-
const duration = message.time.completed - message.time.created;
|
|
141
|
-
lines.push('');
|
|
142
|
-
lines.push(`*Completed in ${this.formatDuration(duration)}*`);
|
|
245
|
+
body.push(...(opts.compactTools
|
|
246
|
+
? this.renderPartCompact(part, opts)
|
|
247
|
+
: this.renderPart(part, opts)));
|
|
143
248
|
}
|
|
249
|
+
if (body.length === 0)
|
|
250
|
+
return lines;
|
|
251
|
+
const modelId = [message.providerID, message.modelID].filter(Boolean).join('/') ||
|
|
252
|
+
'unknown model';
|
|
253
|
+
lines.push(`### assistant (${modelId})`);
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push(...body);
|
|
144
256
|
}
|
|
145
257
|
return lines;
|
|
146
258
|
}
|
|
147
|
-
renderPart(part,
|
|
259
|
+
renderPart(part, opts) {
|
|
148
260
|
const lines = [];
|
|
149
261
|
switch (part.type) {
|
|
150
262
|
case 'text':
|
|
@@ -154,14 +266,11 @@ export class ShareMarkdown {
|
|
|
154
266
|
}
|
|
155
267
|
break;
|
|
156
268
|
case 'reasoning':
|
|
157
|
-
if (part.text) {
|
|
158
|
-
lines.push('
|
|
159
|
-
lines.push('<summary>💭 Thinking</summary>');
|
|
269
|
+
if (opts.includeThinking && part.text) {
|
|
270
|
+
lines.push('thinking:');
|
|
160
271
|
lines.push('');
|
|
161
272
|
lines.push(part.text);
|
|
162
273
|
lines.push('');
|
|
163
|
-
lines.push('</details>');
|
|
164
|
-
lines.push('');
|
|
165
274
|
}
|
|
166
275
|
break;
|
|
167
276
|
case 'tool':
|
|
@@ -169,10 +278,10 @@ export class ShareMarkdown {
|
|
|
169
278
|
const output = part.state.output || '';
|
|
170
279
|
const isOversized = output.length > TOOL_OUTPUT_MAX_CHARS;
|
|
171
280
|
if (isOversized) {
|
|
172
|
-
lines.push(`>
|
|
281
|
+
lines.push(`> Large tool output (${output.length.toLocaleString()} chars, truncated to ${TOOL_OUTPUT_MAX_CHARS.toLocaleString()})`);
|
|
173
282
|
lines.push('');
|
|
174
283
|
}
|
|
175
|
-
lines.push(`####
|
|
284
|
+
lines.push(`#### tool: ${part.tool}`);
|
|
176
285
|
lines.push('');
|
|
177
286
|
// Render input parameters in YAML
|
|
178
287
|
if (part.state.input && Object.keys(part.state.input).length > 0) {
|
|
@@ -193,17 +302,9 @@ export class ShareMarkdown {
|
|
|
193
302
|
lines.push('```');
|
|
194
303
|
lines.push('');
|
|
195
304
|
}
|
|
196
|
-
// Add timing info if significant
|
|
197
|
-
if (part.state.time?.start && part.state.time?.end) {
|
|
198
|
-
const duration = part.state.time.end - part.state.time.start;
|
|
199
|
-
if (duration > 2000) {
|
|
200
|
-
lines.push(`*Duration: ${this.formatDuration(duration)}*`);
|
|
201
|
-
lines.push('');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
305
|
}
|
|
205
306
|
else if (part.state.status === 'error') {
|
|
206
|
-
lines.push(`####
|
|
307
|
+
lines.push(`#### tool-error: ${part.tool}`);
|
|
207
308
|
lines.push('');
|
|
208
309
|
lines.push('```');
|
|
209
310
|
lines.push(part.state.error || 'Unknown error');
|
|
@@ -211,75 +312,36 @@ export class ShareMarkdown {
|
|
|
211
312
|
lines.push('');
|
|
212
313
|
}
|
|
213
314
|
break;
|
|
214
|
-
case 'step-start':
|
|
215
|
-
lines.push(`**Started using ${message.providerID}/${message.modelID}**`);
|
|
216
|
-
lines.push('');
|
|
217
|
-
break;
|
|
218
315
|
}
|
|
219
316
|
return lines;
|
|
220
317
|
}
|
|
221
318
|
/** Compact rendering: tool calls become a single line with line count instead of full output. */
|
|
222
|
-
renderPartCompact(part,
|
|
223
|
-
// Non-tool parts render the same as verbose
|
|
319
|
+
renderPartCompact(part, opts) {
|
|
224
320
|
if (part.type !== 'tool') {
|
|
225
|
-
return this.renderPart(part,
|
|
321
|
+
return this.renderPart(part, opts);
|
|
226
322
|
}
|
|
227
323
|
const lines = [];
|
|
228
324
|
if (part.state.status === 'completed') {
|
|
229
325
|
const output = part.state.output || '';
|
|
230
326
|
const lineCount = output ? output.split('\n').length : 0;
|
|
231
|
-
const inputSummary =
|
|
327
|
+
const inputSummary = formatCompactToolSummary({
|
|
328
|
+
tool: part.tool,
|
|
329
|
+
input: part.state.input,
|
|
330
|
+
maxChars: opts.toolInputMaxChars,
|
|
331
|
+
metadata: part.state.metadata,
|
|
332
|
+
});
|
|
232
333
|
const outputLabel = lineCount > 0 ? `(${lineCount} lines)` : '';
|
|
233
334
|
const parts = [inputSummary, outputLabel].filter(Boolean).join(' ');
|
|
234
|
-
lines.push(
|
|
335
|
+
lines.push(`tool: ${part.tool}${parts ? ` ${parts}` : ''}`);
|
|
235
336
|
lines.push('');
|
|
236
337
|
}
|
|
237
338
|
else if (part.state.status === 'error') {
|
|
238
339
|
const errorText = (part.state.error || 'Unknown error').split('\n')[0].slice(0, 120);
|
|
239
|
-
lines.push(
|
|
340
|
+
lines.push(`tool-error: ${part.tool} ${errorText}`);
|
|
240
341
|
lines.push('');
|
|
241
342
|
}
|
|
242
343
|
return lines;
|
|
243
344
|
}
|
|
244
|
-
/** Build a compact key=value summary of tool input, max 2 params, 80 chars each. */
|
|
245
|
-
compactInputSummary(input) {
|
|
246
|
-
if (!input)
|
|
247
|
-
return '';
|
|
248
|
-
const entries = Object.entries(input);
|
|
249
|
-
if (entries.length === 0)
|
|
250
|
-
return '';
|
|
251
|
-
const parts = [];
|
|
252
|
-
const maxParams = 2;
|
|
253
|
-
for (let i = 0; i < Math.min(entries.length, maxParams); i++) {
|
|
254
|
-
const [key, value] = entries[i];
|
|
255
|
-
let val;
|
|
256
|
-
try {
|
|
257
|
-
val = typeof value === 'string'
|
|
258
|
-
? value
|
|
259
|
-
: (JSON.stringify(value) ?? String(value));
|
|
260
|
-
}
|
|
261
|
-
catch {
|
|
262
|
-
val = String(value);
|
|
263
|
-
}
|
|
264
|
-
// Collapse whitespace for readability
|
|
265
|
-
const collapsed = val.replace(/\s+/g, ' ').trim();
|
|
266
|
-
const normalized = collapsed.length > 80 ? `${collapsed.slice(0, 80)}…` : collapsed;
|
|
267
|
-
parts.push(`${key}=${normalized}`);
|
|
268
|
-
}
|
|
269
|
-
if (entries.length > maxParams) {
|
|
270
|
-
parts.push('...');
|
|
271
|
-
}
|
|
272
|
-
return parts.join(', ');
|
|
273
|
-
}
|
|
274
|
-
formatDuration(ms) {
|
|
275
|
-
if (ms < 1000)
|
|
276
|
-
return `${ms}ms`;
|
|
277
|
-
if (ms < 60000)
|
|
278
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
279
|
-
const minutes = Math.floor(ms / 60000);
|
|
280
|
-
const seconds = Math.floor((ms % 60000) / 1000);
|
|
281
|
-
return `${minutes}m ${seconds}s`;
|
|
282
|
-
}
|
|
283
345
|
}
|
|
284
346
|
/**
|
|
285
347
|
* Generate compact session context for voice transcription.
|
package/dist/markdown.test.js
CHANGED
|
@@ -8,10 +8,68 @@ import url from 'node:url';
|
|
|
8
8
|
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
9
9
|
import * as errore from 'errore';
|
|
10
10
|
import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
|
|
11
|
-
import { ShareMarkdown, getCompactSessionContext } from './markdown.js';
|
|
11
|
+
import { ShareMarkdown, fileBaseName, formatCompactToolSummary, formatDuration, getCompactSessionContext, truncateChars, userPromptDurationMs, } from './markdown.js';
|
|
12
12
|
import { setDataDir } from './config.js';
|
|
13
13
|
import { initializeOpencodeForDirectory, getOpencodeClient, stopOpencodeServer } from './opencode.js';
|
|
14
14
|
import { cleanupTestSessions, initTestGitRepo } from './test-utils.js';
|
|
15
|
+
test('truncateChars keeps short text and ellipsizes long text', () => {
|
|
16
|
+
expect(truncateChars('hello world', 80)).toBe('hello world');
|
|
17
|
+
expect(truncateChars(' a b ', 80)).toBe('a b');
|
|
18
|
+
expect(truncateChars('abcdefghij', 5)).toBe('abcd…');
|
|
19
|
+
expect(truncateChars('ab', 1)).toBe('…');
|
|
20
|
+
});
|
|
21
|
+
test('formatDuration uses ms, seconds, then minutes', () => {
|
|
22
|
+
expect(formatDuration(250)).toBe('250ms');
|
|
23
|
+
expect(formatDuration(1500)).toBe('1.5s');
|
|
24
|
+
expect(formatDuration(65_000)).toBe('1m 5s');
|
|
25
|
+
});
|
|
26
|
+
test('userPromptDurationMs spans user send to last following assistant', () => {
|
|
27
|
+
const messages = [
|
|
28
|
+
{ info: { role: 'user', time: { created: 1000 } } },
|
|
29
|
+
{ info: { role: 'assistant', time: { created: 1100, completed: 4000 } } },
|
|
30
|
+
{ info: { role: 'assistant', time: { created: 4100, completed: 9000 } } },
|
|
31
|
+
{ info: { role: 'user', time: { created: 20_000 } } },
|
|
32
|
+
{ info: { role: 'assistant', time: { created: 20_100, completed: 21_000 } } },
|
|
33
|
+
];
|
|
34
|
+
expect(userPromptDurationMs({ messages, userIndex: 0 })).toBe(8000);
|
|
35
|
+
expect(userPromptDurationMs({ messages, userIndex: 3 })).toBe(1000);
|
|
36
|
+
});
|
|
37
|
+
test('fileBaseName strips directories on posix and windows paths', () => {
|
|
38
|
+
expect(fileBaseName('/Users/morse/README.md')).toBe('README.md');
|
|
39
|
+
expect(fileBaseName('src\\cli.ts')).toBe('cli.ts');
|
|
40
|
+
});
|
|
41
|
+
test('formatCompactToolSummary indexes read, task, and bash inputs', () => {
|
|
42
|
+
expect(formatCompactToolSummary({
|
|
43
|
+
tool: 'read',
|
|
44
|
+
input: { filePath: '/Users/morse/Documents/GitHub/kimakivoice/README.md', offset: 1 },
|
|
45
|
+
})).toBe('README.md');
|
|
46
|
+
expect(formatCompactToolSummary({
|
|
47
|
+
tool: 'task',
|
|
48
|
+
input: { description: 'analyze gpuix-solid', task_id: 'ses_child123' },
|
|
49
|
+
})).toBe('analyze gpuix-solid ses_child123');
|
|
50
|
+
expect(formatCompactToolSummary({
|
|
51
|
+
tool: 'task',
|
|
52
|
+
input: { description: 'analyze gpuix-solid' },
|
|
53
|
+
metadata: { sessionId: 'ses_from_meta' },
|
|
54
|
+
})).toBe('analyze gpuix-solid ses_from_meta');
|
|
55
|
+
expect(formatCompactToolSummary({
|
|
56
|
+
tool: 'bash',
|
|
57
|
+
input: { command: 'echo hello world', description: 'Print greeting' },
|
|
58
|
+
})).toBe('echo hello world');
|
|
59
|
+
expect(formatCompactToolSummary({
|
|
60
|
+
tool: 'bash',
|
|
61
|
+
input: {
|
|
62
|
+
command: 'a'.repeat(120),
|
|
63
|
+
description: 'Rebuild native addon',
|
|
64
|
+
},
|
|
65
|
+
maxChars: 80,
|
|
66
|
+
})).toBe('Rebuild native addon');
|
|
67
|
+
expect(formatCompactToolSummary({
|
|
68
|
+
tool: 'grep',
|
|
69
|
+
input: { pattern: 'ShareMarkdown', path: 'cli/src/markdown.ts' },
|
|
70
|
+
maxChars: 40,
|
|
71
|
+
})).toBe('pattern=ShareMarkdown path=cli/src/mark…');
|
|
72
|
+
});
|
|
15
73
|
const ROOT = path.resolve(process.cwd(), 'tmp', 'markdown-test');
|
|
16
74
|
function createRunDirectories() {
|
|
17
75
|
fs.mkdirSync(ROOT, { recursive: true });
|
|
@@ -181,10 +239,7 @@ afterAll(async () => {
|
|
|
181
239
|
// Strip dynamic parts (timestamps, durations, branch names) for stable assertions
|
|
182
240
|
function normalizeMarkdown(md) {
|
|
183
241
|
return md
|
|
184
|
-
|
|
185
|
-
.replace(/\*Completed in [\d.]+[ms]+\*/g, '*Completed in Xs*')
|
|
186
|
-
// Normalize "Duration: Xs" tool timing
|
|
187
|
-
.replace(/\*Duration: [\d.]+[ms]+\*/g, '*Duration: Xs*')
|
|
242
|
+
.replace(/^duration: .+$/gm, 'duration: <duration>')
|
|
188
243
|
// Normalize ISO dates in session info
|
|
189
244
|
.replace(/\*\*Created\*\*: .+/g, '**Created**: <date>')
|
|
190
245
|
.replace(/\*\*Updated\*\*: .+/g, '**Updated**: <date>')
|
|
@@ -206,11 +261,13 @@ test('generate markdown with system info', async () => {
|
|
|
206
261
|
expect(markdown).toContain('# Markdown Test Session');
|
|
207
262
|
expect(markdown).toContain('## Session Information');
|
|
208
263
|
expect(markdown).toContain('## Conversation');
|
|
209
|
-
expect(markdown).toContain('###
|
|
264
|
+
expect(markdown).toContain('### user');
|
|
210
265
|
expect(markdown).toContain('hello markdown test');
|
|
211
|
-
expect(markdown).toContain('###
|
|
266
|
+
expect(markdown).toContain('### assistant (deterministic-provider/deterministic-v2)');
|
|
212
267
|
expect(markdown).toContain('Hello! This is a deterministic markdown test response.');
|
|
213
|
-
expect(markdown).toContain('
|
|
268
|
+
expect(markdown).not.toContain('Started using');
|
|
269
|
+
expect(markdown).not.toContain('Completed in');
|
|
270
|
+
expect(markdown).toMatch(/^duration: /m);
|
|
214
271
|
const normalized = normalizeMarkdown(markdown);
|
|
215
272
|
expect(normalized).toMatchInlineSnapshot(`
|
|
216
273
|
"# Markdown Test Session
|
|
@@ -223,19 +280,17 @@ test('generate markdown with system info', async () => {
|
|
|
223
280
|
|
|
224
281
|
## Conversation
|
|
225
282
|
|
|
226
|
-
###
|
|
283
|
+
### user
|
|
227
284
|
|
|
228
285
|
hello markdown test
|
|
229
286
|
|
|
230
287
|
|
|
231
|
-
###
|
|
232
|
-
|
|
233
|
-
**Started using deterministic-provider/deterministic-v2**
|
|
288
|
+
### assistant (deterministic-provider/deterministic-v2)
|
|
234
289
|
|
|
235
290
|
Hello! This is a deterministic markdown test response.
|
|
236
291
|
|
|
237
292
|
|
|
238
|
-
|
|
293
|
+
duration: <duration>
|
|
239
294
|
"
|
|
240
295
|
`);
|
|
241
296
|
});
|
|
@@ -256,19 +311,17 @@ test('generate markdown without system info', async () => {
|
|
|
256
311
|
|
|
257
312
|
## Conversation
|
|
258
313
|
|
|
259
|
-
###
|
|
314
|
+
### user
|
|
260
315
|
|
|
261
316
|
hello markdown test
|
|
262
317
|
|
|
263
318
|
|
|
264
|
-
###
|
|
265
|
-
|
|
266
|
-
**Started using deterministic-provider/deterministic-v2**
|
|
319
|
+
### assistant (deterministic-provider/deterministic-v2)
|
|
267
320
|
|
|
268
321
|
Hello! This is a deterministic markdown test response.
|
|
269
322
|
|
|
270
323
|
|
|
271
|
-
|
|
324
|
+
duration: <duration>
|
|
272
325
|
"
|
|
273
326
|
`);
|
|
274
327
|
});
|
|
@@ -310,6 +363,21 @@ test('generate markdown with lastAssistantOnly', async () => {
|
|
|
310
363
|
// Should contain the assistant response
|
|
311
364
|
expect(markdown).toContain('Hello! This is a deterministic markdown test response.');
|
|
312
365
|
});
|
|
366
|
+
test('thinking is omitted unless includeThinking is set', async () => {
|
|
367
|
+
const exporter = new ShareMarkdown(client);
|
|
368
|
+
const hidden = await exporter.generate({
|
|
369
|
+
sessionID,
|
|
370
|
+
includeThinking: false,
|
|
371
|
+
});
|
|
372
|
+
expect(errore.isOk(hidden)).toBe(true);
|
|
373
|
+
expect(errore.unwrap(hidden)).not.toContain('thinking:');
|
|
374
|
+
const shown = await exporter.generate({
|
|
375
|
+
sessionID,
|
|
376
|
+
includeThinking: true,
|
|
377
|
+
});
|
|
378
|
+
expect(errore.isOk(shown)).toBe(true);
|
|
379
|
+
expect(errore.unwrap(shown)).not.toContain('💭');
|
|
380
|
+
});
|
|
313
381
|
test('compact tools: tool calls show one-liner with line count', async () => {
|
|
314
382
|
const exporter = new ShareMarkdown(client);
|
|
315
383
|
const result = await exporter.generate({
|
|
@@ -319,7 +387,7 @@ test('compact tools: tool calls show one-liner with line count', async () => {
|
|
|
319
387
|
expect(errore.isOk(result)).toBe(true);
|
|
320
388
|
const md = errore.unwrap(result);
|
|
321
389
|
// Compact mode: exact one-liner format with params and line count
|
|
322
|
-
expect(md).toContain('
|
|
390
|
+
expect(md).toContain('tool: bash echo hello world');
|
|
323
391
|
expect(md).toMatch(/\(\d+ lines?\)/);
|
|
324
392
|
// Should NOT contain full output code blocks or input YAML
|
|
325
393
|
expect(md).not.toContain('**Output:**');
|
|
@@ -335,7 +403,7 @@ test('verbose tools: tool calls show full input and output', async () => {
|
|
|
335
403
|
expect(errore.isOk(result)).toBe(true);
|
|
336
404
|
const md = errore.unwrap(result);
|
|
337
405
|
// Verbose mode: full tool rendering with input YAML and output code block
|
|
338
|
-
expect(md).toContain('####
|
|
406
|
+
expect(md).toContain('#### tool: bash');
|
|
339
407
|
expect(md).toContain('**Input:**');
|
|
340
408
|
expect(md).toContain('```yaml');
|
|
341
409
|
expect(md).toContain('**Output:**');
|
|
@@ -12,9 +12,20 @@ const logger = createLogger(LogPrefix.FORMATTING);
|
|
|
12
12
|
export const TOOL_PREFIX = '┣ ';
|
|
13
13
|
export const FILE_EDIT_PREFIX = '◼︎ ';
|
|
14
14
|
export const THINKING_PREFIX = '┣ ';
|
|
15
|
-
|
|
15
|
+
// Discord subtext. Not `#-`.
|
|
16
|
+
export const SYSTEM_LINE_PREFIX = '-# ';
|
|
17
|
+
export const STATUS_PREFIX = SYSTEM_LINE_PREFIX;
|
|
16
18
|
export const QUEUE_PREFIX = '» ';
|
|
17
|
-
export const WORKTREE_PREFIX =
|
|
19
|
+
export const WORKTREE_PREFIX = '⬦ ';
|
|
20
|
+
export function asSystemLine(text) {
|
|
21
|
+
const lead = text.startsWith('\n') ? '\n' : '';
|
|
22
|
+
const body = lead ? text.slice(1) : text;
|
|
23
|
+
return lead + body.split('\n').map((line) => {
|
|
24
|
+
if (line.startsWith(SYSTEM_LINE_PREFIX) || line.startsWith('-#'))
|
|
25
|
+
return line;
|
|
26
|
+
return `${SYSTEM_LINE_PREFIX}${line}`;
|
|
27
|
+
}).join('\n');
|
|
28
|
+
}
|
|
18
29
|
/**
|
|
19
30
|
* Serialize Discord embeds into plain text so the AI model can read them.
|
|
20
31
|
* Each embed becomes an <embed> XML block with title, author, description,
|
|
@@ -165,6 +176,11 @@ export function asDiscordQuote(text) {
|
|
|
165
176
|
return `> ${line}`;
|
|
166
177
|
}).join('\n');
|
|
167
178
|
}
|
|
179
|
+
// Leave one char for the kind-change blank line in sessionPartContent.
|
|
180
|
+
const DISCORD_MESSAGE_MAX_LENGTH = 2000;
|
|
181
|
+
export function quotedTextFitsOneDiscordMessage(text) {
|
|
182
|
+
return asDiscordQuote(text).length < DISCORD_MESSAGE_MAX_LENGTH;
|
|
183
|
+
}
|
|
168
184
|
function isNonEmptyTextPart(part) {
|
|
169
185
|
return part.type === 'text' && Boolean(part.text?.trim());
|
|
170
186
|
}
|
|
@@ -187,6 +203,8 @@ export function shouldQuoteIntermediateTextPart({ part, isLastInTurn, nextToolNa
|
|
|
187
203
|
return false;
|
|
188
204
|
if (text.trim().split('\n').length > 2)
|
|
189
205
|
return false;
|
|
206
|
+
if (!quotedTextFitsOneDiscordMessage(text))
|
|
207
|
+
return false;
|
|
190
208
|
if (nextToolName === 'question'
|
|
191
209
|
|| nextToolName?.endsWith('kimaki_sleep')
|
|
192
210
|
|| nextToolName?.endsWith('kimaki_action_buttons')) {
|
|
@@ -195,31 +213,22 @@ export function shouldQuoteIntermediateTextPart({ part, isLastInTurn, nextToolNa
|
|
|
195
213
|
return true;
|
|
196
214
|
}
|
|
197
215
|
export function planAssistantTurnFlush({ parts, mode, throughPartId, }) {
|
|
198
|
-
const lastText = parts.filter(isNonEmptyTextPart).at(-1);
|
|
199
|
-
const lastTextIndex = lastText
|
|
200
|
-
? parts.findLastIndex((part) => part.id === lastText.id)
|
|
201
|
-
: -1;
|
|
202
216
|
const throughIndex = throughPartId
|
|
203
217
|
? parts.findIndex((part) => part.id === throughPartId)
|
|
204
218
|
: -1;
|
|
205
219
|
const sendUntil = (() => {
|
|
206
|
-
if (mode === '
|
|
207
|
-
return
|
|
208
|
-
if (mode === 'interactive') {
|
|
209
|
-
if (throughIndex >= 0)
|
|
210
|
-
return throughIndex + 1;
|
|
211
|
-
return parts.length;
|
|
212
|
-
}
|
|
213
|
-
if (!lastText)
|
|
214
|
-
return parts.length;
|
|
215
|
-
// Hold unfinished last text and anything after it so tool order stays intact.
|
|
216
|
-
if (!lastText.time?.end)
|
|
217
|
-
return lastTextIndex;
|
|
220
|
+
if (mode === 'interactive' && throughIndex >= 0)
|
|
221
|
+
return throughIndex + 1;
|
|
218
222
|
return parts.length;
|
|
219
223
|
})();
|
|
220
224
|
const sendParts = [];
|
|
221
225
|
const holdParts = [];
|
|
222
226
|
for (const [index, part] of parts.entries()) {
|
|
227
|
+
// Live tools must not wait for the current text part to end.
|
|
228
|
+
if (mode === 'progress' && part.type === 'text' && !part.time?.end) {
|
|
229
|
+
holdParts.push(part);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
223
232
|
if (index < sendUntil) {
|
|
224
233
|
sendParts.push({
|
|
225
234
|
part,
|
|
@@ -632,6 +641,8 @@ export function formatTaskToolTitle(part) {
|
|
|
632
641
|
export function formatPart(part, prefix) {
|
|
633
642
|
const pfx = prefix ? `${prefix} ⋅ ` : '';
|
|
634
643
|
if (part.type === 'text') {
|
|
644
|
+
if (part.synthetic === true)
|
|
645
|
+
return '';
|
|
635
646
|
const text = part.text?.trim();
|
|
636
647
|
if (!text)
|
|
637
648
|
return '';
|