@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,34 @@
1
+ import { effect as _$effect } from "@opentui/solid";
2
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
3
+ import { insertNode as _$insertNode } from "@opentui/solid";
4
+ import { insert as _$insert } from "@opentui/solid";
5
+ import { createComponent as _$createComponent } from "@opentui/solid";
6
+ import { setProp as _$setProp } from "@opentui/solid";
7
+ import { createElement as _$createElement } from "@opentui/solid";
8
+ /** @jsxImportSource @opentui/solid */
9
+
10
+ import { For } from "solid-js";
11
+ export function HintBar(props) {
12
+ return (() => {
13
+ var _el$ = _$createElement("box");
14
+ _$setProp(_el$, "flexDirection", "row");
15
+ _$setProp(_el$, "gap", 2);
16
+ _$insert(_el$, _$createComponent(For, {
17
+ get each() {
18
+ return props.items.filter(item => item.keybind);
19
+ },
20
+ children: item => (() => {
21
+ var _el$2 = _$createElement("text"),
22
+ _el$3 = _$createElement("b"),
23
+ _el$4 = _$createTextNode(` `);
24
+ _$insertNode(_el$2, _el$3);
25
+ _$insertNode(_el$2, _el$4);
26
+ _$insert(_el$3, () => item.keybind);
27
+ _$insert(_el$2, () => item.label, null);
28
+ _$effect(_$p => _$setProp(_el$2, "fg", props.context.theme.text.subdued, _$p));
29
+ return _el$2;
30
+ })()
31
+ }));
32
+ return _el$;
33
+ })();
34
+ }
package/dist/config.js ADDED
@@ -0,0 +1,27 @@
1
+ import { DEFAULT_FRESH_KEYBIND, DEFAULT_FULL_TOKEN_LIMIT, DEFAULT_KEYBIND, DEFAULT_TOGGLE_THINKING_KEYBIND } from "./constants.js";
2
+ export function parseConfig(options) {
3
+ const input = options && typeof options === "object" ? options : {};
4
+ return {
5
+ model: parseStringOption(input.model),
6
+ variant: parseStringOption(input.variant),
7
+ agent: parseStringOption(input.agent),
8
+ tokenLimit: parsePositiveNumber(input.tokenLimit, DEFAULT_FULL_TOKEN_LIMIT),
9
+ keybind: parseKeybind(input.keybind, DEFAULT_KEYBIND),
10
+ freshKeybind: parseKeybind(input.freshKeybind, DEFAULT_FRESH_KEYBIND),
11
+ enableThinking: typeof input.enableThinking === "boolean" ? input.enableThinking : false,
12
+ toggleThinkingKeybind: parseKeybind(input.toggleThinkingKeybind, DEFAULT_TOGGLE_THINKING_KEYBIND)
13
+ };
14
+ }
15
+ function parsePositiveNumber(value, fallback) {
16
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
17
+ }
18
+ function parseKeybind(value, fallback) {
19
+ if (value === false) return false;
20
+ if (typeof value !== "string") return fallback;
21
+ const keybind = value.trim();
22
+ if (!keybind) return fallback;
23
+ return keybind === "none" ? false : keybind;
24
+ }
25
+ function parseStringOption(value) {
26
+ return typeof value === "string" && value.trim() ? value.trim() : null;
27
+ }
@@ -0,0 +1,24 @@
1
+ export const PLUGIN_ID = "opencode-mini-session";
2
+ export const CMD_OPEN = "mini.open";
3
+ export const CMD_OPEN_FRESH = "mini.open-fresh";
4
+ export const CMD_TOGGLE_MAIN = "mini.toggle-main";
5
+ export const CMD_TOGGLE_FRESH = "mini.toggle-fresh";
6
+ export const CMD_HIDE = "mini.hide";
7
+ export const CMD_CLOSE = "mini.close";
8
+ export const CMD_CONTINUE = "mini.continue";
9
+ export const CMD_CHANGE_MODEL = "mini.change-model";
10
+ export const CMD_TOGGLE_THINKING = "mini.toggle-thinking";
11
+ export const CMD_SCROLL_UP = "mini.scroll-up";
12
+ export const CMD_SCROLL_DOWN = "mini.scroll-down";
13
+ export const CMD_PAGE_UP = "mini.page-up";
14
+ export const CMD_PAGE_DOWN = "mini.page-down";
15
+ export const CMD_SUBMIT = "mini.submit";
16
+ export const CMD_SCROLL_TOP = "mini.scroll-top";
17
+ export const CMD_SCROLL_BOTTOM = "mini.scroll-bottom";
18
+ export const SCROLL_LINE_DELTA = 4;
19
+ export const SCROLL_PAGE_DELTA = 14;
20
+ export const DEFAULT_FULL_TOKEN_LIMIT = 50_000;
21
+ export const DEFAULT_KEYBIND = "ctrl+n";
22
+ export const DEFAULT_FRESH_KEYBIND = "ctrl+f";
23
+ export const DEFAULT_TOGGLE_THINKING_KEYBIND = "ctrl+o";
24
+ export const THINKING_TEXT = "Thinking...";
@@ -0,0 +1,72 @@
1
+ export function getSessionEntries(context, sessionID) {
2
+ return context.data.session.message.list(sessionID).slice().sort((a, b) => {
3
+ const aTime = isFinite(a.time?.created ?? NaN) ? a.time.created : 0;
4
+ const bTime = isFinite(b.time?.created ?? NaN) ? b.time.created : 0;
5
+ return aTime - bTime;
6
+ });
7
+ }
8
+ export function buildCopiedContext(entries, tokenLimit) {
9
+ const chunks = entries.map(entry => {
10
+ const text = formatEntry(entry);
11
+ return text ? {
12
+ text,
13
+ tokens: estimateTokens(text)
14
+ } : undefined;
15
+ }).filter(chunk => Boolean(chunk));
16
+ const totalAvailableTokens = chunks.reduce((total, chunk) => total + chunk.tokens, 0);
17
+ const selected = [];
18
+ let usedTokens = 0;
19
+ for (let i = chunks.length - 1; i >= 0; i--) {
20
+ const chunk = chunks[i];
21
+ if (selected.length > 0 && usedTokens + chunk.tokens > tokenLimit) break;
22
+ selected.push(chunk.text);
23
+ usedTokens += chunk.tokens;
24
+ if (usedTokens >= tokenLimit) break;
25
+ }
26
+ if (selected.length === 0) {
27
+ return {
28
+ text: "No conversation context available.",
29
+ usedTokens: 0,
30
+ totalAvailableTokens
31
+ };
32
+ }
33
+ return {
34
+ text: selected.reverse().join("\n\n"),
35
+ usedTokens,
36
+ totalAvailableTokens
37
+ };
38
+ }
39
+ function formatEntry(entry) {
40
+ if (entry.type === "user") {
41
+ return entry.text.trim() ? `user:\n${entry.text.trim()}` : "";
42
+ }
43
+ if (entry.type === "assistant") {
44
+ const lines = [];
45
+ for (const part of entry.content) {
46
+ if (part.type === "text" && part.text.trim()) lines.push(part.text.trim());
47
+ if (part.type === "tool") lines.push(`[tool: ${part.name}${formatToolInput(part.state)}]`);
48
+ }
49
+ return lines.length > 0 ? `assistant:\n${lines.join("\n")}` : "";
50
+ }
51
+ return "";
52
+ }
53
+ function formatToolInput(state) {
54
+ const input = state.input;
55
+ if (!input) return "";
56
+ if (typeof input === "string") return input ? ` ${input}` : "";
57
+ const pairs = Object.entries(input).slice(0, 4).map(([k, v]) => `${k}=${summarizeValue(v)}`);
58
+ return pairs.length > 0 ? ` ${pairs.join(" ")}` : "";
59
+ }
60
+ function summarizeValue(value) {
61
+ if (typeof value === "string") return truncate(value.replace(/\s+/g, " "), 48);
62
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
63
+ if (Array.isArray(value)) return `[${value.length}]`;
64
+ if (value && typeof value === "object") return "{...}";
65
+ return String(value);
66
+ }
67
+ function truncate(value, maxLength) {
68
+ return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
69
+ }
70
+ export function estimateTokens(text) {
71
+ return Math.ceil(text.length / 3.4);
72
+ }
@@ -0,0 +1,55 @@
1
+ export const MINI_SESSION_WARNING_PERCENT = 85;
2
+ export const MINI_SESSION_LIMIT_PERCENT = 95;
3
+ export function formatTokenCount(value) {
4
+ if (value < 1000) return `${Math.round(value)}`;
5
+ if (value < 1_000_000) return `${(value / 1000).toFixed(1)}K`;
6
+ return `${(value / 1_000_000).toFixed(1)}M`;
7
+ }
8
+ export function formatPercent(value) {
9
+ return `${Math.round(value)}%`;
10
+ }
11
+ export function getUsagePercent(usedTokens, limit) {
12
+ if (limit <= 0) return undefined;
13
+ return usedTokens / limit * 100;
14
+ }
15
+ export function getDisplayPercent(usedTokens, limit) {
16
+ const percent = getUsagePercent(usedTokens, limit);
17
+ return percent === undefined ? undefined : Math.round(percent);
18
+ }
19
+ export function isMiniSessionWarning(percentUsed) {
20
+ return Boolean(percentUsed !== undefined && percentUsed >= MINI_SESSION_WARNING_PERCENT);
21
+ }
22
+ export function isMiniSessionLimitReached(percentUsed) {
23
+ return Boolean(percentUsed !== undefined && percentUsed >= MINI_SESSION_LIMIT_PERCENT);
24
+ }
25
+ export function buildFooterCounterState(options) {
26
+ const copiedContextTotalTokens = options.copiedContextTotalTokens ?? options.copiedContextTokens;
27
+ const copiedContextTruncated = options.copiedContextTokens !== undefined && copiedContextTotalTokens !== undefined && copiedContextTotalTokens > options.copiedContextTokens;
28
+ const copiedContext = options.mode === "main" && options.copiedContextTokens !== undefined ? {
29
+ usedTokens: options.copiedContextTokens,
30
+ totalAvailableTokens: copiedContextTotalTokens ?? options.copiedContextTokens,
31
+ tokenLimit: options.tokenLimit,
32
+ text: formatCopiedContextCounter(options.copiedContextTokens, copiedContextTotalTokens ?? options.copiedContextTokens, options.tokenLimit),
33
+ truncated: copiedContextTruncated
34
+ } : undefined;
35
+ const miniSession = options.lastCompletedMiniInputTokens !== undefined ? buildMiniSessionCounter(options.lastCompletedMiniInputTokens, options.modelContextWindow) : undefined;
36
+ return {
37
+ copiedContext,
38
+ miniSession,
39
+ placeholder: miniSession?.limitReached ? "Session context limit reached..." : undefined
40
+ };
41
+ }
42
+ function formatCopiedContextCounter(usedTokens, totalAvailableTokens, tokenLimit) {
43
+ if (totalAvailableTokens <= usedTokens) return `main ${formatTokenCount(usedTokens)}`;
44
+ return `main ${formatTokenCount(Math.min(usedTokens, tokenLimit))}/${formatTokenCount(totalAvailableTokens)}`;
45
+ }
46
+ function buildMiniSessionCounter(usedTokens, modelContextWindow) {
47
+ const percentUsed = modelContextWindow !== undefined ? getDisplayPercent(usedTokens, modelContextWindow) : undefined;
48
+ return {
49
+ usedTokens,
50
+ percentUsed,
51
+ text: percentUsed !== undefined ? `${formatTokenCount(usedTokens)} (${formatPercent(percentUsed)})` : formatTokenCount(usedTokens),
52
+ warning: isMiniSessionWarning(percentUsed),
53
+ limitReached: isMiniSessionLimitReached(percentUsed)
54
+ };
55
+ }
@@ -0,0 +1,59 @@
1
+ const FALLBACK_ERROR_MESSAGE = "The side question failed.";
2
+ const NESTED_ERROR_KEYS = ["error", "cause", "details"];
3
+ export function extractErrorMessage(error) {
4
+ const message = readErrorMessage(error);
5
+ if (message) return message;
6
+ return FALLBACK_ERROR_MESSAGE;
7
+ }
8
+ export function getErrorMessage(cause) {
9
+ const extracted = extractErrorMessage(cause);
10
+ if (cause instanceof Error && cause.message) {
11
+ const normalizedMessage = normalizeErrorText(cause.message);
12
+ if (normalizedMessage && !isGenericErrorLabel(normalizedMessage)) {
13
+ return normalizedMessage;
14
+ }
15
+ }
16
+ if (extracted) return extracted;
17
+ if (cause instanceof Error && cause.name) return cause.name;
18
+ return FALLBACK_ERROR_MESSAGE;
19
+ }
20
+ function readErrorMessage(error, depth = 0) {
21
+ if (depth > 3 || !error) return undefined;
22
+ if (typeof error === "string") return normalizeErrorText(error);
23
+ if (Array.isArray(error)) {
24
+ for (const item of error) {
25
+ const message = readErrorMessage(item, depth + 1);
26
+ if (message) return message;
27
+ }
28
+ return undefined;
29
+ }
30
+ if (!isRecord(error)) return undefined;
31
+ const dataMessage = readDataMessage(error);
32
+ if (dataMessage) return dataMessage;
33
+ for (const key of NESTED_ERROR_KEYS) {
34
+ const message = readErrorMessage(error[key], depth + 1);
35
+ if (message) return message;
36
+ }
37
+ const directMessage = normalizeErrorText(error.message);
38
+ if (directMessage && !isGenericErrorLabel(directMessage)) return directMessage;
39
+ const name = normalizeErrorText(error.name);
40
+ if (name) return name;
41
+ return undefined;
42
+ }
43
+ function readDataMessage(error) {
44
+ const data = error.data;
45
+ if (!isRecord(data)) return undefined;
46
+ return normalizeErrorText(data.message);
47
+ }
48
+ function normalizeErrorText(value) {
49
+ if (typeof value !== "string") return undefined;
50
+ const trimmed = value.trim();
51
+ if (!trimmed) return undefined;
52
+ return trimmed.length > 600 ? `${trimmed.slice(0, 597)}...` : trimmed;
53
+ }
54
+ function isGenericErrorLabel(value) {
55
+ return /^(unknown(error)?|error)$/i.test(value.trim());
56
+ }
57
+ function isRecord(value) {
58
+ return typeof value === "object" && value !== null;
59
+ }
package/dist/index.js ADDED
@@ -0,0 +1,157 @@
1
+ import { memo as _$memo } from "@opentui/solid";
2
+ import { createComponent as _$createComponent } from "@opentui/solid";
3
+ /** @jsxImportSource @opentui/solid */
4
+
5
+ import { createEffect, createSignal } from "solid-js";
6
+ import { createOverlaySlot } from "./components/AnswerDialog.js";
7
+ import { parseConfig } from "./config.js";
8
+ import { PLUGIN_ID } from "./constants.js";
9
+ import { buildGlobalCommands, buildPanelCommands } from "./keybinds.js";
10
+ import { openMiniSession, openModelPicker } from "./session.js";
11
+ import { resolveMiniRouteAction, runMiniRouteAction } from "./routing.js";
12
+ function KeymapManager(props) {
13
+ props.context.keymap.layer(() => ({
14
+ mode: "global",
15
+ commands: props.globalCmds.map(c => ({
16
+ id: c.id,
17
+ title: c.title,
18
+ description: c.description,
19
+ group: c.group,
20
+ palette: c.palette,
21
+ slash: c.slash,
22
+ bind: c.bind,
23
+ enabled: c.enabled,
24
+ run: c.run
25
+ }))
26
+ }));
27
+ props.context.keymap.layer(() => ({
28
+ mode: "global",
29
+ priority: 1000,
30
+ enabled: () => Boolean(props.overlay()),
31
+ commands: props.panelCmds.map(c => ({
32
+ id: c.id,
33
+ title: c.title,
34
+ bind: c.bind,
35
+ enabled: c.enabled,
36
+ run: c.run
37
+ }))
38
+ }));
39
+ return [];
40
+ }
41
+ export default {
42
+ id: PLUGIN_ID,
43
+ setup(context) {
44
+ const config = parseConfig(context.options);
45
+ const [overlay, setOverlay] = createSignal(undefined, {
46
+ equals: false
47
+ });
48
+ const [selectedModel, setSelectedModel] = createSignal(undefined, {
49
+ equals: false
50
+ });
51
+ const [thinkingEnabled, setThinkingEnabled] = createSignal(config.enableThinking);
52
+ let activeDialog;
53
+ let activeMode;
54
+ let modelPickerOpen = false;
55
+ const thinkingPreference = {
56
+ get: thinkingEnabled,
57
+ set: setThinkingEnabled
58
+ };
59
+ const globalCmds = buildGlobalCommands({
60
+ context,
61
+ config,
62
+ overlay,
63
+ modelPickerOpen: {
64
+ get: () => modelPickerOpen,
65
+ set: v => {
66
+ modelPickerOpen = v;
67
+ }
68
+ },
69
+ triggerMiniMode: (mode, source) => triggerMiniMode(mode, source),
70
+ openModelPicker: () => {
71
+ const route = context.ui.router.current();
72
+ if (route.type !== "session") return;
73
+ openModelPicker(context, config, route.sessionID, {
74
+ get: selectedModel,
75
+ set: setSelectedModel
76
+ });
77
+ }
78
+ });
79
+ const panelCmds = buildPanelCommands({
80
+ context,
81
+ config,
82
+ overlay,
83
+ modelPickerOpen: {
84
+ get: () => modelPickerOpen,
85
+ set: v => {
86
+ modelPickerOpen = v;
87
+ }
88
+ },
89
+ triggerMiniMode: (mode, source) => triggerMiniMode(mode, source),
90
+ openModelPicker: () => {
91
+ const route = context.ui.router.current();
92
+ if (route.type !== "session") return;
93
+ openModelPicker(context, config, route.sessionID, {
94
+ get: selectedModel,
95
+ set: setSelectedModel
96
+ });
97
+ }
98
+ });
99
+ context.ui.slot({
100
+ append: "app",
101
+ render: () => [_$createComponent(KeymapManager, {
102
+ context: context,
103
+ overlay: overlay,
104
+ globalCmds: globalCmds,
105
+ panelCmds: panelCmds
106
+ }), _$memo(() => createOverlaySlot(overlay)())]
107
+ });
108
+ createEffect(() => {
109
+ const route = context.ui.router.current();
110
+ if (route.type === "session") return;
111
+ if (!activeDialog) return;
112
+ setOverlay(undefined);
113
+ context.ui.toast.show({
114
+ variant: "info",
115
+ message: "mini session closed.",
116
+ duration: 1000
117
+ });
118
+ void activeDialog.close();
119
+ });
120
+ async function triggerMiniMode(mode, source) {
121
+ const route = context.ui.router.current();
122
+ if (route.type !== "session") return;
123
+ const nextAction = resolveMiniRouteAction({
124
+ source,
125
+ requestedMode: mode,
126
+ activeMode,
127
+ isVisible: activeDialog?.isVisible()
128
+ });
129
+ await runMiniRouteAction({
130
+ action: nextAction,
131
+ activeDialog,
132
+ open: () => {
133
+ const opened = openMiniSession(context, config, mode, setOverlay, {
134
+ get: () => activeDialog,
135
+ set: d => {
136
+ activeDialog = d;
137
+ if (!d) {
138
+ activeMode = undefined;
139
+ }
140
+ }
141
+ }, {
142
+ get: selectedModel,
143
+ set: setSelectedModel
144
+ }, thinkingPreference, onAfterSelect => openModelPicker(context, config, route.sessionID, {
145
+ get: selectedModel,
146
+ set: setSelectedModel
147
+ }, () => {
148
+ modelPickerOpen = false;
149
+ onAfterSelect();
150
+ }));
151
+ if (opened) activeMode = mode;
152
+ }
153
+ });
154
+ }
155
+ return () => void activeDialog?.close();
156
+ }
157
+ };
@@ -0,0 +1,152 @@
1
+ import { CMD_CHANGE_MODEL, CMD_CLOSE, CMD_CONTINUE, CMD_HIDE, CMD_OPEN, CMD_OPEN_FRESH, CMD_PAGE_DOWN, CMD_PAGE_UP, CMD_SCROLL_BOTTOM, CMD_SCROLL_DOWN, CMD_SCROLL_TOP, CMD_SCROLL_UP, CMD_SUBMIT, CMD_TOGGLE_FRESH, CMD_TOGGLE_MAIN, CMD_TOGGLE_THINKING, SCROLL_LINE_DELTA, SCROLL_PAGE_DELTA } from "./constants.js";
2
+ export function buildPanelCommands(ctx) {
3
+ const {
4
+ overlay,
5
+ modelPickerOpen
6
+ } = ctx;
7
+ const closePanel = () => {
8
+ if (modelPickerOpen.get()) {
9
+ modelPickerOpen.set(false);
10
+ } else {
11
+ overlay()?.onClose();
12
+ }
13
+ };
14
+ const whenOpen = () => Boolean(overlay());
15
+ return [{
16
+ id: CMD_HIDE,
17
+ bind: "up",
18
+ enabled: whenOpen,
19
+ run: () => overlay()?.onHide()
20
+ }, {
21
+ id: CMD_CLOSE,
22
+ title: "Close",
23
+ bind: "escape",
24
+ enabled: whenOpen,
25
+ run: closePanel
26
+ }, {
27
+ id: CMD_CLOSE,
28
+ bind: "ctrl+c",
29
+ enabled: whenOpen,
30
+ run: closePanel
31
+ }, {
32
+ id: CMD_CONTINUE,
33
+ title: "Continue",
34
+ bind: "shift+return",
35
+ enabled: whenOpen,
36
+ run: () => overlay()?.onContinue()
37
+ }, {
38
+ id: CMD_SUBMIT,
39
+ title: "Submit",
40
+ bind: "return",
41
+ enabled: whenOpen,
42
+ run: () => overlay()?.submit()
43
+ }, ...(ctx.config.toggleThinkingKeybind ? [{
44
+ id: CMD_TOGGLE_THINKING,
45
+ title: "Toggle thinking",
46
+ bind: ctx.config.toggleThinkingKeybind,
47
+ enabled: whenOpen,
48
+ run: () => overlay()?.onToggleThinking()
49
+ }] : []), {
50
+ id: CMD_CHANGE_MODEL,
51
+ title: "Change model",
52
+ bind: "tab",
53
+ enabled: whenOpen,
54
+ run: () => {
55
+ modelPickerOpen.set(true);
56
+ overlay()?.onChangeModel();
57
+ }
58
+ }, {
59
+ id: CMD_SCROLL_UP,
60
+ bind: "down",
61
+ enabled: whenOpen,
62
+ run: () => overlay()?.scrollBy(-SCROLL_LINE_DELTA)
63
+ }, {
64
+ id: CMD_SCROLL_DOWN,
65
+ bind: "down",
66
+ enabled: whenOpen,
67
+ run: () => overlay()?.scrollBy(SCROLL_LINE_DELTA)
68
+ }, {
69
+ id: CMD_PAGE_UP,
70
+ bind: "pageup",
71
+ enabled: whenOpen,
72
+ run: () => overlay()?.scrollBy(-SCROLL_PAGE_DELTA)
73
+ }, {
74
+ id: CMD_PAGE_DOWN,
75
+ bind: "pagedown",
76
+ enabled: whenOpen,
77
+ run: () => overlay()?.scrollBy(SCROLL_PAGE_DELTA)
78
+ }, {
79
+ id: CMD_SCROLL_TOP,
80
+ bind: "home",
81
+ enabled: whenOpen,
82
+ run: () => overlay()?.scrollTo(0)
83
+ }, {
84
+ id: CMD_SCROLL_BOTTOM,
85
+ bind: "end",
86
+ enabled: whenOpen,
87
+ run: () => overlay()?.scrollTo(Number.MAX_SAFE_INTEGER)
88
+ }];
89
+ }
90
+ export function buildGlobalCommands(ctx) {
91
+ const {
92
+ config,
93
+ triggerMiniMode,
94
+ openModelPicker
95
+ } = ctx;
96
+ const onSession = () => {
97
+ const route = ctx.context.ui.router.current();
98
+ return route.type === "session";
99
+ };
100
+ const commands = [];
101
+ if (config.keybind) {
102
+ commands.push({
103
+ id: CMD_TOGGLE_MAIN,
104
+ title: "Toggle mini session",
105
+ bind: config.keybind,
106
+ run: () => void triggerMiniMode("main", "keybind")
107
+ });
108
+ }
109
+ if (config.freshKeybind) {
110
+ commands.push({
111
+ id: CMD_TOGGLE_FRESH,
112
+ title: "Toggle mini fresh session",
113
+ bind: config.freshKeybind,
114
+ run: () => void triggerMiniMode("fresh", "keybind")
115
+ });
116
+ }
117
+ commands.push({
118
+ id: CMD_OPEN,
119
+ title: "mini",
120
+ description: "Open a mini session for side questions",
121
+ group: "Mini",
122
+ palette: true,
123
+ slash: {
124
+ name: "mini"
125
+ },
126
+ enabled: onSession,
127
+ run: () => void triggerMiniMode("main", "command")
128
+ }, {
129
+ id: CMD_OPEN_FRESH,
130
+ title: "mini fresh",
131
+ description: "Open a mini session without copied context",
132
+ group: "Mini",
133
+ palette: true,
134
+ slash: {
135
+ name: "mini-fresh"
136
+ },
137
+ enabled: onSession,
138
+ run: () => void triggerMiniMode("fresh", "command")
139
+ }, {
140
+ id: CMD_CHANGE_MODEL + ".global",
141
+ title: "mini model",
142
+ description: "Change the model for future mini-session questions",
143
+ group: "Mini",
144
+ palette: true,
145
+ slash: {
146
+ name: "mini-model"
147
+ },
148
+ enabled: onSession,
149
+ run: openModelPicker
150
+ });
151
+ return commands;
152
+ }
package/dist/model.js ADDED
@@ -0,0 +1,75 @@
1
+ export function parseModelOverride(value) {
2
+ const [providerID, ...rest] = value.split("/");
3
+ const id = rest.join("/");
4
+ if (!providerID || !id) return undefined;
5
+ return {
6
+ providerID,
7
+ id
8
+ };
9
+ }
10
+ export function resolveModel(modelOverride, variantOverride, entries, sessionModel) {
11
+ if (modelOverride) {
12
+ const model = parseModelOverride(modelOverride);
13
+ if (model) return {
14
+ model: {
15
+ ...model,
16
+ ...(variantOverride ? {
17
+ variant: variantOverride
18
+ } : {})
19
+ },
20
+ source: "config"
21
+ };
22
+ }
23
+ if (sessionModel) {
24
+ return {
25
+ model: {
26
+ providerID: sessionModel.providerID,
27
+ id: sessionModel.id,
28
+ variant: sessionModel.variant
29
+ },
30
+ source: "session"
31
+ };
32
+ }
33
+ for (let i = entries.length - 1; i >= 0; i--) {
34
+ const entry = entries[i];
35
+ if (entry.type === "assistant" && entry.model) {
36
+ return {
37
+ model: {
38
+ providerID: entry.model.providerID,
39
+ id: entry.model.id,
40
+ variant: entry.model.variant
41
+ },
42
+ source: "session"
43
+ };
44
+ }
45
+ }
46
+ return {
47
+ model: {},
48
+ source: "default"
49
+ };
50
+ }
51
+ export function resolveDefaultModel(models, configuredModel, configuredVariant, sessionModel, entries) {
52
+ const resolved = resolveModel(configuredModel, configuredVariant, entries, sessionModel);
53
+ if (resolved.source !== "config") return resolved;
54
+ if (isAvailableModel(models, resolved.model)) return resolved;
55
+ return {
56
+ ...resolveModel(null, null, entries, sessionModel),
57
+ notice: `Configured mini model ${formatResolvedModel(resolved.model)} was not found. The main session model will be used.`
58
+ };
59
+ }
60
+ function isAvailableModel(models, resolved) {
61
+ if (!resolved.providerID || !resolved.id) return false;
62
+ const model = models?.find(m => m.providerID === resolved.providerID && m.id === resolved.id);
63
+ if (!model) return false;
64
+ if (!resolved.variant) return true;
65
+ return model.variants.some(v => v.id === resolved.variant);
66
+ }
67
+ export function formatResolvedModel(resolved) {
68
+ if (!resolved.providerID || !resolved.id) return "default";
69
+ const base = `${resolved.providerID}/${resolved.id}`;
70
+ return resolved.variant ? `${base} (${resolved.variant})` : base;
71
+ }
72
+ export function resolveModelContextWindow(models, resolved) {
73
+ if (!resolved.providerID || !resolved.id) return undefined;
74
+ return models?.find(m => m.providerID === resolved.providerID && m.id === resolved.id)?.limit?.context;
75
+ }
@@ -0,0 +1,20 @@
1
+ export function resolveMiniRouteAction(options) {
2
+ if (!options.activeMode) return "open";
3
+ if (options.activeMode !== options.requestedMode) return "switch";
4
+ if (options.isVisible === false) return "show";
5
+ return options.source === "keybind" ? "hide" : "show";
6
+ }
7
+ export async function runMiniRouteAction(options) {
8
+ if (options.action === "hide") {
9
+ options.activeDialog?.hide();
10
+ return;
11
+ }
12
+ if (options.action === "show") {
13
+ options.activeDialog?.show();
14
+ return;
15
+ }
16
+ if (options.action === "switch") {
17
+ await options.activeDialog?.close();
18
+ }
19
+ options.open();
20
+ }