@openchambery/web 2.0.0-beta.22 → 2.0.0-beta.24
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/assets/{AssistantView-BkjDwtaQ.js → AssistantView-qn9Hipja.js} +3 -3
- package/dist/assets/{DiagramView-DRuf0R6u.js → DiagramView-BJxfEzpH.js} +1 -1
- package/dist/assets/{MarkdownRenderer-DalDLpzv.js → MarkdownRenderer-DHo-HJTP.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-DBwCHT5d.js → MarkdownRendererImpl-Cyx24H_y.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-B9BVC6Vb.js → MarkstreamRendererImpl-DKT8cFlu.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-y40xCSR7.js → MobileDetailNavigation-C1Pfx1ab.js} +1 -1
- package/dist/assets/{MobileShareBridge-Cbq4bKs6.js → MobileShareBridge-yzsL3AUy.js} +1 -1
- package/dist/assets/{MobileSurface-D-F3eHiN.js → MobileSurface-DOn29tPs.js} +1 -1
- package/dist/assets/{MultiRunWindow-EJuLTLUQ.js → MultiRunWindow-CZPkrJUM.js} +1 -1
- package/dist/assets/{SettingsView-CVqmT8RN.js → SettingsView-BMqMEWMN.js} +1 -1
- package/dist/assets/{SettingsWindow-BNDmIW89.js → SettingsWindow-CekCsYBw.js} +1 -1
- package/dist/assets/{TerminalView-gZgQfbuO.js → TerminalView-k9o96CEf.js} +1 -1
- package/dist/assets/{ToolOutputDialog-QoCh-2gJ.js → ToolOutputDialog-CEtY95oc.js} +1 -1
- package/dist/assets/{appThemeRegistry-kguN_Kk3.js → appThemeRegistry-DbhqYhdg.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-ygAGnsZQ.js → fileMentionTouchSelection-iDGupHSi.js} +1 -1
- package/dist/assets/{gitApi-CHNElSO9.js → gitApi-BnEbIrdK.js} +1 -1
- package/dist/assets/{insertionBoundaries-CAGUdmJD.js → insertionBoundaries-By_qiwC0.js} +1 -1
- package/dist/assets/{main-CMC55kzu.js → main-CaXU4OGJ.js} +4 -4
- package/dist/assets/{main-BbcLOyi3.js → main-D8lh8zYp.js} +2 -2
- package/dist/assets/{mcpQueries-B9Je_SGv.js → mcpQueries-BmqKC8WC.js} +1 -1
- package/dist/assets/{miniChat-CcSfMnJH.js → miniChat-zZJBQLvR.js} +2 -2
- package/dist/assets/{mobile-B9WA1Y98.js → mobile-DeaWxGOe.js} +2 -2
- package/dist/assets/{multirun-CsyU-7QN.js → multirun-CmuIahrn.js} +1 -1
- package/dist/assets/{projectMeta-BDEHdXJ9.js → projectMeta-bhpIcoPU.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-Dx8MwWwO.js → renderElectronMiniChatApp-t8FY3CLa.js} +2 -2
- package/dist/assets/{renderMobileApp-BFXRsHXI.js → renderMobileApp-BYbN4f39.js} +4 -4
- package/dist/assets/{runtimeConfig-DoIlK5gp.js → runtimeConfig-D0osnvg3.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-CAYxvd4H.js → runtimeEndpointReset-WWmoRWPS.js} +1 -1
- package/dist/assets/{sessionChangeSummary-D9uXfzZ_.js → sessionChangeSummary-BvA1TWzS.js} +1 -1
- package/dist/assets/{sessionLookup-DSuGYE36.js → sessionLookup-BJFtStrg.js} +1 -1
- package/dist/assets/src-OzbnJsph.css +2 -0
- package/dist/assets/{useAppFontEffects-Cakdb23p.js → useAppFontEffects-Dc7ZKwKO.js} +6 -6
- package/dist/assets/{useDesktopWindowControlsLayout-DaReS2qH.js → useDesktopWindowControlsLayout-iq6hANF2.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-CbfHKmnU.js → useEffectiveDirectory-DwBnQQpk.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-BLy93WtU.js → useMobileNavigationStore-CNtROYYh.js} +7 -7
- package/dist/assets/{useSessionAutoCleanup-BiTaPK87.js → useSessionAutoCleanup-DFLjs5K8.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DXEA8HEy.js → useWorkerHighlightedLines-CMBw7PAo.js} +1 -1
- package/dist/index.html +3 -3
- package/dist/mini-chat.html +3 -3
- package/dist/mobile.html +3 -3
- package/package.json +1 -1
- package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
- package/server/lib/question-auto-delegate/core.d.ts +5 -1
- package/server/lib/question-auto-delegate/core.js +21 -13
- package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
- package/server/lib/question-auto-delegate/form-io.js +39 -0
- package/server/lib/question-auto-delegate/runtime.js +4 -24
- package/server/lib/question-auto-delegate/runtime.test.js +138 -0
- package/server/lib/relay/DOCUMENTATION.md +1 -0
- package/server/lib/relay/service.js +1 -0
- package/server/lib/relay/service.test.js +13 -0
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/session-goal/runtime.js +4 -4
- package/server/lib/session-goal/runtime.test.js +32 -3
- package/server/lib/session-title/DOCUMENTATION.md +16 -6
- package/server/lib/session-title/native-runtime.test.js +17 -5
- package/server/lib/session-title/runtime.js +45 -9
- package/dist/assets/src-qYyYBKwA.css +0 -2
|
@@ -722,20 +722,20 @@ export const createSessionGoalRuntime = ({
|
|
|
722
722
|
const client = openCodeClient();
|
|
723
723
|
if (typeof client?.message?.list === 'function') {
|
|
724
724
|
const listed = await client.message.list(
|
|
725
|
-
{ sessionID: sessionId, limit: MESSAGE_FETCH_LIMIT, order: '
|
|
725
|
+
{ sessionID: sessionId, limit: MESSAGE_FETCH_LIMIT, order: 'desc' },
|
|
726
726
|
directoryRequestOptions(directory),
|
|
727
727
|
);
|
|
728
728
|
const projected = projectGoalMessages(listed, sessionId);
|
|
729
|
-
if (projected) return projected;
|
|
729
|
+
if (projected) return projected.reverse();
|
|
730
730
|
}
|
|
731
731
|
} catch {
|
|
732
732
|
// Fall through to controlled HTTP seam for tests / older fixtures.
|
|
733
733
|
}
|
|
734
734
|
const raw = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}/message`, {
|
|
735
735
|
directory,
|
|
736
|
-
query: { limit: String(MESSAGE_FETCH_LIMIT) },
|
|
736
|
+
query: { limit: String(MESSAGE_FETCH_LIMIT), order: 'desc' },
|
|
737
737
|
}).catch(() => null);
|
|
738
|
-
return projectGoalMessages(raw, sessionId);
|
|
738
|
+
return projectGoalMessages(raw, sessionId)?.reverse() ?? null;
|
|
739
739
|
};
|
|
740
740
|
|
|
741
741
|
/**
|
|
@@ -136,7 +136,7 @@ const makeFetch = ({ messages, liveStatus }) => {
|
|
|
136
136
|
}
|
|
137
137
|
if (pathEndsWith(reqPath, '/session/ses-goal/message')) {
|
|
138
138
|
// Official v2 SessionMessagesResponse shape.
|
|
139
|
-
return jsonResponse({ data: messages, cursor: {} });
|
|
139
|
+
return jsonResponse({ data: new URL(url).searchParams.get('order') === 'desc' ? [...messages].reverse() : messages, cursor: {} });
|
|
140
140
|
}
|
|
141
141
|
if (pathEndsWith(reqPath, '/session/ses-goal') && !reqPath.includes('/message')) {
|
|
142
142
|
if (init.method === 'PATCH') {
|
|
@@ -263,6 +263,33 @@ describe('session-goal v2 message projection helpers', () => {
|
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
describe('session-goal continuation delivery', () => {
|
|
266
|
+
it.each([false, true])('continues after a completed compaction beyond the first message page (HTTP fallback: %s)', async (fallback) => {
|
|
267
|
+
const messages = Array.from({ length: 42 }, (_, index) => ({
|
|
268
|
+
id: `msg_${String(index).padStart(3, '0')}`,
|
|
269
|
+
type: index === 41 ? 'compaction' : index % 2 ? 'user' : 'assistant',
|
|
270
|
+
time: index === 41 ? { created: index + 1 } : { created: index + 1, completed: index + 2 },
|
|
271
|
+
model: { providerID: 'p', id: 'm' },
|
|
272
|
+
agent: 'build',
|
|
273
|
+
...(index === 41 ? { status: 'completed', summary: 'Work remains' } : { text: 'Working' }),
|
|
274
|
+
}));
|
|
275
|
+
const fixture = makeFetch({ messages, liveStatus: 'idle' });
|
|
276
|
+
const runtime = makeRuntime();
|
|
277
|
+
try {
|
|
278
|
+
await withFetch(async (input, init) => {
|
|
279
|
+
const url = new URL(String(input));
|
|
280
|
+
if (pathEndsWith(url.pathname, '/session/ses-goal/message')) {
|
|
281
|
+
if (fallback && url.pathname.startsWith('/api/')) return jsonResponse({ error: 'unavailable' }, 503);
|
|
282
|
+
const ordered = url.searchParams.get('order') === 'desc' ? [...messages].reverse() : messages;
|
|
283
|
+
return jsonResponse({ data: ordered.slice(0, Number(url.searchParams.get('limit') || 40)), cursor: {} });
|
|
284
|
+
}
|
|
285
|
+
return fixture.fetch(input, init);
|
|
286
|
+
}, async () => runtime.runTick('ses-goal', '/repo'));
|
|
287
|
+
expect(fixture.calls.prompt).toBe(1);
|
|
288
|
+
} finally {
|
|
289
|
+
runtime.stop();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
266
293
|
it('dispatches v2 session.prompt with stable continuation id and metadata', async () => {
|
|
267
294
|
const promptBodies = [];
|
|
268
295
|
const messages = [
|
|
@@ -291,7 +318,7 @@ describe('session-goal continuation delivery', () => {
|
|
|
291
318
|
return jsonResponse({ data: {} });
|
|
292
319
|
}
|
|
293
320
|
if (pathEndsWith(reqPath, '/session/ses-goal/message')) {
|
|
294
|
-
return jsonResponse({ data: messages, cursor: {} });
|
|
321
|
+
return jsonResponse({ data: new URL(url).searchParams.get('order') === 'desc' ? [...messages].reverse() : messages, cursor: {} });
|
|
295
322
|
}
|
|
296
323
|
if (pathEndsWith(reqPath, '/session/ses-goal') && !reqPath.includes('/message')) {
|
|
297
324
|
if (init.method === 'PATCH' && init.body) {
|
|
@@ -984,7 +1011,9 @@ describe('session-goal runtime — pause / audit / dispatch boundary', () => {
|
|
|
984
1011
|
const body = Array.isArray(messages)
|
|
985
1012
|
? { data: messages, cursor: {} }
|
|
986
1013
|
: messages;
|
|
987
|
-
return jsonResponse(
|
|
1014
|
+
return jsonResponse(new URL(String(input)).searchParams.get('order') === 'desc'
|
|
1015
|
+
? { ...body, data: [...body.data].reverse() }
|
|
1016
|
+
: body);
|
|
988
1017
|
}
|
|
989
1018
|
if (pathEndsWith(reqPath, '/session/ses-goal')) {
|
|
990
1019
|
if (init.method === 'PATCH') {
|
|
@@ -50,18 +50,21 @@ extension-only VS Code retains the limitation below.
|
|
|
50
50
|
assistant reply run concurrently. Generation publishes `isGenerating` via
|
|
51
51
|
the existing Host metadata path, then the final title through `session.updated`.
|
|
52
52
|
Later user admissions/idle events do not regenerate the initial title.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
If `session.created` was missed, the admission's session read recovers
|
|
54
|
+
an empty/default title with no previous auto-title metadata and starts
|
|
55
|
+
immediately. The legacy first-idle edge remains a fallback when no
|
|
56
|
+
admission was observed.
|
|
57
|
+
A fork title (`(fork #n)`) refreshes on its first newly-admitted user
|
|
58
|
+
message, bypassing inherited title metadata and throttle without waiting
|
|
59
|
+
for the assistant. Legacy message-only events retain the idle fallback.
|
|
57
60
|
If the fork's `session.created` was lost (SSE reconnect gap, server or
|
|
58
61
|
OpenCode restart), the first newly-created user message lazily re-registers
|
|
59
62
|
the pending fork from the session read inside `recordUserActivity` (fork
|
|
60
63
|
title + message created after the fork + activity timestamp not yet past
|
|
61
|
-
|
|
64
|
+
the fork time), so the first-send refresh still fires.
|
|
62
65
|
Ordinary later idle transitions do **not** arm another refresh. Any
|
|
63
66
|
`busy`/`retry` status or a fresh user `message.updated` still clears an
|
|
64
|
-
|
|
67
|
+
ordinary already-armed timer; admitted initial/fork-title timers are exempt.
|
|
65
68
|
A sidebar smart-title request sets `titleRefresh.requestedAt`; its
|
|
66
69
|
`session.updated` event arms the same flow immediately (forced / manual
|
|
67
70
|
refresh is unaffected by the background gate).
|
|
@@ -141,6 +144,13 @@ auto updates for that session.
|
|
|
141
144
|
Title language follows the user's real message text. Assistant responses, tool
|
|
142
145
|
output, and transcript labels provide context without selecting the title language.
|
|
143
146
|
|
|
147
|
+
Attachments do not gate title generation. Admission retains user text plus at
|
|
148
|
+
most eight bounded filename/MIME descriptors, never attachment bytes or URLs.
|
|
149
|
+
Attachment-only messages also start generation; the text-only summary model can
|
|
150
|
+
name the attachment subject from those descriptors, but is explicitly told that
|
|
151
|
+
file contents are not included. Transcript-based/manual generation uses the
|
|
152
|
+
same file descriptors. This does not perform image recognition or file parsing.
|
|
153
|
+
|
|
144
154
|
- Lives in the web server, so VS Code (extension-only) does not generate
|
|
145
155
|
refreshes; it still receives `session.updated` title changes produced by a
|
|
146
156
|
web/desktop instance of the same OpenCode server.
|
|
@@ -110,10 +110,12 @@ it('uses the real v2 client and publishes the generated title without another se
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
it('
|
|
113
|
+
it.each(['new', 'missed-create', 'fork', 'missed-fork-create'].flatMap((scenario) =>
|
|
114
|
+
['text', 'text-and-file', 'file-only'].map((content) => ({ scenario, content })),
|
|
115
|
+
))('starts the first title before assistant completion ($scenario, $content)', async ({ scenario, content }) => {
|
|
114
116
|
vi.useFakeTimers();
|
|
115
117
|
const session = {
|
|
116
|
-
id: 'ses_first', title: '', time: { created: 1, updated: 1 },
|
|
118
|
+
id: 'ses_first', title: scenario.includes('fork') ? 'Original (fork #1)' : '', time: { created: 1, updated: 1 },
|
|
117
119
|
metadata: {},
|
|
118
120
|
};
|
|
119
121
|
let finishTitle;
|
|
@@ -135,26 +137,36 @@ it('starts the first title from admitted user text while the assistant is still
|
|
|
135
137
|
});
|
|
136
138
|
const emit = (type, data) => runtime.processPayload({ type, data, created: 1000, location: { directory: '/repo' } });
|
|
137
139
|
try {
|
|
138
|
-
|
|
140
|
+
if (!scenario.startsWith('missed')) {
|
|
141
|
+
emit('session.created', { sessionID: session.id, title: session.title, time: session.time, location: { directory: '/repo' } });
|
|
142
|
+
}
|
|
139
143
|
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_synthetic', item: {
|
|
140
144
|
type: 'synthetic', delivery: 'steer', payload: { text: 'Internal instructions' },
|
|
141
145
|
} });
|
|
142
146
|
await vi.runAllTimersAsync();
|
|
143
147
|
expect(generate).not.toHaveBeenCalled();
|
|
144
148
|
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_first', item: {
|
|
145
|
-
type: 'user', delivery: 'steer', payload: {
|
|
149
|
+
type: 'user', delivery: 'steer', payload: {
|
|
150
|
+
text: content === 'file-only' ? '' : 'Implement a session title generator',
|
|
151
|
+
...(content !== 'text' ? { files: [{ name: 'title-bug.png', mime: 'image/png', source: { type: 'inline' }, data: 'YQ=='.repeat(100_000) }] } : {}),
|
|
152
|
+
},
|
|
146
153
|
} });
|
|
147
154
|
emit('session.execution.started', { sessionID: session.id });
|
|
148
155
|
emit('session.status', { sessionID: session.id, status: { type: 'busy' } });
|
|
149
156
|
await vi.runAllTimersAsync();
|
|
150
157
|
expect(generate).toHaveBeenCalledTimes(1);
|
|
151
|
-
|
|
158
|
+
const prompt = generate.mock.calls[0][0].prompt;
|
|
159
|
+
if (content !== 'file-only') expect(prompt).toContain('Implement a session title generator');
|
|
160
|
+
if (content !== 'text') expect(prompt).toContain('title-bug.png');
|
|
161
|
+
expect(prompt).not.toContain('YQ==');
|
|
162
|
+
expect(prompt.length).toBeLessThan(4000);
|
|
152
163
|
expect(session.metadata.openchamber?.titleRefresh?.isGenerating).toBe(true);
|
|
153
164
|
expect(access.messages).not.toHaveBeenCalled();
|
|
154
165
|
finishTitle({ text: 'Session titles', providerID: 'test', modelID: 'test' });
|
|
155
166
|
await vi.runAllTimersAsync();
|
|
156
167
|
expect(session.title).toBe('Session titles');
|
|
157
168
|
expect(session.metadata.openchamber.titleRefresh.isGenerating).toBeUndefined();
|
|
169
|
+
expect(access.messages).not.toHaveBeenCalled();
|
|
158
170
|
emit('session.execution.succeeded', { sessionID: session.id });
|
|
159
171
|
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_second', item: {
|
|
160
172
|
type: 'user', delivery: 'steer', payload: { text: 'Follow up on the implementation' },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Session title refresh: regenerate the sidebar title from the conversation's
|
|
2
2
|
// MAIN SUBJECT (overall feature / goal), not just the last wrap-up utterance.
|
|
3
3
|
// OpenCode only titles once from the first user message. Auto refresh is sparse
|
|
4
|
-
// for title stability: first
|
|
4
|
+
// for title stability: first user admission of a new session, first send on a
|
|
5
5
|
// fork, plus explicit smart-title / forced refresh. Continuing the same work
|
|
6
6
|
// should keep naming the thing being done — not "commit and push". This
|
|
7
7
|
// watcher throttles to at most one refresh per session every TITLE_THROTTLE_MS
|
|
@@ -201,13 +201,22 @@ const extractUserMessage = (payload) => {
|
|
|
201
201
|
const item = properties.item;
|
|
202
202
|
if (typeof properties.sessionID !== 'string' || item?.type !== 'user') return null;
|
|
203
203
|
const text = typeof item.payload?.text === 'string' ? item.payload.text.slice(0, TRANSCRIPT_USER_CHAR_LIMIT) : '';
|
|
204
|
+
const files = Array.isArray(item.payload?.files) ? item.payload.files.slice(0, 8) : [];
|
|
205
|
+
const parts = [
|
|
206
|
+
...(text.trim() ? [{ type: 'text', text }] : []),
|
|
207
|
+
...files.filter((file) => file && typeof file === 'object').map((file) => ({
|
|
208
|
+
type: 'file',
|
|
209
|
+
filename: typeof file.name === 'string' ? file.name.slice(0, 200) : '',
|
|
210
|
+
mime: typeof file.mime === 'string' ? file.mime.slice(0, 100) : '',
|
|
211
|
+
})),
|
|
212
|
+
];
|
|
204
213
|
return {
|
|
205
214
|
sessionId: properties.sessionID,
|
|
206
215
|
createdAt: payload.created ?? 0,
|
|
207
216
|
directory: payload.location?.directory ?? '',
|
|
208
|
-
initialRecord:
|
|
217
|
+
initialRecord: parts.length ? {
|
|
209
218
|
info: { id: properties.inboxID, role: 'user' },
|
|
210
|
-
parts
|
|
219
|
+
parts,
|
|
211
220
|
} : null,
|
|
212
221
|
};
|
|
213
222
|
}
|
|
@@ -230,7 +239,13 @@ const messagePartsToText = (message) => {
|
|
|
230
239
|
? TRANSCRIPT_ASSISTANT_CHAR_LIMIT
|
|
231
240
|
: TRANSCRIPT_USER_CHAR_LIMIT;
|
|
232
241
|
return parts
|
|
233
|
-
.map((part) =>
|
|
242
|
+
.map((part) => {
|
|
243
|
+
if (part?.type === 'text' && typeof part.text === 'string') return part.text;
|
|
244
|
+
if (part?.type !== 'file') return '';
|
|
245
|
+
const filename = typeof part.filename === 'string' ? part.filename.slice(0, 200) : '';
|
|
246
|
+
const mime = typeof part.mime === 'string' ? part.mime.slice(0, 100) : '';
|
|
247
|
+
return `[Attachment: ${filename || mime || 'file'}${filename && mime ? ` (${mime})` : ''}; contents not included]`;
|
|
248
|
+
})
|
|
234
249
|
.filter(Boolean)
|
|
235
250
|
.join('\n')
|
|
236
251
|
.slice(0, charLimit);
|
|
@@ -495,7 +510,7 @@ export const createSessionTitleRuntime = ({
|
|
|
495
510
|
});
|
|
496
511
|
};
|
|
497
512
|
|
|
498
|
-
const recordUserActivity = async (sessionId, directory, createdAt) => {
|
|
513
|
+
const recordUserActivity = async (sessionId, directory, createdAt, initialRecord) => {
|
|
499
514
|
if (!Number.isFinite(createdAt) || createdAt <= 0) return;
|
|
500
515
|
const session = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, { directory })
|
|
501
516
|
.catch(() => null);
|
|
@@ -508,7 +523,7 @@ export const createSessionTitleRuntime = ({
|
|
|
508
523
|
// never learned about the fork. The first newly-created user message on a
|
|
509
524
|
// fork-titled session (message created after the fork, before any activity
|
|
510
525
|
// timestamp advanced past the fork) rebuilds that pending entry so the
|
|
511
|
-
//
|
|
526
|
+
// first admission can trigger the refresh. Idempotent: the normal
|
|
512
527
|
// session.created path already holds a pending entry, and once activity
|
|
513
528
|
// advances past the fork time later messages never re-register.
|
|
514
529
|
const forkCreated = Number(session?.time?.created);
|
|
@@ -526,6 +541,26 @@ export const createSessionTitleRuntime = ({
|
|
|
526
541
|
});
|
|
527
542
|
}
|
|
528
543
|
|
|
544
|
+
// Recover first-send admission even when session.created was missed.
|
|
545
|
+
// Use the admitted text: the message projection may not exist yet.
|
|
546
|
+
if (initialRecord && !stopped && !inflight.has(sessionId) && !initialRefreshes.has(sessionId)
|
|
547
|
+
&& !forkFirstRefreshes.has(sessionId) && !session.parentID && !isSystemOwnedSession(session)) {
|
|
548
|
+
const pendingFork = forkFirstSendPending.get(sessionId);
|
|
549
|
+
const firstForkSend = pendingFork && createdAt > pendingFork.createdAt;
|
|
550
|
+
const untitled = !session.title || isDefaultSessionTitle(session.title);
|
|
551
|
+
if (firstForkSend || (untitled && !meta.generatedAt && !meta.lastAutoTitle)) {
|
|
552
|
+
newSessions.delete(sessionId);
|
|
553
|
+
if (firstForkSend) {
|
|
554
|
+
forkFirstSendPending.delete(sessionId);
|
|
555
|
+
forkFirstRefreshes.add(sessionId);
|
|
556
|
+
} else {
|
|
557
|
+
initialRefreshes.add(sessionId);
|
|
558
|
+
}
|
|
559
|
+
initialUserMessages.set(sessionId, initialRecord);
|
|
560
|
+
armTimer(sessionId, directory, 0);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
529
564
|
const previous = Number(meta.titleRefresh.activityUpdatedAt);
|
|
530
565
|
const activityUpdatedAt = Number.isFinite(previous) ? Math.max(previous, createdAt) : createdAt;
|
|
531
566
|
if (activityUpdatedAt === previous) return;
|
|
@@ -683,7 +718,7 @@ export const createSessionTitleRuntime = ({
|
|
|
683
718
|
return;
|
|
684
719
|
}
|
|
685
720
|
|
|
686
|
-
const latestMessages = await fetchRecentMessages(sessionId, directory);
|
|
721
|
+
const latestMessages = initialUserMessage ? null : await fetchRecentMessages(sessionId, directory);
|
|
687
722
|
const latestAssistantId = (() => {
|
|
688
723
|
if (!latestMessages) return null;
|
|
689
724
|
for (let i = latestMessages.length - 1; i >= 0; i -= 1) {
|
|
@@ -777,7 +812,7 @@ export const createSessionTitleRuntime = ({
|
|
|
777
812
|
initialRefreshes.add(status.sessionId);
|
|
778
813
|
armTimer(status.sessionId, status.directory || directoryHint, 0);
|
|
779
814
|
}
|
|
780
|
-
} else if (!initialRefreshes.has(status.sessionId)) {
|
|
815
|
+
} else if (!initialRefreshes.has(status.sessionId) && !forkFirstRefreshes.has(status.sessionId)) {
|
|
781
816
|
clearTimer(status.sessionId);
|
|
782
817
|
}
|
|
783
818
|
return;
|
|
@@ -797,6 +832,7 @@ export const createSessionTitleRuntime = ({
|
|
|
797
832
|
userMessage.sessionId,
|
|
798
833
|
userMessage.directory || directoryHint,
|
|
799
834
|
userMessage.createdAt,
|
|
835
|
+
userMessage.initialRecord,
|
|
800
836
|
).catch((error) => {
|
|
801
837
|
console.warn('[session-title] failed to record user activity:', error?.message || error);
|
|
802
838
|
});
|
|
@@ -804,7 +840,7 @@ export const createSessionTitleRuntime = ({
|
|
|
804
840
|
// session settles. Only a message created after the timer was armed
|
|
805
841
|
// means the user actually moved on.
|
|
806
842
|
const armed = timers.get(userMessage.sessionId);
|
|
807
|
-
if (armed && !initialRefreshes.has(userMessage.sessionId) && userMessage.createdAt >= armed.armedAt) {
|
|
843
|
+
if (armed && !initialRefreshes.has(userMessage.sessionId) && !forkFirstRefreshes.has(userMessage.sessionId) && userMessage.createdAt >= armed.armedAt) {
|
|
808
844
|
clearTimer(userMessage.sessionId);
|
|
809
845
|
}
|
|
810
846
|
}
|