@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.
Files changed (99) hide show
  1. package/dist/adapter.d.ts +3 -1
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js +24 -0
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/command-builders.d.ts +7 -2
  6. package/dist/command-builders.d.ts.map +1 -1
  7. package/dist/command-builders.js +17 -2
  8. package/dist/command-builders.js.map +1 -1
  9. package/dist/connection-status.d.ts +42 -0
  10. package/dist/connection-status.d.ts.map +1 -0
  11. package/dist/connection-status.js +52 -0
  12. package/dist/connection-status.js.map +1 -0
  13. package/dist/d200h-layout.d.ts +131 -0
  14. package/dist/d200h-layout.d.ts.map +1 -0
  15. package/dist/d200h-layout.js +627 -0
  16. package/dist/d200h-layout.js.map +1 -0
  17. package/dist/design-tokens.d.ts +259 -0
  18. package/dist/design-tokens.d.ts.map +1 -0
  19. package/dist/design-tokens.js +156 -0
  20. package/dist/design-tokens.js.map +1 -0
  21. package/dist/eval-schema.d.ts +26 -2
  22. package/dist/eval-schema.d.ts.map +1 -1
  23. package/dist/eval-schema.js.map +1 -1
  24. package/dist/format-utils.d.ts +25 -0
  25. package/dist/format-utils.d.ts.map +1 -1
  26. package/dist/format-utils.js +45 -0
  27. package/dist/format-utils.js.map +1 -1
  28. package/dist/gateway-protocol.d.ts +1 -1
  29. package/dist/gateway-protocol.js +1 -1
  30. package/dist/index.d.ts +8 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +8 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/llm-settings.d.ts +3 -3
  35. package/dist/llm-settings.d.ts.map +1 -1
  36. package/dist/llm-settings.js +3 -3
  37. package/dist/llm-settings.js.map +1 -1
  38. package/dist/net-utils.d.ts +2 -1
  39. package/dist/net-utils.d.ts.map +1 -1
  40. package/dist/net-utils.js +100 -3
  41. package/dist/net-utils.js.map +1 -1
  42. package/dist/pricing.d.ts +45 -0
  43. package/dist/pricing.d.ts.map +1 -0
  44. package/dist/pricing.js +93 -0
  45. package/dist/pricing.js.map +1 -0
  46. package/dist/protocol.d.ts +175 -5
  47. package/dist/protocol.d.ts.map +1 -1
  48. package/dist/protocol.js +11 -1
  49. package/dist/protocol.js.map +1 -1
  50. package/dist/sample.d.ts +152 -0
  51. package/dist/sample.d.ts.map +1 -0
  52. package/dist/sample.js +22 -0
  53. package/dist/sample.js.map +1 -0
  54. package/dist/session-utils.d.ts +82 -2
  55. package/dist/session-utils.d.ts.map +1 -1
  56. package/dist/session-utils.js +205 -10
  57. package/dist/session-utils.js.map +1 -1
  58. package/dist/state-colors.d.ts.map +1 -1
  59. package/dist/state-colors.js +2 -0
  60. package/dist/state-colors.js.map +1 -1
  61. package/dist/states.d.ts +1 -0
  62. package/dist/states.d.ts.map +1 -1
  63. package/dist/states.js +6 -1
  64. package/dist/states.js.map +1 -1
  65. package/dist/svg-renderers/agent-logos.d.ts +17 -5
  66. package/dist/svg-renderers/agent-logos.d.ts.map +1 -1
  67. package/dist/svg-renderers/agent-logos.js +177 -39
  68. package/dist/svg-renderers/agent-logos.js.map +1 -1
  69. package/dist/svg-renderers/session-slot-renderer.d.ts +59 -6
  70. package/dist/svg-renderers/session-slot-renderer.d.ts.map +1 -1
  71. package/dist/svg-renderers/session-slot-renderer.js +398 -209
  72. package/dist/svg-renderers/session-slot-renderer.js.map +1 -1
  73. package/dist/svg-renderers/text-utils.d.ts +14 -0
  74. package/dist/svg-renderers/text-utils.d.ts.map +1 -1
  75. package/dist/svg-renderers/text-utils.js +34 -0
  76. package/dist/svg-renderers/text-utils.js.map +1 -1
  77. package/dist/telemetry-envelope.d.ts +1 -1
  78. package/dist/telemetry-envelope.d.ts.map +1 -1
  79. package/dist/timeline-icons.d.ts +70 -0
  80. package/dist/timeline-icons.d.ts.map +1 -0
  81. package/dist/timeline-icons.js +161 -0
  82. package/dist/timeline-icons.js.map +1 -0
  83. package/dist/timeline-label.d.ts +38 -0
  84. package/dist/timeline-label.d.ts.map +1 -0
  85. package/dist/timeline-label.js +95 -0
  86. package/dist/timeline-label.js.map +1 -0
  87. package/dist/timeline-markdown.d.ts +80 -0
  88. package/dist/timeline-markdown.d.ts.map +1 -0
  89. package/dist/timeline-markdown.js +221 -0
  90. package/dist/timeline-markdown.js.map +1 -0
  91. package/dist/timeline-summarizer.d.ts +24 -0
  92. package/dist/timeline-summarizer.d.ts.map +1 -1
  93. package/dist/timeline-summarizer.js +55 -13
  94. package/dist/timeline-summarizer.js.map +1 -1
  95. package/dist/timeline.d.ts +72 -2
  96. package/dist/timeline.d.ts.map +1 -1
  97. package/dist/timeline.js +187 -10
  98. package/dist/timeline.js.map +1 -1
  99. 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 MAX_TEXT_PX = 124; // 144 - 10px padding each side
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 'OFFLINE';
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, '&amp;')
29
- .replace(/</g, '&lt;')
30
- .replace(/>/g, '&gt;')
31
- .replace(/"/g, '&quot;')
32
- .replace(/'/g, '&apos;');
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(modelName, maxLen);
51
- const combined = `${modelName} · ${effortLevel}`;
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(modelName, modelBudget) + effortSuffix;
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
- // Custom palette mimicking the premium demo
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 ? '#FDE68A' : isAsking ? '#FECACA' : p1;
217
+ const colorText = isWorking ? '#CCFBF1' : isAsking ? '#FCD34D' : p1;
83
218
  const gradId = `sd-bg-${agent}-${session.state || 'idle'}`;
84
- let defs = `
85
- <linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%">
86
- <stop offset="0%" stop-color="#1C1C1E"/>
87
- <stop offset="100%" stop-color="#0C0C0E"/>
88
- </linearGradient>
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
- const filterId = `pg-${animFrame}`;
94
- const blurDef = `
95
- <filter id="${filterId}" x="-10%" y="-10%" width="120%" height="120%">
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
- // Thick outer glow + crisp inner ring distinct "breathing" solid border.
108
- glowBorder =
109
- `<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${sColor}" stroke-width="4.5" opacity="${pulseOpacity.toFixed(2)}" filter="url(#${filterId})"/>` +
110
- `<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${sColor}" stroke-width="1.5" opacity="${(pulseOpacity * 0.9).toFixed(2)}"/>`;
111
- askDot = `
112
- <circle cx="114" cy="24" r="5" fill="#F5B942" filter="url(#${filterId})"/>
113
- <circle cx="114" cy="24" r="3" fill="#ffffff" />
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 (isWorking) {
117
- if (animated) {
118
- // Flowing-light dashed border SD/SD+ 150ms tick advances animFrame
119
- // dashoffset drifts chasing light.
120
- const perimeter = 128 * 4;
121
- const dashOffset = ((animFrame * 4) % perimeter).toFixed(0);
122
- defs += blurDef;
123
- glowBorder =
124
- `<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${sColor}" stroke-width="4.5" stroke-dasharray="22 14" stroke-dashoffset="-${dashOffset}" opacity="0.85" filter="url(#${filterId})"/>` +
125
- `<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${sColor}" stroke-width="1.5" stroke-dasharray="22 14" stroke-dashoffset="-${dashOffset}" opacity="0.95"/>`;
126
- }
127
- else {
128
- // Non-animated callers (D200H still-image pipeline): render a solid
129
- // glowing border + a play-chevron badge so WORKING is obvious without
130
- // motion. The amber border is still the primary signal; the ▶ badge
131
- // distinguishes this from the red awaiting state at a glance.
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
- glowBorder =
134
- `<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${sColor}" stroke-width="4.5" opacity="0.9" filter="url(#${filterId})"/>` +
135
- `<rect x="8" y="8" width="128" height="128" rx="12" fill="none" stroke="${sColor}" stroke-width="1.5" opacity="0.95"/>`;
136
- runBadge = `
137
- <rect x="100" y="14" width="28" height="16" rx="8" fill="${sColor}" opacity="0.9" />
138
- <text x="114" y="26" font-size="11" font-weight="800" text-anchor="middle" fill="#0C0C0E" font-family="${fontFam}">▶ RUN</text>
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
- // Simplified Agent watermark at bottom-right
143
- // agentLogoIcon renders at x,y with given width/height
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
- glowBorder,
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
- const label = `<text x="72" y="76" text-anchor="middle" font-family="Arial,sans-serif" font-size="12" fill="#333333">Empty</text>`;
177
- return svgFrame('#111111', label);
297
+ return renderQuietSlot();
178
298
  }
179
- export function renderNoDaemonSlot(slot) {
180
- if (slot === 0) {
181
- // START button — launches macOS app
182
- const elements = [
183
- `<text x="72" y="56" text-anchor="middle" font-family="Arial,sans-serif" font-size="14" fill="#94a3b8">AgentDeck</text>`,
184
- `<text x="72" y="96" text-anchor="middle" font-family="Arial,sans-serif" font-size="28" font-weight="bold" fill="#22c55e">\u25B6 START</text>`,
185
- ].join('');
186
- return svgFrame('#0f1a0f', elements);
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
- return svgFrame('#111111', '');
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
- const arrow = `<text x="72" y="76" text-anchor="middle" font-family="Arial,sans-serif" font-size="32" fill="#94a3b8">\u2190</text>`;
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
- const arrow = `<text x="72" y="64" text-anchor="middle" font-family="Arial,sans-serif" font-size="28" fill="#60a5fa">\u25B6</text>`;
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
- const c = active ? '#f97316' : '#a0855a';
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
- const c = active ? '#ef4444' : '#666666';
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 sColor = stateColor(session.state);
230
- const stateLbl = stateLabel(session.state, agent);
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 defs = `
233
- <linearGradient id="${gradId}" x1="0%" y1="0%" x2="0%" y2="100%">
234
- <stop offset="0%" stop-color="#151720"/>
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>${defs}</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
- leftStrip,
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
- // ---- Detail View: Option Button ----
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 lines = wrapTextByWidth(label, MAX_TEXT_PX, 16);
269
- const lineHeight = 20;
270
- const startY = lines.length === 1 ? 76 : 76 - ((lines.length - 1) * lineHeight) / 2;
271
- // Slot number (top-right)
272
- const slotNum = `<text x="${SIZE - 10}" y="18" text-anchor="end" font-family="Arial,sans-serif" font-size="13" fill="#ffffff" opacity="0.25">${index + 1}</text>`;
273
- const textEls = lines.slice(0, 3).map((line, i) => `<text x="72" y="${startY + i * lineHeight}" text-anchor="middle" font-family="Arial,sans-serif" font-size="16" font-weight="bold" fill="#ffffff">${escXml(line)}</text>`).join('');
274
- // Use different bg colors for visual distinction
275
- const colors = ['#1e3a5f', '#1e3a4a', '#2a1e4a', '#1e4a3a', '#3a1e2a'];
276
- const bgColor = colors[index % colors.length];
277
- return svgFrame(bgColor, slotNum + textEls);
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="Arial,sans-serif" font-size="16" font-weight="bold" fill="${textColor}">${escXml(label)}</text>`;
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
- // ---- Detail View: Info slot (tool, model/mode) ----
291
- export function renderInfoSlot(label, subtitle) {
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>${defs}</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