@poolzin/pool-bot 2026.3.7 → 2026.3.10
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/CHANGELOG.md +40 -0
- package/README.md +147 -69
- package/dist/.buildstamp +1 -1
- package/dist/agents/error-classifier.js +251 -0
- package/dist/agents/skills/security.js +211 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cron-cli/register.cron-dashboard.js +339 -0
- package/dist/cli/cron-cli/register.js +2 -0
- package/dist/cli/errors.js +187 -0
- package/dist/cli/lazy-commands.example.js +113 -0
- package/dist/cli/lazy-commands.js +329 -0
- package/dist/cli/program/command-registry.js +26 -0
- package/dist/cli/program/register.maintenance.js +21 -0
- package/dist/cli/program/register.skills.js +4 -0
- package/dist/cli/program/register.subclis.js +9 -0
- package/dist/cli/swarm-cli/register.js +8 -0
- package/dist/cli/swarm-cli/register.swarm-status.js +488 -0
- package/dist/cli/telemetry-cli/register.js +10 -0
- package/dist/cli/telemetry-cli/register.telemetry-alerts.js +176 -0
- package/dist/cli/telemetry-cli/register.telemetry-metrics.js +323 -0
- package/dist/cli/telemetry-cli/register.telemetry-status.js +179 -0
- package/dist/commands/doctor-checks.js +498 -0
- package/dist/config/config.js +1 -0
- package/dist/config/secrets-integration.js +88 -0
- package/dist/context-engine/index.js +33 -0
- package/dist/context-engine/legacy.js +179 -0
- package/dist/context-engine/registry.js +86 -0
- package/dist/context-engine/summarizing.js +290 -0
- package/dist/context-engine/types.js +7 -0
- package/dist/cron/service/timer.js +18 -0
- package/dist/gateway/protocol/index.js +5 -2
- package/dist/gateway/protocol/schema/error-codes.js +1 -0
- package/dist/gateway/protocol/schema/swarm.js +80 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-close.js +4 -0
- package/dist/gateway/server-constants.js +1 -0
- package/dist/gateway/server-cron.js +29 -0
- package/dist/gateway/server-maintenance.js +35 -2
- package/dist/gateway/server-methods/swarm.js +58 -0
- package/dist/gateway/server-methods/telemetry.js +71 -0
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +9 -2
- package/dist/gateway/server.impl.js +33 -16
- package/dist/infra/abort-pattern.js +106 -0
- package/dist/infra/retry.js +96 -0
- package/dist/secrets/index.js +28 -0
- package/dist/secrets/resolver.js +185 -0
- package/dist/secrets/runtime.js +142 -0
- package/dist/secrets/types.js +11 -0
- package/dist/security/dangerous-tools.js +80 -0
- package/dist/security/types.js +12 -0
- package/dist/skills/commands.js +333 -0
- package/dist/skills/index.js +164 -0
- package/dist/skills/loader.js +282 -0
- package/dist/skills/parser.js +446 -0
- package/dist/skills/registry.js +394 -0
- package/dist/skills/security.js +312 -0
- package/dist/skills/types.js +21 -0
- package/dist/swarm/service.js +247 -0
- package/dist/telemetry/alert-engine.js +258 -0
- package/dist/telemetry/cron-instrumentation.js +49 -0
- package/dist/telemetry/gateway-instrumentation.js +80 -0
- package/dist/telemetry/instrumentation.js +66 -0
- package/dist/telemetry/service.js +345 -0
- package/dist/test-utils/index.js +219 -0
- package/dist/tui/components/assistant-message.js +6 -2
- package/dist/tui/components/hyperlink-markdown.js +32 -0
- package/dist/tui/components/searchable-select-list.js +12 -1
- package/dist/tui/components/user-message.js +6 -2
- package/dist/tui/index.js +611 -0
- package/dist/tui/theme/theme-detection.js +226 -0
- package/dist/tui/tui-command-handlers.js +20 -0
- package/dist/tui/tui-formatters.js +4 -3
- package/dist/tui/utils/ctrl-c-handler.js +67 -0
- package/dist/tui/utils/osc8-hyperlinks.js +208 -0
- package/dist/tui/utils/safe-stop.js +180 -0
- package/dist/tui/utils/session-key-utils.js +81 -0
- package/dist/tui/utils/text-sanitization.js +284 -0
- package/dist/utils/lru-cache.js +116 -0
- package/dist/utils/performance.js +199 -0
- package/dist/utils/retry.js +240 -0
- package/docs/INTEGRATION_PLAN.md +475 -0
- package/docs/INTEGRATION_SUMMARY.md +215 -0
- package/docs/MELHORIAS_IMPLEMENTADAS.md +228 -0
- package/docs/MELHORIAS_PROFISSIONAIS.md +282 -0
- package/docs/PLANO_ACAO_TUI.md +357 -0
- package/docs/PROGRESSO_TUI.md +66 -0
- package/docs/RELATORIO_FINAL.md +217 -0
- package/docs/diagnostico-shell-completion.md +265 -0
- package/docs/features/advanced-memory.md +585 -0
- package/docs/features/discord-components-v2.md +277 -0
- package/docs/features/swarm.md +100 -0
- package/docs/features/telemetry.md +284 -0
- package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
- package/docs/integrations/INTEGRATION_PLAN.md +744 -0
- package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
- package/docs/integrations/XYOPS_PLAN.md +978 -0
- package/docs/models/provider-infrastructure.md +400 -0
- package/docs/security/exec-approvals.md +294 -0
- package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
- package/docs/skills/SKILL.md +524 -0
- package/docs/skills.md +405 -0
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/hexstrike-bridge/README.md +119 -0
- package/extensions/hexstrike-bridge/index.test.ts +247 -0
- package/extensions/hexstrike-bridge/index.ts +487 -0
- package/extensions/hexstrike-bridge/package.json +17 -0
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mcp-server/index.ts +14 -0
- package/extensions/mcp-server/package.json +11 -0
- package/extensions/mcp-server/src/service.ts +540 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +8 -1
- package/skills/example-skill/SKILL.md +195 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme detection and light/dark mode utilities
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Automatic light/dark detection via environment variables
|
|
6
|
+
* - WCAG contrast ratio calculation
|
|
7
|
+
* - COLORFGBG parsing
|
|
8
|
+
* - Manual theme override via POOLBOT_THEME
|
|
9
|
+
*/
|
|
10
|
+
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
11
|
+
const themeLog = createSubsystemLogger("theme");
|
|
12
|
+
/**
|
|
13
|
+
* Detect theme mode from environment
|
|
14
|
+
*
|
|
15
|
+
* Detection order:
|
|
16
|
+
* 1. POOLBOT_THEME environment variable
|
|
17
|
+
* 2. COLORFGBG environment variable (terminal background color)
|
|
18
|
+
* 3. Default to "dark"
|
|
19
|
+
*/
|
|
20
|
+
export function detectThemeMode() {
|
|
21
|
+
// Check for explicit theme override
|
|
22
|
+
const envTheme = process.env.POOLBOT_THEME?.toLowerCase();
|
|
23
|
+
if (envTheme === "light" || envTheme === "dark") {
|
|
24
|
+
themeLog.debug(`Theme set via POOLBOT_THEME: ${envTheme}`);
|
|
25
|
+
return envTheme;
|
|
26
|
+
}
|
|
27
|
+
// Try to detect from COLORFGBG
|
|
28
|
+
const colorFgbg = process.env.COLORFGBG;
|
|
29
|
+
if (colorFgbg) {
|
|
30
|
+
const detected = parseColorFgbg(colorFgbg);
|
|
31
|
+
if (detected) {
|
|
32
|
+
themeLog.debug(`Theme detected from COLORFGBG: ${detected}`);
|
|
33
|
+
return detected;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Default to dark mode
|
|
37
|
+
themeLog.debug("Theme detection: defaulting to dark");
|
|
38
|
+
return "dark";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse COLORFGBG environment variable
|
|
42
|
+
*
|
|
43
|
+
* Format: "fg;bg" or "fg:bg" where colors are 0-15 or RGB hex
|
|
44
|
+
* Examples:
|
|
45
|
+
* "0;15" - black on white (light mode)
|
|
46
|
+
* "15;0" - white on black (dark mode)
|
|
47
|
+
* "rgb:0000/0000/0000;rgb:ffff/ffff/ffff" - RGB values
|
|
48
|
+
*/
|
|
49
|
+
export function parseColorFgbg(colorFgbg) {
|
|
50
|
+
try {
|
|
51
|
+
// Split by semicolon or colon
|
|
52
|
+
const parts = colorFgbg.split(/[;:]/);
|
|
53
|
+
if (parts.length < 2)
|
|
54
|
+
return null;
|
|
55
|
+
const bgPart = parts[1] ?? "";
|
|
56
|
+
// Check for RGB format: rgb:R/G/B
|
|
57
|
+
if (bgPart.startsWith("rgb:")) {
|
|
58
|
+
const rgb = parseRgbHex(bgPart);
|
|
59
|
+
if (rgb) {
|
|
60
|
+
return isLightColor(rgb) ? "light" : "dark";
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Check for numeric color (0-15)
|
|
65
|
+
const colorNum = parseInt(bgPart, 10);
|
|
66
|
+
if (!isNaN(colorNum)) {
|
|
67
|
+
// Standard 16-color palette:
|
|
68
|
+
// 0-7: dark colors, 8-15: light colors
|
|
69
|
+
if (colorNum >= 0 && colorNum <= 7) {
|
|
70
|
+
return "dark";
|
|
71
|
+
}
|
|
72
|
+
if (colorNum >= 8 && colorNum <= 15) {
|
|
73
|
+
return "light";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse RGB hex format: rgb:RRRR/GGGG/BBBB or rgb:RR/GG/BB
|
|
84
|
+
*/
|
|
85
|
+
export function parseRgbHex(rgbStr) {
|
|
86
|
+
try {
|
|
87
|
+
// Remove "rgb:" prefix
|
|
88
|
+
const parts = rgbStr.replace("rgb:", "").split("/");
|
|
89
|
+
if (parts.length !== 3)
|
|
90
|
+
return null;
|
|
91
|
+
// Parse each component
|
|
92
|
+
const parseComponent = (hex) => {
|
|
93
|
+
const len = hex.length;
|
|
94
|
+
if (len === 2) {
|
|
95
|
+
// 8-bit color
|
|
96
|
+
return parseInt(hex, 16);
|
|
97
|
+
}
|
|
98
|
+
if (len === 4) {
|
|
99
|
+
// 16-bit color, take high byte
|
|
100
|
+
return parseInt(hex.slice(0, 2), 16);
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
r: parseComponent(parts[0] ?? "0"),
|
|
106
|
+
g: parseComponent(parts[1] ?? "0"),
|
|
107
|
+
b: parseComponent(parts[2] ?? "0"),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Calculate relative luminance of an RGB color (WCAG formula)
|
|
116
|
+
* https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
|
117
|
+
*/
|
|
118
|
+
export function relativeLuminance(rgb) {
|
|
119
|
+
// Normalize to 0-1
|
|
120
|
+
const rsRGB = rgb.r / 255;
|
|
121
|
+
const gsRGB = rgb.g / 255;
|
|
122
|
+
const bsRGB = rgb.b / 255;
|
|
123
|
+
// Apply gamma correction
|
|
124
|
+
const r = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
|
|
125
|
+
const g = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
|
|
126
|
+
const b = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
|
|
127
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Calculate contrast ratio between two colors (WCAG formula)
|
|
131
|
+
* https://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
|
132
|
+
*/
|
|
133
|
+
export function contrastRatio(color1, color2) {
|
|
134
|
+
const lum1 = relativeLuminance(color1);
|
|
135
|
+
const lum2 = relativeLuminance(color2);
|
|
136
|
+
const lighter = Math.max(lum1, lum2);
|
|
137
|
+
const darker = Math.min(lum1, lum2);
|
|
138
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Determine if a color is "light" based on its luminance
|
|
142
|
+
* Uses threshold of 0.5 (midpoint between black and white)
|
|
143
|
+
*/
|
|
144
|
+
export function isLightColor(rgb) {
|
|
145
|
+
const luminance = relativeLuminance(rgb);
|
|
146
|
+
return luminance > 0.5;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if contrast ratio meets WCAG AA standards
|
|
150
|
+
* AA requires 4.5:1 for normal text, 3:1 for large text
|
|
151
|
+
*/
|
|
152
|
+
export function meetsContrastStandard(fg, bg, level = "AA", largeText = false) {
|
|
153
|
+
const ratio = contrastRatio(fg, bg);
|
|
154
|
+
if (level === "AAA") {
|
|
155
|
+
return largeText ? ratio >= 4.5 : ratio >= 7;
|
|
156
|
+
}
|
|
157
|
+
// AA level
|
|
158
|
+
return largeText ? ratio >= 3 : ratio >= 4.5;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Light theme color palette
|
|
162
|
+
*/
|
|
163
|
+
export const lightThemePalette = {
|
|
164
|
+
// Backgrounds
|
|
165
|
+
bg: { r: 250, g: 250, b: 250 }, // Very light gray
|
|
166
|
+
userBg: { r: 230, g: 245, b: 255 }, // Light blue
|
|
167
|
+
dimBg: { r: 240, g: 240, b: 240 }, // Light gray
|
|
168
|
+
// Foregrounds
|
|
169
|
+
fg: { r: 30, g: 30, b: 30 }, // Near black
|
|
170
|
+
dim: { r: 100, g: 100, b: 100 }, // Gray
|
|
171
|
+
muted: { r: 120, g: 120, b: 120 }, // Medium gray
|
|
172
|
+
// Accents
|
|
173
|
+
accent: { r: 0, g: 100, b: 200 }, // Blue
|
|
174
|
+
accentText: { r: 0, g: 80, b: 160 }, // Darker blue
|
|
175
|
+
userText: { r: 0, g: 60, b: 120 }, // Dark blue
|
|
176
|
+
// Semantic
|
|
177
|
+
error: { r: 200, g: 0, b: 0 }, // Red
|
|
178
|
+
success: { r: 0, g: 150, b: 0 }, // Green
|
|
179
|
+
warning: { r: 200, g: 150, b: 0 }, // Orange
|
|
180
|
+
// Assistant text (uses terminal default)
|
|
181
|
+
assistantText: { r: 30, g: 30, b: 30 },
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Dark theme color palette (current default)
|
|
185
|
+
*/
|
|
186
|
+
export const darkThemePalette = {
|
|
187
|
+
// Backgrounds
|
|
188
|
+
bg: { r: 20, g: 20, b: 20 }, // Very dark gray
|
|
189
|
+
userBg: { r: 40, g: 60, b: 80 }, // Dark blue
|
|
190
|
+
dimBg: { r: 40, g: 40, b: 40 }, // Dark gray
|
|
191
|
+
// Foregrounds
|
|
192
|
+
fg: { r: 230, g: 230, b: 230 }, // Near white
|
|
193
|
+
dim: { r: 150, g: 150, b: 150 }, // Gray
|
|
194
|
+
muted: { r: 120, g: 120, b: 120 }, // Medium gray
|
|
195
|
+
// Accents
|
|
196
|
+
accent: { r: 100, g: 180, b: 255 }, // Light blue
|
|
197
|
+
accentText: { r: 100, g: 200, b: 255 }, // Cyan
|
|
198
|
+
userText: { r: 150, g: 220, b: 255 }, // Light cyan
|
|
199
|
+
// Semantic
|
|
200
|
+
error: { r: 255, g: 100, b: 100 }, // Light red
|
|
201
|
+
success: { r: 100, g: 255, b: 100 }, // Light green
|
|
202
|
+
warning: { r: 255, g: 200, b: 100 }, // Light orange
|
|
203
|
+
// Assistant text (uses terminal default)
|
|
204
|
+
assistantText: { r: 230, g: 230, b: 230 },
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Get the appropriate color palette for the current theme
|
|
208
|
+
*/
|
|
209
|
+
export function getThemePalette(mode = detectThemeMode()) {
|
|
210
|
+
return mode === "light" ? lightThemePalette : darkThemePalette;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Convert RGB to ANSI color code (approximate)
|
|
214
|
+
* This is a simplified version - for exact colors, terminals support true color
|
|
215
|
+
*/
|
|
216
|
+
export function rgbToAnsi(rgb, foreground = true) {
|
|
217
|
+
// Use 24-bit true color if terminal supports it
|
|
218
|
+
const prefix = foreground ? "38" : "48";
|
|
219
|
+
return `\x1b[${prefix};2;${rgb.r};${rgb.g};${rgb.b}m`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Reset ANSI colors
|
|
223
|
+
*/
|
|
224
|
+
export function resetAnsi() {
|
|
225
|
+
return "\x1b[0m";
|
|
226
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { formatThinkingLevels, normalizeUsageDisplay, resolveResponseUsageMode, } from "../auto-reply/thinking.js";
|
|
2
3
|
import { formatRelativeTimestamp } from "../infra/format-time/format-relative.js";
|
|
3
4
|
import { normalizeAgentId } from "../routing/session-key.js";
|
|
@@ -364,6 +365,25 @@ export function createCommandHandlers(context) {
|
|
|
364
365
|
}
|
|
365
366
|
break;
|
|
366
367
|
case "new":
|
|
368
|
+
try {
|
|
369
|
+
// Create a new isolated session with unique key
|
|
370
|
+
const newSessionKey = `tui-${randomUUID()}`;
|
|
371
|
+
// Clear token counts
|
|
372
|
+
state.sessionInfo.inputTokens = null;
|
|
373
|
+
state.sessionInfo.outputTokens = null;
|
|
374
|
+
state.sessionInfo.totalTokens = null;
|
|
375
|
+
state.sessionInfo.updatedAt = null; // Reset freshness timestamp
|
|
376
|
+
tui.requestRender();
|
|
377
|
+
// Switch to new session
|
|
378
|
+
await setSession(newSessionKey);
|
|
379
|
+
chatLog.addSystem(`created new session: ${newSessionKey}`);
|
|
380
|
+
chatLog.clear(); // Clear chat log for fresh session
|
|
381
|
+
await loadHistory();
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
chatLog.addSystem(`failed to create new session: ${String(err)}`);
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
367
387
|
case "reset":
|
|
368
388
|
try {
|
|
369
389
|
// Clear token counts immediately to avoid stale display (#1523)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatTokenCount } from "../utils/usage-format.js";
|
|
2
2
|
import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js";
|
|
3
|
+
import { sanitizeRenderableText } from "./utils/text-sanitization.js";
|
|
3
4
|
export function resolveFinalAssistantText(params) {
|
|
4
5
|
const finalText = params.finalText ?? "";
|
|
5
6
|
if (finalText.trim())
|
|
@@ -61,7 +62,7 @@ export function extractContentFromMessage(message) {
|
|
|
61
62
|
const stopReason = typeof record.stopReason === "string" ? record.stopReason : "";
|
|
62
63
|
if (stopReason === "error") {
|
|
63
64
|
const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "";
|
|
64
|
-
return formatRawAssistantErrorForUi(errorMessage);
|
|
65
|
+
return sanitizeRenderableText(formatRawAssistantErrorForUi(errorMessage));
|
|
65
66
|
}
|
|
66
67
|
return "";
|
|
67
68
|
}
|
|
@@ -79,7 +80,7 @@ export function extractContentFromMessage(message) {
|
|
|
79
80
|
const stopReason = typeof record.stopReason === "string" ? record.stopReason : "";
|
|
80
81
|
if (stopReason === "error") {
|
|
81
82
|
const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "";
|
|
82
|
-
return formatRawAssistantErrorForUi(errorMessage);
|
|
83
|
+
return sanitizeRenderableText(formatRawAssistantErrorForUi(errorMessage));
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
return parts.join("\n").trim();
|
|
@@ -121,7 +122,7 @@ export function extractTextFromMessage(message, opts) {
|
|
|
121
122
|
if (stopReason !== "error")
|
|
122
123
|
return "";
|
|
123
124
|
const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "";
|
|
124
|
-
return formatRawAssistantErrorForUi(errorMessage);
|
|
125
|
+
return sanitizeRenderableText(formatRawAssistantErrorForUi(errorMessage));
|
|
125
126
|
}
|
|
126
127
|
export function isCommandMessage(message) {
|
|
127
128
|
if (!message || typeof message !== "object")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ctrl+C handling utilities with state machine
|
|
3
|
+
*
|
|
4
|
+
* States:
|
|
5
|
+
* - "clear": Clear input if text present
|
|
6
|
+
* - "warn": Show warning on first Ctrl+C with empty input
|
|
7
|
+
* - "exit": Exit on second Ctrl+C within timeout
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Create a new Ctrl+C state machine
|
|
11
|
+
*/
|
|
12
|
+
export function createCtrlCStateMachine(timeoutMs = 1000) {
|
|
13
|
+
return {
|
|
14
|
+
state: "clear",
|
|
15
|
+
lastCtrlCAt: 0,
|
|
16
|
+
timeoutMs,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Process Ctrl+C event and determine action
|
|
21
|
+
*
|
|
22
|
+
* @param machine - State machine instance
|
|
23
|
+
* @param hasInput - Whether there's text in the input
|
|
24
|
+
* @returns The action to take: "clear", "warn", or "exit"
|
|
25
|
+
*/
|
|
26
|
+
export function processCtrlC(machine, hasInput) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
// If has input, always clear
|
|
29
|
+
if (hasInput) {
|
|
30
|
+
machine.state = "clear";
|
|
31
|
+
machine.lastCtrlCAt = now;
|
|
32
|
+
return "clear";
|
|
33
|
+
}
|
|
34
|
+
// Check if within double-press timeout
|
|
35
|
+
const withinTimeout = now - machine.lastCtrlCAt < machine.timeoutMs;
|
|
36
|
+
if (machine.state === "warn" && withinTimeout) {
|
|
37
|
+
// Second press within timeout -> exit
|
|
38
|
+
machine.state = "exit";
|
|
39
|
+
return "exit";
|
|
40
|
+
}
|
|
41
|
+
// First press or timeout expired -> warn
|
|
42
|
+
machine.state = "warn";
|
|
43
|
+
machine.lastCtrlCAt = now;
|
|
44
|
+
return "warn";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Format status message based on Ctrl+C state
|
|
48
|
+
*/
|
|
49
|
+
export function getCtrlCStatusMessage(state, timeoutMs) {
|
|
50
|
+
switch (state) {
|
|
51
|
+
case "clear":
|
|
52
|
+
return "cleared input";
|
|
53
|
+
case "warn":
|
|
54
|
+
return `press ctrl+c again within ${Math.ceil(timeoutMs / 1000)}s to exit`;
|
|
55
|
+
case "exit":
|
|
56
|
+
return "exiting...";
|
|
57
|
+
default:
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reset state machine
|
|
63
|
+
*/
|
|
64
|
+
export function resetCtrlCState(machine) {
|
|
65
|
+
machine.state = "clear";
|
|
66
|
+
machine.lastCtrlCAt = 0;
|
|
67
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSC 8 Hyperlink utilities for clickable terminal URLs
|
|
3
|
+
*
|
|
4
|
+
* OSC 8 is a terminal escape sequence that makes URLs clickable.
|
|
5
|
+
* Supported terminals: iTerm2, Windows Terminal, GNOME Terminal,
|
|
6
|
+
* VS Code terminal, Hyper, and most modern terminals.
|
|
7
|
+
*
|
|
8
|
+
* Format: \e]8;;URL\e\\TEXT\e]8;;\e\\
|
|
9
|
+
* ^^^^^ start ^^^^^^ end
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Automatic URL detection in text
|
|
13
|
+
* - OSC 8 hyperlink generation
|
|
14
|
+
* - Terminal capability detection
|
|
15
|
+
* - Graceful fallback for unsupported terminals
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* OSC 8 escape sequences
|
|
19
|
+
*/
|
|
20
|
+
const OSC8_START = "\x1b]8;;";
|
|
21
|
+
const OSC8_END = "\x1b\\";
|
|
22
|
+
const OSC8_ST = "\x07"; // Alternative terminator (Bell)
|
|
23
|
+
/**
|
|
24
|
+
* URL pattern for detection
|
|
25
|
+
* Matches http://, https://, file://, and other common schemes
|
|
26
|
+
*/
|
|
27
|
+
const URL_REGEX = /(https?:\/\/|file:\/\/|ftp:\/\/|mailto:|ssh:|sftp:)[^\s<>"{}|\\^`[\]]+/gi;
|
|
28
|
+
/**
|
|
29
|
+
* Check if terminal supports OSC 8 hyperlinks
|
|
30
|
+
*
|
|
31
|
+
* Detection methods:
|
|
32
|
+
* 1. Check for known supporting terminals via TERM or TERM_PROGRAM
|
|
33
|
+
* 2. Check for explicit HYPERLINK_SUPPORT env var
|
|
34
|
+
* 3. Check for VTE_VERSION (VTE-based terminals)
|
|
35
|
+
*/
|
|
36
|
+
export function isOsc8Supported() {
|
|
37
|
+
const env = process.env;
|
|
38
|
+
// Explicit opt-in/opt-out
|
|
39
|
+
if (env.HYPERLINK_SUPPORT === "1" || env.HYPERLINK_SUPPORT === "true") {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (env.HYPERLINK_SUPPORT === "0" || env.HYPERLINK_SUPPORT === "false") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const termProgram = (env.TERM_PROGRAM ?? "").toLowerCase();
|
|
46
|
+
const term = (env.TERM ?? "").toLowerCase();
|
|
47
|
+
// Known supporting terminals
|
|
48
|
+
const supportingPrograms = [
|
|
49
|
+
"iterm",
|
|
50
|
+
"iterm2",
|
|
51
|
+
"vscode",
|
|
52
|
+
"code",
|
|
53
|
+
"hyper",
|
|
54
|
+
"alacritty",
|
|
55
|
+
"wezterm",
|
|
56
|
+
"tabby",
|
|
57
|
+
"warp",
|
|
58
|
+
"rio",
|
|
59
|
+
"rio-terminal",
|
|
60
|
+
];
|
|
61
|
+
for (const program of supportingPrograms) {
|
|
62
|
+
if (termProgram.includes(program)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// VTE-based terminals (GNOME Terminal, Tilix, etc.)
|
|
67
|
+
if (env.VTE_VERSION) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
// Windows Terminal (new)
|
|
71
|
+
if (env.WT_SESSION || env.WT_PROFILE_ID) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// Konsole (KDE terminal)
|
|
75
|
+
if (env.KONSOLE_VERSION) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
// Screen/Tmux might support it if the outer terminal does
|
|
79
|
+
if (term.includes("screen") || term.includes("tmux")) {
|
|
80
|
+
// Check if we're in a supported outer terminal
|
|
81
|
+
if (env.TMUX || env.STY) {
|
|
82
|
+
// Conservative: assume supported if in tmux/screen
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Default: assume modern terminals support it
|
|
87
|
+
// This can be overridden with HYPERLINK_SUPPORT env var
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create an OSC 8 hyperlink
|
|
92
|
+
*
|
|
93
|
+
* @param url - The URL to link to
|
|
94
|
+
* @param text - The visible text
|
|
95
|
+
* @param params - Optional parameters (id, etc.)
|
|
96
|
+
* @returns Formatted hyperlink string
|
|
97
|
+
*/
|
|
98
|
+
export function createHyperlink(url, text, params) {
|
|
99
|
+
if (!isOsc8Supported()) {
|
|
100
|
+
return text;
|
|
101
|
+
}
|
|
102
|
+
// Validate URL
|
|
103
|
+
try {
|
|
104
|
+
new URL(url);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Invalid URL, return plain text
|
|
108
|
+
return text;
|
|
109
|
+
}
|
|
110
|
+
const terminator = params?.useSt ? OSC8_ST : OSC8_END;
|
|
111
|
+
const idPart = params?.id ? `id=${params.id}:` : "";
|
|
112
|
+
return `${OSC8_START}${idPart}${url}${terminator}${text}${OSC8_START}${terminator}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Convert plain URLs in text to OSC 8 hyperlinks
|
|
116
|
+
*
|
|
117
|
+
* @param text - Input text containing URLs
|
|
118
|
+
* @returns Text with URLs converted to hyperlinks
|
|
119
|
+
*/
|
|
120
|
+
export function hyperlinkUrls(text) {
|
|
121
|
+
if (!isOsc8Supported()) {
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
return text.replace(URL_REGEX, (url) => {
|
|
125
|
+
return createHyperlink(url, url);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Strip OSC 8 sequences from text
|
|
130
|
+
* Useful for getting plain text or for length calculations
|
|
131
|
+
*
|
|
132
|
+
* @param text - Text that may contain OSC 8 sequences
|
|
133
|
+
* @returns Text with OSC 8 sequences removed
|
|
134
|
+
*/
|
|
135
|
+
export function stripOsc8(text) {
|
|
136
|
+
// Match OSC 8 start sequences: ESC]8;;params:urlESC\ or ESC]8;;params:urlBEL
|
|
137
|
+
// Match OSC 8 end sequences: ESC]8;;ESC\ or ESC]8;;BEL
|
|
138
|
+
// eslint-disable-next-line no-control-regex
|
|
139
|
+
const osc8Regex = /\x1b\]8;;[^\x07\x1b]*(?:\x1b\\|\x07)/g;
|
|
140
|
+
return text.replace(osc8Regex, "");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the visible length of text (excluding OSC 8 sequences)
|
|
144
|
+
*
|
|
145
|
+
* @param text - Text that may contain OSC 8 sequences
|
|
146
|
+
* @returns Visible character count
|
|
147
|
+
*/
|
|
148
|
+
export function getVisibleLength(text) {
|
|
149
|
+
return stripOsc8(text).length;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Truncate text with OSC 8 support
|
|
153
|
+
* Preserves hyperlinks when truncating
|
|
154
|
+
*
|
|
155
|
+
* @param text - Text to truncate
|
|
156
|
+
* @param maxLength - Maximum visible length
|
|
157
|
+
* @returns Truncated text
|
|
158
|
+
*/
|
|
159
|
+
export function truncateWithHyperlinks(text, maxLength) {
|
|
160
|
+
if (getVisibleLength(text) <= maxLength) {
|
|
161
|
+
return text;
|
|
162
|
+
}
|
|
163
|
+
// Simple approach: strip OSC 8, truncate, return plain text
|
|
164
|
+
// A more sophisticated approach would preserve partial hyperlinks
|
|
165
|
+
const plain = stripOsc8(text);
|
|
166
|
+
return plain.slice(0, maxLength - 3) + "...";
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Detect URLs in text and return their positions
|
|
170
|
+
*
|
|
171
|
+
* @param text - Input text
|
|
172
|
+
* @returns Array of URL matches with positions
|
|
173
|
+
*/
|
|
174
|
+
export function detectUrls(text) {
|
|
175
|
+
const matches = [];
|
|
176
|
+
let match;
|
|
177
|
+
// Reset regex lastIndex
|
|
178
|
+
URL_REGEX.lastIndex = 0;
|
|
179
|
+
while ((match = URL_REGEX.exec(text)) !== null) {
|
|
180
|
+
matches.push({
|
|
181
|
+
url: match[0],
|
|
182
|
+
start: match.index,
|
|
183
|
+
end: match.index + match[0].length,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return matches;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create a hyperlink with display text different from URL
|
|
190
|
+
* Example: createNamedHyperlink("https://example.com", "Click here")
|
|
191
|
+
*
|
|
192
|
+
* @param url - The target URL
|
|
193
|
+
* @param displayText - The visible text
|
|
194
|
+
* @returns Hyperlink string
|
|
195
|
+
*/
|
|
196
|
+
export function createNamedHyperlink(url, displayText) {
|
|
197
|
+
return createHyperlink(url, displayText);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Wrap a URL in OSC 8 sequence
|
|
201
|
+
* Shorthand for createHyperlink(url, url)
|
|
202
|
+
*
|
|
203
|
+
* @param url - URL to wrap
|
|
204
|
+
* @returns Hyperlink string
|
|
205
|
+
*/
|
|
206
|
+
export function wrapUrl(url) {
|
|
207
|
+
return createHyperlink(url, url);
|
|
208
|
+
}
|