@agentdeck/shared 0.2.0 → 1.0.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/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 +8 -2
- package/dist/command-builders.d.ts.map +1 -1
- package/dist/command-builders.js +20 -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/creature-layout.d.ts +18 -0
- package/dist/creature-layout.d.ts.map +1 -0
- package/dist/creature-layout.js +131 -0
- package/dist/creature-layout.js.map +1 -0
- package/dist/d200h-layout.d.ts +146 -0
- package/dist/d200h-layout.d.ts.map +1 -0
- package/dist/d200h-layout.js +808 -0
- package/dist/d200h-layout.js.map +1 -0
- package/dist/design-tokens.d.ts +261 -0
- package/dist/design-tokens.d.ts.map +1 -0
- package/dist/design-tokens.js +157 -0
- package/dist/design-tokens.js.map +1 -0
- package/dist/eval-schema.d.ts +32 -2
- package/dist/eval-schema.d.ts.map +1 -1
- package/dist/eval-schema.js.map +1 -1
- package/dist/format-utils.d.ts +38 -0
- package/dist/format-utils.d.ts.map +1 -1
- package/dist/format-utils.js +70 -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 +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -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 +220 -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 +20 -12
- package/dist/svg-renderers/agent-logos.d.ts.map +1 -1
- package/dist/svg-renderers/agent-logos.js +154 -46
- package/dist/svg-renderers/agent-logos.js.map +1 -1
- package/dist/svg-renderers/session-slot-renderer.d.ts +71 -6
- package/dist/svg-renderers/session-slot-renderer.d.ts.map +1 -1
- package/dist/svg-renderers/session-slot-renderer.js +429 -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/terrarium-rules.d.ts +46 -0
- package/dist/terrarium-rules.d.ts.map +1 -0
- package/dist/terrarium-rules.js +63 -0
- package/dist/terrarium-rules.js.map +1 -0
- package/dist/timeline-icons.d.ts +80 -0
- package/dist/timeline-icons.d.ts.map +1 -0
- package/dist/timeline-icons.js +174 -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-task-display.d.ts +66 -0
- package/dist/timeline-task-display.d.ts.map +1 -0
- package/dist/timeline-task-display.js +94 -0
- package/dist/timeline-task-display.js.map +1 -0
- package/dist/timeline.d.ts +101 -2
- package/dist/timeline.d.ts.map +1 -1
- package/dist/timeline.js +249 -11
- package/dist/timeline.js.map +1 -1
- package/dist/trmnl-layout.d.ts.map +1 -0
- package/dist/trmnl-layout.js.map +1 -0
- package/package.json +2 -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,496 @@ 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
|
+
/**
|
|
134
|
+
* Small-size reduction of {@link renderAgentDeckMark}.
|
|
135
|
+
*
|
|
136
|
+
* The full mark carries a waterline, a dome highlight and interior bubbles at
|
|
137
|
+
* 0.34–0.62 opacity. Those read at 72px+ but collapse into a grey blob by 20px,
|
|
138
|
+
* which is the size Stream Deck draws action-list icons at. This keeps the
|
|
139
|
+
* mark's three identifying features — dome arc, deck body, three keys — at full
|
|
140
|
+
* opacity and drops everything that cannot survive the downsample.
|
|
141
|
+
*
|
|
142
|
+
* Keep the two in sync: if the mark's silhouette changes, change this too.
|
|
143
|
+
*/
|
|
144
|
+
export function renderAgentDeckMarkCompact(x, y, size, color) {
|
|
145
|
+
const u = size / 24;
|
|
146
|
+
const ax = (p) => +(x + (p - 12) * u).toFixed(2);
|
|
147
|
+
const ay = (p) => +(y + (p - 12) * u).toFixed(2);
|
|
148
|
+
const w = (n) => +(n * u).toFixed(2);
|
|
149
|
+
const cap = 'stroke-linecap="round" stroke-linejoin="round" fill="none"';
|
|
150
|
+
const key = (kx) => `<rect x="${ax(kx)}" y="${ay(14.6)}" width="${w(3.4)}" height="${w(2.4)}" rx="${w(1.2)}" fill="${color}"/>`;
|
|
151
|
+
return [
|
|
152
|
+
// dome arc, seated on the deck lip
|
|
153
|
+
`<path d="M${ax(4.6)} ${ay(11.6)} C${ax(5.3)} ${ay(3.9)} ${ax(18.7)} ${ay(3.9)} ${ax(19.4)} ${ay(11.6)}" stroke="${color}" stroke-width="${w(1.7)}" ${cap}/>`,
|
|
154
|
+
// deck body
|
|
155
|
+
`<rect x="${ax(3.2)}" y="${ay(11.6)}" width="${w(17.6)}" height="${w(8.6)}" rx="${w(2.3)}" stroke="${color}" stroke-width="${w(1.7)}" fill="none"/>`,
|
|
156
|
+
key(6.0), key(10.3), key(14.6),
|
|
157
|
+
].join('');
|
|
158
|
+
}
|
|
159
|
+
function renderGlyphIcon(kind, color, accent, x = 72, y = 43, scale = 1) {
|
|
160
|
+
const s = scale;
|
|
161
|
+
const sx = (n) => x + n * s;
|
|
162
|
+
const sy = (n) => y + n * s;
|
|
163
|
+
const common = `stroke="${color}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
164
|
+
const accentStroke = `stroke="${accent}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
165
|
+
switch (kind) {
|
|
166
|
+
case 'hub':
|
|
167
|
+
return [
|
|
168
|
+
`<circle cx="${x}" cy="${y}" r="${9 * s}" fill="${accent}" opacity="0.18" stroke="${color}" stroke-width="${2.4 * s}"/>`,
|
|
169
|
+
`<circle cx="${sx(-24)}" cy="${sy(17)}" r="${5 * s}" fill="${color}" opacity="0.9"/>`,
|
|
170
|
+
`<circle cx="${sx(24)}" cy="${sy(17)}" r="${5 * s}" fill="${color}" opacity="0.9"/>`,
|
|
171
|
+
`<circle cx="${x}" cy="${sy(-25)}" r="${5 * s}" fill="${color}" opacity="0.9"/>`,
|
|
172
|
+
`<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"/>`,
|
|
173
|
+
].join('');
|
|
174
|
+
case 'no-session':
|
|
175
|
+
return [
|
|
176
|
+
`<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}"/>`,
|
|
177
|
+
`<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"/>`,
|
|
178
|
+
`<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"/>`,
|
|
179
|
+
].join('');
|
|
180
|
+
case 'agentdeck':
|
|
181
|
+
// The canonical dome-over-deck brand mark (parity with the native overlays).
|
|
182
|
+
return renderAgentDeckMark(x, y, 54 * s, color, accent);
|
|
183
|
+
case 'tool':
|
|
184
|
+
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"/>`;
|
|
185
|
+
case 'model':
|
|
186
|
+
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"/>`;
|
|
187
|
+
case 'mode':
|
|
188
|
+
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}"/>`;
|
|
189
|
+
case 'ready':
|
|
190
|
+
case 'allow':
|
|
191
|
+
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"/>`;
|
|
192
|
+
case 'activity':
|
|
193
|
+
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}"/>`;
|
|
194
|
+
case 'open-app':
|
|
195
|
+
case 'play':
|
|
196
|
+
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}"/>`;
|
|
197
|
+
case 'retry':
|
|
198
|
+
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}"/>`;
|
|
199
|
+
case 'offline':
|
|
200
|
+
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"/>`;
|
|
201
|
+
case 'back':
|
|
202
|
+
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"/>`;
|
|
203
|
+
case 'more':
|
|
204
|
+
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"/>`;
|
|
205
|
+
case 'esc':
|
|
206
|
+
case 'deny':
|
|
207
|
+
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"/>`;
|
|
208
|
+
case 'stop':
|
|
209
|
+
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}"/>`;
|
|
210
|
+
case 'review':
|
|
211
|
+
case 'diff':
|
|
212
|
+
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"/>`;
|
|
213
|
+
case 'commit':
|
|
214
|
+
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"/>`;
|
|
215
|
+
case 'clear':
|
|
216
|
+
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"/>`;
|
|
217
|
+
case 'gateway':
|
|
218
|
+
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"/>`;
|
|
219
|
+
case 'status':
|
|
220
|
+
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}"/>`;
|
|
221
|
+
case 'option':
|
|
222
|
+
default:
|
|
223
|
+
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}"/>`;
|
|
224
|
+
}
|
|
57
225
|
}
|
|
58
|
-
// ---- Session List Button ----
|
|
59
226
|
export function renderSessionSlot(session, isActive, animFrame, displayName, options) {
|
|
60
227
|
const isWorking = session.state === 'processing';
|
|
61
228
|
const isAsking = session.state?.startsWith('awaiting') ?? false;
|
|
62
229
|
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
230
|
const animated = options?.animated ?? true;
|
|
70
231
|
const agent = session.agentType || 'claude-code';
|
|
71
232
|
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
233
|
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';
|
|
234
|
+
const p1 = agent === 'claude-code' ? '#D97757' : (agent === 'codex-cli' || agent === 'codex-app') ? '#8BA4FF' : agent === 'openclaw' ? '#FF6B6B' : '#F1ECEC';
|
|
79
235
|
const sColor = stateColor(session.state);
|
|
236
|
+
// RUNNING reads as a COOL teal (calm "in progress"); PERM keeps the semantic
|
|
237
|
+
// amber (stateColor → #f59e0b). Previously RUNNING used a gold #F5B942 that
|
|
238
|
+
// collided with awaiting's amber, so the two states looked near-identical.
|
|
239
|
+
const WORKING_COLOR = '#2DD4BF'; // teal — cool, clearly not amber
|
|
240
|
+
const signalColor = isWorking ? WORKING_COLOR : sColor;
|
|
80
241
|
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
81
242
|
const stateLbl = isWorking ? 'RUNNING' : isAsking ? 'PERMIT?' : 'IDLE';
|
|
82
|
-
const colorText = isWorking ? '#
|
|
243
|
+
const colorText = isWorking ? '#CCFBF1' : isAsking ? '#FCD34D' : p1;
|
|
83
244
|
const gradId = `sd-bg-${agent}-${session.state || 'idle'}`;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`;
|
|
90
|
-
let glowBorder = '';
|
|
245
|
+
const filterId = `pg-${animFrame}`;
|
|
246
|
+
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>`;
|
|
247
|
+
const blurDef = `<filter id="${filterId}" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur in="SourceGraphic" stdDeviation="2.4"/></filter>`;
|
|
248
|
+
let stateBorder = '';
|
|
249
|
+
let activeRing = '';
|
|
91
250
|
let askDot = '';
|
|
92
251
|
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;
|
|
252
|
+
if (isWorking) {
|
|
253
|
+
// RUNNING: a teal border of marching dashes ORBITING the key — motion that
|
|
254
|
+
// reads as "work travelling around", distinct from PERM's steady breath.
|
|
106
255
|
defs += blurDef;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
256
|
+
const pulseOpacity = animated ? 0.72 + 0.20 * Math.abs(Math.sin(animFrame * 0.12)) : 0.9;
|
|
257
|
+
const borderColor = signalColor;
|
|
258
|
+
const orbitSpeedPx = 22;
|
|
259
|
+
// Anchor the rotation to when this session entered the animated state so
|
|
260
|
+
// sibling buttons that started later orbit out of phase, not in lockstep.
|
|
261
|
+
const startFrame = options?.processingStartFrame ?? animFrame;
|
|
262
|
+
const processingPhasePx = -((startFrame * orbitSpeedPx) % BORDER_PERIMETER);
|
|
263
|
+
stateBorder = animated
|
|
264
|
+
? renderOrbitingRect({
|
|
265
|
+
x: 8, y: 8, width: 128, height: 128, rx: 12, color: borderColor, animFrame,
|
|
266
|
+
speedPx: orbitSpeedPx, dashPx: 92,
|
|
267
|
+
phasePx: processingPhasePx,
|
|
268
|
+
glowOpacity: pulseOpacity * 0.72, coreOpacity: Math.min(1, pulseOpacity + 0.06), filterId,
|
|
269
|
+
})
|
|
270
|
+
: `<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)}"/>`;
|
|
271
|
+
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
272
|
}
|
|
116
|
-
else if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
273
|
+
else if (isAsking) {
|
|
274
|
+
// PERM / AWAITING: a SOLID amber border that BREATHES (full perimeter, no
|
|
275
|
+
// marching dashes) — a deliberately different motion + hue from RUNNING so
|
|
276
|
+
// "needs you" is unmistakable at a glance. Plus a bold amber PERM badge.
|
|
277
|
+
defs += blurDef;
|
|
278
|
+
const amber = sColor; // #f59e0b
|
|
279
|
+
const breathe = animated ? 0.45 + 0.55 * Math.abs(Math.sin(animFrame * 0.14)) : 1;
|
|
280
|
+
stateBorder = [
|
|
281
|
+
`<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})"/>`,
|
|
282
|
+
`<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${amber}" stroke-width="3" opacity="0.97"/>`,
|
|
283
|
+
].join('');
|
|
284
|
+
// Bold filled amber pill — far more legible than the old 5px dot.
|
|
285
|
+
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>`;
|
|
286
|
+
}
|
|
287
|
+
if (isActive) {
|
|
288
|
+
if (animated && isIdle)
|
|
132
289
|
defs += blurDef;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
290
|
+
activeRing = animated
|
|
291
|
+
? renderOrbitingRect({
|
|
292
|
+
x: 10.5, y: 10.5, width: 123, height: 123, rx: 10.5, color: '#60A5FA', animFrame,
|
|
293
|
+
speedPx: 16, phasePx: 170, dashPx: isIdle ? 72 : 52,
|
|
294
|
+
railOpacity: isIdle ? 0.20 : 0.10, glowOpacity: isIdle ? 0.42 : 0.24,
|
|
295
|
+
coreOpacity: isIdle ? 0.95 : 0.58, railWidth: 1, glowWidth: isIdle ? 3.6 : 2.6,
|
|
296
|
+
coreWidth: isIdle ? 1.8 : 1.3, filterId: isIdle ? filterId : undefined,
|
|
297
|
+
})
|
|
298
|
+
: `<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
299
|
}
|
|
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
|
-
` : '';
|
|
300
|
+
const watermark = `<g transform="translate(92, 80)" opacity="${isIdle ? '0.62' : '0.55'}">${agentLogoIcon(agent, 72, 1, 0, 0)}</g>`;
|
|
301
|
+
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
302
|
const toolStr = isWorking ? 'Running task' : modelText;
|
|
157
303
|
const elements = [
|
|
158
304
|
`<defs>${defs}</defs>`,
|
|
159
305
|
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
160
306
|
`<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,
|
|
307
|
+
stateBorder, activeRing, watermark, askDot, runBadge, badgeObj,
|
|
168
308
|
`<text x="20" y="32" font-size="17" font-weight="800" text-anchor="start" fill="${colorText}" font-family="${fontFam}">${escXml(stateLbl)}</text>`,
|
|
169
309
|
`<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
|
|
310
|
+
`<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>`,
|
|
311
|
+
// Stale overlay: the daemon stopped responding (no pings/state for the
|
|
312
|
+
// stale window) but hasn't yet hit the hard disconnect. Dim the last-known
|
|
313
|
+
// render and flag it so the state on screen isn't mistaken for live.
|
|
314
|
+
options?.isStale
|
|
315
|
+
? `<rect width="${SIZE}" height="${SIZE}" rx="16" fill="#0C0C0E" opacity="0.5"/>`
|
|
316
|
+
+ `<rect x="20" y="62" width="50" height="17" rx="8" fill="#71717A" opacity="0.92"/>`
|
|
317
|
+
+ `<text x="45" y="74" font-size="10" font-weight="800" text-anchor="middle" fill="#0C0C0E" font-family="${fontFam}">STALE</text>`
|
|
318
|
+
: '',
|
|
171
319
|
].join('');
|
|
172
320
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">${elements}</svg>`;
|
|
173
321
|
}
|
|
174
|
-
// ---- Empty Slot ----
|
|
175
322
|
export function renderEmptySlot() {
|
|
176
|
-
|
|
177
|
-
return svgFrame('#111111', label);
|
|
323
|
+
return renderQuietSlot();
|
|
178
324
|
}
|
|
179
|
-
export function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
325
|
+
export function renderQuietSlot() {
|
|
326
|
+
return svgFrame('#0a0a0a', '');
|
|
327
|
+
}
|
|
328
|
+
export function renderStatusCard(config) {
|
|
329
|
+
const colors = toneColors(config.tone);
|
|
330
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
331
|
+
const label = truncate(config.label, 14);
|
|
332
|
+
const subtitle = truncate(config.subtitle ?? '', 18);
|
|
333
|
+
// 22, not 17: the only detail we ship is the daemon install hint
|
|
334
|
+
// (`npx @agentdeck/setup`, 20 chars) on the D200H offline hero, and a 17-char
|
|
335
|
+
// cap rendered it as `npx @agentdeck/s…` — a truncated command is worse than
|
|
336
|
+
// none. At font-size 9 on the 144-wide canvas, 22 chars is ~99px, still well
|
|
337
|
+
// inside the frame.
|
|
338
|
+
const detail = truncate(config.detail ?? '', 22);
|
|
339
|
+
const elements = [
|
|
340
|
+
`<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"/>`,
|
|
341
|
+
renderGlyphIcon(config.icon, colors.icon, colors.accent, 72, 43, 0.94),
|
|
342
|
+
`<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>`,
|
|
343
|
+
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>` : '',
|
|
344
|
+
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>` : '',
|
|
345
|
+
].join('');
|
|
346
|
+
return svgFrame(colors.bg, elements);
|
|
347
|
+
}
|
|
348
|
+
export function renderDisconnectedSlot(config) {
|
|
349
|
+
if (config.kind === 'empty')
|
|
350
|
+
return renderQuietSlot();
|
|
351
|
+
if (config.col !== undefined &&
|
|
352
|
+
config.row !== undefined &&
|
|
353
|
+
config.cols !== undefined &&
|
|
354
|
+
config.rows !== undefined) {
|
|
355
|
+
return renderOpenAppGrid(config.col, config.row, config.cols, config.rows, config.label ?? PASSIVE_OFFLINE_LABEL, config.subtitle ?? OPEN_AGENTDECK_LABEL);
|
|
187
356
|
}
|
|
188
|
-
|
|
357
|
+
if (config.quadrant) {
|
|
358
|
+
return renderOpenAppQuadrant(config.quadrant, config.label ?? PASSIVE_OFFLINE_LABEL, config.subtitle ?? OPEN_AGENTDECK_LABEL);
|
|
359
|
+
}
|
|
360
|
+
return renderStatusCard({ icon: 'agentdeck', label: config.label ?? PASSIVE_OFFLINE_LABEL, subtitle: config.subtitle ?? OPEN_AGENTDECK_LABEL, detail: config.detail, tone: 'brand' });
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Renders a single 144×144 viewport into a 288×288 cluster hero. Each of the
|
|
364
|
+
* four center keys (tl/tr/bl/br) emits the same logical content offset by its
|
|
365
|
+
* quadrant; viewBox clipping keeps only the relevant 144×144 visible. The
|
|
366
|
+
* cluster reads as one card visually, with the inner panel + glyph + text
|
|
367
|
+
* spanning all four keys while each key keeps its own outer rounded bezel.
|
|
368
|
+
*/
|
|
369
|
+
export function renderOpenAppQuadrant(quadrant, label = PASSIVE_OFFLINE_LABEL, subtitle = OPEN_AGENTDECK_LABEL) {
|
|
370
|
+
const colors = toneColors('brand');
|
|
371
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
372
|
+
const offsetX = (quadrant === 'tr' || quadrant === 'br') ? -SIZE : 0;
|
|
373
|
+
const offsetY = (quadrant === 'bl' || quadrant === 'br') ? -SIZE : 0;
|
|
374
|
+
const gradId = `frame-bg-cluster-${quadrant}`;
|
|
375
|
+
// 288×288 cluster content. All coordinates are in the logical cluster space;
|
|
376
|
+
// the per-key viewBox + transform crops to one quadrant.
|
|
377
|
+
const cluster = [
|
|
378
|
+
`<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"/>`,
|
|
379
|
+
renderGlyphIcon('agentdeck', colors.icon, colors.accent, 144, 86, 1.88),
|
|
380
|
+
`<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>`,
|
|
381
|
+
`<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>`,
|
|
382
|
+
].join('');
|
|
383
|
+
return [
|
|
384
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">`,
|
|
385
|
+
`<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>`,
|
|
386
|
+
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
387
|
+
`<g transform="translate(${offsetX} ${offsetY})">${cluster}</g>`,
|
|
388
|
+
`</svg>`,
|
|
389
|
+
].join('');
|
|
189
390
|
}
|
|
190
|
-
// ---- Navigation Buttons ----
|
|
191
391
|
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);
|
|
392
|
+
return renderStatusCard({ icon: 'back', label: 'BACK', subtitle: 'sessions', tone: 'muted' });
|
|
195
393
|
}
|
|
196
394
|
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);
|
|
395
|
+
return renderStatusCard({ icon: 'more', label: 'MORE', subtitle: pageLabel, tone: 'info' });
|
|
201
396
|
}
|
|
202
397
|
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);
|
|
398
|
+
return renderStatusCard({ icon: 'esc', label: 'ESC', subtitle: active ? 'cancel' : 'idle', tone: active ? 'warning' : 'muted' });
|
|
211
399
|
}
|
|
212
400
|
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);
|
|
401
|
+
return renderStatusCard({ icon: 'stop', label: 'STOP', subtitle: active ? 'interrupt' : 'idle', tone: active ? 'danger' : 'muted' });
|
|
221
402
|
}
|
|
222
|
-
// ---- Detail View: Session Info ----
|
|
223
403
|
export function renderDetailInfo(session, state, tool, modelName, mode, displayName, effortLevel) {
|
|
224
404
|
if (!session)
|
|
225
405
|
return renderEmptySlot();
|
|
226
406
|
const agent = session.agentType || 'claude-code';
|
|
227
407
|
const nameForDisplay = displayName ?? session.projectName;
|
|
228
408
|
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
229
|
-
const
|
|
230
|
-
const
|
|
409
|
+
const effectiveState = state || session.state;
|
|
410
|
+
const sColor = stateColor(effectiveState);
|
|
411
|
+
const stateLbl = stateLabel(effectiveState, agent);
|
|
231
412
|
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;
|
|
413
|
+
const watermark = `<g transform="translate(92, 80)" opacity="0.42">${agentLogoIcon(agent, 48, 1, 0, 0)}</g>`;
|
|
414
|
+
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>`;
|
|
415
|
+
const toolDisplay = tool ? `▶ ${truncate(tool, 18)}` : stateLbl;
|
|
247
416
|
const elements = [
|
|
248
|
-
`<defs
|
|
417
|
+
`<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
418
|
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
250
419
|
`<rect x="8" y="8" width="128" height="128" rx="12" fill="#1C1F2E" opacity="0.8"/>`,
|
|
251
|
-
|
|
252
|
-
watermark,
|
|
253
|
-
badgeObj,
|
|
254
|
-
// Title
|
|
420
|
+
watermark, badgeObj,
|
|
255
421
|
`<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
422
|
(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
423
|
(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
424
|
`<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
425
|
].join('');
|
|
263
426
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">${elements}</svg>`;
|
|
264
427
|
}
|
|
265
|
-
|
|
428
|
+
function optionVisual(label) {
|
|
429
|
+
const lower = label.toLowerCase();
|
|
430
|
+
if (lower.includes('diff') || lower.includes('review') || lower.includes('view'))
|
|
431
|
+
return { icon: 'diff', tone: 'info' };
|
|
432
|
+
if (lower.startsWith('yes') || lower.startsWith('allow') || lower.startsWith('apply') || lower.includes("don't ask") || lower.includes('always'))
|
|
433
|
+
return { icon: 'allow', tone: 'ready' };
|
|
434
|
+
if (lower.startsWith('no') || lower.startsWith('deny') || lower.startsWith('reject') || lower.includes('cancel'))
|
|
435
|
+
return { icon: 'deny', tone: 'danger' };
|
|
436
|
+
return { icon: 'option', tone: 'purple' };
|
|
437
|
+
}
|
|
266
438
|
export function renderOptionButton(option, index) {
|
|
267
439
|
const label = option.label || `Option ${index + 1}`;
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
440
|
+
const { icon, tone } = optionVisual(label);
|
|
441
|
+
const colors = toneColors(tone);
|
|
442
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
443
|
+
const lines = wrapTextByWidth(label, 112, 13).slice(0, 2);
|
|
444
|
+
const startY = lines.length === 1 ? 103 : 96;
|
|
445
|
+
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>`;
|
|
446
|
+
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('');
|
|
447
|
+
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"/>`
|
|
448
|
+
+ renderGlyphIcon(icon, colors.icon, colors.accent, 72, 43, 0.84)
|
|
449
|
+
+ slotNum
|
|
450
|
+
+ textEls);
|
|
278
451
|
}
|
|
279
|
-
// ---- Detail View: Preset Action Button ----
|
|
280
452
|
export function renderPresetButton(label, iconSvg, color, textColor, subtitle, loading) {
|
|
453
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
281
454
|
if (loading) {
|
|
282
|
-
return svgFrame(color, iconSvg);
|
|
455
|
+
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
456
|
}
|
|
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
|
-
: '';
|
|
457
|
+
const labelEl = `<text x="72" y="100" text-anchor="middle" font-family="${fontFam}" font-size="16" font-weight="800" fill="${textColor}">${escXml(label)}</text>`;
|
|
458
|
+
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
459
|
return svgFrame(color, iconSvg + labelEl + subEl);
|
|
289
460
|
}
|
|
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);
|
|
461
|
+
export function renderInfoSlot(label, subtitle, icon = 'activity', tone = 'info', detail) {
|
|
462
|
+
return renderStatusCard({ icon, label, subtitle, detail, tone });
|
|
297
463
|
}
|
|
298
|
-
// ---- SVG Frame ----
|
|
299
464
|
export function svgFrame(bgColor, innerElements) {
|
|
300
|
-
// Use a random ID to prevent SVG constraint clashes
|
|
301
465
|
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
466
|
return [
|
|
309
467
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">`,
|
|
310
|
-
`<defs
|
|
468
|
+
`<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
469
|
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
312
470
|
`<rect x="8" y="8" width="128" height="128" rx="12" fill="#2C2C2E" opacity="0.6"/>`,
|
|
313
471
|
innerElements,
|
|
314
472
|
`</svg>`,
|
|
315
473
|
].join('');
|
|
316
474
|
}
|
|
475
|
+
export function renderOpenAppGrid(col, row, cols, rows, label = PASSIVE_OFFLINE_LABEL, subtitle = OPEN_AGENTDECK_LABEL) {
|
|
476
|
+
const colors = toneColors('brand');
|
|
477
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
478
|
+
const offsetX = -col * SIZE;
|
|
479
|
+
const offsetY = -row * SIZE;
|
|
480
|
+
const gradId = `frame-bg-grid-${col}-${row}`;
|
|
481
|
+
const minDim = Math.min(cols, rows);
|
|
482
|
+
const baseScale = Math.max(1.0, minDim * 0.8);
|
|
483
|
+
const totalW = cols * SIZE;
|
|
484
|
+
const totalH = rows * SIZE;
|
|
485
|
+
const padX = Math.max(8, 16 * cols / 4);
|
|
486
|
+
const padY = Math.max(8, 16 * rows / 2);
|
|
487
|
+
const rectW = totalW - padX * 2;
|
|
488
|
+
const rectH = totalH - padY * 2;
|
|
489
|
+
const rx = Math.max(12, 28 * minDim / 2);
|
|
490
|
+
const glyphX = totalW / 2;
|
|
491
|
+
const glyphY = totalH / 2 - (24 * baseScale);
|
|
492
|
+
const labelY = totalH / 2 + (20 * baseScale);
|
|
493
|
+
const subY = labelY + (28 * baseScale);
|
|
494
|
+
const fontSizeLabel = Math.max(16, Math.round(36 * (minDim / 2)));
|
|
495
|
+
const fontSizeSub = Math.max(11, Math.round(20 * (minDim / 2)));
|
|
496
|
+
const cluster = [
|
|
497
|
+
`<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"/>`,
|
|
498
|
+
renderGlyphIcon('agentdeck', colors.icon, colors.accent, glyphX, glyphY, baseScale),
|
|
499
|
+
`<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>`,
|
|
500
|
+
`<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>`,
|
|
501
|
+
].join('');
|
|
502
|
+
return [
|
|
503
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">`,
|
|
504
|
+
`<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>`,
|
|
505
|
+
`<rect width="${SIZE}" height="${SIZE}" rx="16" fill="url(#${gradId})"/>`,
|
|
506
|
+
`<g transform="translate(${offsetX} ${offsetY})">${cluster}</g>`,
|
|
507
|
+
`</svg>`,
|
|
508
|
+
].join('');
|
|
509
|
+
}
|
|
510
|
+
export function renderOfflineTouchStrip(index) {
|
|
511
|
+
const fontFam = 'Inter, -apple-system, system-ui, Helvetica Neue, sans-serif';
|
|
512
|
+
const colors = toneColors('brand');
|
|
513
|
+
const W_total = 800;
|
|
514
|
+
const H_total = 100;
|
|
515
|
+
const offsetX = -index * 200;
|
|
516
|
+
const gradId = `touchstrip-offline-bg-${index}`;
|
|
517
|
+
// Entire 800x100 content
|
|
518
|
+
const content = [
|
|
519
|
+
// Background card panel
|
|
520
|
+
`<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"/>`,
|
|
521
|
+
// Left side: AgentDeck brand mark (x=120, y=50)
|
|
522
|
+
renderGlyphIcon('agentdeck', colors.icon, colors.accent, 120, 50, 1.2),
|
|
523
|
+
// Center: Offline text and helper instruction
|
|
524
|
+
`<text x="400" y="44" text-anchor="middle" font-family="${fontFam}" font-size="22" font-weight="800" fill="${colors.text}">AGENTDECK OFFLINE</text>`,
|
|
525
|
+
`<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>`,
|
|
526
|
+
// Right side: offline icon (x=680, y=50)
|
|
527
|
+
renderGlyphIcon('offline', colors.icon, colors.accent, 680, 50, 1.2),
|
|
528
|
+
].join('');
|
|
529
|
+
return [
|
|
530
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 200 100">`,
|
|
531
|
+
`<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>`,
|
|
532
|
+
`<rect width="200" height="100" fill="url(#${gradId})"/>`,
|
|
533
|
+
`<g transform="translate(${offsetX} 0)">${content}</g>`,
|
|
534
|
+
`</svg>`
|
|
535
|
+
].join('');
|
|
536
|
+
}
|
|
317
537
|
//# sourceMappingURL=session-slot-renderer.js.map
|