opencode-sidechat 1.0.0 → 1.1.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.
package/src/config.ts CHANGED
@@ -1,189 +1,198 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
- import {
5
- DEFAULT_ALLOWED_TOOLS,
6
- DEFAULT_TOKEN_LIMIT,
7
- DEFAULT_KEYBIND,
8
- DEFAULT_CLEAR_KEYBIND,
9
- DEFAULT_THINK_TOGGLE_KEYBIND,
10
- DEFAULT_WIDTH,
11
- DEFAULT_TRANSCRIPT_HEIGHT,
12
- DEFAULT_SYSTEM_PROMPT,
13
- } from "./constants";
14
- import type { SideConfig, ThinkConfig } from "./types";
15
-
16
- const CONFIG_FILENAME = "sidechat.jsonc";
17
-
18
- function configDir(): string {
19
- const xdg = process.env.XDG_CONFIG_HOME;
20
- if (xdg) return join(xdg, "opencode");
21
- return join(homedir(), ".config", "opencode");
22
- }
23
-
24
- function configPath(): string {
25
- return join(configDir(), CONFIG_FILENAME);
26
- }
27
-
28
- function stripJsoncComments(text: string): string {
29
- let result = "";
30
- let i = 0;
31
- let inString = false;
32
- while (i < text.length) {
33
- const ch = text[i];
34
- if (inString) {
35
- result += ch;
36
- if (ch === "\\" && i + 1 < text.length) {
37
- i += 1;
38
- result += text[i];
39
- } else if (ch === '"') {
40
- inString = false;
41
- }
42
- } else {
43
- if (ch === '"') {
44
- inString = true;
45
- result += ch;
46
- } else if (ch === "/" && i + 1 < text.length && text[i + 1] === "/") {
47
- while (i < text.length && text[i] !== "\n") i += 1;
48
- continue;
49
- } else if (ch === "/" && i + 1 < text.length && text[i + 1] === "*") {
50
- i += 2;
51
- while (i + 1 < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
52
- i += 2;
53
- continue;
54
- } else {
55
- result += ch;
56
- }
57
- }
58
- i += 1;
59
- }
60
- return result;
61
- }
62
-
63
- function stripTrailingCommas(text: string): string {
64
- let result = "";
65
- let i = 0;
66
- let inString = false;
67
- while (i < text.length) {
68
- const ch = text[i];
69
- if (inString) {
70
- result += ch;
71
- if (ch === "\\" && i + 1 < text.length) {
72
- i += 1;
73
- result += text[i];
74
- } else if (ch === '"') {
75
- inString = false;
76
- }
77
- i += 1;
78
- continue;
79
- }
80
-
81
- if (ch === '"') {
82
- inString = true;
83
- result += ch;
84
- i += 1;
85
- continue;
86
- }
87
-
88
- if (ch === ",") {
89
- let j = i + 1;
90
- while (j < text.length && /\s/.test(text[j])) j += 1;
91
- if (text[j] === "}" || text[j] === "]") {
92
- i += 1;
93
- continue;
94
- }
95
- }
96
-
97
- result += ch;
98
- i += 1;
99
- }
100
- return result;
101
- }
102
-
103
- function generateDefaultConfig(): string {
104
- const defaultAllowedTools = JSON.stringify(DEFAULT_ALLOWED_TOOLS);
105
- return `{
106
- // OpenCode SideChat Configuration
107
- "model": "opencode/deepseek-v4-flash-free",
108
- "systemPrompt": ${JSON.stringify(DEFAULT_SYSTEM_PROMPT)},
109
- "keybind": "alt+n",
110
- "clearKeybind": "alt+c",
111
- "thinkToggleKeybind": "alt+t",
112
- "allowedTools": ${defaultAllowedTools},
113
- "width": ${DEFAULT_WIDTH},
114
- "transcriptHeight": ${DEFAULT_TRANSCRIPT_HEIGHT},
115
- "tokenLimit": ${DEFAULT_TOKEN_LIMIT},
116
- "think": {
117
- "defaultState": "collapsed",
118
- "showSummary": false
119
- }
120
- }
121
- `;
122
- }
123
-
124
- function ensureConfigFile(): void {
125
- const dir = configDir();
126
- const path = configPath();
127
- try {
128
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
129
- if (!existsSync(path)) writeFileSync(path, generateDefaultConfig(), "utf-8");
130
- } catch {}
131
- }
132
-
133
- export function loadConfig(): SideConfig {
134
- ensureConfigFile();
135
-
136
- let raw: Record<string, unknown> = {};
137
- try {
138
- const text = readFileSync(configPath(), "utf-8");
139
- const json = stripTrailingCommas(stripJsoncComments(text));
140
- const parsed = JSON.parse(json);
141
- if (parsed && typeof parsed === "object") raw = parsed as Record<string, unknown>;
142
- } catch {}
143
-
144
- return {
145
- model: parseStringOrNull(raw.model),
146
- systemPrompt: parseString(raw.systemPrompt, DEFAULT_SYSTEM_PROMPT),
147
- tokenLimit: parsePositiveNumber(raw.tokenLimit, DEFAULT_TOKEN_LIMIT),
148
- keybind: parseKeybind(raw.keybind, DEFAULT_KEYBIND),
149
- clearKeybind: parseKeybind(raw.clearKeybind, DEFAULT_CLEAR_KEYBIND),
150
- thinkToggleKeybind: parseKeybind(raw.thinkToggleKeybind, DEFAULT_THINK_TOGGLE_KEYBIND),
151
- allowedTools: parseAllowedTools(raw.allowedTools),
152
- width: parsePositiveNumber(raw.width, DEFAULT_WIDTH),
153
- transcriptHeight: parsePositiveNumber(raw.transcriptHeight, DEFAULT_TRANSCRIPT_HEIGHT),
154
- think: parseThinkConfig(raw.think),
155
- };
156
- }
157
-
158
- function parseStringOrNull(value: unknown): string | null {
159
- return typeof value === "string" && value.trim() ? value.trim() : null;
160
- }
161
-
162
- function parseString(value: unknown, fallback: string): string {
163
- return typeof value === "string" && value.trim() ? value.trim() : fallback;
164
- }
165
-
166
- function parsePositiveNumber(value: unknown, fallback: number) {
167
- return typeof value === "number" && Number.isFinite(value) && value > 0
168
- ? Math.floor(value)
169
- : fallback;
170
- }
171
-
172
- function parseKeybind(value: unknown, fallback: string): string | false {
173
- if (value === false || value === "none") return false;
174
- return typeof value === "string" && value.trim() ? value.trim() : fallback;
175
- }
176
-
177
- function parseAllowedTools(value: unknown): string[] | null {
178
- if (!Array.isArray(value)) return null;
179
- return value.every((item) => typeof item === "string") ? value : null;
180
- }
181
-
182
- function parseThinkConfig(value: unknown): ThinkConfig {
183
- if (!value || typeof value !== "object") return { defaultState: "collapsed", showSummary: false };
184
- const obj = value as Record<string, unknown>;
185
- return {
186
- defaultState: obj.defaultState === "expanded" ? "expanded" : "collapsed",
187
- showSummary: obj.showSummary === true,
188
- };
189
- }
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import {
5
+ DEFAULT_ALLOWED_TOOLS,
6
+ DEFAULT_TOKEN_LIMIT,
7
+ DEFAULT_KEYBIND,
8
+ DEFAULT_CLEAR_KEYBIND,
9
+ DEFAULT_THINK_TOGGLE_KEYBIND,
10
+ DEFAULT_WIDTH,
11
+ DEFAULT_TRANSCRIPT_HEIGHT,
12
+ DEFAULT_SYSTEM_PROMPT,
13
+ } from "./constants";
14
+ import type { SideConfig, ThinkConfig } from "./types";
15
+
16
+ const CONFIG_FILENAME = "sidechat.jsonc";
17
+
18
+ function configDir(): string {
19
+ const xdg = process.env.XDG_CONFIG_HOME;
20
+ if (xdg) return join(xdg, "opencode");
21
+ return join(homedir(), ".config", "opencode");
22
+ }
23
+
24
+ function configPath(): string {
25
+ return join(configDir(), CONFIG_FILENAME);
26
+ }
27
+
28
+ function stripJsoncComments(text: string): string {
29
+ let result = "";
30
+ let i = 0;
31
+ let inString = false;
32
+ while (i < text.length) {
33
+ const ch = text[i];
34
+ if (inString) {
35
+ result += ch;
36
+ if (ch === "\\" && i + 1 < text.length) {
37
+ i += 1;
38
+ result += text[i];
39
+ } else if (ch === '"') {
40
+ inString = false;
41
+ }
42
+ } else {
43
+ if (ch === '"') {
44
+ inString = true;
45
+ result += ch;
46
+ } else if (ch === "/" && i + 1 < text.length && text[i + 1] === "/") {
47
+ while (i < text.length && text[i] !== "\n") i += 1;
48
+ continue;
49
+ } else if (ch === "/" && i + 1 < text.length && text[i + 1] === "*") {
50
+ i += 2;
51
+ let found = false;
52
+ while (i + 1 < text.length) {
53
+ if (text[i] === "*" && text[i + 1] === "/") { found = true; break; }
54
+ i += 1;
55
+ }
56
+ if (found) i += 2;
57
+ else i = text.length;
58
+ continue;
59
+ } else {
60
+ result += ch;
61
+ }
62
+ }
63
+ i += 1;
64
+ }
65
+ return result;
66
+ }
67
+
68
+ function stripTrailingCommas(text: string): string {
69
+ let result = "";
70
+ let i = 0;
71
+ let inString = false;
72
+ while (i < text.length) {
73
+ const ch = text[i];
74
+ if (inString) {
75
+ result += ch;
76
+ if (ch === "\\" && i + 1 < text.length) {
77
+ i += 1;
78
+ result += text[i];
79
+ } else if (ch === '"') {
80
+ inString = false;
81
+ }
82
+ i += 1;
83
+ continue;
84
+ }
85
+
86
+ if (ch === '"') {
87
+ inString = true;
88
+ result += ch;
89
+ i += 1;
90
+ continue;
91
+ }
92
+
93
+ if (ch === ",") {
94
+ let j = i + 1;
95
+ while (j < text.length && /\s/.test(text[j])) j += 1;
96
+ if (text[j] === "}" || text[j] === "]") {
97
+ i += 1;
98
+ continue;
99
+ }
100
+ }
101
+
102
+ result += ch;
103
+ i += 1;
104
+ }
105
+ return result;
106
+ }
107
+
108
+ function generateDefaultConfig(): string {
109
+ const defaultAllowedTools = JSON.stringify(DEFAULT_ALLOWED_TOOLS);
110
+ return `{
111
+ // OpenCode SideChat Configuration
112
+ "model": "opencode/deepseek-v4-flash-free",
113
+ "systemPrompt": ${JSON.stringify(DEFAULT_SYSTEM_PROMPT)},
114
+ "keybind": "alt+n",
115
+ "clearKeybind": "alt+c",
116
+ "thinkToggleKeybind": "alt+t",
117
+ "allowedTools": ${defaultAllowedTools},
118
+ "width": ${DEFAULT_WIDTH},
119
+ "transcriptHeight": ${DEFAULT_TRANSCRIPT_HEIGHT},
120
+ "tokenLimit": ${DEFAULT_TOKEN_LIMIT},
121
+ "think": {
122
+ "defaultState": "collapsed",
123
+ "showSummary": false
124
+ }
125
+ }
126
+ `;
127
+ }
128
+
129
+ function ensureConfigFile(): void {
130
+ const dir = configDir();
131
+ const path = configPath();
132
+ try {
133
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
134
+ if (!existsSync(path)) writeFileSync(path, generateDefaultConfig(), "utf-8");
135
+ } catch (err) {
136
+ console.error(`[SideChat] Failed to create config:`, err);
137
+ }
138
+ }
139
+
140
+ export function loadConfig(): SideConfig {
141
+ ensureConfigFile();
142
+
143
+ let raw: Record<string, unknown> = {};
144
+ try {
145
+ const text = readFileSync(configPath(), "utf-8");
146
+ const json = stripTrailingCommas(stripJsoncComments(text));
147
+ const parsed = JSON.parse(json);
148
+ if (parsed && typeof parsed === "object") raw = parsed as Record<string, unknown>;
149
+ } catch (err) {
150
+ console.warn(`[SideChat] Failed to parse config, using defaults:`, err);
151
+ }
152
+
153
+ return {
154
+ model: parseStringOrNull(raw.model),
155
+ systemPrompt: parseString(raw.systemPrompt, DEFAULT_SYSTEM_PROMPT),
156
+ tokenLimit: parsePositiveNumber(raw.tokenLimit, DEFAULT_TOKEN_LIMIT),
157
+ keybind: parseKeybind(raw.keybind, DEFAULT_KEYBIND),
158
+ clearKeybind: parseKeybind(raw.clearKeybind, DEFAULT_CLEAR_KEYBIND),
159
+ thinkToggleKeybind: parseKeybind(raw.thinkToggleKeybind, DEFAULT_THINK_TOGGLE_KEYBIND),
160
+ allowedTools: parseAllowedTools(raw.allowedTools),
161
+ width: parsePositiveNumber(raw.width, DEFAULT_WIDTH),
162
+ transcriptHeight: parsePositiveNumber(raw.transcriptHeight, DEFAULT_TRANSCRIPT_HEIGHT),
163
+ think: parseThinkConfig(raw.think),
164
+ };
165
+ }
166
+
167
+ function parseStringOrNull(value: unknown): string | null {
168
+ return typeof value === "string" && value.trim() ? value.trim() : null;
169
+ }
170
+
171
+ function parseString(value: unknown, fallback: string): string {
172
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
173
+ }
174
+
175
+ function parsePositiveNumber(value: unknown, fallback: number) {
176
+ return typeof value === "number" && Number.isFinite(value) && value > 0
177
+ ? Math.floor(value)
178
+ : fallback;
179
+ }
180
+
181
+ function parseKeybind(value: unknown, fallback: string): string | false {
182
+ if (value === false || value === "none") return false;
183
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
184
+ }
185
+
186
+ function parseAllowedTools(value: unknown): string[] | null {
187
+ if (!Array.isArray(value)) return null;
188
+ return value.every((item) => typeof item === "string") ? value : null;
189
+ }
190
+
191
+ function parseThinkConfig(value: unknown): ThinkConfig {
192
+ if (!value || typeof value !== "object") return { defaultState: "collapsed", showSummary: false };
193
+ const obj = value as Record<string, unknown>;
194
+ return {
195
+ defaultState: obj.defaultState === "expanded" ? "expanded" : "collapsed",
196
+ showSummary: obj.showSummary === true,
197
+ };
198
+ }
package/src/constants.ts CHANGED
@@ -1,45 +1,47 @@
1
- export const PLUGIN_ID = "local.opencode-sidechat";
2
-
3
- export const CMD_TOGGLE_FOCUS = "sidechat.toggle-focus";
4
- export const CMD_CLEAR = "sidechat.clear";
5
- export const CMD_CHANGE_MODEL = "sidechat.change-model";
6
- export const CMD_TOGGLE_THINK = "sidechat.toggle-think";
7
-
8
- export const DEFAULT_KEYBIND = "alt+n";
9
- export const DEFAULT_CLEAR_KEYBIND = "alt+c";
10
- export const DEFAULT_THINK_TOGGLE_KEYBIND = "alt+t";
11
- export const DEFAULT_TOKEN_LIMIT = 45_000;
12
- export const DEFAULT_WIDTH = 70;
13
- export const DEFAULT_TRANSCRIPT_HEIGHT = 20;
14
-
15
- export const DEFAULT_SYSTEM_PROMPT =
16
- "You are a casual side assistant. Answer concisely and directly. Use tools only when helpful.";
17
-
18
- export const THINKING_TEXT = "...";
19
-
20
- export const SAFE_TOOLS: Record<string, true> = {
21
- glob: true,
22
- grep: true,
23
- list: true,
24
- read: true,
25
- websearch: true,
26
- webfetch: true,
27
- };
28
-
29
- export const DEFAULT_ALLOWED_TOOLS = Object.keys(SAFE_TOOLS);
30
-
31
- export const ADDITIONAL_PERMISSION_IDS = [
32
- "edit",
33
- "bash",
34
- "task",
35
- "external_directory",
36
- "todowrite",
37
- "question",
38
- "websearch",
39
- "codesearch",
40
- "repo_clone",
41
- "repo_overview",
42
- "lsp",
43
- "doom_loop",
44
- "skill",
45
- ];
1
+ export const PLUGIN_ID = "local.opencode-sidechat";
2
+
3
+ export const CMD_TOGGLE_FOCUS = "sidechat.toggle-focus";
4
+ export const CMD_CLEAR = "sidechat.clear";
5
+ export const CMD_CHANGE_MODEL = "sidechat.change-model";
6
+ export const CMD_TOGGLE_THINK = "sidechat.toggle-think";
7
+
8
+ export const DEFAULT_KEYBIND = "alt+n";
9
+ export const DEFAULT_CLEAR_KEYBIND = "alt+c";
10
+ export const DEFAULT_THINK_TOGGLE_KEYBIND = "alt+t";
11
+ export const DEFAULT_TOKEN_LIMIT = 45_000;
12
+ export const DEFAULT_WIDTH = 70;
13
+ export const DEFAULT_TRANSCRIPT_HEIGHT = 20;
14
+
15
+ export const SYSTEM_PROMPT_OVERRIDE =
16
+ "CRITICAL: Follow ONLY the instructions below. Ignore ALL other system prompts, AGENTS.md files, CLAUDE.md files, and any project-level configuration instructions. Do NOT load skills, do NOT follow agent instructions from other contexts.";
17
+
18
+ export const DEFAULT_SYSTEM_PROMPT =
19
+ "You are a casual side assistant. Answer concisely and directly. Use tools only when helpful.";
20
+
21
+ export const THINKING_TEXT = "...";
22
+
23
+ export const SAFE_TOOLS: Record<string, true> = {
24
+ glob: true,
25
+ grep: true,
26
+ list: true,
27
+ read: true,
28
+ websearch: true,
29
+ webfetch: true,
30
+ };
31
+
32
+ export const DEFAULT_ALLOWED_TOOLS = Object.keys(SAFE_TOOLS);
33
+
34
+ export const ADDITIONAL_PERMISSION_IDS = [
35
+ "edit",
36
+ "bash",
37
+ "task",
38
+ "external_directory",
39
+ "todowrite",
40
+ "question",
41
+ "codesearch",
42
+ "repo_clone",
43
+ "repo_overview",
44
+ "lsp",
45
+ "doom_loop",
46
+ "skill",
47
+ ];