oc-tweaks 0.4.2 → 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 +295 -85
- 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 ?? "") ?? "";
|
|
@@ -475,74 +477,104 @@ var leaderboardPlugin = async () => {
|
|
|
475
477
|
})
|
|
476
478
|
};
|
|
477
479
|
};
|
|
478
|
-
// src/
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
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
|
+
}
|
|
495
508
|
};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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)
|
|
503
520
|
return;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const normalized = directory.replace(/\\/g, "/");
|
|
516
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
517
|
-
return segments[segments.length - 1] || "opencode";
|
|
518
|
-
}
|
|
519
|
-
async function extractIdleMessage(client, sessionId) {
|
|
520
|
-
let message = "✓ Task completed";
|
|
521
|
-
if (!sessionId || !client?.session?.messages)
|
|
522
|
-
return message;
|
|
523
|
-
try {
|
|
524
|
-
const result = await client.session.messages({ path: { id: sessionId } });
|
|
525
|
-
const messages = result?.data;
|
|
526
|
-
if (!Array.isArray(messages))
|
|
527
|
-
return message;
|
|
528
|
-
for (let i = messages.length - 1;i >= 0; i -= 1) {
|
|
529
|
-
const msg = messages[i];
|
|
530
|
-
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)
|
|
531
532
|
continue;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
message = `✓ ${truncateText(cleanMarkdown(part.text), 400)}`;
|
|
535
|
-
return message;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
533
|
+
clearTimeout(state.timer);
|
|
534
|
+
this.slots.delete(slotIndex);
|
|
538
535
|
}
|
|
539
|
-
return message;
|
|
540
|
-
} catch {
|
|
541
|
-
return message;
|
|
542
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
|
+
};
|
|
543
572
|
}
|
|
573
|
+
|
|
574
|
+
// src/utils/wpf-notify.ts
|
|
544
575
|
async function detectNotifySender($, client, logConfig) {
|
|
545
|
-
|
|
576
|
+
const isWindows = globalThis?.process?.platform === "win32";
|
|
577
|
+
if (isWindows && await commandExists($, "pwsh")) {
|
|
546
578
|
return { kind: "wpf", command: "pwsh" };
|
|
547
579
|
}
|
|
548
580
|
if (await commandExists($, "powershell.exe")) {
|
|
@@ -560,10 +592,7 @@ async function detectNotifySender($, client, logConfig) {
|
|
|
560
592
|
await log(logConfig, "WARN", "[oc-tweaks] notify: no available notifier, set notify.command to override");
|
|
561
593
|
return { kind: "none" };
|
|
562
594
|
}
|
|
563
|
-
async function
|
|
564
|
-
return Bun.which(command) !== null;
|
|
565
|
-
}
|
|
566
|
-
async function notifyWithSender($, sender, title, message, tag, style, logConfig) {
|
|
595
|
+
async function notifyWithSender($, sender, title, message, tag, style, _logConfig, position, renderOptions) {
|
|
567
596
|
try {
|
|
568
597
|
if (sender.kind === "custom") {
|
|
569
598
|
const command = sender.commandTemplate.replace(/\$TITLE/g, title).replace(/\$MESSAGE/g, message);
|
|
@@ -571,7 +600,7 @@ async function notifyWithSender($, sender, title, message, tag, style, logConfig
|
|
|
571
600
|
return;
|
|
572
601
|
}
|
|
573
602
|
if (sender.kind === "wpf") {
|
|
574
|
-
await runWpfNotification($, sender.command, title, message, tag, style);
|
|
603
|
+
await runWpfNotification($, sender.command, title, message, tag, style, position, renderOptions);
|
|
575
604
|
return;
|
|
576
605
|
}
|
|
577
606
|
if (sender.kind === "osascript") {
|
|
@@ -588,11 +617,34 @@ async function notifyWithSender($, sender, title, message, tag, style, logConfig
|
|
|
588
617
|
}
|
|
589
618
|
} catch {}
|
|
590
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
|
+
}
|
|
591
643
|
async function runCustomCommand($, command) {
|
|
592
644
|
const escaped = command.replace(/"/g, "\\\"");
|
|
593
645
|
await $`bun -e ${`const { exec } = require("node:child_process"); exec("${escaped}")`}`;
|
|
594
646
|
}
|
|
595
|
-
async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
647
|
+
async function runWpfNotification($, shellCommand, title, message, tag, style, position, renderOptions) {
|
|
596
648
|
const backgroundColor = style?.backgroundColor ?? "#101018";
|
|
597
649
|
const backgroundOpacity = style?.backgroundOpacity ?? 0.95;
|
|
598
650
|
const textColor = style?.textColor ?? "#AAAAAA";
|
|
@@ -604,16 +656,26 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
604
656
|
const contentFontSize = style?.contentFontSize ?? 11;
|
|
605
657
|
const iconFontSize = style?.iconFontSize ?? 30;
|
|
606
658
|
const duration = style?.duration ?? 1e4;
|
|
607
|
-
const
|
|
659
|
+
const stylePosition = style?.position ?? "center";
|
|
608
660
|
const shadow = style?.shadow !== false;
|
|
609
661
|
const idleColor = style?.idleColor ?? "#4ADE80";
|
|
610
662
|
const errorColor = style?.errorColor ?? "#EF4444";
|
|
611
|
-
const accentColor = tag === "Error" ? errorColor : idleColor;
|
|
612
|
-
const icon = tag === "Error" ? "❌" : "✅";
|
|
613
|
-
const
|
|
614
|
-
const
|
|
615
|
-
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));
|
|
616
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"/>` : "";
|
|
617
679
|
const xaml = [
|
|
618
680
|
`<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"`,
|
|
619
681
|
` xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"`,
|
|
@@ -633,7 +695,7 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
633
695
|
` <StackPanel VerticalAlignment="Center" MaxWidth="320">`,
|
|
634
696
|
` <TextBlock Name="TitleText" FontSize="${titleFontSize}" FontWeight="SemiBold"/>`,
|
|
635
697
|
` <TextBlock Name="ContentText" Foreground="${textColor}" FontSize="${contentFontSize}" Margin="0,4,0,0" TextWrapping="Wrap"/>`,
|
|
636
|
-
|
|
698
|
+
dismissHintXaml,
|
|
637
699
|
` </StackPanel>`,
|
|
638
700
|
` </StackPanel>`,
|
|
639
701
|
` </Grid>`,
|
|
@@ -641,6 +703,11 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
641
703
|
`</Window>`
|
|
642
704
|
].join(`
|
|
643
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
|
+
] : [];
|
|
644
711
|
const psScript = [
|
|
645
712
|
"Add-Type -AssemblyName PresentationFramework",
|
|
646
713
|
"Add-Type -AssemblyName PresentationCore",
|
|
@@ -671,8 +738,8 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
671
738
|
"}",
|
|
672
739
|
"'@ -ErrorAction SilentlyContinue",
|
|
673
740
|
"",
|
|
674
|
-
`$title = '${
|
|
675
|
-
`$text = '${
|
|
741
|
+
`$title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${titleB64}'))`,
|
|
742
|
+
`$text = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${textB64}'))`,
|
|
676
743
|
`$accentColor = '${accentColor}'`,
|
|
677
744
|
`$icon = '${icon}'`,
|
|
678
745
|
`$duration = ${duration}`,
|
|
@@ -683,6 +750,7 @@ async function runWpfNotification($, shellCommand, title, message, tag, style) {
|
|
|
683
750
|
"",
|
|
684
751
|
"$reader = New-Object System.Xml.XmlNodeReader $xaml",
|
|
685
752
|
"$window = [Windows.Markup.XamlReader]::Load($reader)",
|
|
753
|
+
...manualPositionLines,
|
|
686
754
|
"",
|
|
687
755
|
"$colorBar = $window.FindName('ColorBar')",
|
|
688
756
|
"$iconText = $window.FindName('IconText')",
|
|
@@ -741,18 +809,160 @@ async function showToastWithFallback(showToast, title, message) {
|
|
|
741
809
|
} catch {}
|
|
742
810
|
await Promise.resolve(showToast(title, message));
|
|
743
811
|
}
|
|
744
|
-
function truncateText(text, maxChars) {
|
|
745
|
-
if (text.length <= maxChars)
|
|
746
|
-
return text;
|
|
747
|
-
return `${text.slice(0, maxChars)}...`;
|
|
748
|
-
}
|
|
749
|
-
function cleanMarkdown(text) {
|
|
750
|
-
return text.replace(/[`*#]/g, "").replace(/\n+/g, " ").replace(/\s+/g, " ").trim();
|
|
751
|
-
}
|
|
752
812
|
function escapeAppleScript(text) {
|
|
753
813
|
return text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
754
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
|
+
};
|
|
755
964
|
export {
|
|
965
|
+
toolCallNotifyPlugin,
|
|
756
966
|
notifyPlugin,
|
|
757
967
|
leaderboardPlugin,
|
|
758
968
|
compactionPlugin,
|