@npv12/opencode-mini-session 0.0.1

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.
@@ -0,0 +1,617 @@
1
+ import { buildFooterCounterState } from "./counter.js";
2
+ import { buildMiniPreamble, buildSessionCreatePayload, buildMiniErrorDetail, formatMiniNotice, resolveRuntimeMiniAgent } from "./agent.js";
3
+ import { getSessionEntries, buildCopiedContext } from "./context.js";
4
+ import { getErrorMessage } from "./diagnostics.js";
5
+ import { resolveDefaultModel, formatResolvedModel, resolveModelContextWindow } from "./model.js";
6
+ export function openMiniSession(context, config, mode, setOverlay, active, modelPreference, thinkingPreference, openPickerFn) {
7
+ const currentRoute = context.ui.router.current();
8
+ if (currentRoute.type !== "session") {
9
+ context.ui.toast.show({
10
+ variant: "error",
11
+ message: "mini only works inside a session."
12
+ });
13
+ return false;
14
+ }
15
+ if (active.get()) {
16
+ active.get().show();
17
+ return false;
18
+ }
19
+ void startQuestion(context, config, mode, currentRoute.sessionID, setOverlay, active, modelPreference, thinkingPreference, openPickerFn);
20
+ return true;
21
+ }
22
+ export async function startQuestion(context, config, mode, originSessionID, setOverlay, active, modelPreference, thinkingPreference, openPickerFn) {
23
+ const entries = getSessionEntries(context, originSessionID);
24
+ const copiedContext = mode === "main" ? buildCopiedContext(entries, config.tokenLimit) : {
25
+ text: "",
26
+ usedTokens: undefined,
27
+ totalAvailableTokens: undefined
28
+ };
29
+ const contextText = copiedContext.text;
30
+ const sessionInfo = context.data.session.get(originSessionID);
31
+ const models = context.data.location.model.list(sessionInfo?.location);
32
+ const providers = context.data.location.provider.list(sessionInfo?.location);
33
+ const defaultResolvedModel = resolveDefaultModel(models, config.model, config.variant, sessionInfo?.model, entries);
34
+ const getResolvedModel = () => modelPreference.get() ?? defaultResolvedModel.model;
35
+ const getModelName = () => formatResolvedModel(getResolvedModel());
36
+ const hideKey = mode === "fresh" ? config.freshKeybind : config.keybind;
37
+ const hiddenCommand = mode === "fresh" ? "/mini-fresh" : "/mini";
38
+ const title = mode === "fresh" ? "mini fresh" : "mini session";
39
+ const previousFocus = context.renderer.currentFocusedRenderable;
40
+ let resolvedAgent;
41
+ let system = "";
42
+ let preamble = "";
43
+ const dialogState = {
44
+ mode,
45
+ entries: [],
46
+ streamingAnswer: "",
47
+ loading: false,
48
+ scrollbarVisible: false,
49
+ spinnerFrame: 0,
50
+ copiedContextTokens: copiedContext.usedTokens,
51
+ copiedContextTotalTokens: copiedContext.totalAvailableTokens,
52
+ lastCompletedMiniInputTokens: undefined,
53
+ modelContextWindow: undefined,
54
+ footerCounter: {},
55
+ inputPlaceholder: undefined,
56
+ thinkingEnabled: thinkingPreference.get(),
57
+ expandedThinkingPartIDs: {},
58
+ notice: undefined,
59
+ errorDetail: undefined
60
+ };
61
+ const unsubscribers = [];
62
+ let tempSessionID;
63
+ let closed = false;
64
+ let hidden = false;
65
+ let continuing = false;
66
+ let renderTimer;
67
+ let scrollTimer;
68
+ let focusTimer;
69
+ let spinnerTimer;
70
+ let overlayInput;
71
+ let overlayScroller;
72
+ let followStreamingToBottom = true;
73
+ let forceScrollToBottom = true;
74
+ let pendingScrollToBottom = false;
75
+ let lastScrollTop = 0;
76
+ let lastScrollHeight = 0;
77
+ let currentTokenMessageID;
78
+ const incrementedTokenMessageIDs = new Set();
79
+ const syncCounterState = () => {
80
+ dialogState.modelContextWindow = resolveModelContextWindow(models, getResolvedModel());
81
+ dialogState.footerCounter = buildFooterCounterState({
82
+ mode: dialogState.mode,
83
+ copiedContextTokens: dialogState.copiedContextTokens,
84
+ copiedContextTotalTokens: dialogState.copiedContextTotalTokens,
85
+ tokenLimit: config.tokenLimit,
86
+ lastCompletedMiniInputTokens: dialogState.lastCompletedMiniInputTokens,
87
+ modelContextWindow: dialogState.modelContextWindow
88
+ });
89
+ dialogState.inputPlaceholder = dialogState.footerCounter.placeholder;
90
+ };
91
+ const clearScrollTimer = () => {
92
+ pendingScrollToBottom = false;
93
+ if (!scrollTimer) return;
94
+ clearTimeout(scrollTimer);
95
+ scrollTimer = undefined;
96
+ };
97
+ const clearFocusTimer = () => {
98
+ if (!focusTimer) return;
99
+ clearTimeout(focusTimer);
100
+ focusTimer = undefined;
101
+ };
102
+ const clearSpinnerTimer = () => {
103
+ if (!spinnerTimer) return;
104
+ clearInterval(spinnerTimer);
105
+ spinnerTimer = undefined;
106
+ };
107
+ const startSpinnerTimer = () => {
108
+ if (spinnerTimer || closed || hidden || !dialogState.loading) return;
109
+ spinnerTimer = setInterval(() => {
110
+ if (closed || hidden || !dialogState.loading) {
111
+ clearSpinnerTimer();
112
+ return;
113
+ }
114
+ dialogState.spinnerFrame = (dialogState.spinnerFrame + 1) % 10;
115
+ renderOverlay();
116
+ }, 80);
117
+ };
118
+ const scheduleInputFocus = () => {
119
+ if (closed || hidden) return;
120
+ clearFocusTimer();
121
+ focusTimer = setTimeout(() => {
122
+ focusTimer = undefined;
123
+ if (closed || hidden) return;
124
+ overlayInput?.focus();
125
+ context.renderer.requestRender();
126
+ }, 0);
127
+ };
128
+ const isScrollerAtBottom = () => {
129
+ if (!overlayScroller) return true;
130
+ return overlayScroller.scrollTop >= Math.max(0, overlayScroller.scrollHeight - overlayScroller.viewport.height) - 1;
131
+ };
132
+ const updateScrollSnapshot = () => {
133
+ lastScrollTop = overlayScroller?.scrollTop ?? 0;
134
+ lastScrollHeight = overlayScroller?.scrollHeight ?? 0;
135
+ };
136
+ const scheduleScrollToBottom = () => {
137
+ if (closed || hidden) return;
138
+ clearScrollTimer();
139
+ pendingScrollToBottom = true;
140
+ scrollTimer = setTimeout(() => {
141
+ scrollTimer = undefined;
142
+ if (closed || hidden) {
143
+ pendingScrollToBottom = false;
144
+ return;
145
+ }
146
+ overlayScroller?.scrollTo(Number.MAX_SAFE_INTEGER);
147
+ updateScrollSnapshot();
148
+ pendingScrollToBottom = false;
149
+ context.renderer.requestRender();
150
+ }, 0);
151
+ };
152
+ const scrollBy = delta => {
153
+ followStreamingToBottom = false;
154
+ forceScrollToBottom = false;
155
+ pendingScrollToBottom = false;
156
+ clearScrollTimer();
157
+ overlayScroller?.scrollBy(delta);
158
+ updateScrollSnapshot();
159
+ };
160
+ const scrollTo = position => {
161
+ followStreamingToBottom = position === Number.MAX_SAFE_INTEGER;
162
+ forceScrollToBottom = position === Number.MAX_SAFE_INTEGER;
163
+ pendingScrollToBottom = false;
164
+ if (position !== Number.MAX_SAFE_INTEGER) clearScrollTimer();
165
+ overlayScroller?.scrollTo(position);
166
+ updateScrollSnapshot();
167
+ };
168
+ const restorePreviousFocus = () => {
169
+ setTimeout(() => {
170
+ if (previousFocus && !previousFocus.isDestroyed) previousFocus.focus();
171
+ context.renderer.requestRender();
172
+ }, 0);
173
+ };
174
+ const hide = () => {
175
+ if (closed || hidden) return;
176
+ hidden = true;
177
+ if (renderTimer) {
178
+ clearTimeout(renderTimer);
179
+ renderTimer = undefined;
180
+ }
181
+ clearScrollTimer();
182
+ clearFocusTimer();
183
+ clearSpinnerTimer();
184
+ setOverlay(undefined);
185
+ restorePreviousFocus();
186
+ context.ui.toast.show({
187
+ variant: "info",
188
+ message: hideKey ? `mini hidden. Press ${hideKey} to show it.` : `mini hidden. Run ${hiddenCommand} to show it.`,
189
+ duration: 1000
190
+ });
191
+ };
192
+ const closeFromUser = async () => {
193
+ context.ui.toast.show({
194
+ variant: "info",
195
+ message: "mini session closed.",
196
+ duration: 1000
197
+ });
198
+ await cleanup();
199
+ };
200
+ const cleanup = async () => {
201
+ if (closed) return;
202
+ closed = true;
203
+ if (active.get() === controller) active.set(undefined);
204
+ while (unsubscribers.length > 0) {
205
+ try {
206
+ unsubscribers.pop()?.();
207
+ } catch {}
208
+ }
209
+ if (renderTimer) clearTimeout(renderTimer);
210
+ clearScrollTimer();
211
+ clearFocusTimer();
212
+ clearSpinnerTimer();
213
+ setOverlay(undefined);
214
+ restorePreviousFocus();
215
+ if (!tempSessionID) return;
216
+ const ephemeralSessionID = tempSessionID;
217
+ tempSessionID = undefined;
218
+ try {
219
+ await context.client.session.interrupt({
220
+ sessionID: ephemeralSessionID
221
+ });
222
+ } catch {}
223
+ try {
224
+ await context.client.session.remove({
225
+ sessionID: ephemeralSessionID
226
+ });
227
+ } catch {}
228
+ };
229
+ const continueInMainThread = async () => {
230
+ const transcript = buildMiniSessionTranscript(dialogState);
231
+ if (continuing || dialogState.loading || dialogState.error || !transcript) return;
232
+ continuing = true;
233
+ try {
234
+ await context.client.session.prompt({
235
+ sessionID: originSessionID,
236
+ text: buildContinuePrompt(transcript)
237
+ });
238
+ context.ui.toast.show({
239
+ variant: "success",
240
+ message: "Side answer added to main session."
241
+ });
242
+ await cleanup();
243
+ } catch (cause) {
244
+ context.ui.toast.show({
245
+ variant: "error",
246
+ message: `Failed to continue in main thread: ${getErrorMessage(cause)}`
247
+ });
248
+ } finally {
249
+ continuing = false;
250
+ }
251
+ };
252
+ const toggleThinking = () => {
253
+ dialogState.thinkingEnabled = !dialogState.thinkingEnabled;
254
+ thinkingPreference.set(dialogState.thinkingEnabled);
255
+ dialogState.expandedThinkingPartIDs = {};
256
+ renderOverlay();
257
+ };
258
+ const toggleThinkingPart = partID => {
259
+ if (dialogState.expandedThinkingPartIDs[partID]) delete dialogState.expandedThinkingPartIDs[partID];else dialogState.expandedThinkingPartIDs[partID] = true;
260
+ renderOverlay();
261
+ };
262
+ const renderOverlay = (options = {}) => {
263
+ if (closed) return;
264
+ syncCounterState();
265
+ const streamingActive = dialogState.loading || Boolean(dialogState.streamingAnswer);
266
+ const currentScrollTop = overlayScroller?.scrollTop ?? 0;
267
+ const currentScrollHeight = overlayScroller?.scrollHeight ?? 0;
268
+ if (streamingActive && !forceScrollToBottom && !pendingScrollToBottom) {
269
+ if (isScrollerAtBottom()) followStreamingToBottom = true;else if (currentScrollTop < lastScrollTop || currentScrollHeight <= lastScrollHeight) followStreamingToBottom = false;
270
+ }
271
+ const shouldScrollToBottom = forceScrollToBottom || streamingActive && followStreamingToBottom;
272
+ forceScrollToBottom = false;
273
+ updateScrollSnapshot();
274
+ if (renderTimer) {
275
+ clearTimeout(renderTimer);
276
+ renderTimer = undefined;
277
+ }
278
+ if (hidden) return;
279
+ setOverlay({
280
+ context,
281
+ title,
282
+ modelName: getModelName(),
283
+ hideKey,
284
+ toggleThinkingKeybind: config.toggleThinkingKeybind,
285
+ state: dialogState,
286
+ onScroller: scroller => {
287
+ overlayScroller = scroller;
288
+ },
289
+ onInput: input => {
290
+ overlayInput = input;
291
+ },
292
+ onHide: () => hide(),
293
+ onClose: () => void closeFromUser(),
294
+ onContinue: () => void continueInMainThread(),
295
+ onChangeModel: () => openPickerFn(() => renderOverlay({
296
+ focusInput: true
297
+ })),
298
+ onToggleThinking: toggleThinking,
299
+ onToggleThinkingPart: toggleThinkingPart,
300
+ onSubmit: submitPrompt,
301
+ scrollBy,
302
+ scrollTo,
303
+ submit: () => {
304
+ const value = (overlayInput?.value || "").trim();
305
+ if (value && !dialogState.loading && submitPrompt(value)) {
306
+ if (overlayInput) overlayInput.value = "";
307
+ }
308
+ }
309
+ });
310
+ if (options.focusInput) scheduleInputFocus();
311
+ if (dialogState.loading) startSpinnerTimer();else clearSpinnerTimer();
312
+ if (shouldScrollToBottom) scheduleScrollToBottom();
313
+ };
314
+ const setPromptError = (path, cause) => {
315
+ dialogState.error = getErrorMessage(cause);
316
+ dialogState.errorDetail = buildMiniErrorDetail({
317
+ path,
318
+ sessionID: tempSessionID,
319
+ resolvedModel: getResolvedModel(),
320
+ resolvedAgent
321
+ });
322
+ dialogState.loading = false;
323
+ clearSpinnerTimer();
324
+ };
325
+ const show = () => {
326
+ if (closed) return;
327
+ hidden = false;
328
+ renderOverlay({
329
+ focusInput: true
330
+ });
331
+ };
332
+ const controller = {
333
+ close: cleanup,
334
+ hide,
335
+ show,
336
+ isVisible: () => !hidden
337
+ };
338
+ const scheduleRenderOverlay = () => {
339
+ if (closed || renderTimer) return;
340
+ renderTimer = setTimeout(() => {
341
+ renderTimer = undefined;
342
+ renderOverlay();
343
+ }, 50);
344
+ };
345
+ active.set(controller);
346
+ renderOverlay({
347
+ focusInput: true
348
+ });
349
+ try {
350
+ resolvedAgent = await resolveRuntimeMiniAgent(context, config);
351
+ } catch (cause) {
352
+ if (closed) return;
353
+ context.ui.toast.show({
354
+ variant: "error",
355
+ message: `Failed to open mini session: ${getErrorMessage(cause)}`
356
+ });
357
+ await cleanup();
358
+ return;
359
+ }
360
+ if (closed) return;
361
+ preamble = buildMiniPreamble(contextText, resolvedAgent, mode);
362
+ dialogState.notice = formatMiniNotice(defaultResolvedModel.notice, ...resolvedAgent.notices);
363
+ renderOverlay();
364
+ function submitPrompt(value) {
365
+ const prompt = value.trim();
366
+ if (!prompt || closed) return false;
367
+ if (dialogState.loading) {
368
+ context.ui.toast.show({
369
+ variant: "warning",
370
+ message: "Wait for the current response."
371
+ });
372
+ return false;
373
+ }
374
+ if (!tempSessionID) {
375
+ context.ui.toast.show({
376
+ variant: "warning",
377
+ message: "mini session is still opening."
378
+ });
379
+ return false;
380
+ }
381
+ const promptSessionID = tempSessionID;
382
+ dialogState.error = undefined;
383
+ dialogState.errorDetail = undefined;
384
+ dialogState.loading = true;
385
+ dialogState.spinnerFrame = 0;
386
+ dialogState.streamingAnswer = "";
387
+ followStreamingToBottom = true;
388
+ forceScrollToBottom = true;
389
+ renderOverlay({
390
+ focusInput: true
391
+ });
392
+ void (async () => {
393
+ try {
394
+ const isFirst = !dialogState.lastCompletedMiniInputTokens;
395
+ const text = isFirst ? `${preamble}\n\n---\n\n${prompt}` : prompt;
396
+ await context.client.session.prompt({
397
+ sessionID: promptSessionID,
398
+ text
399
+ });
400
+ } catch (cause) {
401
+ if (closed) return;
402
+ setPromptError("promptAsync throw", cause);
403
+ renderOverlay();
404
+ }
405
+ })();
406
+ return true;
407
+ }
408
+ try {
409
+ const resolvedModel = getResolvedModel();
410
+ const created = await context.client.session.create(buildSessionCreatePayload(resolvedAgent, {
411
+ title: "mini session",
412
+ directory: context.location?.directory ?? "",
413
+ model: resolvedModel
414
+ }));
415
+ tempSessionID = created.id;
416
+ const ephemeralSessionID = tempSessionID;
417
+ const refreshSession = () => {
418
+ dialogState.entries = getSessionEntries(context, ephemeralSessionID);
419
+ dialogState.streamingAnswer = "";
420
+ refreshLastCompletedMiniInputTokens();
421
+ };
422
+ const refreshLastCompletedMiniInputTokens = () => {
423
+ const latest = getLastCompletedMiniInputUsage(dialogState.entries);
424
+ if (!latest) return;
425
+ const current = dialogState.lastCompletedMiniInputTokens;
426
+ if (current === undefined || latest.totalTokens > current) {
427
+ dialogState.lastCompletedMiniInputTokens = latest.totalTokens;
428
+ currentTokenMessageID = latest.messageID;
429
+ return;
430
+ }
431
+ if (latest.messageID === currentTokenMessageID) return;
432
+ if (incrementedTokenMessageIDs.has(latest.messageID)) return;
433
+ incrementedTokenMessageIDs.add(latest.messageID);
434
+ dialogState.lastCompletedMiniInputTokens = current + latest.inputTokens;
435
+ currentTokenMessageID = latest.messageID;
436
+ };
437
+ if (closed) {
438
+ try {
439
+ await context.client.session.remove({
440
+ sessionID: ephemeralSessionID
441
+ });
442
+ } catch {}
443
+ return;
444
+ }
445
+ unsubscribers.push(context.data.on("session.idle", event => {
446
+ if (event.data.sessionID !== tempSessionID) return;
447
+ refreshSession();
448
+ if (!extractAssistantText(dialogState.entries)) dialogState.streamingAnswer = "No response generated.";
449
+ dialogState.loading = false;
450
+ clearSpinnerTimer();
451
+ renderOverlay();
452
+ }));
453
+ unsubscribers.push(context.data.on("session.text.delta", event => {
454
+ if (event.data.sessionID !== tempSessionID) return;
455
+ dialogState.streamingAnswer += event.data.delta;
456
+ scheduleRenderOverlay();
457
+ }));
458
+ unsubscribers.push(context.data.on("session.step.ended", event => {
459
+ if (event.data.sessionID !== tempSessionID) return;
460
+ refreshSession();
461
+ renderOverlay();
462
+ }));
463
+ unsubscribers.push(context.data.on("session.execution.failed", event => {
464
+ if (event.data.sessionID !== tempSessionID) return;
465
+ setPromptError("session.execution.failed", event.data.error.message);
466
+ renderOverlay();
467
+ }));
468
+ } catch (cause) {
469
+ if (closed) return;
470
+ setPromptError("session.create throw", cause);
471
+ renderOverlay();
472
+ }
473
+ }
474
+ export function openModelPicker(context, config, sessionID, modelPreference, onAfterSelect) {
475
+ const sessionInfo = context.data.session.get(sessionID);
476
+ const models = context.data.location.model.list(sessionInfo?.location);
477
+ const providers = context.data.location.provider.list(sessionInfo?.location);
478
+ const {
479
+ model: defaultModel,
480
+ source: defaultSource
481
+ } = resolveDefaultModel(models, config.model, config.variant, sessionInfo?.model, getSessionEntries(context, sessionID));
482
+ const options = buildModelOptions(models, providers, defaultModel, defaultSource);
483
+ const sourceLabel = {
484
+ config: "config",
485
+ session: "main session",
486
+ default: "default"
487
+ };
488
+ void (async () => {
489
+ const result = await context.ui.dialog.select({
490
+ title: "mini model",
491
+ placeholder: "Select model for future mini-session questions",
492
+ options: options.map(o => ({
493
+ title: o.title,
494
+ value: o.value,
495
+ description: o.description,
496
+ category: o.category
497
+ }))
498
+ });
499
+ if (!result) return;
500
+ if (result.type === "default") {
501
+ modelPreference.set(undefined);
502
+ context.ui.toast.show({
503
+ variant: "success",
504
+ message: "mini model reset to default."
505
+ });
506
+ } else {
507
+ modelPreference.set(result.model);
508
+ context.ui.toast.show({
509
+ variant: "success",
510
+ message: `mini model set to ${formatResolvedModel(result.model)}.`
511
+ });
512
+ }
513
+ onAfterSelect?.();
514
+ })();
515
+ }
516
+ function buildModelOptions(models, providers, defaultModel, defaultSource) {
517
+ const sourceLabel = {
518
+ config: "config",
519
+ session: "main session",
520
+ default: "default"
521
+ };
522
+ const providerName = id => providers?.find(p => p.id === id)?.name ?? id;
523
+ const defaultModelName = defaultModel.providerID && defaultModel.id ? models?.find(m => m.providerID === defaultModel.providerID && m.id === defaultModel.id)?.name ?? defaultModel.id : "default";
524
+ const options = [{
525
+ title: defaultModelName + (defaultModel.variant ? ` (${defaultModel.variant})` : ""),
526
+ value: {
527
+ type: "default"
528
+ },
529
+ description: formatResolvedModel(defaultModel),
530
+ category: `Default [${sourceLabel[defaultSource]}]`
531
+ }];
532
+ if (!models) return options;
533
+ const byProvider = new Map();
534
+ for (const m of models) {
535
+ const arr = byProvider.get(m.providerID) ?? [];
536
+ arr.push(m);
537
+ byProvider.set(m.providerID, arr);
538
+ }
539
+ for (const [providerID, providerModels] of [...byProvider.entries()].sort((a, b) => providerName(a[0]).localeCompare(providerName(b[0])))) {
540
+ for (const m of providerModels.sort((a, b) => a.name.localeCompare(b.name))) {
541
+ const value = {
542
+ providerID: m.providerID,
543
+ id: m.id
544
+ };
545
+ options.push({
546
+ title: m.name || m.id,
547
+ value: {
548
+ type: "model",
549
+ model: value
550
+ },
551
+ description: `${providerID}/${m.id}`,
552
+ category: providerName(providerID)
553
+ });
554
+ for (const variant of [...(m.variants ?? [])].sort((a, b) => a.id.localeCompare(b.id))) {
555
+ options.push({
556
+ title: `${m.name || m.id} (${variant.id})`,
557
+ value: {
558
+ type: "model",
559
+ model: {
560
+ ...value,
561
+ variant: variant.id
562
+ }
563
+ },
564
+ description: `${providerID}/${m.id}`,
565
+ category: providerName(providerID)
566
+ });
567
+ }
568
+ }
569
+ }
570
+ return options;
571
+ }
572
+ export function extractAssistantText(entries) {
573
+ const chunks = [];
574
+ for (const entry of entries) {
575
+ if (entry.type !== "assistant") continue;
576
+ for (const part of entry.content) {
577
+ if (part.type === "text" && part.text.trim()) chunks.push(part.text);
578
+ }
579
+ }
580
+ return chunks.join("\n\n").trim();
581
+ }
582
+ function buildMiniSessionTranscript(state) {
583
+ const lines = [];
584
+ for (const entry of state.entries) {
585
+ const chunks = [];
586
+ if (entry.type === "user" && entry.text.trim()) chunks.push(entry.text.trim());
587
+ if (entry.type === "assistant") {
588
+ for (const part of entry.content) {
589
+ if (part.type === "text" && part.text.trim()) chunks.push(part.text.trim());
590
+ }
591
+ }
592
+ if (chunks.length > 0) {
593
+ const role = entry.type === "assistant" ? "assistant" : entry.type === "user" ? "user" : "system";
594
+ lines.push(`${role}:\n${chunks.join("\n\n")}`);
595
+ }
596
+ }
597
+ if (state.streamingAnswer.trim()) lines.push(`assistant:\n${state.streamingAnswer.trim()}`);
598
+ return lines.join("\n\n").trim();
599
+ }
600
+ function buildContinuePrompt(transcript) {
601
+ return ["[Context from a mini session]", transcript, "---\n"].join("\n\n");
602
+ }
603
+ function getLastCompletedMiniInputUsage(entries) {
604
+ for (let i = entries.length - 1; i >= 0; i--) {
605
+ const entry = entries[i];
606
+ if (entry.type !== "assistant") continue;
607
+ if (!entry.time.completed) continue;
608
+ if (entry.tokens) {
609
+ return {
610
+ messageID: entry.id,
611
+ inputTokens: entry.tokens.input + (entry.tokens.cache?.read ?? 0) + (entry.tokens.cache?.write ?? 0),
612
+ totalTokens: entry.tokens.input + entry.tokens.output + entry.tokens.reasoning + (entry.tokens.cache?.read ?? 0) + (entry.tokens.cache?.write ?? 0)
613
+ };
614
+ }
615
+ }
616
+ return undefined;
617
+ }
package/dist/theme.js ADDED
@@ -0,0 +1,34 @@
1
+ export function buildMiniTheme(theme) {
2
+ const feedback = theme.text.feedback;
3
+ const action = theme.text.action;
4
+ return {
5
+ text: theme.text.default,
6
+ textMuted: theme.text.subdued,
7
+ primary: action.primary.default,
8
+ secondary: action.secondary.default,
9
+ error: feedback.error.default,
10
+ warning: feedback.warning.default,
11
+ info: feedback.info.default,
12
+ backgroundPanel: theme.background.surface.overlay,
13
+ borderSubtle: theme.background.surface.offset,
14
+ border: theme.border.default,
15
+ markdownHeading: theme.markdown.heading,
16
+ markdownStrong: theme.markdown.strong,
17
+ markdownEmph: theme.markdown.emphasis,
18
+ markdownLink: theme.markdown.link,
19
+ markdownLinkText: theme.markdown.linkText,
20
+ markdownCode: theme.markdown.code,
21
+ markdownCodeBlock: theme.markdown.codeBlock,
22
+ markdownBlockQuote: theme.markdown.blockQuote,
23
+ markdownText: theme.markdown.text,
24
+ syntaxComment: theme.syntax.comment,
25
+ syntaxKeyword: theme.syntax.keyword,
26
+ syntaxFunction: theme.syntax.function,
27
+ syntaxVariable: theme.syntax.variable,
28
+ syntaxString: theme.syntax.string,
29
+ syntaxNumber: theme.syntax.number,
30
+ syntaxType: theme.syntax.type,
31
+ syntaxOperator: theme.syntax.operator,
32
+ syntaxPunctuation: theme.syntax.punctuation
33
+ };
34
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@npv12/opencode-mini-session",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "engines": {
6
+ "opencode": "0.0.0-beta-17963"
7
+ },
8
+ "exports": {
9
+ "./tui": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/npv12/opencode-mini-session"
17
+ },
18
+ "scripts": {
19
+ "build": "node scripts/build.mjs",
20
+ "prepack": "npm run build",
21
+ "release": "node scripts/release.mjs",
22
+ "test": "vitest run",
23
+ "test:package": "node scripts/verify-packaged-tui.mjs",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "dependencies": {
27
+ "@opencode-ai/client": "0.0.0-beta-17963",
28
+ "@opencode-ai/plugin": "0.0.0-beta-17963",
29
+ "@opencode-ai/theme": "0.0.0-beta-17963",
30
+ "@opentui/core": "0.5.7",
31
+ "@opentui/solid": "0.5.7",
32
+ "solid-js": "1.9.14"
33
+ },
34
+ "devDependencies": {
35
+ "@babel/core": "^7.28.0",
36
+ "@babel/preset-typescript": "^7.27.1",
37
+ "@types/node": "^25.9.1",
38
+ "babel-preset-solid": "^1.9.12",
39
+ "typescript": "^5.9.3",
40
+ "vitest": "^4.1.7"
41
+ }
42
+ }