kimaki 0.4.90 → 0.4.91
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 +80 -2
- package/dist/anthropic-auth-plugin.js +246 -195
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +6 -3
- package/dist/cli-parsing.test.js +23 -0
- package/dist/cli-send-thread.e2e.test.js +2 -2
- package/dist/cli.js +72 -46
- package/dist/commands/merge-worktree.js +6 -3
- package/dist/commands/new-worktree.js +18 -7
- package/dist/commands/worktrees.js +71 -7
- package/dist/context-awareness-plugin.js +52 -50
- package/dist/context-awareness-plugin.test.js +68 -1
- package/dist/discord-bot.js +126 -54
- package/dist/discord-utils.test.js +19 -0
- package/dist/errors.js +0 -5
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +33 -72
- package/dist/forum-sync/config.js +2 -2
- package/dist/forum-sync/markdown.js +4 -8
- package/dist/hrana-server.js +11 -3
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/ipc-tools-plugin.js +11 -4
- package/dist/kimaki-opencode-plugin.js +1 -0
- package/dist/logger.js +0 -1
- package/dist/markdown.js +2 -2
- package/dist/message-preprocessing.js +100 -16
- package/dist/onboarding-tutorial.js +1 -1
- package/dist/opencode-command-detection.js +70 -0
- package/dist/opencode-command-detection.test.js +210 -0
- package/dist/opencode-interrupt-plugin.js +64 -8
- package/dist/opencode-interrupt-plugin.test.js +23 -39
- package/dist/opencode.js +16 -20
- package/dist/pkce.js +23 -0
- package/dist/plugin-logger.js +59 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -1
- package/dist/queue-advanced-question.e2e.test.js +127 -42
- package/dist/sentry.js +7 -114
- package/dist/session-handler/event-stream-state.js +1 -1
- package/dist/session-handler/thread-runtime-state.js +9 -0
- package/dist/session-handler/thread-session-runtime.js +197 -45
- package/dist/session-title-rename.test.js +80 -0
- package/dist/store.js +1 -2
- package/dist/system-message.js +105 -49
- package/dist/system-message.test.js +598 -15
- package/dist/task-runner.js +7 -4
- package/dist/task-schedule.js +2 -0
- package/dist/thread-message-queue.e2e.test.js +18 -11
- package/dist/unnest-code-blocks.js +11 -1
- package/dist/unnest-code-blocks.test.js +32 -0
- package/dist/voice-handler.js +15 -5
- package/dist/voice.js +53 -23
- package/dist/voice.test.js +2 -0
- package/dist/worktrees.js +111 -120
- package/package.json +15 -19
- package/skills/lintcn/SKILL.md +6 -1
- package/skills/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +3 -2
- package/skills/spiceflow/SKILL.md +1 -1
- package/skills/usecomputer/SKILL.md +174 -249
- package/src/agent-model.e2e.test.ts +95 -2
- package/src/anthropic-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +474 -403
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +6 -3
- package/src/cli-parsing.test.ts +32 -0
- package/src/cli-send-thread.e2e.test.ts +2 -2
- package/src/cli.ts +93 -62
- package/src/commands/merge-worktree.ts +8 -3
- package/src/commands/new-worktree.ts +22 -10
- package/src/commands/worktrees.ts +86 -5
- package/src/context-awareness-plugin.test.ts +77 -1
- package/src/context-awareness-plugin.ts +85 -64
- package/src/discord-bot.ts +135 -56
- package/src/discord-utils.test.ts +21 -0
- package/src/errors.ts +0 -6
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +39 -85
- package/src/forum-sync/config.ts +2 -2
- package/src/forum-sync/markdown.ts +5 -9
- package/src/hrana-server.ts +15 -3
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/ipc-tools-plugin.ts +16 -8
- package/src/kimaki-opencode-plugin.ts +1 -0
- package/src/logger.ts +0 -1
- package/src/markdown.ts +2 -2
- package/src/message-preprocessing.ts +117 -16
- package/src/onboarding-tutorial.ts +1 -1
- package/src/opencode-command-detection.test.ts +268 -0
- package/src/opencode-command-detection.ts +79 -0
- package/src/opencode-interrupt-plugin.test.ts +93 -50
- package/src/opencode-interrupt-plugin.ts +86 -9
- package/src/opencode.ts +16 -22
- package/src/plugin-logger.ts +68 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -1
- package/src/queue-advanced-question.e2e.test.ts +243 -158
- package/src/sentry.ts +7 -120
- package/src/session-handler/event-stream-state.ts +1 -1
- package/src/session-handler/thread-runtime-state.ts +17 -0
- package/src/session-handler/thread-session-runtime.ts +232 -46
- package/src/session-title-rename.test.ts +112 -0
- package/src/store.ts +3 -8
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +136 -63
- package/src/task-runner.ts +7 -4
- package/src/task-schedule.ts +3 -0
- package/src/thread-message-queue.e2e.test.ts +22 -11
- package/src/undici.d.ts +12 -0
- package/src/unnest-code-blocks.test.ts +34 -0
- package/src/unnest-code-blocks.ts +18 -1
- package/src/voice-handler.ts +18 -4
- package/src/voice.test.ts +2 -0
- package/src/voice.ts +68 -23
- package/src/worktrees.ts +152 -156
|
@@ -3,46 +3,94 @@
|
|
|
3
3
|
// The user's message must appear as a real user message in the thread, not
|
|
4
4
|
// get consumed as a tool result answer (which lost voice/image content).
|
|
5
5
|
import { describe, test, expect, afterEach } from 'vitest';
|
|
6
|
-
import { setupQueueAdvancedSuite, TEST_USER_ID
|
|
7
|
-
import { waitForBotMessageContaining, waitForFooterMessage
|
|
8
|
-
import { pendingQuestionContexts } from './commands/ask-question.js';
|
|
6
|
+
import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js';
|
|
7
|
+
import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js';
|
|
9
8
|
import { store } from './store.js';
|
|
9
|
+
import { getOpencodeClient } from './opencode.js';
|
|
10
|
+
import { getThreadSession } from './database.js';
|
|
10
11
|
const TEXT_CHANNEL_ID = '200000000000001007';
|
|
11
12
|
const VOICE_CHANNEL_ID = '200000000000001017';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
await new Promise((resolve) => {
|
|
22
|
-
setTimeout(resolve, 100);
|
|
23
|
-
});
|
|
13
|
+
function setDeterministicTranscription(config) {
|
|
14
|
+
store.setState({
|
|
15
|
+
test: { deterministicTranscription: config },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function getOpencodeClientForTest(projectDirectory) {
|
|
19
|
+
const client = getOpencodeClient(projectDirectory);
|
|
20
|
+
if (!client) {
|
|
21
|
+
throw new Error('OpenCode client not found for project directory');
|
|
24
22
|
}
|
|
25
|
-
|
|
23
|
+
return client;
|
|
24
|
+
}
|
|
25
|
+
function getTextFromParts(parts) {
|
|
26
|
+
return parts.flatMap((part) => {
|
|
27
|
+
if (part.type === 'text') {
|
|
28
|
+
return [part.text];
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function normalizeSessionText(text) {
|
|
34
|
+
return text
|
|
35
|
+
.replace(/\[current git branch is [^\]]+\]/g, '')
|
|
36
|
+
.replace(/<discord-user[^>]*\/>/g, '<discord-user />')
|
|
37
|
+
.trim();
|
|
26
38
|
}
|
|
27
|
-
|
|
39
|
+
function getSessionRoleTextTimeline(messages) {
|
|
40
|
+
return messages.flatMap((message) => {
|
|
41
|
+
const text = normalizeSessionText(getTextFromParts(message.parts).join(''));
|
|
42
|
+
if (!text.trim()) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return [{ role: message.info.role, text }];
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function getSessionMessageSummary(messages) {
|
|
49
|
+
return messages.map((message) => {
|
|
50
|
+
return {
|
|
51
|
+
role: message.info.role,
|
|
52
|
+
parts: message.parts.map((part) => {
|
|
53
|
+
if (part.type === 'text') {
|
|
54
|
+
return {
|
|
55
|
+
type: part.type,
|
|
56
|
+
text: normalizeSessionText(part.text),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (part.type === 'tool') {
|
|
60
|
+
return {
|
|
61
|
+
type: part.type,
|
|
62
|
+
tool: part.tool,
|
|
63
|
+
status: part.state.status,
|
|
64
|
+
title: part.state.status === 'completed' ? part.state.title : undefined,
|
|
65
|
+
output: part.state.status === 'completed' ? part.state.output : undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { type: part.type };
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function waitForSessionMessages({ projectDirectory, sessionId, timeoutMs, predicate, }) {
|
|
74
|
+
const client = getOpencodeClientForTest(projectDirectory);
|
|
28
75
|
const start = Date.now();
|
|
29
76
|
while (Date.now() - start < timeoutMs) {
|
|
30
|
-
const
|
|
31
|
-
|
|
77
|
+
const response = await client.session.messages({
|
|
78
|
+
sessionID: sessionId,
|
|
79
|
+
directory: projectDirectory,
|
|
32
80
|
});
|
|
33
|
-
|
|
34
|
-
|
|
81
|
+
const messages = response.data ?? [];
|
|
82
|
+
if (predicate(messages)) {
|
|
83
|
+
return messages;
|
|
35
84
|
}
|
|
36
85
|
await new Promise((resolve) => {
|
|
37
86
|
setTimeout(resolve, 100);
|
|
38
87
|
});
|
|
39
88
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
store.setState({
|
|
44
|
-
test: { deterministicTranscription: config },
|
|
89
|
+
const finalResponse = await client.session.messages({
|
|
90
|
+
sessionID: sessionId,
|
|
91
|
+
directory: projectDirectory,
|
|
45
92
|
});
|
|
93
|
+
return finalResponse.data ?? [];
|
|
46
94
|
}
|
|
47
95
|
describe('queue advanced: question tool answer', () => {
|
|
48
96
|
const ctx = setupQueueAdvancedSuite({
|
|
@@ -130,12 +178,16 @@ describe('queue advanced: voice message during pending question', () => {
|
|
|
130
178
|
timeout: 12_000,
|
|
131
179
|
});
|
|
132
180
|
// Send a voice message while the question is pending.
|
|
133
|
-
//
|
|
181
|
+
// Reproduction: Discord voice messages can still carry non-empty
|
|
182
|
+
// message.content. The bug consumed that raw text before transcription,
|
|
183
|
+
// so the session never received the spoken content.
|
|
134
184
|
setDeterministicTranscription({
|
|
135
185
|
transcription: 'I want option Alpha please',
|
|
136
186
|
queueMessage: false,
|
|
137
187
|
});
|
|
138
|
-
await th.user(TEST_USER_ID).sendVoiceMessage(
|
|
188
|
+
await th.user(TEST_USER_ID).sendVoiceMessage({
|
|
189
|
+
content: 'VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL',
|
|
190
|
+
});
|
|
139
191
|
// Give time for question cleanup to propagate
|
|
140
192
|
await new Promise((r) => {
|
|
141
193
|
setTimeout(r, 1_000);
|
|
@@ -155,21 +207,54 @@ describe('queue advanced: voice message during pending question', () => {
|
|
|
155
207
|
afterMessageIncludes: 'I want option Alpha please',
|
|
156
208
|
afterAuthorId: ctx.discord.botUserId,
|
|
157
209
|
});
|
|
210
|
+
const sessionId = await getThreadSession(thread.id);
|
|
211
|
+
expect(sessionId).toBeTruthy();
|
|
212
|
+
const sessionMessages = await waitForSessionMessages({
|
|
213
|
+
projectDirectory: ctx.directories.projectDirectory,
|
|
214
|
+
sessionId: sessionId,
|
|
215
|
+
timeoutMs: 8_000,
|
|
216
|
+
predicate: (messages) => {
|
|
217
|
+
const timeline = getSessionRoleTextTimeline(messages);
|
|
218
|
+
return timeline.some((entry) => {
|
|
219
|
+
return entry.text.includes('I want option Alpha please');
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const sessionTimeline = getSessionRoleTextTimeline(sessionMessages);
|
|
224
|
+
const sessionSummary = getSessionMessageSummary(sessionMessages);
|
|
225
|
+
const latestUserText = sessionTimeline
|
|
226
|
+
.filter((entry) => {
|
|
227
|
+
return entry.role === 'user';
|
|
228
|
+
})
|
|
229
|
+
.at(-1)?.text;
|
|
230
|
+
const assistantTexts = sessionTimeline.flatMap((entry) => {
|
|
231
|
+
if (entry.role === 'assistant') {
|
|
232
|
+
return [entry.text];
|
|
233
|
+
}
|
|
234
|
+
return [];
|
|
235
|
+
});
|
|
236
|
+
expect(latestUserText).toContain('I want option Alpha please');
|
|
237
|
+
expect(latestUserText).not.toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL');
|
|
238
|
+
expect(assistantTexts).toContain('ok');
|
|
239
|
+
expect(sessionSummary.some((message) => {
|
|
240
|
+
return message.role === 'user'
|
|
241
|
+
&& message.parts.some((part) => {
|
|
242
|
+
return part.type === 'text' && part.text.includes('I want option Alpha please');
|
|
243
|
+
});
|
|
244
|
+
})).toBe(true);
|
|
245
|
+
expect(sessionSummary.some((message) => {
|
|
246
|
+
return message.role === 'assistant'
|
|
247
|
+
&& message.parts.some((part) => {
|
|
248
|
+
return part.type === 'text' && part.text === 'ok';
|
|
249
|
+
});
|
|
250
|
+
})).toBe(true);
|
|
158
251
|
const timeline = await th.text({ showInteractions: true });
|
|
159
|
-
expect(timeline).
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
**
|
|
164
|
-
|
|
165
|
-
--- from: user (queue-question-tester)
|
|
166
|
-
[attachment: voice-message.ogg]
|
|
167
|
-
--- from: assistant (TestBot)
|
|
168
|
-
🎤 Transcribing voice message...
|
|
169
|
-
📝 **Transcribed message:** I want option Alpha please
|
|
170
|
-
⬥ ok
|
|
171
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
172
|
-
`);
|
|
252
|
+
expect(timeline).toContain('QUESTION_TEXT_ANSWER_MARKER');
|
|
253
|
+
expect(timeline).toContain('Which option do you prefer?');
|
|
254
|
+
expect(timeline).toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL');
|
|
255
|
+
expect(timeline).toContain('🎤 Transcribing voice message...');
|
|
256
|
+
expect(timeline).toContain('📝 **Transcribed message:** I want option Alpha please');
|
|
257
|
+
expect(timeline).toContain('⬥ ok');
|
|
173
258
|
// Voice content must be present as a real transcribed message, not lost
|
|
174
259
|
expect(timeline).toContain('I want option Alpha please');
|
|
175
260
|
}, 20_000);
|
package/dist/sentry.js
CHANGED
|
@@ -1,123 +1,16 @@
|
|
|
1
|
-
// Sentry
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// (kimaki-opencode-plugin.ts). The plugin process receives the DSN via KIMAKI_SENTRY_DSN env var.
|
|
5
|
-
import * as Sentry from '@sentry/node';
|
|
6
|
-
import * as errore from 'errore';
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
8
|
-
import { sanitizeSensitiveText, sanitizeUnknownValue } from './privacy-sanitizer.js';
|
|
9
|
-
// DSN placeholder — replace with your Sentry project DSN.
|
|
10
|
-
// Users can also set KIMAKI_SENTRY_DSN env var.
|
|
11
|
-
const HARDCODED_DSN = 'https://3b87e21ac01cb9c66225719ea65111d2@o4510952031715328.ingest.us.sentry.io/4510952088469504';
|
|
12
|
-
function readKimakiVersion() {
|
|
13
|
-
try {
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
|
-
const pkg = require('../package.json');
|
|
16
|
-
const version = pkg.version;
|
|
17
|
-
if (!version) {
|
|
18
|
-
return 'unknown';
|
|
19
|
-
}
|
|
20
|
-
return version;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return 'unknown';
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const kimakiVersion = readKimakiVersion();
|
|
27
|
-
const kimakiRelease = `kimaki@${kimakiVersion}`;
|
|
28
|
-
let initialized = false;
|
|
1
|
+
// Sentry stubs. @sentry/node was removed — these are no-op placeholders
|
|
2
|
+
// so the 20+ files importing notifyError/initSentry don't need changing.
|
|
3
|
+
// If Sentry is re-enabled in the future, replace these stubs with real calls.
|
|
29
4
|
/**
|
|
30
|
-
* Initialize Sentry.
|
|
31
|
-
* No-op if DSN is empty or --no-sentry was passed.
|
|
5
|
+
* Initialize Sentry. Currently a no-op.
|
|
32
6
|
*/
|
|
33
|
-
export function initSentry({
|
|
34
|
-
if (process.env.KIMAKI_SENTRY_DISABLED === '1') {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const resolvedDsn = dsn || process.env.KIMAKI_SENTRY_DSN || HARDCODED_DSN;
|
|
38
|
-
if (!resolvedDsn || initialized) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
Sentry.init({
|
|
42
|
-
dsn: resolvedDsn,
|
|
43
|
-
release: kimakiRelease,
|
|
44
|
-
integrations: [],
|
|
45
|
-
tracesSampleRate: 0,
|
|
46
|
-
sendDefaultPii: false,
|
|
47
|
-
profilesSampleRate: 0,
|
|
48
|
-
beforeSend(event, hint) {
|
|
49
|
-
// Skip in development — too noisy, errors appear in terminal
|
|
50
|
-
if (process.env.NODE_ENV === 'development') {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
// Skip abort errors — walks the cause chain so wrapped aborts are caught
|
|
54
|
-
if (errore.isAbortError(hint.originalException)) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
const sanitizedEvent = sanitizeUnknownValue(event, {
|
|
59
|
-
redactPaths: false,
|
|
60
|
-
});
|
|
61
|
-
if (sanitizedEvent && typeof sanitizedEvent === 'object') {
|
|
62
|
-
return sanitizedEvent;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return event;
|
|
67
|
-
}
|
|
68
|
-
return event;
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
Sentry.setTag('kimaki_version', kimakiVersion);
|
|
72
|
-
initialized = true;
|
|
73
|
-
}
|
|
7
|
+
export function initSentry(_opts) { }
|
|
74
8
|
/**
|
|
75
|
-
* Report an unexpected error
|
|
9
|
+
* Report an unexpected error. Currently a no-op.
|
|
76
10
|
* Safe to call even if Sentry is not initialized.
|
|
77
11
|
* Fire-and-forget only: use `void notifyError(error, msg)` and never await it.
|
|
78
|
-
* This helper must never throw.
|
|
79
|
-
* Use this at terminal error handlers — the "last catch" in a chain
|
|
80
|
-
* where the error would otherwise be invisible.
|
|
81
12
|
*/
|
|
82
|
-
export function notifyError(
|
|
83
|
-
try {
|
|
84
|
-
if (!initialized) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
// TODO re enable sentry?
|
|
88
|
-
if (!process.env.KIMAKI_SENTRY)
|
|
89
|
-
return;
|
|
90
|
-
const safeMsg = (() => {
|
|
91
|
-
if (!msg) {
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
return sanitizeSensitiveText(msg, { redactPaths: false });
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return msg;
|
|
99
|
-
}
|
|
100
|
-
})();
|
|
101
|
-
const safeError = (() => {
|
|
102
|
-
try {
|
|
103
|
-
return sanitizeUnknownValue(error, { redactPaths: false });
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return error;
|
|
107
|
-
}
|
|
108
|
-
})();
|
|
109
|
-
Sentry.captureException(error, {
|
|
110
|
-
tags: { kimaki_version: kimakiVersion },
|
|
111
|
-
extra: { msg: safeMsg, kimakiVersion, error: safeError },
|
|
112
|
-
});
|
|
113
|
-
void Sentry.flush(1000).catch(() => {
|
|
114
|
-
return;
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
13
|
+
export function notifyError(_error, _msg) { }
|
|
121
14
|
/**
|
|
122
15
|
* User-readable error class. Messages from AppError instances
|
|
123
16
|
* are forwarded to the user as-is; regular Error messages may be obfuscated.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { getOpencodeEventSessionId } from './opencode-session-event-log.js';
|
|
6
6
|
function getTaskChildSessionId({ part, }) {
|
|
7
7
|
// Event-shape reference:
|
|
8
|
-
// -
|
|
8
|
+
// - cli/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl
|
|
9
9
|
// - In real task events, state.metadata.sessionId appears on running/completed
|
|
10
10
|
// tool updates and is the canonical child-session identifier.
|
|
11
11
|
// We intentionally do not parse state.output because it is user-facing text
|
|
@@ -15,6 +15,7 @@ import { store } from '../store.js';
|
|
|
15
15
|
export function initialThreadState() {
|
|
16
16
|
return {
|
|
17
17
|
sessionId: undefined,
|
|
18
|
+
sessionUsername: undefined,
|
|
18
19
|
queueItems: [],
|
|
19
20
|
listenerController: undefined,
|
|
20
21
|
sentPartIds: new Set(),
|
|
@@ -60,6 +61,14 @@ export function removeThread(threadId) {
|
|
|
60
61
|
export function setSessionId(threadId, sessionId) {
|
|
61
62
|
updateThread(threadId, (t) => ({ ...t, sessionId }));
|
|
62
63
|
}
|
|
64
|
+
export function setSessionUsername(threadId, username) {
|
|
65
|
+
updateThread(threadId, (t) => {
|
|
66
|
+
if (t.sessionUsername) {
|
|
67
|
+
return t;
|
|
68
|
+
}
|
|
69
|
+
return { ...t, sessionUsername: username };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
63
72
|
export function enqueueItem(threadId, item) {
|
|
64
73
|
updateThread(threadId, (t) => ({
|
|
65
74
|
...t,
|