opencode-sidechat 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.tsx CHANGED
@@ -1,379 +1,406 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui";
3
- import { createSignal, Show } from "solid-js";
4
- import { loadConfig } from "./config";
5
- import { SideChat } from "./components/SideChat";
6
- import {
7
- CMD_TOGGLE_FOCUS,
8
- CMD_CLEAR,
9
- CMD_CHANGE_MODEL,
10
- CMD_TOGGLE_THINK,
11
- PLUGIN_ID,
12
- } from "./constants";
13
- import {
14
- getAvailableToolIDs,
15
- resolveAllowedTools,
16
- buildToolSelection,
17
- buildPermissionRules,
18
- buildSideSystemPrompt,
19
- resolveModel,
20
- formatPreference,
21
- openModelPicker,
22
- getErrorMessage,
23
- } from "./session";
24
- import type { SideDialogState, ModelPreference } from "./types";
25
-
26
- const SIDE_AGENT = "general";
27
-
28
- const tui: TuiPlugin = async (api, _options) => {
29
- const config = loadConfig();
30
- const keybind = config.keybind;
31
- const clearKeybind = config.clearKeybind;
32
- const thinkToggleKeybind = config.thinkToggleKeybind;
33
-
34
- const [state, setState] = createSignal<SideDialogState>({
35
- entries: [],
36
- streamingAnswer: "",
37
- loading: false,
38
- error: undefined,
39
- tokenCount: 0,
40
- }, { equals: false });
41
-
42
- const [tempSessionID, setTempSessionID] = createSignal<string | undefined>(undefined);
43
- const [selectedModel, setSelectedModel] = createSignal<ModelPreference>(undefined);
44
- const [visible, setVisible] = createSignal(false);
45
- const [thinkCollapsed, setThinkCollapsed] = createSignal(config.think.defaultState === "collapsed");
46
-
47
- let overlayInput: { focus: () => void } | undefined;
48
- let unsubscribers: Array<() => void> = [];
49
- let sessionInitPromise: Promise<string | undefined> | undefined;
50
-
51
- const getModelName = () =>
52
- formatPreference(
53
- selectedModel() ?? resolveModel(config.model, state().entries, api).model,
54
- );
55
-
56
- const clearListeners = () => {
57
- while (unsubscribers.length > 0) {
58
- try { unsubscribers.pop()?.(); } catch {}
59
- }
60
- };
61
-
62
- const refreshSession = () => {
63
- const sid = tempSessionID();
64
- if (!sid) return;
65
- try {
66
- const messages = api.state.session.messages(sid);
67
- const entries: SideDialogState["entries"] = [];
68
- let tokenCount = 0;
69
- for (const info of messages) {
70
- entries.push({ info, parts: [...api.state.part(info.id)] });
71
- if (info.role === "assistant") {
72
- tokenCount += (info.tokens?.input ?? 0) + (info.tokens?.output ?? 0);
73
- }
74
- }
75
- setState((s) => ({ ...s, entries, tokenCount }));
76
- } catch {}
77
- };
78
-
79
- const buildSystemPrompt = async () => {
80
- const toolIDs = await getAvailableToolIDs(api);
81
- const resolvedTools = resolveAllowedTools(config.allowedTools, toolIDs);
82
- return {
83
- system: buildSideSystemPrompt(config.systemPrompt, resolvedTools),
84
- toolIDs,
85
- resolvedTools,
86
- tools: buildToolSelection(toolIDs, resolvedTools),
87
- permission: buildPermissionRules(toolIDs, resolvedTools),
88
- };
89
- };
90
-
91
- const initSession = async (): Promise<string | undefined> => {
92
- clearListeners();
93
-
94
- try {
95
- const { permission } = await buildSystemPrompt();
96
-
97
- const created = await api.client.session.create(
98
- {
99
- title: "side chat",
100
- directory: api.state.path.directory,
101
- agent: SIDE_AGENT,
102
- permission,
103
- },
104
- { throwOnError: true },
105
- );
106
-
107
- const sid = created.data.id;
108
- setTempSessionID(sid);
109
-
110
- unsubscribers.push(
111
- api.event.on("session.idle", (event) => {
112
- if (event.properties.sessionID !== sid) return;
113
- refreshSession();
114
- setState((s) => ({
115
- ...s,
116
- loading: false,
117
- streamingAnswer: "",
118
- }));
119
- }),
120
- );
121
-
122
- unsubscribers.push(
123
- api.event.on("message.updated", (event) => {
124
- if (event.properties.sessionID !== sid) return;
125
- refreshSession();
126
- }),
127
- );
128
-
129
- unsubscribers.push(
130
- api.event.on("message.part.delta", (event) => {
131
- if (
132
- event.properties.sessionID !== sid ||
133
- event.properties.field !== "text"
134
- ) return;
135
- setState((s) => ({
136
- ...s,
137
- streamingAnswer: s.streamingAnswer + event.properties.delta,
138
- }));
139
- }),
140
- );
141
-
142
- unsubscribers.push(
143
- api.event.on("message.part.updated", (event) => {
144
- if (event.properties.sessionID !== sid) return;
145
- refreshSession();
146
- }),
147
- );
148
-
149
- unsubscribers.push(
150
- api.event.on("session.error", (event) => {
151
- if (event.properties.sessionID !== sid) return;
152
- setState((s) => ({
153
- ...s,
154
- error: getErrorMessage(event.properties.error),
155
- loading: false,
156
- }));
157
- }),
158
- );
159
-
160
- setState((s) => ({ ...s, sessionReady: true, error: undefined }));
161
- return sid;
162
- } catch (cause) {
163
- const msg = getErrorMessage(cause);
164
- setState((s) => ({ ...s, error: msg, sessionReady: false }));
165
- return undefined;
166
- }
167
- };
168
-
169
- const ensureSession = (): Promise<string | undefined> => {
170
- if (tempSessionID()) return Promise.resolve(tempSessionID());
171
- if (!sessionInitPromise) sessionInitPromise = initSession();
172
- return sessionInitPromise;
173
- };
174
-
175
- const destroySession = async () => {
176
- const sid = tempSessionID();
177
- if (!sid) return;
178
- setTempSessionID(undefined);
179
- clearListeners();
180
- try {
181
- await api.client.session.abort(
182
- { sessionID: sid },
183
- { throwOnError: true },
184
- );
185
- } catch {}
186
- try {
187
- await api.client.session.delete(
188
- { sessionID: sid },
189
- { throwOnError: true },
190
- );
191
- } catch {}
192
- };
193
-
194
- const handleSubmit = (text: string): boolean => {
195
- if (state().loading) return false;
196
-
197
- void ensureSession().then((sid) => {
198
- if (!sid) {
199
- setState((s) => ({
200
- ...s,
201
- error: "Failed to create session.",
202
- loading: false,
203
- }));
204
- return;
205
- }
206
-
207
- setState((s) => ({
208
- ...s,
209
- error: undefined,
210
- loading: true,
211
- streamingAnswer: "",
212
- }));
213
-
214
- void (async () => {
215
- try {
216
- const { system, tools } = await buildSystemPrompt();
217
- const resolved =
218
- selectedModel() ??
219
- resolveModel(config.model, state().entries, api).model;
220
-
221
- await api.client.session.promptAsync(
222
- {
223
- sessionID: sid,
224
- system,
225
- agent: SIDE_AGENT,
226
- tools,
227
- parts: [{ type: "text", text }],
228
- ...(resolved.model ? { model: resolved.model } : {}),
229
- ...(resolved.variant ? { variant: resolved.variant } : {}),
230
- },
231
- { throwOnError: true },
232
- );
233
- } catch (cause) {
234
- setState((s) => ({
235
- ...s,
236
- error: getErrorMessage(cause),
237
- loading: false,
238
- }));
239
- }
240
- })();
241
- });
242
-
243
- return true;
244
- };
245
-
246
- const handleClear = async () => {
247
- await destroySession();
248
- setState({
249
- entries: [],
250
- streamingAnswer: "",
251
- loading: false,
252
- error: undefined,
253
- tokenCount: 0,
254
- });
255
- sessionInitPromise = undefined;
256
- setThinkCollapsed(config.think.defaultState === "collapsed");
257
- await ensureSession();
258
- setVisible(true);
259
- setTimeout(() => overlayInput?.focus(), 0);
260
- };
261
-
262
- const handleToggle = () => {
263
- const currentRoute = api.route.current;
264
- if (currentRoute.name !== "session") return;
265
- setVisible((prev) => {
266
- if (!prev) setTimeout(() => overlayInput?.focus(), 0);
267
- return !prev;
268
- });
269
- };
270
-
271
- const handleToggleThink = () => {
272
- setThinkCollapsed((prev) => !prev);
273
- };
274
-
275
- const handleChangeModel = () => {
276
- const currentRoute = api.route.current;
277
- if (currentRoute.name !== "session") return;
278
- openModelPicker(api, config, selectedModel(), (model) => {
279
- setSelectedModel(model);
280
- });
281
- };
282
-
283
- api.lifecycle.onDispose(() => {
284
- clearListeners();
285
- void destroySession();
286
- });
287
-
288
- api.slots.register({
289
- slots: {
290
- app: () => (
291
- <Show when={visible()}>
292
- <SideChat
293
- api={api}
294
- modelName={getModelName()}
295
- state={state()}
296
- width={config.width}
297
- transcriptHeight={config.transcriptHeight}
298
- tokenLimit={config.tokenLimit}
299
- thinkCollapsed={thinkCollapsed()}
300
- thinkConfig={config.think}
301
- onInput={(node) => { overlayInput = node; }}
302
- onChangeModel={handleChangeModel}
303
- onSubmit={handleSubmit}
304
- />
305
- </Show>
306
- ),
307
- },
308
- });
309
-
310
- api.keymap.registerLayer({
311
- commands: [
312
- {
313
- namespace: "palette",
314
- name: CMD_TOGGLE_FOCUS,
315
- title: "side",
316
- desc: "Open/side chat overlay",
317
- category: "Plugin",
318
- slashName: "side",
319
- enabled: () => api.route.current.name === "session",
320
- run: () => handleToggle(),
321
- },
322
- {
323
- namespace: "palette",
324
- name: CMD_CLEAR,
325
- title: "side clear",
326
- desc: "Clear the side chat conversation",
327
- category: "Plugin",
328
- slashName: "side-clear",
329
- enabled: () => api.route.current.name === "session",
330
- run: () => void handleClear(),
331
- },
332
- {
333
- namespace: "palette",
334
- name: CMD_CHANGE_MODEL,
335
- title: "side model",
336
- desc: "Change the side chat model",
337
- category: "Plugin",
338
- slashName: "side-model",
339
- enabled: () => api.route.current.name === "session",
340
- run: () => handleChangeModel(),
341
- },
342
- ],
343
- bindings: [
344
- ...(keybind !== false
345
- ? [{
346
- key: keybind,
347
- cmd: CMD_TOGGLE_FOCUS,
348
- desc: "Toggle side chat",
349
- }]
350
- : []),
351
- ],
352
- });
353
-
354
- api.keymap.registerLayer({
355
- priority: 1000,
356
- enabled: () => visible(),
357
- commands: [
358
- { name: CMD_CLEAR, run: () => void handleClear() },
359
- { name: CMD_CHANGE_MODEL, run: () => handleChangeModel() },
360
- { name: CMD_TOGGLE_THINK, run: () => handleToggleThink() },
361
- ],
362
- bindings: [
363
- ...(clearKeybind !== false
364
- ? [{ key: clearKeybind, cmd: CMD_CLEAR }]
365
- : []),
366
- ...(thinkToggleKeybind !== false
367
- ? [{ key: thinkToggleKeybind, cmd: CMD_TOGGLE_THINK }]
368
- : []),
369
- { key: "tab", cmd: CMD_CHANGE_MODEL },
370
- ],
371
- });
372
- };
373
-
374
- const plugin: TuiPluginModule & { id: string } = {
375
- id: PLUGIN_ID,
376
- tui,
377
- };
378
-
379
- export default plugin;
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui";
3
+ import { createSignal, Show, ErrorBoundary } from "solid-js";
4
+ import { loadConfig } from "./config";
5
+ import { SideChat } from "./components/SideChat";
6
+ import {
7
+ CMD_TOGGLE_FOCUS,
8
+ CMD_CLEAR,
9
+ CMD_CHANGE_MODEL,
10
+ CMD_TOGGLE_THINK,
11
+ PLUGIN_ID,
12
+ } from "./constants";
13
+ import {
14
+ getAvailableToolIDs,
15
+ resolveAllowedTools,
16
+ buildToolSelection,
17
+ buildPermissionRules,
18
+ buildSideSystemPrompt,
19
+ resolveModel,
20
+ formatPreference,
21
+ openModelPicker,
22
+ getErrorMessage,
23
+ } from "./session";
24
+ import type { SideDialogState, ModelPreference } from "./types";
25
+
26
+ const PROMPT_TIMEOUT_MS = 120_000;
27
+
28
+ const tui: TuiPlugin = async (api, _options) => {
29
+ const config = loadConfig();
30
+ const keybind = config.keybind;
31
+ const clearKeybind = config.clearKeybind;
32
+ const thinkToggleKeybind = config.thinkToggleKeybind;
33
+
34
+ const [state, setState] = createSignal<SideDialogState>({
35
+ entries: [],
36
+ loading: false,
37
+ error: undefined,
38
+ tokenCount: 0,
39
+ });
40
+ const [streamingAnswer, setStreamingAnswer] = createSignal("", { equals: false });
41
+
42
+ const [tempSessionID, setTempSessionID] = createSignal<string | undefined>(undefined);
43
+ const [selectedModel, setSelectedModel] = createSignal<ModelPreference>(undefined);
44
+ const [visible, setVisible] = createSignal(false);
45
+ const [thinkCollapsed, setThinkCollapsed] = createSignal(config.think.defaultState === "collapsed");
46
+
47
+ let overlayInput: { focus: () => void } | undefined;
48
+ let previousFocus: import("@opentui/core").Renderable | null = null;
49
+ let unsubscribers: Array<() => void> = [];
50
+ let sessionInitPromise: Promise<string | undefined> | undefined;
51
+ let promptTimeout: ReturnType<typeof setTimeout> | undefined;
52
+ let cachedToolIDs: string[] | undefined;
53
+
54
+ const getModelName = () =>
55
+ formatPreference(
56
+ selectedModel() ?? resolveModel(config.model, state().entries, api).model,
57
+ );
58
+
59
+ const clearListeners = () => {
60
+ while (unsubscribers.length > 0) {
61
+ try { unsubscribers.pop()?.(); } catch (err) { console.error("[SideChat] listener cleanup:", err); }
62
+ }
63
+ };
64
+
65
+ const refreshSession = () => {
66
+ const sid = tempSessionID();
67
+ if (!sid) return;
68
+ try {
69
+ const messages = api.state.session.messages(sid);
70
+ const entries: SideDialogState["entries"] = [];
71
+ let tokenCount = 0;
72
+ for (const info of messages) {
73
+ entries.push({ info, parts: [...(api.state.part(info.id) ?? [])] });
74
+ if ("tokens" in info && info.tokens) {
75
+ tokenCount += (info.tokens.input ?? 0) + (info.tokens.output ?? 0);
76
+ }
77
+ }
78
+ const MAX_STORED_ENTRIES = 100;
79
+ setState((s) => ({ ...s, entries: entries.slice(-MAX_STORED_ENTRIES), tokenCount }));
80
+ } catch (err) {
81
+ console.error("[SideChat] refreshSession failed:", err);
82
+ }
83
+ };
84
+
85
+ const buildSystemPrompt = async () => {
86
+ if (!cachedToolIDs) {
87
+ cachedToolIDs = await getAvailableToolIDs(api);
88
+ }
89
+ const toolIDs = cachedToolIDs;
90
+ const resolvedTools = resolveAllowedTools(config.allowedTools, toolIDs);
91
+ return {
92
+ system: buildSideSystemPrompt(config.systemPrompt, resolvedTools),
93
+ toolIDs,
94
+ resolvedTools,
95
+ tools: buildToolSelection(toolIDs, resolvedTools),
96
+ permission: buildPermissionRules(toolIDs, resolvedTools),
97
+ };
98
+ };
99
+
100
+ const initSession = async (): Promise<string | undefined> => {
101
+ clearListeners();
102
+
103
+ try {
104
+ const { permission } = await buildSystemPrompt();
105
+
106
+ const created = await api.client.session.create(
107
+ {
108
+ title: "side chat",
109
+ directory: api.state.path.directory,
110
+ permission,
111
+ },
112
+ { throwOnError: true },
113
+ );
114
+
115
+ const sid = created.data.id;
116
+ setTempSessionID(sid);
117
+
118
+ unsubscribers.push(
119
+ api.event.on("session.idle", (event) => {
120
+ if (event.properties.sessionID !== sid) return;
121
+ if (promptTimeout) { clearTimeout(promptTimeout); promptTimeout = undefined; }
122
+ refreshSession();
123
+ setState((s) => ({
124
+ ...s,
125
+ loading: false,
126
+ }));
127
+ setStreamingAnswer("");
128
+ }),
129
+ );
130
+
131
+ unsubscribers.push(
132
+ api.event.on("message.updated", (event) => {
133
+ if (event.properties.sessionID !== sid) return;
134
+ refreshSession();
135
+ }),
136
+ );
137
+
138
+ unsubscribers.push(
139
+ api.event.on("message.part.delta", (event) => {
140
+ if (
141
+ event.properties.sessionID !== sid ||
142
+ event.properties.field !== "text" ||
143
+ !state().loading
144
+ ) return;
145
+ setStreamingAnswer((prev) => prev + event.properties.delta);
146
+ }),
147
+ );
148
+
149
+ unsubscribers.push(
150
+ api.event.on("message.part.updated", (event) => {
151
+ if (event.properties.sessionID !== sid) return;
152
+ refreshSession();
153
+ }),
154
+ );
155
+
156
+ unsubscribers.push(
157
+ api.event.on("session.error", (event) => {
158
+ if (event.properties.sessionID !== sid) return;
159
+ if (promptTimeout) { clearTimeout(promptTimeout); promptTimeout = undefined; }
160
+ setState((s) => ({
161
+ ...s,
162
+ error: getErrorMessage(event.properties.error),
163
+ loading: false,
164
+ }));
165
+ }),
166
+ );
167
+
168
+ setState((s) => ({ ...s, error: undefined }));
169
+ return sid;
170
+ } catch (cause) {
171
+ const msg = getErrorMessage(cause);
172
+ setState((s) => ({ ...s, error: msg }));
173
+ sessionInitPromise = undefined;
174
+ return undefined;
175
+ }
176
+ };
177
+
178
+ const ensureSession = (): Promise<string | undefined> => {
179
+ if (tempSessionID()) return Promise.resolve(tempSessionID());
180
+ if (!sessionInitPromise) sessionInitPromise = initSession();
181
+ return sessionInitPromise;
182
+ };
183
+
184
+ const destroySession = async () => {
185
+ const sid = tempSessionID();
186
+ if (!sid) return;
187
+ setTempSessionID(undefined);
188
+ clearListeners();
189
+ try {
190
+ await api.client.session.abort(
191
+ { sessionID: sid },
192
+ { throwOnError: true },
193
+ );
194
+ } catch (err) { console.error("[SideChat] session abort:", err); }
195
+ try {
196
+ await api.client.session.delete(
197
+ { sessionID: sid },
198
+ { throwOnError: true },
199
+ );
200
+ } catch (err) { console.error("[SideChat] session delete:", err); }
201
+ };
202
+
203
+ const handleSubmit = (text: string): boolean => {
204
+ if (state().loading) return false;
205
+
206
+ setState((s) => ({
207
+ ...s,
208
+ error: undefined,
209
+ loading: true,
210
+ }));
211
+ setStreamingAnswer("");
212
+
213
+ if (promptTimeout) clearTimeout(promptTimeout);
214
+ promptTimeout = setTimeout(() => {
215
+ setState((s) => s.loading ? { ...s, loading: false, error: "Request timed out." } : s);
216
+ }, PROMPT_TIMEOUT_MS);
217
+
218
+ void ensureSession().then((sid) => {
219
+ if (!sid) {
220
+ setState((s) => ({
221
+ ...s,
222
+ error: "Failed to create session.",
223
+ loading: false,
224
+ }));
225
+ return;
226
+ }
227
+
228
+ void (async () => {
229
+ try {
230
+ const { system, tools } = await buildSystemPrompt();
231
+ const resolved =
232
+ selectedModel() ??
233
+ resolveModel(config.model, state().entries, api).model;
234
+
235
+ await api.client.session.promptAsync(
236
+ {
237
+ sessionID: sid,
238
+ system,
239
+ tools,
240
+ parts: [{ type: "text", text }],
241
+ ...(resolved.model ? { model: resolved.model } : {}),
242
+ ...(resolved.variant ? { variant: resolved.variant } : {}),
243
+ },
244
+ { throwOnError: true },
245
+ );
246
+ } catch (cause) {
247
+ setState((s) => ({
248
+ ...s,
249
+ error: getErrorMessage(cause),
250
+ loading: false,
251
+ }));
252
+ }
253
+ })();
254
+ });
255
+
256
+ return true;
257
+ };
258
+
259
+ const handleClear = async () => {
260
+ await destroySession();
261
+ setState({
262
+ entries: [],
263
+ loading: false,
264
+ error: undefined,
265
+ tokenCount: 0,
266
+ });
267
+ setStreamingAnswer("");
268
+ sessionInitPromise = undefined;
269
+ setThinkCollapsed(config.think.defaultState === "collapsed");
270
+ await ensureSession();
271
+ if (visible()) {
272
+ setTimeout(() => overlayInput?.focus(), 0);
273
+ }
274
+ };
275
+
276
+ const handleToggle = () => {
277
+ const currentRoute = api.route.current;
278
+ if (currentRoute.name !== "session" && !visible()) return;
279
+ const wasVisible = visible();
280
+ if (!wasVisible) {
281
+ previousFocus = api.renderer.currentFocusedRenderable;
282
+ }
283
+ setVisible(!wasVisible);
284
+ if (wasVisible && previousFocus) {
285
+ const restore = previousFocus;
286
+ previousFocus = null;
287
+ setTimeout(() => {
288
+ try { restore.focus(); } catch {}
289
+ }, 50);
290
+ } else if (!wasVisible) {
291
+ setTimeout(() => overlayInput?.focus(), 50);
292
+ }
293
+ };
294
+
295
+ const handleToggleThink = () => {
296
+ setThinkCollapsed((prev) => !prev);
297
+ };
298
+
299
+ const handleChangeModel = () => {
300
+ const currentRoute = api.route.current;
301
+ if (currentRoute.name !== "session") return;
302
+ openModelPicker(api, config, selectedModel(), (model) => {
303
+ setSelectedModel(model);
304
+ });
305
+ };
306
+
307
+ api.lifecycle.onDispose(() => {
308
+ clearListeners();
309
+ void destroySession();
310
+ });
311
+
312
+ api.slots.register({
313
+ slots: {
314
+ app: () => (
315
+ <Show when={visible()}>
316
+ <ErrorBoundary fallback={(err) => <text>{String(err)}</text>}>
317
+ <SideChat
318
+ api={api}
319
+ modelName={getModelName()}
320
+ state={state()}
321
+ streamingAnswer={streamingAnswer()}
322
+ width={config.width}
323
+ transcriptHeight={config.transcriptHeight}
324
+ tokenLimit={config.tokenLimit}
325
+ thinkCollapsed={thinkCollapsed()}
326
+ thinkConfig={config.think}
327
+ onInput={(node) => { overlayInput = node; }}
328
+ onChangeModel={handleChangeModel}
329
+ onSubmit={handleSubmit}
330
+ />
331
+ </ErrorBoundary>
332
+ </Show>
333
+ ),
334
+ },
335
+ });
336
+
337
+ api.keymap.registerLayer({
338
+ commands: [
339
+ {
340
+ namespace: "palette",
341
+ name: CMD_TOGGLE_FOCUS,
342
+ title: "side",
343
+ desc: "Open/side chat overlay",
344
+ category: "Plugin",
345
+ slashName: "side",
346
+ enabled: () => api.route.current.name === "session" || visible(),
347
+ run: () => handleToggle(),
348
+ },
349
+ {
350
+ namespace: "palette",
351
+ name: CMD_CLEAR,
352
+ title: "side clear",
353
+ desc: "Clear the side chat conversation",
354
+ category: "Plugin",
355
+ slashName: "side-clear",
356
+ enabled: () => api.route.current.name === "session",
357
+ run: () => void handleClear(),
358
+ },
359
+ {
360
+ namespace: "palette",
361
+ name: CMD_CHANGE_MODEL,
362
+ title: "side model",
363
+ desc: "Change the side chat model",
364
+ category: "Plugin",
365
+ slashName: "side-model",
366
+ enabled: () => api.route.current.name === "session",
367
+ run: () => handleChangeModel(),
368
+ },
369
+ ],
370
+ bindings: [
371
+ ...(keybind !== false
372
+ ? [{
373
+ key: keybind,
374
+ cmd: CMD_TOGGLE_FOCUS,
375
+ desc: "Toggle side chat",
376
+ }]
377
+ : []),
378
+ ],
379
+ });
380
+
381
+ api.keymap.registerLayer({
382
+ priority: 1000,
383
+ enabled: () => visible(),
384
+ commands: [
385
+ { name: CMD_CLEAR, run: () => void handleClear() },
386
+ { name: CMD_CHANGE_MODEL, run: () => handleChangeModel() },
387
+ { name: CMD_TOGGLE_THINK, run: () => handleToggleThink() },
388
+ ],
389
+ bindings: [
390
+ ...(clearKeybind !== false
391
+ ? [{ key: clearKeybind, cmd: CMD_CLEAR }]
392
+ : []),
393
+ ...(thinkToggleKeybind !== false
394
+ ? [{ key: thinkToggleKeybind, cmd: CMD_TOGGLE_THINK }]
395
+ : []),
396
+ { key: "tab", cmd: CMD_CHANGE_MODEL },
397
+ ],
398
+ });
399
+ };
400
+
401
+ const plugin: TuiPluginModule & { id: string } = {
402
+ id: PLUGIN_ID,
403
+ tui,
404
+ };
405
+
406
+ export default plugin;