@agentdeck/shared 0.2.0 → 0.2.3
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/dist/adapter.d.ts +3 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +24 -0
- package/dist/adapter.js.map +1 -1
- package/dist/command-builders.d.ts +7 -2
- package/dist/command-builders.d.ts.map +1 -1
- package/dist/command-builders.js +17 -2
- package/dist/command-builders.js.map +1 -1
- package/dist/connection-status.d.ts +42 -0
- package/dist/connection-status.d.ts.map +1 -0
- package/dist/connection-status.js +52 -0
- package/dist/connection-status.js.map +1 -0
- package/dist/d200h-layout.d.ts +131 -0
- package/dist/d200h-layout.d.ts.map +1 -0
- package/dist/d200h-layout.js +627 -0
- package/dist/d200h-layout.js.map +1 -0
- package/dist/design-tokens.d.ts +259 -0
- package/dist/design-tokens.d.ts.map +1 -0
- package/dist/design-tokens.js +156 -0
- package/dist/design-tokens.js.map +1 -0
- package/dist/eval-schema.d.ts +26 -2
- package/dist/eval-schema.d.ts.map +1 -1
- package/dist/eval-schema.js.map +1 -1
- package/dist/format-utils.d.ts +25 -0
- package/dist/format-utils.d.ts.map +1 -1
- package/dist/format-utils.js +45 -0
- package/dist/format-utils.js.map +1 -1
- package/dist/gateway-protocol.d.ts +1 -1
- package/dist/gateway-protocol.js +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/llm-settings.d.ts +3 -3
- package/dist/llm-settings.d.ts.map +1 -1
- package/dist/llm-settings.js +3 -3
- package/dist/llm-settings.js.map +1 -1
- package/dist/net-utils.d.ts +2 -1
- package/dist/net-utils.d.ts.map +1 -1
- package/dist/net-utils.js +100 -3
- package/dist/net-utils.js.map +1 -1
- package/dist/pricing.d.ts +45 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +93 -0
- package/dist/pricing.js.map +1 -0
- package/dist/protocol.d.ts +175 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +11 -1
- package/dist/protocol.js.map +1 -1
- package/dist/sample.d.ts +152 -0
- package/dist/sample.d.ts.map +1 -0
- package/dist/sample.js +22 -0
- package/dist/sample.js.map +1 -0
- package/dist/session-utils.d.ts +82 -2
- package/dist/session-utils.d.ts.map +1 -1
- package/dist/session-utils.js +205 -10
- package/dist/session-utils.js.map +1 -1
- package/dist/state-colors.d.ts.map +1 -1
- package/dist/state-colors.js +2 -0
- package/dist/state-colors.js.map +1 -1
- package/dist/states.d.ts +1 -0
- package/dist/states.d.ts.map +1 -1
- package/dist/states.js +6 -1
- package/dist/states.js.map +1 -1
- package/dist/svg-renderers/agent-logos.d.ts +17 -5
- package/dist/svg-renderers/agent-logos.d.ts.map +1 -1
- package/dist/svg-renderers/agent-logos.js +177 -39
- package/dist/svg-renderers/agent-logos.js.map +1 -1
- package/dist/svg-renderers/session-slot-renderer.d.ts +59 -6
- package/dist/svg-renderers/session-slot-renderer.d.ts.map +1 -1
- package/dist/svg-renderers/session-slot-renderer.js +398 -209
- package/dist/svg-renderers/session-slot-renderer.js.map +1 -1
- package/dist/svg-renderers/text-utils.d.ts +14 -0
- package/dist/svg-renderers/text-utils.d.ts.map +1 -1
- package/dist/svg-renderers/text-utils.js +34 -0
- package/dist/svg-renderers/text-utils.js.map +1 -1
- package/dist/telemetry-envelope.d.ts +1 -1
- package/dist/telemetry-envelope.d.ts.map +1 -1
- package/dist/timeline-icons.d.ts +70 -0
- package/dist/timeline-icons.d.ts.map +1 -0
- package/dist/timeline-icons.js +161 -0
- package/dist/timeline-icons.js.map +1 -0
- package/dist/timeline-label.d.ts +38 -0
- package/dist/timeline-label.d.ts.map +1 -0
- package/dist/timeline-label.js +95 -0
- package/dist/timeline-label.js.map +1 -0
- package/dist/timeline-markdown.d.ts +80 -0
- package/dist/timeline-markdown.d.ts.map +1 -0
- package/dist/timeline-markdown.js +221 -0
- package/dist/timeline-markdown.js.map +1 -0
- package/dist/timeline-summarizer.d.ts +24 -0
- package/dist/timeline-summarizer.d.ts.map +1 -1
- package/dist/timeline-summarizer.js +55 -13
- package/dist/timeline-summarizer.js.map +1 -1
- package/dist/timeline.d.ts +72 -2
- package/dist/timeline.d.ts.map +1 -1
- package/dist/timeline.js +187 -10
- package/dist/timeline.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { stateColor } from '../state-colors.js';
|
|
2
|
+
import { PASSIVE_OFFLINE_LABEL, OPEN_AGENTDECK_LABEL } from '../connection-status.js';
|
|
2
3
|
import { agentLogoIcon } from './agent-logos.js';
|
|
3
|
-
import { wrapTextByWidth } from './text-utils.js';
|
|
4
|
+
import { wrapTextByWidth, escSvgText } from './text-utils.js';
|
|
4
5
|
const SIZE = 144;
|
|
5
|
-
const
|
|
6
|
+
const BORDER_PERIMETER = 512;
|
|
7
|
+
// Shared sanitizer: strips ANSI/control chars before entity-escaping (a raw
|
|
8
|
+
// control char in interpolated text makes resvg reject the entire SVG).
|
|
9
|
+
const escXml = escSvgText;
|
|
10
|
+
function truncate(s, max) {
|
|
11
|
+
return s.length <= max ? s : s.slice(0, max - 1) + '\u2026';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compact, readable model strings for narrow surfaces (StreamDeck 144\u00d7144 keys).
|
|
15
|
+
* - claude-sonnet-4-6 \u2192 "sonnet 4.6"
|
|
16
|
+
* - claude-opus-4-7 \u2192 "opus 4.7"
|
|
17
|
+
* - claude-haiku-4-5-20251001 \u2192 "haiku 4.5" (date suffix dropped)
|
|
18
|
+
* - gpt-5-codex / gpt-5 / others \u2192 unchanged (caller still truncates if needed)
|
|
19
|
+
*/
|
|
20
|
+
export function aliasModelName(name) {
|
|
21
|
+
const claude = /^claude-([a-z]+)-(\d+)-(\d+)(?:-\d+)?$/i.exec(name);
|
|
22
|
+
if (claude)
|
|
23
|
+
return `${claude[1].toLowerCase()} ${claude[2]}.${claude[3]}`;
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
6
26
|
function stateLabel(state, agentType) {
|
|
7
27
|
if (!state)
|
|
8
|
-
return
|
|
9
|
-
// OpenClaw-specific labels
|
|
28
|
+
return PASSIVE_OFFLINE_LABEL;
|
|
10
29
|
if (agentType === 'openclaw') {
|
|
11
30
|
if (state === 'idle')
|
|
12
31
|
return 'STANDBY';
|
|
@@ -23,295 +42,465 @@ function stateLabel(state, agentType) {
|
|
|
23
42
|
default: return state.toUpperCase();
|
|
24
43
|
}
|
|
25
44
|
}
|
|
26
|
-
function escXml(s) {
|
|
27
|
-
return s
|
|
28
|
-
.replace(/&/g, '&')
|
|
29
|
-
.replace(/</g, '<')
|
|
30
|
-
.replace(/>/g, '>')
|
|
31
|
-
.replace(/"/g, '"')
|
|
32
|
-
.replace(/'/g, ''');
|
|
33
|
-
}
|
|
34
|
-
function truncate(s, max) {
|
|
35
|
-
if (s.length <= max)
|
|
36
|
-
return s;
|
|
37
|
-
return s.slice(0, max - 1) + '\u2026';
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Format "Model · effort" suffix for session tile / detail view.
|
|
41
|
-
* Skips neutral efforts ("medium" legacy, "default" is Claude Code 2.1+
|
|
42
|
-
* per-model default) so they don't clutter the tile. Truncates the model
|
|
43
|
-
* name preferentially so the effort label survives in tight pixel budgets.
|
|
44
|
-
*/
|
|
45
45
|
export function formatModelEffort(modelName, effortLevel, maxLen = 14) {
|
|
46
46
|
if (!modelName)
|
|
47
47
|
return '';
|
|
48
|
+
const aliased = aliasModelName(modelName);
|
|
48
49
|
const showEffort = effortLevel && effortLevel !== 'medium' && effortLevel !== 'default';
|
|
49
50
|
if (!showEffort)
|
|
50
|
-
return truncate(
|
|
51
|
-
const combined = `${
|
|
51
|
+
return truncate(aliased, maxLen);
|
|
52
|
+
const combined = `${aliased} · ${effortLevel}`;
|
|
52
53
|
if (combined.length <= maxLen)
|
|
53
54
|
return combined;
|
|
54
55
|
const effortSuffix = ` · ${effortLevel}`;
|
|
55
56
|
const modelBudget = Math.max(4, maxLen - effortSuffix.length);
|
|
56
|
-
return truncate(
|
|
57
|
+
return truncate(aliased, modelBudget) + effortSuffix;
|
|
58
|
+
}
|
|
59
|
+
function toneColors(tone = 'info') {
|
|
60
|
+
switch (tone) {
|
|
61
|
+
case 'ready':
|
|
62
|
+
return { bg: '#06160d', panel: '#12331f', icon: '#86efac', accent: '#22c55e', text: '#dcfce7', sub: '#86efac' };
|
|
63
|
+
case 'idle':
|
|
64
|
+
return { bg: '#0b1320', panel: '#172033', icon: '#93c5fd', accent: '#60a5fa', text: '#dbeafe', sub: '#93c5fd' };
|
|
65
|
+
case 'warning':
|
|
66
|
+
return { bg: '#17110a', panel: '#2a1b0c', icon: '#fbbf24', accent: '#f59e0b', text: '#fde68a', sub: '#fbbf24' };
|
|
67
|
+
case 'danger':
|
|
68
|
+
return { bg: '#1c0c0c', panel: '#341312', icon: '#fca5a5', accent: '#ef4444', text: '#fee2e2', sub: '#fca5a5' };
|
|
69
|
+
case 'muted':
|
|
70
|
+
return { bg: '#0a0a0c', panel: '#17171a', icon: '#a1a1aa', accent: '#52525b', text: '#e4e4e7', sub: '#a1a1aa' };
|
|
71
|
+
case 'agent':
|
|
72
|
+
return { bg: '#141016', panel: '#241a2b', icon: '#f0abfc', accent: '#c084fc', text: '#fae8ff', sub: '#d8b4fe' };
|
|
73
|
+
case 'action':
|
|
74
|
+
return { bg: '#07170f', panel: '#12331f', icon: '#bbf7d0', accent: '#22c55e', text: '#dcfce7', sub: '#86efac' };
|
|
75
|
+
case 'brand':
|
|
76
|
+
// Aquarium-tide OFFLINE tone — matches the dark-scrim + cyan AgentDeck mark
|
|
77
|
+
// the macOS/iOS/Android connection overlays and the ESP32 splash draw.
|
|
78
|
+
return { bg: '#071a1e', panel: '#0e2e33', icon: '#3ED6E8', accent: '#3ED6E8', text: '#e6f7fa', sub: '#7fb2bc' };
|
|
79
|
+
case 'purple':
|
|
80
|
+
return { bg: '#120d1d', panel: '#24163a', icon: '#d8b4fe', accent: '#a78bfa', text: '#f3e8ff', sub: '#d8b4fe' };
|
|
81
|
+
case 'info':
|
|
82
|
+
default:
|
|
83
|
+
return { bg: '#0b1626', panel: '#12223a', icon: '#bfdbfe', accent: '#60a5fa', text: '#dbeafe', sub: '#93c5fd' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function orbitOffset(animFrame, speedPx, phasePx = 0) {
|
|
87
|
+
return -((animFrame * speedPx + phasePx) % BORDER_PERIMETER);
|
|
88
|
+
}
|
|
89
|
+
function renderOrbitingRect(params) {
|
|
90
|
+
const { x, y, width, height, rx, color, animFrame, speedPx = 18, phasePx = 0, dashPx = 80, gapPx = BORDER_PERIMETER - dashPx, railOpacity = 0.18, glowOpacity = 0.58, coreOpacity = 0.96, railWidth = 1.2, glowWidth = 4.8, coreWidth = 2.2, filterId, } = params;
|
|
91
|
+
const dashOffset = orbitOffset(animFrame, speedPx, phasePx);
|
|
92
|
+
const filterAttr = filterId ? ` filter="url(#${filterId})"` : '';
|
|
93
|
+
const common = `x="${x}" y="${y}" width="${width}" height="${height}" rx="${rx}" fill="none" stroke="${color}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
94
|
+
return [
|
|
95
|
+
`<rect ${common} stroke-width="${railWidth}" opacity="${railOpacity.toFixed(2)}"/>`,
|
|
96
|
+
`<rect ${common} stroke-width="${glowWidth}" stroke-dasharray="${dashPx} ${gapPx}" stroke-dashoffset="${dashOffset}" opacity="${glowOpacity.toFixed(2)}"${filterAttr}/>`,
|
|
97
|
+
`<rect ${common} stroke-width="${coreWidth}" stroke-dasharray="${dashPx} ${gapPx}" stroke-dashoffset="${dashOffset}" opacity="${coreOpacity.toFixed(2)}"/>`,
|
|
98
|
+
].join('');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* AgentDeck product mark — the aquarium dome over a hardware deck (glass dome +
|
|
102
|
+
* waterline + highlight + rounded deck base + three keys + two bubbles). Ported
|
|
103
|
+
* unit-for-unit (0..24 space) from the SwiftUI `AgentDeckLogo` / Compose
|
|
104
|
+
* `AgentDeckMark` so the Stream Deck / D200H offline hero shows the SAME brand
|
|
105
|
+
* mark that the macOS/iOS/Android connection overlays and the ESP32 splash draw.
|
|
106
|
+
* Centered on (x,y); `size` is the mark's width in px. `color` draws the dome/deck
|
|
107
|
+
* strokes + keys; `accent` tints the waterline + bubbles.
|
|
108
|
+
*/
|
|
109
|
+
export function renderAgentDeckMark(x, y, size, color, accent = color) {
|
|
110
|
+
const u = size / 24; // unit-space (0..24) → px
|
|
111
|
+
const ax = (p) => +(x + (p - 12) * u).toFixed(2);
|
|
112
|
+
const ay = (p) => +(y + (p - 12) * u).toFixed(2);
|
|
113
|
+
const w = (n) => +(n * u).toFixed(2);
|
|
114
|
+
const cap = 'stroke-linecap="round" stroke-linejoin="round" fill="none"';
|
|
115
|
+
return [
|
|
116
|
+
// glass dome
|
|
117
|
+
`<path d="M${ax(4.7)} ${ay(12.8)} C${ax(5.3)} ${ay(4.9)} ${ax(18.7)} ${ay(4.9)} ${ax(19.3)} ${ay(12.8)}" stroke="${color}" stroke-width="${w(1.6)}" ${cap}/>`,
|
|
118
|
+
// waterline
|
|
119
|
+
`<path d="M${ax(6.1)} ${ay(11.2)} C${ax(8.8)} ${ay(12.5)} ${ax(15.2)} ${ay(12.5)} ${ax(17.9)} ${ay(11.2)}" stroke="${accent}" stroke-width="${w(1.15)}" opacity="0.6" ${cap}/>`,
|
|
120
|
+
// dome highlight
|
|
121
|
+
`<path d="M${ax(8.0)} ${ay(7.7)} C${ax(10.0)} ${ay(5.7)} ${ax(13.2)} ${ay(5.4)} ${ax(15.8)} ${ay(6.1)}" stroke="${color}" stroke-width="${w(0.9)}" opacity="0.34" ${cap}/>`,
|
|
122
|
+
// deck base
|
|
123
|
+
`<rect x="${ax(3.4)}" y="${ay(12.2)}" width="${w(17.2)}" height="${w(7.8)}" rx="${w(2.2)}" stroke="${color}" stroke-width="${w(1.6)}" opacity="0.88" fill="none"/>`,
|
|
124
|
+
// three deck keys
|
|
125
|
+
`<rect x="${ax(6.5)}" y="${ay(15.4)}" width="${w(3.1)}" height="${w(2.0)}" rx="${w(1.0)}" fill="${color}" opacity="0.70"/>`,
|
|
126
|
+
`<rect x="${ax(10.4)}" y="${ay(15.4)}" width="${w(3.1)}" height="${w(2.0)}" rx="${w(1.0)}" fill="${color}" opacity="0.92"/>`,
|
|
127
|
+
`<rect x="${ax(14.3)}" y="${ay(15.4)}" width="${w(3.1)}" height="${w(2.0)}" rx="${w(1.0)}" fill="${color}" opacity="0.70"/>`,
|
|
128
|
+
// interior bubbles
|
|
129
|
+
`<circle cx="${ax(9.6)}" cy="${ay(9.0)}" r="${w(0.95)}" fill="${accent}" opacity="0.62"/>`,
|
|
130
|
+
`<circle cx="${ax(14.8)}" cy="${ay(8.2)}" r="${w(0.6)}" fill="${accent}" opacity="0.42"/>`,
|
|
131
|
+
].join('');
|
|
132
|
+
}
|
|
133
|
+
function renderGlyphIcon(kind, color, accent, x = 72, y = 43, scale = 1) {
|
|
134
|
+
const s = scale;
|
|
135
|
+
const sx = (n) => x + n * s;
|
|
136
|
+
const sy = (n) => y + n * s;
|
|
137
|
+
const common = `stroke="${color}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
138
|
+
const accentStroke = `stroke="${accent}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
139
|
+
switch (kind) {
|
|
140
|
+
case 'hub':
|
|
141
|
+
return [
|
|
142
|
+
`<circle cx="${x}" cy="${y}" r="${9 * s}" fill="${accent}" opacity="0.18" stroke="${color}" stroke-width="${2.4 * s}"/>`,
|
|
143
|
+
`<circle cx="${sx(-24)}" cy="${sy(17)}" r="${5 * s}" fill="${color}" opacity="0.9"/>`,
|
|
144
|
+
`<circle cx="${sx(24)}" cy="${sy(17)}" r="${5 * s}" fill="${color}" opacity="0.9"/>`,
|
|
145
|
+
`<circle cx="${x}" cy="${sy(-25)}" r="${5 * s}" fill="${color}" opacity="0.9"/>`,
|
|
146
|
+
`<path d="M${sx(-7)} ${sy(7)} L${sx(-20)} ${sy(14)} M${sx(7)} ${sy(7)} L${sx(20)} ${sy(14)} M${x} ${sy(-9)} L${x} ${sy(-20)}" ${accentStroke} stroke-width="${2.6 * s}" opacity="0.72" fill="none"/>`,
|
|
147
|
+
].join('');
|
|
148
|
+
case 'no-session':
|
|
149
|
+
return [
|
|
150
|
+
`<path d="M${sx(-25)} ${sy(-3)} H${sx(25)} V${sy(21)} Q${sx(25)} ${sy(27)} ${sx(19)} ${sy(27)} H${sx(-19)} Q${sx(-25)} ${sy(27)} ${sx(-25)} ${sy(21)} Z" fill="${accent}" opacity="0.14" stroke="${color}" stroke-width="${2.4 * s}"/>`,
|
|
151
|
+
`<path d="M${sx(-14)} ${sy(-14)} H${sx(14)} M${sx(-7)} ${sy(-24)} H${sx(7)}" ${common} stroke-width="${2.6 * s}" opacity="0.82"/>`,
|
|
152
|
+
`<circle cx="${sx(-10)}" cy="${sy(13)}" r="${2.8 * s}" fill="${color}" opacity="0.55"/><circle cx="${x}" cy="${sy(13)}" r="${2.8 * s}" fill="${color}" opacity="0.38"/><circle cx="${sx(10)}" cy="${sy(13)}" r="${2.8 * s}" fill="${color}" opacity="0.24"/>`,
|
|
153
|
+
].join('');
|
|
154
|
+
case 'agentdeck':
|
|
155
|
+
// The canonical dome-over-deck brand mark (parity with the native overlays).
|
|
156
|
+
return renderAgentDeckMark(x, y, 54 * s, color, accent);
|
|
157
|
+
case 'tool':
|
|
158
|
+
return `<circle cx="${sx(-18)}" cy="${sy(-13)}" r="${7 * s}" fill="none" stroke="${color}" stroke-width="${2.6 * s}"/><path d="M${sx(-12)} ${sy(-7)} L${sx(4)} ${sy(9)}" ${common} stroke-width="${3.8 * s}" fill="none"/><rect x="${sx(4)}" y="${sy(5)}" width="${22 * s}" height="${12 * s}" rx="${5 * s}" transform="rotate(45 ${sx(15)} ${sy(11)})" fill="${accent}" opacity="0.82"/>`;
|
|
159
|
+
case 'model':
|
|
160
|
+
return `<path d="M${x} ${sy(-27)} L${sx(24)} ${sy(-13)} V${sy(15)} L${x} ${sy(29)} L${sx(-24)} ${sy(15)} V${sy(-13)} Z" fill="${accent}" opacity="0.12" stroke="${color}" stroke-width="${2.4 * s}"/><path d="M${sx(-24)} ${sy(-13)} L${x} ${sy(1)} L${sx(24)} ${sy(-13)} M${x} ${sy(1)} V${sy(29)}" ${common} stroke-width="${2 * s}" opacity="0.7" fill="none"/>`;
|
|
161
|
+
case 'mode':
|
|
162
|
+
return `<path d="M${sx(-24)} ${sy(-18)} H${sx(24)} M${sx(-24)} ${y} H${sx(24)} M${sx(-24)} ${sy(18)} H${sx(24)}" ${common} stroke-width="${2.8 * s}" opacity="0.72"/><circle cx="${sx(-8)}" cy="${sy(-18)}" r="${5 * s}" fill="${accent}" stroke="${color}" stroke-width="${2 * s}"/><circle cx="${sx(13)}" cy="${y}" r="${5 * s}" fill="${accent}" stroke="${color}" stroke-width="${2 * s}"/><circle cx="${sx(-15)}" cy="${sy(18)}" r="${5 * s}" fill="${accent}" stroke="${color}" stroke-width="${2 * s}"/>`;
|
|
163
|
+
case 'ready':
|
|
164
|
+
case 'allow':
|
|
165
|
+
return `<circle cx="${x}" cy="${y}" r="${27 * s}" fill="${accent}" opacity="0.15" stroke="${color}" stroke-width="${2.6 * s}"/><path d="M${sx(-13)} ${sy(0)} L${sx(-3)} ${sy(11)} L${sx(16)} ${sy(-12)}" ${common} stroke-width="${5 * s}" fill="none"/>`;
|
|
166
|
+
case 'activity':
|
|
167
|
+
return `<path d="M${sx(-29)} ${y} H${sx(-18)} L${sx(-10)} ${sy(-16)} L${sx(3)} ${sy(18)} L${sx(12)} ${sy(-4)} H${sx(29)}" ${common} stroke-width="${4 * s}" fill="none"/><circle cx="${sx(29)}" cy="${y}" r="${4 * s}" fill="${accent}"/>`;
|
|
168
|
+
case 'open-app':
|
|
169
|
+
case 'play':
|
|
170
|
+
return `<rect x="${sx(-25)}" y="${sy(-22)}" width="${50 * s}" height="${44 * s}" rx="${9 * s}" fill="${accent}" opacity="0.16" stroke="${color}" stroke-width="${2.2 * s}"/><polygon points="${sx(-7)},${sy(-13)} ${sx(-7)},${sy(13)} ${sx(15)},${y}" fill="${color}"/>`;
|
|
171
|
+
case 'retry':
|
|
172
|
+
return `<path d="M${sx(20)} ${sy(-12)} A${26 * s} ${26 * s} 0 1 0 ${sx(23)} ${sy(13)}" fill="none" ${common} stroke-width="${4.5 * s}"/><path d="M${sx(20)} ${sy(-28)} V${sy(-10)} H${sx(2)}" fill="none" ${common} stroke-width="${4.5 * s}"/>`;
|
|
173
|
+
case 'offline':
|
|
174
|
+
return `<circle cx="${x}" cy="${y}" r="${25 * s}" fill="${accent}" opacity="0.14" stroke="${color}" stroke-width="${2.4 * s}"/><circle cx="${x}" cy="${y}" r="${4.5 * s}" fill="${color}"/><path d="M${x} ${sy(5)} V${sy(17)} M${sx(-16)} ${sy(18)} H${sx(16)}" ${accentStroke} stroke-width="${2.6 * s}" opacity="0.68"/>`;
|
|
175
|
+
case 'back':
|
|
176
|
+
return `<path d="M${sx(21)} ${y} H${sx(-18)} M${sx(-18)} ${y} L${sx(-2)} ${sy(-16)} M${sx(-18)} ${y} L${sx(-2)} ${sy(16)}" ${common} stroke-width="${5 * s}" fill="none"/>`;
|
|
177
|
+
case 'more':
|
|
178
|
+
return `<path d="M${sx(-18)} ${sy(-15)} L${sx(0)} ${y} L${sx(-18)} ${sy(15)} M${sx(3)} ${sy(-15)} L${sx(21)} ${y} L${sx(3)} ${sy(15)}" ${common} stroke-width="${4.5 * s}" fill="none"/>`;
|
|
179
|
+
case 'esc':
|
|
180
|
+
case 'deny':
|
|
181
|
+
return `<circle cx="${x}" cy="${y}" r="${27 * s}" fill="${accent}" opacity="0.14" stroke="${color}" stroke-width="${2.2 * s}"/><path d="M${sx(-12)} ${sy(-12)} L${sx(12)} ${sy(12)} M${sx(12)} ${sy(-12)} L${sx(-12)} ${sy(12)}" ${common} stroke-width="${4.8 * s}" fill="none"/>`;
|
|
182
|
+
case 'stop':
|
|
183
|
+
return `<rect x="${sx(-22)}" y="${sy(-22)}" width="${44 * s}" height="${44 * s}" rx="${8 * s}" fill="${accent}" opacity="0.16" stroke="${color}" stroke-width="${2.2 * s}"/><rect x="${sx(-11)}" y="${sy(-11)}" width="${22 * s}" height="${22 * s}" rx="${3 * s}" fill="${color}"/>`;
|
|
184
|
+
case 'review':
|
|
185
|
+
case 'diff':
|
|
186
|
+
return `<rect x="${sx(-18)}" y="${sy(-25)}" width="${36 * s}" height="${50 * s}" rx="${5 * s}" fill="${accent}" opacity="0.13" stroke="${color}" stroke-width="${2.4 * s}"/><path d="M${sx(-9)} ${sy(-10)} H${sx(10)} M${sx(-9)} ${sy(1)} H${sx(6)} M${sx(-9)} ${sy(12)} H${sx(2)}" ${common} stroke-width="${2.2 * s}" opacity="0.76"/>`;
|
|
187
|
+
case 'commit':
|
|
188
|
+
return `<circle cx="${x}" cy="${sy(-15)}" r="${7 * s}" fill="${color}"/><circle cx="${sx(-21)}" cy="${sy(18)}" r="${7 * s}" fill="${color}"/><circle cx="${sx(21)}" cy="${sy(18)}" r="${7 * s}" fill="${color}"/><path d="M${x} ${sy(-8)} V${sy(7)} M${x} ${sy(7)} H${sx(-21)} V${sy(11)} M${x} ${sy(7)} H${sx(21)} V${sy(11)}" ${accentStroke} stroke-width="${3 * s}" fill="none"/>`;
|
|
189
|
+
case 'clear':
|
|
190
|
+
return `<path d="M${sx(-16)} ${sy(-16)} L${sx(16)} ${sy(16)} M${sx(16)} ${sy(-16)} L${sx(-16)} ${sy(16)}" ${common} stroke-width="${4.5 * s}" fill="none"/>`;
|
|
191
|
+
case 'gateway':
|
|
192
|
+
return `<rect x="${sx(-28)}" y="${sy(-21)}" width="${56 * s}" height="${42 * s}" rx="${6 * s}" fill="${accent}" opacity="0.12" stroke="${color}" stroke-width="${2.2 * s}"/><path d="M${sx(-28)} ${sy(-8)} H${sx(28)} M${sx(-12)} ${sy(7)} H${sx(12)} M${x} ${sy(-5)} V${sy(19)}" ${common} stroke-width="${2.2 * s}" opacity="0.72"/>`;
|
|
193
|
+
case 'status':
|
|
194
|
+
return `<circle cx="${x}" cy="${y}" r="${27 * s}" fill="${accent}" opacity="0.14" stroke="${color}" stroke-width="${2.4 * s}"/><path d="M${x} ${sy(-15)} V${sy(4)}" ${common} stroke-width="${4.4 * s}"/><circle cx="${x}" cy="${sy(17)}" r="${4.5 * s}" fill="${color}"/>`;
|
|
195
|
+
case 'option':
|
|
196
|
+
default:
|
|
197
|
+
return `<rect x="${sx(-25)}" y="${sy(-20)}" width="${50 * s}" height="${40 * s}" rx="${8 * s}" fill="${accent}" opacity="0.13" stroke="${color}" stroke-width="${2.2 * s}"/><circle cx="${sx(-12)}" cy="${y}" r="${4 * s}" fill="${color}"/><circle cx="${x}" cy="${y}" r="${4 * s}" fill="${color}"/><circle cx="${sx(12)}" cy="${y}" r="${4 * s}" fill="${color}"/>`;
|
|
198
|
+
}
|
|
57
199
|
}
|
|
58
|
-
// ---- Session List Button ----
|
|
59
200
|
export function renderSessionSlot(session, isActive, animFrame, displayName, options) {
|
|
60
201
|
const isWorking = session.state === 'processing';
|
|
61
202
|
const isAsking = session.state?.startsWith('awaiting') ?? false;
|
|
62
203
|
const isIdle = !isWorking && !isAsking;
|
|
63
|
-
// Stream Deck / SD+ run a 150ms tick — animated. D200H's TypeScript image
|
|
64
|
-
// pipeline (bridge/src/d200h/image-renderer.ts) has no such loop: passing
|
|
65
|
-
// animFrame=0 every time freezes dashed strokes and leaves the sin-based
|
|
66
|
-
// pulse at a dim phase. Callers without a loop should pass animated:false
|
|
67
|
-
// so we emit a motion-free visual (solid borders + badges) that still
|
|
68
|
-
// clearly distinguishes working vs awaiting vs idle.
|
|
69
204
|
const animated = options?.animated ?? true;
|
|
70
205
|
const agent = session.agentType || 'claude-code';
|
|
71
206
|
const nameForDisplay = displayName ?? session.projectName;
|
|
72
|
-
// Bottom line budget: tile width ~124px at 14px font ≈ 16 chars. Leave a
|
|
73
|
-
// little headroom so "Opus 4.7 · max" fits without running into the
|
|
74
|
-
// watermark on the right.
|
|
75
207
|
const modelText = formatModelEffort(session.modelName, session.effortLevel, 15);
|
|
76
|
-
|
|
77
|
-
const p1 = agent === 'claude-code' ? '#D97757' : agent === 'codex-cli' ? '#8BA4FF' : agent === 'openclaw' ? '#FF6B6B' : '#F1ECEC';
|
|
78
|
-
const p2 = agent === 'claude-code' ? '#BE6D52' : agent === 'codex-cli' ? '#5981FF' : agent === 'openclaw' ? '#CC3333' : '#AFAFAF';
|
|
208
|
+
const p1 = agent === 'claude-code' ? '#D97757' : (agent === 'codex-cli' || agent === 'codex-app') ? '#8BA4FF' : agent === 'openclaw' ? '#FF6B6B' : '#F1ECEC';
|
|
79
209
|
const sColor = stateColor(session.state);
|
|
210
|
+
// RUNNING reads as a COOL teal (calm "in progress"); PERM keeps the semantic
|
|
211
|
+
// amber (stateColor → #f59e0b). Previously RUNNING used a gold #F5B942 that
|
|
212
|
+
// collided with awaiting's amber, so the two states looked near-identical.
|
|
213
|
+
const WORKING_COLOR = '#2DD4BF'; // teal — cool, clearly not amber
|
|
214
|
+
const signalColor = isWorking ? WORKING_COLOR : sColor;
|
|
80
215
|
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
81
216
|
const stateLbl = isWorking ? 'RUNNING' : isAsking ? 'PERMIT?' : 'IDLE';
|
|
82
|
-
const colorText = isWorking ? '#
|
|
217
|
+
const colorText = isWorking ? '#CCFBF1' : isAsking ? '#FCD34D' : p1;
|
|
83
218
|
const gradId = `sd-bg-${agent}-${session.state || 'idle'}`;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`;
|
|
90
|
-
let glowBorder = '';
|
|
219
|
+
const filterId = `pg-${animFrame}`;
|
|
220
|
+
let defs = `<linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#1C1C1E"/><stop offset="100%" stop-color="#0C0C0E"/></linearGradient>`;
|
|
221
|
+
const blurDef = `<filter id="${filterId}" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur in="SourceGraphic" stdDeviation="2.4"/></filter>`;
|
|
222
|
+
let stateBorder = '';
|
|
223
|
+
let activeRing = '';
|
|
91
224
|
let askDot = '';
|
|
92
225
|
let runBadge = '';
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<feGaussianBlur in="SourceGraphic" stdDeviation="2.4"/>
|
|
97
|
-
</filter>
|
|
98
|
-
`;
|
|
99
|
-
if (isAsking) {
|
|
100
|
-
// Animated callers pulse the opacity (0.55…1.0 over a sin wave). Static
|
|
101
|
-
// callers (D200H) pin to a near-peak value so the frozen snapshot reads
|
|
102
|
-
// as bright and attention-grabbing instead of dim.
|
|
103
|
-
const pulseOpacity = animated
|
|
104
|
-
? 0.55 + 0.45 * Math.abs(Math.sin(animFrame * 0.15))
|
|
105
|
-
: 0.95;
|
|
226
|
+
if (isWorking) {
|
|
227
|
+
// RUNNING: a teal border of marching dashes ORBITING the key — motion that
|
|
228
|
+
// reads as "work travelling around", distinct from PERM's steady breath.
|
|
106
229
|
defs += blurDef;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
230
|
+
const pulseOpacity = animated ? 0.72 + 0.20 * Math.abs(Math.sin(animFrame * 0.12)) : 0.9;
|
|
231
|
+
const borderColor = signalColor;
|
|
232
|
+
const orbitSpeedPx = 22;
|
|
233
|
+
// Anchor the rotation to when this session entered the animated state so
|
|
234
|
+
// sibling buttons that started later orbit out of phase, not in lockstep.
|
|
235
|
+
const startFrame = options?.processingStartFrame ?? animFrame;
|
|
236
|
+
const processingPhasePx = -((startFrame * orbitSpeedPx) % BORDER_PERIMETER);
|
|
237
|
+
stateBorder = animated
|
|
238
|
+
? renderOrbitingRect({
|
|
239
|
+
x: 8, y: 8, width: 128, height: 128, rx: 12, color: borderColor, animFrame,
|
|
240
|
+
speedPx: orbitSpeedPx, dashPx: 92,
|
|
241
|
+
phasePx: processingPhasePx,
|
|
242
|
+
glowOpacity: pulseOpacity * 0.72, coreOpacity: Math.min(1, pulseOpacity + 0.06), filterId,
|
|
243
|
+
})
|
|
244
|
+
: `<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${borderColor}" stroke-width="4.5" opacity="${pulseOpacity.toFixed(2)}" filter="url(#${filterId})"/><rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${borderColor}" stroke-width="1.5" opacity="${(pulseOpacity * 0.9).toFixed(2)}"/>`;
|
|
245
|
+
runBadge = `<rect x="99" y="14" width="30" height="16" rx="8" fill="${signalColor}" opacity="0.9" /><text x="114" y="25" font-size="9" font-weight="800" text-anchor="middle" fill="#0C0C0E" font-family="${fontFam}">RUN</text>`;
|
|
115
246
|
}
|
|
116
|
-
else if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
247
|
+
else if (isAsking) {
|
|
248
|
+
// PERM / AWAITING: a SOLID amber border that BREATHES (full perimeter, no
|
|
249
|
+
// marching dashes) — a deliberately different motion + hue from RUNNING so
|
|
250
|
+
// "needs you" is unmistakable at a glance. Plus a bold amber PERM badge.
|
|
251
|
+
defs += blurDef;
|
|
252
|
+
const amber = sColor; // #f59e0b
|
|
253
|
+
const breathe = animated ? 0.45 + 0.55 * Math.abs(Math.sin(animFrame * 0.14)) : 1;
|
|
254
|
+
stateBorder = [
|
|
255
|
+
`<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${amber}" stroke-width="7" opacity="${(breathe * 0.6).toFixed(2)}" filter="url(#${filterId})"/>`,
|
|
256
|
+
`<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${amber}" stroke-width="3" opacity="0.97"/>`,
|
|
257
|
+
].join('');
|
|
258
|
+
// Bold filled amber pill — far more legible than the old 5px dot.
|
|
259
|
+
askDot = `<rect x="90" y="12" width="42" height="19" rx="9.5" fill="${amber}"/><text x="111" y="26" font-size="11" font-weight="800" text-anchor="middle" fill="#221500" font-family="${fontFam}">PERM</text>`;
|
|
260
|
+
}
|
|
261
|
+
if (isActive) {
|
|
262
|
+
if (animated && isIdle)
|
|
132
263
|
defs += blurDef;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
264
|
+
activeRing = animated
|
|
265
|
+
? renderOrbitingRect({
|
|
266
|
+
x: 10.5, y: 10.5, width: 123, height: 123, rx: 10.5, color: '#60A5FA', animFrame,
|
|
267
|
+
speedPx: 16, phasePx: 170, dashPx: isIdle ? 72 : 52,
|
|
268
|
+
railOpacity: isIdle ? 0.20 : 0.10, glowOpacity: isIdle ? 0.42 : 0.24,
|
|
269
|
+
coreOpacity: isIdle ? 0.95 : 0.58, railWidth: 1, glowWidth: isIdle ? 3.6 : 2.6,
|
|
270
|
+
coreWidth: isIdle ? 1.8 : 1.3, filterId: isIdle ? filterId : undefined,
|
|
271
|
+
})
|
|
272
|
+
: `<rect x="10.5" y="10.5" width="123" height="123" rx="10.5" fill="none" stroke="#60A5FA" stroke-width="1.5" opacity="${isIdle ? '0.72' : '0.36'}"/>`;
|
|
141
273
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const watermark = `<g transform="translate(90, 86)" opacity="${isIdle ? '0.46' : '0.34'}">
|
|
145
|
-
${agentLogoIcon(agent, 40, 1, 0, 0)}
|
|
146
|
-
</g>`;
|
|
147
|
-
// Left Edge Color Strip
|
|
148
|
-
const leftStrip = `<rect x="8" y="8" width="4" height="128" rx="2" fill="${isWorking ? '#F5B942' : isAsking ? '#F87171' : p1}"/>`;
|
|
149
|
-
// Border is now the primary working/awaiting signal — no tiny spinner glyph.
|
|
150
|
-
const spinner = '';
|
|
151
|
-
// ACT badge only on IDLE — WORKING has the flowing border, AWAITING has the pulse.
|
|
152
|
-
const badgeObj = isIdle ? `
|
|
153
|
-
<rect x="100" y="14" width="28" height="16" rx="8" fill="#ffffff" opacity="0.1" />
|
|
154
|
-
<text x="114" y="25" font-size="10" font-weight="700" text-anchor="middle" fill="#A1A1AA" font-family="${fontFam}">ACT</text>
|
|
155
|
-
` : '';
|
|
274
|
+
const watermark = `<g transform="translate(92, 80)" opacity="${isIdle ? '0.62' : '0.55'}">${agentLogoIcon(agent, 72, 1, 0, 0)}</g>`;
|
|
275
|
+
const badgeObj = isIdle ? `<rect x="100" y="14" width="28" height="16" rx="8" fill="#ffffff" opacity="0.1" /><text x="114" y="25" font-size="10" font-weight="700" text-anchor="middle" fill="#A1A1AA" font-family="${fontFam}">ACT</text>` : '';
|
|
156
276
|
const toolStr = isWorking ? 'Running task' : modelText;
|
|
157
277
|
const elements = [
|
|
158
278
|
`<defs>${defs}</defs>`,
|
|
159
279
|
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
160
280
|
`<rect x="8" y="8" width="128" height="128" rx="12" fill="#2C2C2E" opacity="0.8"/>`,
|
|
161
|
-
|
|
162
|
-
leftStrip,
|
|
163
|
-
watermark,
|
|
164
|
-
spinner,
|
|
165
|
-
askDot,
|
|
166
|
-
runBadge,
|
|
167
|
-
badgeObj,
|
|
281
|
+
stateBorder, activeRing, watermark, askDot, runBadge, badgeObj,
|
|
168
282
|
`<text x="20" y="32" font-size="17" font-weight="800" text-anchor="start" fill="${colorText}" font-family="${fontFam}">${escXml(stateLbl)}</text>`,
|
|
169
283
|
`<text x="20" y="52" font-size="13" font-weight="600" text-anchor="start" fill="#E2E8F0" font-family="${fontFam}">${escXml(truncate(nameForDisplay, 13))}</text>`,
|
|
170
|
-
`<text x="20" y="120" font-size="${isWorking ? '13' : '14'}" font-weight="500" text-anchor="start" fill="${colorText}" opacity="0.8" font-family="${fontFam}">${escXml(toolStr)}</text
|
|
284
|
+
`<text x="20" y="120" font-size="${isWorking ? '13' : '14'}" font-weight="500" text-anchor="start" fill="${colorText}" opacity="0.8" font-family="${fontFam}">${escXml(toolStr)}</text>`,
|
|
285
|
+
// Stale overlay: the daemon stopped responding (no pings/state for the
|
|
286
|
+
// stale window) but hasn't yet hit the hard disconnect. Dim the last-known
|
|
287
|
+
// render and flag it so the state on screen isn't mistaken for live.
|
|
288
|
+
options?.isStale
|
|
289
|
+
? `<rect width="${SIZE}" height="${SIZE}" rx="16" fill="#0C0C0E" opacity="0.5"/>`
|
|
290
|
+
+ `<rect x="20" y="62" width="50" height="17" rx="8" fill="#71717A" opacity="0.92"/>`
|
|
291
|
+
+ `<text x="45" y="74" font-size="10" font-weight="800" text-anchor="middle" fill="#0C0C0E" font-family="${fontFam}">STALE</text>`
|
|
292
|
+
: '',
|
|
171
293
|
].join('');
|
|
172
294
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">${elements}</svg>`;
|
|
173
295
|
}
|
|
174
|
-
// ---- Empty Slot ----
|
|
175
296
|
export function renderEmptySlot() {
|
|
176
|
-
|
|
177
|
-
return svgFrame('#111111', label);
|
|
297
|
+
return renderQuietSlot();
|
|
178
298
|
}
|
|
179
|
-
export function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
299
|
+
export function renderQuietSlot() {
|
|
300
|
+
return svgFrame('#0a0a0a', '');
|
|
301
|
+
}
|
|
302
|
+
export function renderStatusCard(config) {
|
|
303
|
+
const colors = toneColors(config.tone);
|
|
304
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
305
|
+
const label = truncate(config.label, 14);
|
|
306
|
+
const subtitle = truncate(config.subtitle ?? '', 18);
|
|
307
|
+
const detail = truncate(config.detail ?? '', 17);
|
|
308
|
+
const elements = [
|
|
309
|
+
`<rect x="25" y="18" width="94" height="58" rx="14" fill="${colors.panel}" opacity="0.68" stroke="${colors.accent}" stroke-width="1.2" stroke-opacity="0.28"/>`,
|
|
310
|
+
renderGlyphIcon(config.icon, colors.icon, colors.accent, 72, 43, 0.94),
|
|
311
|
+
`<text x="72" y="${subtitle ? 94 : 103}" text-anchor="middle" font-family="${fontFam}" font-size="${label.length > 11 ? '14' : '16'}" font-weight="800" fill="${colors.text}">${escXml(label)}</text>`,
|
|
312
|
+
subtitle ? `<text x="72" y="112" text-anchor="middle" font-family="${fontFam}" font-size="11" font-weight="650" fill="${colors.sub}" opacity="0.86">${escXml(subtitle)}</text>` : '',
|
|
313
|
+
detail ? `<text x="72" y="127" text-anchor="middle" font-family="${fontFam}" font-size="9" font-weight="600" fill="${colors.sub}" opacity="0.52">${escXml(detail)}</text>` : '',
|
|
314
|
+
].join('');
|
|
315
|
+
return svgFrame(colors.bg, elements);
|
|
316
|
+
}
|
|
317
|
+
export function renderDisconnectedSlot(config) {
|
|
318
|
+
if (config.kind === 'empty')
|
|
319
|
+
return renderQuietSlot();
|
|
320
|
+
if (config.col !== undefined &&
|
|
321
|
+
config.row !== undefined &&
|
|
322
|
+
config.cols !== undefined &&
|
|
323
|
+
config.rows !== undefined) {
|
|
324
|
+
return renderOpenAppGrid(config.col, config.row, config.cols, config.rows, config.label ?? PASSIVE_OFFLINE_LABEL, config.subtitle ?? OPEN_AGENTDECK_LABEL);
|
|
187
325
|
}
|
|
188
|
-
|
|
326
|
+
if (config.quadrant) {
|
|
327
|
+
return renderOpenAppQuadrant(config.quadrant, config.label ?? PASSIVE_OFFLINE_LABEL, config.subtitle ?? OPEN_AGENTDECK_LABEL);
|
|
328
|
+
}
|
|
329
|
+
return renderStatusCard({ icon: 'agentdeck', label: config.label ?? PASSIVE_OFFLINE_LABEL, subtitle: config.subtitle ?? OPEN_AGENTDECK_LABEL, detail: config.detail, tone: 'brand' });
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Renders a single 144×144 viewport into a 288×288 cluster hero. Each of the
|
|
333
|
+
* four center keys (tl/tr/bl/br) emits the same logical content offset by its
|
|
334
|
+
* quadrant; viewBox clipping keeps only the relevant 144×144 visible. The
|
|
335
|
+
* cluster reads as one card visually, with the inner panel + glyph + text
|
|
336
|
+
* spanning all four keys while each key keeps its own outer rounded bezel.
|
|
337
|
+
*/
|
|
338
|
+
export function renderOpenAppQuadrant(quadrant, label = PASSIVE_OFFLINE_LABEL, subtitle = OPEN_AGENTDECK_LABEL) {
|
|
339
|
+
const colors = toneColors('brand');
|
|
340
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
341
|
+
const offsetX = (quadrant === 'tr' || quadrant === 'br') ? -SIZE : 0;
|
|
342
|
+
const offsetY = (quadrant === 'bl' || quadrant === 'br') ? -SIZE : 0;
|
|
343
|
+
const gradId = `frame-bg-cluster-${quadrant}`;
|
|
344
|
+
// 288×288 cluster content. All coordinates are in the logical cluster space;
|
|
345
|
+
// the per-key viewBox + transform crops to one quadrant.
|
|
346
|
+
const cluster = [
|
|
347
|
+
`<rect x="16" y="16" width="256" height="256" rx="28" fill="${colors.panel}" opacity="0.68" stroke="${colors.accent}" stroke-width="2.4" stroke-opacity="0.28"/>`,
|
|
348
|
+
renderGlyphIcon('agentdeck', colors.icon, colors.accent, 144, 86, 1.88),
|
|
349
|
+
`<text x="144" y="208" text-anchor="middle" font-family="${fontFam}" font-size="36" font-weight="800" fill="${colors.text}">${escXml(truncate(label, 14))}</text>`,
|
|
350
|
+
`<text x="144" y="240" text-anchor="middle" font-family="${fontFam}" font-size="20" font-weight="650" fill="${colors.sub}" opacity="0.86">${escXml(truncate(subtitle, 18))}</text>`,
|
|
351
|
+
].join('');
|
|
352
|
+
return [
|
|
353
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">`,
|
|
354
|
+
`<defs><linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${colors.bg}"/><stop offset="100%" stop-color="#0A0A0E"/></linearGradient></defs>`,
|
|
355
|
+
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
356
|
+
`<g transform="translate(${offsetX} ${offsetY})">${cluster}</g>`,
|
|
357
|
+
`</svg>`,
|
|
358
|
+
].join('');
|
|
189
359
|
}
|
|
190
|
-
// ---- Navigation Buttons ----
|
|
191
360
|
export function renderBackButton() {
|
|
192
|
-
|
|
193
|
-
const label = `<text x="72" y="108" text-anchor="middle" font-family="Arial,sans-serif" font-size="16" font-weight="600" fill="#94a3b8">BACK</text>`;
|
|
194
|
-
return svgFrame('#1a1a1a', arrow + label);
|
|
361
|
+
return renderStatusCard({ icon: 'back', label: 'BACK', subtitle: 'sessions', tone: 'muted' });
|
|
195
362
|
}
|
|
196
363
|
export function renderNextPageButton(pageLabel) {
|
|
197
|
-
|
|
198
|
-
const label = `<text x="72" y="92" text-anchor="middle" font-family="Arial,sans-serif" font-size="14" font-weight="600" fill="#60a5fa">NEXT</text>`;
|
|
199
|
-
const page = `<text x="72" y="114" text-anchor="middle" font-family="Arial,sans-serif" font-size="11" fill="#64748b">${escXml(pageLabel)}</text>`;
|
|
200
|
-
return svgFrame('#1a1a2e', arrow + label + page);
|
|
364
|
+
return renderStatusCard({ icon: 'more', label: 'MORE', subtitle: pageLabel, tone: 'info' });
|
|
201
365
|
}
|
|
202
366
|
export function renderEscButton(active = true) {
|
|
203
|
-
|
|
204
|
-
const bg = active ? '#2d1810' : '#1a1308';
|
|
205
|
-
const op = active ? '' : ' opacity="0.45"';
|
|
206
|
-
const elements = [
|
|
207
|
-
`<text x="72" y="62" text-anchor="middle" font-family="Arial,sans-serif" font-size="32" fill="${c}"${op}>\u2716</text>`,
|
|
208
|
-
`<text x="72" y="100" text-anchor="middle" font-family="Arial,sans-serif" font-size="28" font-weight="bold" fill="${c}"${op}>ESC</text>`,
|
|
209
|
-
].join('');
|
|
210
|
-
return svgFrame(bg, elements);
|
|
367
|
+
return renderStatusCard({ icon: 'esc', label: 'ESC', subtitle: active ? 'cancel' : 'idle', tone: active ? 'warning' : 'muted' });
|
|
211
368
|
}
|
|
212
369
|
export function renderStopButton(active = true) {
|
|
213
|
-
|
|
214
|
-
const bg = active ? '#2d1010' : '#1a0a0a';
|
|
215
|
-
const op = active ? '' : ' opacity="0.4"';
|
|
216
|
-
const elements = [
|
|
217
|
-
`<text x="72" y="62" text-anchor="middle" font-family="Arial,sans-serif" font-size="32" fill="${c}"${op}>\u25A0</text>`,
|
|
218
|
-
`<text x="72" y="100" text-anchor="middle" font-family="Arial,sans-serif" font-size="28" font-weight="bold" fill="${c}"${op}>STOP</text>`,
|
|
219
|
-
].join('');
|
|
220
|
-
return svgFrame(bg, elements);
|
|
370
|
+
return renderStatusCard({ icon: 'stop', label: 'STOP', subtitle: active ? 'interrupt' : 'idle', tone: active ? 'danger' : 'muted' });
|
|
221
371
|
}
|
|
222
|
-
// ---- Detail View: Session Info ----
|
|
223
372
|
export function renderDetailInfo(session, state, tool, modelName, mode, displayName, effortLevel) {
|
|
224
373
|
if (!session)
|
|
225
374
|
return renderEmptySlot();
|
|
226
375
|
const agent = session.agentType || 'claude-code';
|
|
227
376
|
const nameForDisplay = displayName ?? session.projectName;
|
|
228
377
|
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
229
|
-
const
|
|
230
|
-
const
|
|
378
|
+
const effectiveState = state || session.state;
|
|
379
|
+
const sColor = stateColor(effectiveState);
|
|
380
|
+
const stateLbl = stateLabel(effectiveState, agent);
|
|
231
381
|
const gradId = `sd-bg-detail-${agent}`;
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<stop offset="100%" stop-color="#0A0B10"/>
|
|
236
|
-
</linearGradient>
|
|
237
|
-
`;
|
|
238
|
-
const watermark = `<g transform="translate(90, 86)" opacity="0.36">
|
|
239
|
-
${agentLogoIcon(agent, 40, 1, 0, 0)}
|
|
240
|
-
</g>`;
|
|
241
|
-
const leftStrip = `<rect x="8" y="8" width="4" height="128" rx="2" fill="${sColor}"/>`;
|
|
242
|
-
const badgeObj = `
|
|
243
|
-
<rect x="100" y="14" width="28" height="16" rx="8" fill="#ffffff" opacity="0.1" />
|
|
244
|
-
<text x="114" y="25" font-size="10" font-weight="700" text-anchor="middle" fill="#A1A1AA" font-family="${fontFam}">INFO</text>
|
|
245
|
-
`;
|
|
246
|
-
const toolDisplay = tool ? `\u25B6 ${truncate(tool, 18)}` : stateLbl;
|
|
382
|
+
const watermark = `<g transform="translate(92, 80)" opacity="0.42">${agentLogoIcon(agent, 48, 1, 0, 0)}</g>`;
|
|
383
|
+
const badgeObj = `<rect x="100" y="14" width="28" height="16" rx="8" fill="#ffffff" opacity="0.1" /><text x="114" y="25" font-size="10" font-weight="700" text-anchor="middle" fill="#A1A1AA" font-family="${fontFam}">INFO</text>`;
|
|
384
|
+
const toolDisplay = tool ? `▶ ${truncate(tool, 18)}` : stateLbl;
|
|
247
385
|
const elements = [
|
|
248
|
-
`<defs
|
|
386
|
+
`<defs><linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#151720"/><stop offset="100%" stop-color="#0A0B10"/></linearGradient></defs>`,
|
|
249
387
|
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
250
388
|
`<rect x="8" y="8" width="128" height="128" rx="12" fill="#1C1F2E" opacity="0.8"/>`,
|
|
251
|
-
|
|
252
|
-
watermark,
|
|
253
|
-
badgeObj,
|
|
254
|
-
// Title
|
|
389
|
+
watermark, badgeObj,
|
|
255
390
|
`<text x="20" y="34" font-size="18" font-weight="800" text-anchor="start" fill="#ffffff" font-family="${fontFam}">${escXml(truncate(nameForDisplay, 10))}</text>`,
|
|
256
|
-
// Model/Mode — effort renders inline with model (e.g. "Opus 4.7 · max")
|
|
257
|
-
// so the permission-mode chip below keeps its y=74 slot.
|
|
258
391
|
(modelName && agent !== 'openclaw') ? `<text x="20" y="56" font-size="12" font-weight="600" text-anchor="start" fill="#94a3b8" font-family="${fontFam}">${escXml(formatModelEffort(modelName, effortLevel, 17))}</text>` : '',
|
|
259
392
|
(mode && mode !== 'default' && agent !== 'openclaw') ? `<text x="20" y="74" font-size="11" font-weight="700" text-anchor="start" fill="#a78bfa" font-family="${fontFam}">${escXml(mode.toUpperCase())}</text>` : '',
|
|
260
|
-
// Tool/State
|
|
261
393
|
`<text x="20" y="120" font-size="12" font-weight="700" text-anchor="start" fill="${tool ? '#fbbf24' : sColor}" font-family="${fontFam}">${escXml(toolDisplay)}</text>`,
|
|
262
394
|
].join('');
|
|
263
395
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">${elements}</svg>`;
|
|
264
396
|
}
|
|
265
|
-
|
|
397
|
+
function optionVisual(label) {
|
|
398
|
+
const lower = label.toLowerCase();
|
|
399
|
+
if (lower.includes('diff') || lower.includes('review') || lower.includes('view'))
|
|
400
|
+
return { icon: 'diff', tone: 'info' };
|
|
401
|
+
if (lower.startsWith('yes') || lower.startsWith('allow') || lower.startsWith('apply') || lower.includes("don't ask") || lower.includes('always'))
|
|
402
|
+
return { icon: 'allow', tone: 'ready' };
|
|
403
|
+
if (lower.startsWith('no') || lower.startsWith('deny') || lower.startsWith('reject') || lower.includes('cancel'))
|
|
404
|
+
return { icon: 'deny', tone: 'danger' };
|
|
405
|
+
return { icon: 'option', tone: 'purple' };
|
|
406
|
+
}
|
|
266
407
|
export function renderOptionButton(option, index) {
|
|
267
408
|
const label = option.label || `Option ${index + 1}`;
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
409
|
+
const { icon, tone } = optionVisual(label);
|
|
410
|
+
const colors = toneColors(tone);
|
|
411
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
412
|
+
const lines = wrapTextByWidth(label, 112, 13).slice(0, 2);
|
|
413
|
+
const startY = lines.length === 1 ? 103 : 96;
|
|
414
|
+
const slotNum = `<circle cx="119" cy="23" r="12" fill="${colors.panel}" stroke="${colors.accent}" stroke-width="1" opacity="0.9"/><text x="119" y="28" text-anchor="middle" font-family="${fontFam}" font-size="12" font-weight="800" fill="${colors.text}" opacity="0.8">${index + 1}</text>`;
|
|
415
|
+
const textEls = lines.map((line, i) => `<text x="72" y="${startY + i * 17}" text-anchor="middle" font-family="${fontFam}" font-size="${line.length > 13 ? '12' : '13'}" font-weight="800" fill="${colors.text}">${escXml(line)}</text>`).join('');
|
|
416
|
+
return svgFrame(colors.bg, `<rect x="25" y="18" width="94" height="58" rx="14" fill="${colors.panel}" opacity="0.58" stroke="${colors.accent}" stroke-width="1.2" stroke-opacity="0.26"/>`
|
|
417
|
+
+ renderGlyphIcon(icon, colors.icon, colors.accent, 72, 43, 0.84)
|
|
418
|
+
+ slotNum
|
|
419
|
+
+ textEls);
|
|
278
420
|
}
|
|
279
|
-
// ---- Detail View: Preset Action Button ----
|
|
280
421
|
export function renderPresetButton(label, iconSvg, color, textColor, subtitle, loading) {
|
|
422
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
281
423
|
if (loading) {
|
|
282
|
-
return svgFrame(color, iconSvg);
|
|
424
|
+
return svgFrame(color, iconSvg + `<text x="72" y="103" text-anchor="middle" font-family="${fontFam}" font-size="13" font-weight="800" fill="${textColor}">SWITCHING</text>`);
|
|
283
425
|
}
|
|
284
|
-
const labelEl = `<text x="72" y="100" text-anchor="middle" font-family="
|
|
285
|
-
const subEl = subtitle
|
|
286
|
-
? `<text x="72" y="118" text-anchor="middle" font-family="Arial,sans-serif" font-size="12" fill="${textColor}" opacity="0.7">${escXml(truncate(subtitle, 14))}</text>`
|
|
287
|
-
: '';
|
|
426
|
+
const labelEl = `<text x="72" y="100" text-anchor="middle" font-family="${fontFam}" font-size="16" font-weight="800" fill="${textColor}">${escXml(label)}</text>`;
|
|
427
|
+
const subEl = subtitle ? `<text x="72" y="118" text-anchor="middle" font-family="${fontFam}" font-size="11" font-weight="650" fill="${textColor}" opacity="0.7">${escXml(truncate(subtitle, 14))}</text>` : '';
|
|
288
428
|
return svgFrame(color, iconSvg + labelEl + subEl);
|
|
289
429
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const mainEl = `<text x="72" y="${subtitle ? 68 : 76}" text-anchor="middle" font-family="Arial,sans-serif" font-size="16" font-weight="bold" fill="#94a3b8">${escXml(truncate(label, 18))}</text>`;
|
|
293
|
-
const subEl = subtitle
|
|
294
|
-
? `<text x="72" y="92" text-anchor="middle" font-family="Arial,sans-serif" font-size="12" fill="#64748b">${escXml(truncate(subtitle, 22))}</text>`
|
|
295
|
-
: '';
|
|
296
|
-
return svgFrame('#1a1a1a', mainEl + subEl);
|
|
430
|
+
export function renderInfoSlot(label, subtitle, icon = 'activity', tone = 'info', detail) {
|
|
431
|
+
return renderStatusCard({ icon, label, subtitle, detail, tone });
|
|
297
432
|
}
|
|
298
|
-
// ---- SVG Frame ----
|
|
299
433
|
export function svgFrame(bgColor, innerElements) {
|
|
300
|
-
// Use a random ID to prevent SVG constraint clashes
|
|
301
434
|
const gradId = 'frame-bg-' + Math.floor(Math.random() * 1000000);
|
|
302
|
-
const defs = `
|
|
303
|
-
<linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
304
|
-
<stop offset="0%" stop-color="${bgColor}"/>
|
|
305
|
-
<stop offset="100%" stop-color="#0A0A0E"/>
|
|
306
|
-
</linearGradient>
|
|
307
|
-
`;
|
|
308
435
|
return [
|
|
309
436
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">`,
|
|
310
|
-
`<defs
|
|
437
|
+
`<defs><linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${bgColor}"/><stop offset="100%" stop-color="#0A0A0E"/></linearGradient></defs>`,
|
|
311
438
|
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
312
439
|
`<rect x="8" y="8" width="128" height="128" rx="12" fill="#2C2C2E" opacity="0.6"/>`,
|
|
313
440
|
innerElements,
|
|
314
441
|
`</svg>`,
|
|
315
442
|
].join('');
|
|
316
443
|
}
|
|
444
|
+
export function renderOpenAppGrid(col, row, cols, rows, label = PASSIVE_OFFLINE_LABEL, subtitle = OPEN_AGENTDECK_LABEL) {
|
|
445
|
+
const colors = toneColors('brand');
|
|
446
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
447
|
+
const offsetX = -col * SIZE;
|
|
448
|
+
const offsetY = -row * SIZE;
|
|
449
|
+
const gradId = `frame-bg-grid-${col}-${row}`;
|
|
450
|
+
const minDim = Math.min(cols, rows);
|
|
451
|
+
const baseScale = Math.max(1.0, minDim * 0.8);
|
|
452
|
+
const totalW = cols * SIZE;
|
|
453
|
+
const totalH = rows * SIZE;
|
|
454
|
+
const padX = Math.max(8, 16 * cols / 4);
|
|
455
|
+
const padY = Math.max(8, 16 * rows / 2);
|
|
456
|
+
const rectW = totalW - padX * 2;
|
|
457
|
+
const rectH = totalH - padY * 2;
|
|
458
|
+
const rx = Math.max(12, 28 * minDim / 2);
|
|
459
|
+
const glyphX = totalW / 2;
|
|
460
|
+
const glyphY = totalH / 2 - (24 * baseScale);
|
|
461
|
+
const labelY = totalH / 2 + (20 * baseScale);
|
|
462
|
+
const subY = labelY + (28 * baseScale);
|
|
463
|
+
const fontSizeLabel = Math.max(16, Math.round(36 * (minDim / 2)));
|
|
464
|
+
const fontSizeSub = Math.max(11, Math.round(20 * (minDim / 2)));
|
|
465
|
+
const cluster = [
|
|
466
|
+
`<rect x="${padX}" y="${padY}" width="${rectW}" height="${rectH}" rx="${rx}" fill="${colors.panel}" opacity="0.68" stroke="${colors.accent}" stroke-width="${(2.4 * baseScale).toFixed(1)}" stroke-opacity="0.28"/>`,
|
|
467
|
+
renderGlyphIcon('agentdeck', colors.icon, colors.accent, glyphX, glyphY, baseScale),
|
|
468
|
+
`<text x="${totalW / 2}" y="${labelY}" text-anchor="middle" font-family="${fontFam}" font-size="${fontSizeLabel}" font-weight="800" fill="${colors.text}">${escXml(truncate(label, 16))}</text>`,
|
|
469
|
+
`<text x="${totalW / 2}" y="${subY}" text-anchor="middle" font-family="${fontFam}" font-size="${fontSizeSub}" font-weight="650" fill="${colors.sub}" opacity="0.86">${escXml(truncate(subtitle, 24))}</text>`,
|
|
470
|
+
].join('');
|
|
471
|
+
return [
|
|
472
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">`,
|
|
473
|
+
`<defs><linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${colors.bg}"/><stop offset="100%" stop-color="#0A0A0E"/></linearGradient></defs>`,
|
|
474
|
+
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
475
|
+
`<g transform="translate(${offsetX} ${offsetY})">${cluster}</g>`,
|
|
476
|
+
`</svg>`,
|
|
477
|
+
].join('');
|
|
478
|
+
}
|
|
479
|
+
export function renderOfflineTouchStrip(index) {
|
|
480
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
481
|
+
const colors = toneColors('brand');
|
|
482
|
+
const W_total = 800;
|
|
483
|
+
const H_total = 100;
|
|
484
|
+
const offsetX = -index * 200;
|
|
485
|
+
const gradId = `touchstrip-offline-bg-${index}`;
|
|
486
|
+
// Entire 800x100 content
|
|
487
|
+
const content = [
|
|
488
|
+
// Background card panel
|
|
489
|
+
`<rect x="15" y="10" width="770" height="80" rx="16" fill="${colors.panel}" opacity="0.68" stroke="${colors.accent}" stroke-width="2" stroke-opacity="0.28"/>`,
|
|
490
|
+
// Left side: AgentDeck brand mark (x=120, y=50)
|
|
491
|
+
renderGlyphIcon('agentdeck', colors.icon, colors.accent, 120, 50, 1.2),
|
|
492
|
+
// Center: Offline text and helper instruction
|
|
493
|
+
`<text x="400" y="44" text-anchor="middle" font-family="${fontFam}" font-size="22" font-weight="800" fill="${colors.text}">AGENTDECK OFFLINE</text>`,
|
|
494
|
+
`<text x="400" y="70" text-anchor="middle" font-family="${fontFam}" font-size="12" font-weight="650" fill="${colors.sub}" opacity="0.86">Open the AgentDeck app — or install: npx @agentdeck/setup</text>`,
|
|
495
|
+
// Right side: offline icon (x=680, y=50)
|
|
496
|
+
renderGlyphIcon('offline', colors.icon, colors.accent, 680, 50, 1.2),
|
|
497
|
+
].join('');
|
|
498
|
+
return [
|
|
499
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 200 100">`,
|
|
500
|
+
`<defs><linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${colors.bg}"/><stop offset="100%" stop-color="#0A0A0E"/></linearGradient></defs>`,
|
|
501
|
+
`<rect width="200" height="100" fill="url(#${gradId})"/>`,
|
|
502
|
+
`<g transform="translate(${offsetX} 0)">${content}</g>`,
|
|
503
|
+
`</svg>`
|
|
504
|
+
].join('');
|
|
505
|
+
}
|
|
317
506
|
//# sourceMappingURL=session-slot-renderer.js.map
|