oc-tweaks 0.4.1 → 0.5.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/README.md +56 -4
- package/dist/index.js +307 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A collection of runtime enhancement plugins for [OpenCode](https://opencode.ai/)
|
|
|
9
9
|
`oc-tweaks` provides a set of plugins to enhance your OpenCode experience. These plugins are activated at runtime and can modify OpenCode's behavior, such as sending notifications, improving multi-language support, and enforcing best practices for sub-agent usage.
|
|
10
10
|
|
|
11
11
|
The currently available plugins are:
|
|
12
|
-
- **`notify`**: Sends desktop notifications when a task is completed or an error occurs.
|
|
12
|
+
- **`notify`**: Sends desktop notifications when a task is completed or an error occurs, and can show WPF tool call transparency toasts (stack + queue).
|
|
13
13
|
- **`compaction`**: Injects a language preference prompt during session compaction to ensure summaries are in your preferred language.
|
|
14
14
|
- **`autoMemory`**: Smart memory assistant that injects memory context, detects memory trigger phrases, and supports active memory writes via `remember` tool and `/remember` command.
|
|
15
15
|
- **`backgroundSubagent`**: Adds a system prompt to encourage using background sub-agents for better responsiveness.
|
|
@@ -50,6 +50,8 @@ All configurations are optional. By default, most plugins are enabled after runn
|
|
|
50
50
|
|
|
51
51
|
This plugin sends desktop notifications upon task completion (session idle) or error.
|
|
52
52
|
|
|
53
|
+
It can also show **tool call transparency notifications** (WPF only) for each model tool invocation, including tool name and arguments, with vertical stacking and queueing (no drop).
|
|
54
|
+
|
|
53
55
|
- **Windows**: Uses a custom, non-intrusive WPF window that works across virtual desktops.
|
|
54
56
|
- **macOS**: Uses `osascript`.
|
|
55
57
|
- **Linux**: Uses `notify-send`.
|
|
@@ -64,6 +66,7 @@ This plugin sends desktop notifications upon task completion (session idle) or e
|
|
|
64
66
|
| `notifyOnError` | boolean | `true` | Notify on session errors. |
|
|
65
67
|
| `command` | string | `null` | A custom command to run for notifications. `$TITLE` and `$MESSAGE` are available as placeholders. |
|
|
66
68
|
| `style` | object | `{...}` | Custom styles for the Windows WPF notification window. See below. |
|
|
69
|
+
| `toolCall` | object | `{...}` | Tool call transparency notification settings. See below. |
|
|
67
70
|
|
|
68
71
|
#### `notify.style` (Windows WPF)
|
|
69
72
|
|
|
@@ -80,13 +83,26 @@ Customize the appearance of the WPF notification window.
|
|
|
80
83
|
| `height` | `105` | Window height in pixels. |
|
|
81
84
|
| `titleFontSize` | `14` | Font size for the title in points. |
|
|
82
85
|
| `contentFontSize` | `11` | Font size for the content in points. |
|
|
83
|
-
| `iconFontSize` | `
|
|
86
|
+
| `iconFontSize` | `30` | Font size for the icon (✅/❌) in points. |
|
|
84
87
|
| `duration` | `10000` | Time in milliseconds before the notification auto-closes. |
|
|
85
88
|
| `position` | `"center"` | Window position: `"center"`, `"top-right"`, or `"bottom-right"`. |
|
|
86
89
|
| `shadow` | `true` | Enable or disable the drop shadow effect. |
|
|
87
90
|
| `idleColor` | `"#4ADE80"` | Accent color for idle (success) notifications. |
|
|
88
91
|
| `errorColor` | `"#EF4444"` | Accent color for error notifications. |
|
|
89
92
|
|
|
93
|
+
#### `notify.toolCall` (Windows WPF)
|
|
94
|
+
|
|
95
|
+
Configure tool call transparency notifications. This feature is designed for visibility: every queued event is eventually shown.
|
|
96
|
+
|
|
97
|
+
| Property | Type | Default | Description |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| `enabled` | boolean | `false` | Enable tool call notifications. |
|
|
100
|
+
| `duration` | number | `3000` | Auto-close delay for each tool call toast (ms). |
|
|
101
|
+
| `position` | string | `"top-right"` | Window position: `"top-right"`, `"bottom-right"`, or `"center"`. |
|
|
102
|
+
| `maxVisible` | number | `3` | Maximum number of visible tool call toasts at the same time. |
|
|
103
|
+
| `maxArgLength` | number | `300` | Max characters for formatted argument JSON before truncation. |
|
|
104
|
+
| `filter.exclude` | string[] | `[]` | Tool names to skip (exact match). |
|
|
105
|
+
|
|
90
106
|
### `compaction`
|
|
91
107
|
|
|
92
108
|
This plugin ensures that when OpenCode compacts a session's context, the resulting summary is generated in your preferred language and writing style.
|
|
@@ -162,6 +178,16 @@ Here is an example of a `~/.config/opencode/oc-tweaks.json` file with all option
|
|
|
162
178
|
"style": {
|
|
163
179
|
"backgroundColor": "#101018",
|
|
164
180
|
"duration": 8000
|
|
181
|
+
},
|
|
182
|
+
"toolCall": {
|
|
183
|
+
"enabled": true,
|
|
184
|
+
"duration": 3000,
|
|
185
|
+
"position": "top-right",
|
|
186
|
+
"maxVisible": 3,
|
|
187
|
+
"maxArgLength": 300,
|
|
188
|
+
"filter": {
|
|
189
|
+
"exclude": ["think_sequentialthinking"]
|
|
190
|
+
}
|
|
165
191
|
}
|
|
166
192
|
},
|
|
167
193
|
"compaction": {
|
|
@@ -199,7 +225,7 @@ Here is an example of a `~/.config/opencode/oc-tweaks.json` file with all option
|
|
|
199
225
|
`oc-tweaks` 提供了一系列插件来增强你的 OpenCode 使用体验。这些插件在运行时激活,可以调整 OpenCode 的行为,例如发送桌面通知、改善多语言支持以及强制执行子代理使用的最佳实践。
|
|
200
226
|
|
|
201
227
|
目前可用的插件包括:
|
|
202
|
-
- **`notify`**:
|
|
228
|
+
- **`notify`**: 在任务完成或发生错误时发送桌面通知,并支持 WPF 工具调用透明度弹窗(堆叠 + 排队)。
|
|
203
229
|
- **`compaction`**: 在会话上下文压缩期间注入语言偏好提示,以确保摘要使用你的首选语言。
|
|
204
230
|
- **`autoMemory`**: 智能记忆助手——自动注入 memory 上下文、识别触发词,并支持 `remember` tool 与 `/remember` 命令主动写入。
|
|
205
231
|
- **`backgroundSubagent`**: 添加系统提示,鼓励使用后台子代理以获得更好的响应性。
|
|
@@ -240,6 +266,8 @@ bunx oc-tweaks init
|
|
|
240
266
|
|
|
241
267
|
此插件在任务完成(会话空闲)或出错时发送桌面通知。
|
|
242
268
|
|
|
269
|
+
它也支持**工具调用透明度通知**(仅 WPF):每次模型调用工具都会显示工具名与参数,并支持垂直堆叠和排队(不丢通知)。
|
|
270
|
+
|
|
243
271
|
- **Windows**: 使用一个自定义的、无侵入性的 WPF 窗口,该窗口可跨虚拟桌面工作。
|
|
244
272
|
- **macOS**: 使用 `osascript`。
|
|
245
273
|
- **Linux**: 使用 `notify-send`。
|
|
@@ -254,6 +282,7 @@ bunx oc-tweaks init
|
|
|
254
282
|
| `notifyOnError` | boolean | `true` | 在会话出错时通知。 |
|
|
255
283
|
| `command` | string | `null` | 用于发送通知的自定义命令。`$TITLE` 和 `$MESSAGE` 可作为占位符。 |
|
|
256
284
|
| `style` | object | `{...}` | 用于 Windows WPF 通知窗口的自定义样式。详见下文。 |
|
|
285
|
+
| `toolCall` | object | `{...}` | 工具调用透明度通知配置。详见下文。 |
|
|
257
286
|
|
|
258
287
|
#### `notify.style` (Windows WPF)
|
|
259
288
|
|
|
@@ -270,13 +299,26 @@ bunx oc-tweaks init
|
|
|
270
299
|
| `height` | `105` | 窗口高度 (像素)。 |
|
|
271
300
|
| `titleFontSize` | `14` | 标题的字体大小 (pt)。 |
|
|
272
301
|
| `contentFontSize` | `11` | 内容的字体大小 (pt)。 |
|
|
273
|
-
| `iconFontSize` | `
|
|
302
|
+
| `iconFontSize` | `30` | 图标 (✅/❌) 的字体大小 (pt)。 |
|
|
274
303
|
| `duration` | `10000` | 通知自动关闭前的延迟时间 (毫秒)。 |
|
|
275
304
|
| `position` | `"center"` | 窗口位置: `"center"`, `"top-right"`, 或 `"bottom-right"`。 |
|
|
276
305
|
| `shadow` | `true` | 启用或禁用下拉阴影效果。 |
|
|
277
306
|
| `idleColor` | `"#4ADE80"` | 空闲 (成功) 通知的强调色。 |
|
|
278
307
|
| `errorColor` | `"#EF4444"` | 错误通知的强调色。 |
|
|
279
308
|
|
|
309
|
+
#### `notify.toolCall` (Windows WPF)
|
|
310
|
+
|
|
311
|
+
配置工具调用透明度通知。该能力以“可见性优先”为目标:进入队列的事件最终都会显示。
|
|
312
|
+
|
|
313
|
+
| 属性 | 类型 | 默认值 | 描述 |
|
|
314
|
+
|---|---|---|---|
|
|
315
|
+
| `enabled` | boolean | `false` | 启用工具调用通知。 |
|
|
316
|
+
| `duration` | number | `3000` | 每条工具调用弹窗自动关闭延迟(毫秒)。 |
|
|
317
|
+
| `position` | string | `"top-right"` | 窗口位置:`"top-right"`、`"bottom-right"` 或 `"center"`。 |
|
|
318
|
+
| `maxVisible` | number | `3` | 同时可见的工具调用弹窗最大数量。 |
|
|
319
|
+
| `maxArgLength` | number | `300` | 参数 JSON 格式化后截断前的最大字符数。 |
|
|
320
|
+
| `filter.exclude` | string[] | `[]` | 要跳过的工具名(精确匹配)。 |
|
|
321
|
+
|
|
280
322
|
### `compaction`
|
|
281
323
|
|
|
282
324
|
此插件确保当 OpenCode 压缩会话上下文时,生成的摘要使用你的首选语言和写作风格。
|
|
@@ -352,6 +394,16 @@ bunx oc-tweaks init
|
|
|
352
394
|
"style": {
|
|
353
395
|
"backgroundColor": "#101018",
|
|
354
396
|
"duration": 8000
|
|
397
|
+
},
|
|
398
|
+
"toolCall": {
|
|
399
|
+
"enabled": true,
|
|
400
|
+
"duration": 3000,
|
|
401
|
+
"position": "top-right",
|
|
402
|
+
"maxVisible": 3,
|
|
403
|
+
"maxArgLength": 300,
|
|
404
|
+
"filter": {
|
|
405
|
+
"exclude": ["think_sequentialthinking"]
|
|
406
|
+
}
|
|
355
407
|
}
|
|
356
408
|
},
|
|
357
409
|
"compaction": {
|
package/dist/index.js
CHANGED
|
@@ -65,7 +65,9 @@ var DEFAULT_CONFIG = {
|
|
|
65
65
|
autoMemory: {},
|
|
66
66
|
backgroundSubagent: {},
|
|
67
67
|
leaderboard: {},
|
|
68
|
-
notify: {
|
|
68
|
+
notify: {
|
|
69
|
+
toolCall: {}
|
|
70
|
+
}
|
|
69
71
|
};
|
|
70
72
|
async function loadOcTweaksConfig() {
|
|
71
73
|
const home = Bun.env?.HOME ?? (globalThis?.process?.env?.HOME ?? "") ?? "";
|
|
@@ -323,10 +325,18 @@ ${VIOLATION_WARNING}`;
|
|
|
323
325
|
// src/plugins/compaction.ts
|
|
324
326
|
function buildCompactionPrompt(language, style) {
|
|
325
327
|
const lang = language || "the language the user used most in this session";
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
Write the compaction summary in ${lang}
|
|
328
|
+
const lines = [
|
|
329
|
+
"## MANDATORY: Language & Writing Style",
|
|
330
|
+
"",
|
|
331
|
+
`**Language**: Write the ENTIRE compaction summary in ${lang}. Keep technical terms (filenames, commands, code) in their original form.`
|
|
332
|
+
];
|
|
333
|
+
if (style) {
|
|
334
|
+
lines.push("", `**Writing Style**: ${style}`, "", "This is a NON-NEGOTIABLE requirement. Every section, every bullet point, every description in the summary MUST be written in this style. The structural format (numbered sections, bullet points) should be preserved, but the TONE, VOICE, and WORD CHOICE must unmistakably reflect the specified writing style throughout the entire output. Do NOT fall back to a neutral or generic tone.");
|
|
335
|
+
} else {
|
|
336
|
+
lines.push("", "**Writing Style**: concise and well-organized");
|
|
337
|
+
}
|
|
338
|
+
return lines.join(`
|
|
339
|
+
`);
|
|
330
340
|
}
|
|
331
341
|
var compactionPlugin = async () => {
|
|
332
342
|
return {
|
|
@@ -467,74 +477,104 @@ var leaderboardPlugin = async () => {
|
|
|
467
477
|
})
|
|
468
478
|
};
|
|
469
479
|
};
|
|
470
|
-
// src/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
480
|
+
// src/utils/wpf-position.ts
|
|
481
|
+
class WpfPositionManager {
|
|
482
|
+
maxVisible;
|
|
483
|
+
slots = new Map;
|
|
484
|
+
queue = [];
|
|
485
|
+
constructor(maxVisible) {
|
|
486
|
+
this.maxVisible = maxVisible;
|
|
487
|
+
}
|
|
488
|
+
allocateSlot(duration) {
|
|
489
|
+
this.cleanupExpiredSlots();
|
|
490
|
+
for (let slotIndex = 0;slotIndex < this.maxVisible; slotIndex += 1) {
|
|
491
|
+
if (this.slots.has(slotIndex))
|
|
492
|
+
continue;
|
|
493
|
+
const ttl = Math.max(0, duration) + 500;
|
|
494
|
+
const timer = setTimeout(() => {
|
|
495
|
+
this.releaseSlot(slotIndex);
|
|
496
|
+
}, ttl);
|
|
497
|
+
const expiresAt = Date.now() + ttl;
|
|
498
|
+
this.slots.set(slotIndex, { expiresAt, timer });
|
|
499
|
+
let released = false;
|
|
500
|
+
return {
|
|
501
|
+
slotIndex,
|
|
502
|
+
release: () => {
|
|
503
|
+
if (released)
|
|
504
|
+
return;
|
|
505
|
+
released = true;
|
|
506
|
+
this.releaseSlot(slotIndex);
|
|
507
|
+
}
|
|
487
508
|
};
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
enqueue(callback) {
|
|
513
|
+
this.queue.push(callback);
|
|
514
|
+
}
|
|
515
|
+
processQueue() {
|
|
516
|
+
this.cleanupExpiredSlots();
|
|
517
|
+
while (this.queue.length > 0 && this.hasAvailableSlot()) {
|
|
518
|
+
const callback = this.queue.shift();
|
|
519
|
+
if (!callback)
|
|
495
520
|
return;
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const normalized = directory.replace(/\\/g, "/");
|
|
508
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
509
|
-
return segments[segments.length - 1] || "opencode";
|
|
510
|
-
}
|
|
511
|
-
async function extractIdleMessage(client, sessionId) {
|
|
512
|
-
let message = "✓ Task completed";
|
|
513
|
-
if (!sessionId || !client?.session?.messages)
|
|
514
|
-
return message;
|
|
515
|
-
try {
|
|
516
|
-
const result = await client.session.messages({ path: { id: sessionId } });
|
|
517
|
-
const messages = result?.data;
|
|
518
|
-
if (!Array.isArray(messages))
|
|
519
|
-
return message;
|
|
520
|
-
for (let i = messages.length - 1;i >= 0; i -= 1) {
|
|
521
|
-
const msg = messages[i];
|
|
522
|
-
if (msg?.info?.role !== "assistant" || !Array.isArray(msg?.parts))
|
|
521
|
+
callback();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
hasAvailableSlot() {
|
|
525
|
+
this.cleanupExpiredSlots();
|
|
526
|
+
return this.slots.size < this.maxVisible;
|
|
527
|
+
}
|
|
528
|
+
cleanupExpiredSlots() {
|
|
529
|
+
const now = Date.now();
|
|
530
|
+
for (const [slotIndex, state] of this.slots.entries()) {
|
|
531
|
+
if (state.expiresAt > now)
|
|
523
532
|
continue;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
message = `✓ ${truncateText(cleanMarkdown(part.text), 400)}`;
|
|
527
|
-
return message;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
533
|
+
clearTimeout(state.timer);
|
|
534
|
+
this.slots.delete(slotIndex);
|
|
530
535
|
}
|
|
531
|
-
return message;
|
|
532
|
-
} catch {
|
|
533
|
-
return message;
|
|
534
536
|
}
|
|
537
|
+
releaseSlot(slotIndex) {
|
|
538
|
+
const state = this.slots.get(slotIndex);
|
|
539
|
+
if (!state)
|
|
540
|
+
return;
|
|
541
|
+
clearTimeout(state.timer);
|
|
542
|
+
this.slots.delete(slotIndex);
|
|
543
|
+
this.processQueue();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function calculatePosition(slotIndex, config) {
|
|
547
|
+
const position = config.position ?? "center";
|
|
548
|
+
if (position === "center") {
|
|
549
|
+
return { startupLocation: "CenterScreen" };
|
|
550
|
+
}
|
|
551
|
+
const width = config.width;
|
|
552
|
+
const height = config.height;
|
|
553
|
+
const gap = config.gap ?? 12;
|
|
554
|
+
const margin = config.screenMargin ?? 20;
|
|
555
|
+
const screenWidthExpr = typeof config.screenWidth === "number" ? `${config.screenWidth}` : "[System.Windows.SystemParameters]::PrimaryScreenWidth";
|
|
556
|
+
const screenHeightExpr = typeof config.screenHeight === "number" ? `${config.screenHeight}` : "[System.Windows.SystemParameters]::PrimaryScreenHeight";
|
|
557
|
+
const leftExpr = `(${screenWidthExpr} - ${width} - ${margin})`;
|
|
558
|
+
if (position === "bottom-right") {
|
|
559
|
+
const topExpr2 = `(${screenHeightExpr} - ${margin} - ((${slotIndex} + 1) * (${height} + ${gap})))`;
|
|
560
|
+
return {
|
|
561
|
+
startupLocation: "Manual",
|
|
562
|
+
leftExpr,
|
|
563
|
+
topExpr: topExpr2
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const topExpr = `(${margin} + (${slotIndex} * (${height} + ${gap})))`;
|
|
567
|
+
return {
|
|
568
|
+
startupLocation: "Manual",
|
|
569
|
+
leftExpr,
|
|
570
|
+
topExpr
|
|
571
|
+
};
|
|
535
572
|
}
|
|
573
|
+
|
|
574
|
+
// src/utils/wpf-notify.ts
|
|
536
575
|
async function detectNotifySender($, client, logConfig) {
|
|
537
|
-
|
|
576
|
+
const isWindows = globalThis?.process?.platform === "win32";
|
|
577
|
+
if (isWindows && await commandExists($, "pwsh")) {
|
|
538
578
|
return { kind: "wpf", command: "pwsh" };
|
|
539
579
|
}
|
|
540
580
|
if (await commandExists($, "powershell.exe")) {
|
|
@@ -552,10 +592,7 @@ async function detectNotifySender($, client, logConfig) {
|
|
|
552
592
|
await log(logConfig, "WARN", "[oc-tweaks] notify: no available notifier, set notify.command to override");
|
|
553
593
|
return { kind: "none" };
|
|
554
594
|
}
|
|
555
|
-
async function
|
|
556
|
-
return Bun.which(command) !== null;
|
|
557
|
-
}
|
|
558
|
-
async function notifyWithSender($, sender, title, message, tag, style, logConfig) {
|
|
595
|
+
async function notifyWithSender($, sender, title, message, tag, style, _logConfig, position, renderOptions) {
|
|
559
596
|
try {
|
|
560
597
|
if (sender.kind === "custom") {
|
|
561
598
|
const command = sender.commandTemplate.replace(/\$TITLE/g, title).replace(/\$MESSAGE/g, message);
|
|
@@ -563,7 +600,7 @@ async function notifyWithSender($, sender, title, message, tag, style, logConfig
|
|
|
563
600
|
return;
|
|
564
601
|
}
|
|
565
602
|
if (sender.kind === "wpf") {
|
|
566
|
-
await runWpfNotification($, sender.command, title, message, tag, style);
|
|
603
|
+
await runWpfNotification($, sender.command, title, message, tag, style, position, renderOptions);
|
|
567
604
|
return;
|
|
568
605
|
}
|
|
569
606
|
if (sender.kind === "osascript") {
|
|
@@ -580,11 +617,34 @@ async function notifyWithSender($, sender, title, message, tag, style, logConfig
|
|
|
580
617
|
}
|
|
581
618
|
} catch {}
|
|
582
619
|
}
|
|
620
|
+
async function sendWpfToast(options) {
|
|
621
|
+
await notifyWithSender(options.$, options.sender, options.title, options.message, options.tag, options.style, options.logConfig, options.position, {
|
|
622
|
+
icon: options.icon,
|
|
623
|
+
accentColor: options.accentColor,
|
|
624
|
+
showDismissHint: options.showDismissHint,
|
|
625
|
+
maxMessageLength: options.maxMessageLength,
|
|
626
|
+
preserveMessageFormatting: options.preserveMessageFormatting
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function escapeForPowerShell(text) {
|
|
630
|
+
return Buffer.from(text, "utf8").toString("base64");
|
|
631
|
+
}
|
|
632
|
+
function truncateText(text, maxChars) {
|
|
633
|
+
if (text.length <= maxChars)
|
|
634
|
+
return text;
|
|
635
|
+
return `${text.slice(0, maxChars)}...`;
|
|
636
|
+
}
|
|
637
|
+
function cleanMarkdown(text) {
|
|
638
|
+
return text.replace(/[`*#]/g, "").replace(/\n+/g, " ").replace(/\s+/g, " ").trim();
|
|
639
|
+
}
|
|
640
|
+
async function commandExists(_$, command) {
|
|
641
|
+
return Bun.which(command) !== null;
|
|
642
|
+
}
|
|
583
643
|
async function runCustomCommand($, command) {
|
|
584
644
|
const escaped = command.replace(/"/g, "\\\"");
|
|
585
645
|
await $`bun -e ${`const { exec } = require("node:child_process"); exec("${escaped}")`}`;
|
|
586
646
|
}
|
|
587
|
-
async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
647
|
+
async function runWpfNotification($, shellCommand, title, message, tag, style, position, renderOptions) {
|
|
588
648
|
const backgroundColor = style?.backgroundColor ?? "#101018";
|
|
589
649
|
const backgroundOpacity = style?.backgroundOpacity ?? 0.95;
|
|
590
650
|
const textColor = style?.textColor ?? "#AAAAAA";
|
|
@@ -596,16 +656,26 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
596
656
|
const contentFontSize = style?.contentFontSize ?? 11;
|
|
597
657
|
const iconFontSize = style?.iconFontSize ?? 30;
|
|
598
658
|
const duration = style?.duration ?? 1e4;
|
|
599
|
-
const
|
|
659
|
+
const stylePosition = style?.position ?? "center";
|
|
600
660
|
const shadow = style?.shadow !== false;
|
|
601
661
|
const idleColor = style?.idleColor ?? "#4ADE80";
|
|
602
662
|
const errorColor = style?.errorColor ?? "#EF4444";
|
|
603
|
-
const accentColor = tag === "Error" ? errorColor : idleColor;
|
|
604
|
-
const icon = tag === "Error" ? "❌" : "✅";
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
const
|
|
663
|
+
const accentColor = renderOptions?.accentColor ?? (tag === "Error" ? errorColor : idleColor);
|
|
664
|
+
const icon = renderOptions?.icon ?? (tag === "Error" ? "❌" : "✅");
|
|
665
|
+
const showDismissHint = renderOptions?.showDismissHint !== false;
|
|
666
|
+
const normalizedStylePosition = stylePosition === "top-right" || stylePosition === "bottom-right" || stylePosition === "center" ? stylePosition : "center";
|
|
667
|
+
const resolvedPosition = position ?? (normalizedStylePosition === "center" ? { startupLocation: "CenterScreen" } : calculatePosition(0, {
|
|
668
|
+
position: normalizedStylePosition,
|
|
669
|
+
width,
|
|
670
|
+
height
|
|
671
|
+
}));
|
|
672
|
+
const startupLocation = resolvedPosition.startupLocation;
|
|
673
|
+
const titleB64 = escapeForPowerShell(title);
|
|
674
|
+
const messageLimit = typeof renderOptions?.maxMessageLength === "number" && renderOptions.maxMessageLength > 0 ? renderOptions.maxMessageLength : 400;
|
|
675
|
+
const normalizedMessage = renderOptions?.preserveMessageFormatting ? message : cleanMarkdown(message);
|
|
676
|
+
const textB64 = escapeForPowerShell(truncateText(normalizedMessage, messageLimit));
|
|
608
677
|
const shadowXaml = shadow ? '<Border.Effect><DropShadowEffect BlurRadius="20" ShadowDepth="2" Opacity="0.7" Color="Black"/></Border.Effect>' : "";
|
|
678
|
+
const dismissHintXaml = showDismissHint ? ` <TextBlock Text="Click to dismiss" Foreground="#555555" FontSize="9" Margin="0,4,0,0"/>` : "";
|
|
609
679
|
const xaml = [
|
|
610
680
|
`<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"`,
|
|
611
681
|
` xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"`,
|
|
@@ -625,7 +695,7 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
625
695
|
` <StackPanel VerticalAlignment="Center" MaxWidth="320">`,
|
|
626
696
|
` <TextBlock Name="TitleText" FontSize="${titleFontSize}" FontWeight="SemiBold"/>`,
|
|
627
697
|
` <TextBlock Name="ContentText" Foreground="${textColor}" FontSize="${contentFontSize}" Margin="0,4,0,0" TextWrapping="Wrap"/>`,
|
|
628
|
-
|
|
698
|
+
dismissHintXaml,
|
|
629
699
|
` </StackPanel>`,
|
|
630
700
|
` </StackPanel>`,
|
|
631
701
|
` </Grid>`,
|
|
@@ -633,6 +703,11 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
633
703
|
`</Window>`
|
|
634
704
|
].join(`
|
|
635
705
|
`);
|
|
706
|
+
const manualPositionLines = resolvedPosition.startupLocation === "Manual" && resolvedPosition.leftExpr && resolvedPosition.topExpr ? [
|
|
707
|
+
"$window.WindowStartupLocation = 'Manual'",
|
|
708
|
+
`$window.Left = ${resolvedPosition.leftExpr}`,
|
|
709
|
+
`$window.Top = ${resolvedPosition.topExpr}`
|
|
710
|
+
] : [];
|
|
636
711
|
const psScript = [
|
|
637
712
|
"Add-Type -AssemblyName PresentationFramework",
|
|
638
713
|
"Add-Type -AssemblyName PresentationCore",
|
|
@@ -663,8 +738,8 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
663
738
|
"}",
|
|
664
739
|
"'@ -ErrorAction SilentlyContinue",
|
|
665
740
|
"",
|
|
666
|
-
`$title = '${
|
|
667
|
-
`$text = '${
|
|
741
|
+
`$title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${titleB64}'))`,
|
|
742
|
+
`$text = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${textB64}'))`,
|
|
668
743
|
`$accentColor = '${accentColor}'`,
|
|
669
744
|
`$icon = '${icon}'`,
|
|
670
745
|
`$duration = ${duration}`,
|
|
@@ -675,6 +750,7 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
675
750
|
"",
|
|
676
751
|
"$reader = New-Object System.Xml.XmlNodeReader $xaml",
|
|
677
752
|
"$window = [Windows.Markup.XamlReader]::Load($reader)",
|
|
753
|
+
...manualPositionLines,
|
|
678
754
|
"",
|
|
679
755
|
"$colorBar = $window.FindName('ColorBar')",
|
|
680
756
|
"$iconText = $window.FindName('IconText')",
|
|
@@ -733,18 +809,160 @@ async function showToastWithFallback(showToast, title, message) {
|
|
|
733
809
|
} catch {}
|
|
734
810
|
await Promise.resolve(showToast(title, message));
|
|
735
811
|
}
|
|
736
|
-
function truncateText(text, maxChars) {
|
|
737
|
-
if (text.length <= maxChars)
|
|
738
|
-
return text;
|
|
739
|
-
return `${text.slice(0, maxChars)}...`;
|
|
740
|
-
}
|
|
741
|
-
function cleanMarkdown(text) {
|
|
742
|
-
return text.replace(/[`*#]/g, "").replace(/\n+/g, " ").replace(/\s+/g, " ").trim();
|
|
743
|
-
}
|
|
744
812
|
function escapeAppleScript(text) {
|
|
745
813
|
return text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
746
814
|
}
|
|
815
|
+
|
|
816
|
+
// src/plugins/notify.ts
|
|
817
|
+
var notifyPlugin = async ({ $, directory, client }) => {
|
|
818
|
+
let cachedSender = null;
|
|
819
|
+
return {
|
|
820
|
+
event: safeHook("notify:event", async ({ event }) => {
|
|
821
|
+
const config = await loadOcTweaksConfig();
|
|
822
|
+
if (!config || config.notify?.enabled !== true)
|
|
823
|
+
return;
|
|
824
|
+
const logConfig = config.logging;
|
|
825
|
+
const notifyOnIdle = config.notify?.notifyOnIdle !== false;
|
|
826
|
+
const notifyOnError = config.notify?.notifyOnError !== false;
|
|
827
|
+
const configuredCommand = typeof config.notify?.command === "string" && config.notify.command.trim().length > 0 ? config.notify.command.trim() : null;
|
|
828
|
+
const style = config.notify?.style;
|
|
829
|
+
const sender = configuredCommand ? { kind: "custom", commandTemplate: configuredCommand } : cachedSender ??= await detectNotifySender($, client, logConfig);
|
|
830
|
+
const sendToast = async (projectName, message, tag) => {
|
|
831
|
+
const title = `oc: ${projectName}`;
|
|
832
|
+
await notifyWithSender($, sender, title, message, tag, style, logConfig);
|
|
833
|
+
};
|
|
834
|
+
if (event?.type === "session.idle") {
|
|
835
|
+
if (!notifyOnIdle)
|
|
836
|
+
return;
|
|
837
|
+
const projectName = getProjectName(directory);
|
|
838
|
+
const sessionId = event.properties?.sessionID ?? event.properties?.sessionId;
|
|
839
|
+
const message = await extractIdleMessage(client, sessionId);
|
|
840
|
+
await sendToast(projectName, message, "Stop");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (event?.type === "session.error") {
|
|
844
|
+
if (!notifyOnError)
|
|
845
|
+
return;
|
|
846
|
+
const projectName = getProjectName(directory);
|
|
847
|
+
await sendToast(projectName, "❌ Session error", "Error");
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
function getProjectName(directory) {
|
|
853
|
+
const normalized = directory.replace(/\\/g, "/");
|
|
854
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
855
|
+
return segments[segments.length - 1] || "opencode";
|
|
856
|
+
}
|
|
857
|
+
async function extractIdleMessage(client, sessionId) {
|
|
858
|
+
let message = "✓ Task completed";
|
|
859
|
+
if (!sessionId || !client?.session?.messages)
|
|
860
|
+
return message;
|
|
861
|
+
try {
|
|
862
|
+
const result = await client.session.messages({ path: { id: sessionId } });
|
|
863
|
+
const messages = result?.data;
|
|
864
|
+
if (!Array.isArray(messages))
|
|
865
|
+
return message;
|
|
866
|
+
for (let i = messages.length - 1;i >= 0; i -= 1) {
|
|
867
|
+
const msg = messages[i];
|
|
868
|
+
if (msg?.info?.role !== "assistant" || !Array.isArray(msg?.parts))
|
|
869
|
+
continue;
|
|
870
|
+
for (const part of msg.parts) {
|
|
871
|
+
if (part?.type === "text" && typeof part.text === "string") {
|
|
872
|
+
message = `✓ ${truncateText(cleanMarkdown(part.text), 400)}`;
|
|
873
|
+
return message;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return message;
|
|
878
|
+
} catch {
|
|
879
|
+
return message;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// src/plugins/tool-call-notify.ts
|
|
883
|
+
var DEFAULT_DURATION = 3000;
|
|
884
|
+
var DEFAULT_MAX_VISIBLE = 3;
|
|
885
|
+
var DEFAULT_MAX_ARG_LENGTH = 300;
|
|
886
|
+
var DEFAULT_POSITION = "top-right";
|
|
887
|
+
var DEFAULT_ACCENT_COLOR = "#60A5FA";
|
|
888
|
+
var toolCallNotifyPlugin = async ({ $, client }) => {
|
|
889
|
+
let cachedSender = null;
|
|
890
|
+
let managerMaxVisible = 0;
|
|
891
|
+
let manager = null;
|
|
892
|
+
const getManager = (maxVisible) => {
|
|
893
|
+
if (!manager || managerMaxVisible !== maxVisible) {
|
|
894
|
+
manager = new WpfPositionManager(maxVisible);
|
|
895
|
+
managerMaxVisible = maxVisible;
|
|
896
|
+
}
|
|
897
|
+
return manager;
|
|
898
|
+
};
|
|
899
|
+
return {
|
|
900
|
+
"tool.execute.before": safeHook("tool-call-notify:tool.execute.before", async (input, output) => {
|
|
901
|
+
const config = await loadOcTweaksConfig();
|
|
902
|
+
if (!config || config.notify?.enabled !== true)
|
|
903
|
+
return;
|
|
904
|
+
const toolCallConfig = config.notify?.toolCall;
|
|
905
|
+
if (!toolCallConfig || toolCallConfig.enabled !== true)
|
|
906
|
+
return;
|
|
907
|
+
const excludeTools = toolCallConfig.filter?.exclude ?? [];
|
|
908
|
+
if (Array.isArray(excludeTools) && excludeTools.includes(input.tool))
|
|
909
|
+
return;
|
|
910
|
+
const sender = cachedSender ??= await detectNotifySender($, client, config.logging);
|
|
911
|
+
if (sender.kind !== "wpf")
|
|
912
|
+
return;
|
|
913
|
+
const duration = typeof toolCallConfig.duration === "number" && toolCallConfig.duration > 0 ? toolCallConfig.duration : DEFAULT_DURATION;
|
|
914
|
+
const maxVisible = typeof toolCallConfig.maxVisible === "number" && toolCallConfig.maxVisible > 0 ? toolCallConfig.maxVisible : DEFAULT_MAX_VISIBLE;
|
|
915
|
+
const maxArgLength = typeof toolCallConfig.maxArgLength === "number" && toolCallConfig.maxArgLength > 0 ? toolCallConfig.maxArgLength : DEFAULT_MAX_ARG_LENGTH;
|
|
916
|
+
const position = typeof toolCallConfig.position === "string" && toolCallConfig.position.trim().length > 0 ? toolCallConfig.position.trim() : DEFAULT_POSITION;
|
|
917
|
+
let serializedArgs = "{}";
|
|
918
|
+
try {
|
|
919
|
+
serializedArgs = JSON.stringify(output.args ?? {}, null, 2) ?? "{}";
|
|
920
|
+
} catch {
|
|
921
|
+
serializedArgs = "[unserializable args]";
|
|
922
|
+
}
|
|
923
|
+
const argsText = truncateText(serializedArgs, maxArgLength);
|
|
924
|
+
const positionManager = getManager(maxVisible);
|
|
925
|
+
const enqueueDisplay = () => {
|
|
926
|
+
const slot = positionManager.allocateSlot(duration);
|
|
927
|
+
if (!slot) {
|
|
928
|
+
positionManager.enqueue(enqueueDisplay);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const baseStyle = config.notify?.style ?? {};
|
|
932
|
+
const width = baseStyle.width ?? 420;
|
|
933
|
+
const height = baseStyle.height ?? 105;
|
|
934
|
+
const calculatedPosition = calculatePosition(slot.slotIndex, {
|
|
935
|
+
position,
|
|
936
|
+
width,
|
|
937
|
+
height
|
|
938
|
+
});
|
|
939
|
+
sendWpfToast({
|
|
940
|
+
$,
|
|
941
|
+
sender,
|
|
942
|
+
title: `\uD83D\uDD27 ${input.tool}`,
|
|
943
|
+
message: argsText,
|
|
944
|
+
tag: "ToolCall",
|
|
945
|
+
style: {
|
|
946
|
+
...baseStyle,
|
|
947
|
+
duration
|
|
948
|
+
},
|
|
949
|
+
position: calculatedPosition,
|
|
950
|
+
icon: "\uD83D\uDD27",
|
|
951
|
+
accentColor: DEFAULT_ACCENT_COLOR,
|
|
952
|
+
showDismissHint: false,
|
|
953
|
+
maxMessageLength: maxArgLength,
|
|
954
|
+
preserveMessageFormatting: true
|
|
955
|
+
}).catch(() => {
|
|
956
|
+
slot.release();
|
|
957
|
+
});
|
|
958
|
+
};
|
|
959
|
+
positionManager.enqueue(enqueueDisplay);
|
|
960
|
+
positionManager.processQueue();
|
|
961
|
+
})
|
|
962
|
+
};
|
|
963
|
+
};
|
|
747
964
|
export {
|
|
965
|
+
toolCallNotifyPlugin,
|
|
748
966
|
notifyPlugin,
|
|
749
967
|
leaderboardPlugin,
|
|
750
968
|
compactionPlugin,
|