pmx-canvas 0.4.2 → 0.4.4
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/.github/extensions/pmx-canvas/extension.mjs +5 -2
- package/CHANGELOG.md +60 -0
- package/Readme.md +35 -12
- package/dist/canvas/global.css +92 -28
- package/dist/canvas/index.js +62 -62
- package/dist/canvas/surface-theme.css +52 -20
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/skills.d.ts +41 -0
- package/dist/types/client/state/sse-bridge.d.ts +1 -0
- package/dist/types/client/state/theme-override.d.ts +27 -0
- package/dist/types/shared/themes.d.ts +1 -1
- package/docs/RELEASE.md +14 -6
- package/docs/bun-webview-integration.md +2 -2
- package/docs/cli.md +24 -1
- package/docs/environment.md +2 -2
- package/docs/http-api.md +14 -3
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -2
- package/docs/screenshot.png +0 -0
- package/package.json +3 -3
- package/skills/pmx-canvas/SKILL.md +18 -4
- package/skills/pmx-canvas/references/full-reference.md +1 -1
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +1 -0
- package/src/cli/commands/skills.ts +183 -0
- package/src/cli/index.ts +3 -1
- package/src/cli/shared.ts +1 -0
- package/src/client/App.tsx +85 -78
- package/src/client/canvas/CommandPalette.tsx +4 -0
- package/src/client/nodes/ExtAppFrame.tsx +15 -4
- package/src/client/nodes/McpAppNode.tsx +6 -1
- package/src/client/state/sse-bridge.ts +22 -5
- package/src/client/state/theme-override.ts +83 -0
- package/src/client/theme/global.css +92 -28
- package/src/client/theme/surface-theme.css +52 -20
- package/src/server/operations/ops/canvas-wire.ts +10 -2
- package/src/server/server.ts +13 -2
- package/src/shared/themes.ts +3 -1
package/src/cli/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ const AGENT_COMMANDS = new Set([
|
|
|
66
66
|
'validate',
|
|
67
67
|
'serve',
|
|
68
68
|
'copilot',
|
|
69
|
+
'skills',
|
|
69
70
|
]);
|
|
70
71
|
|
|
71
72
|
const firstArg = args[0] ?? '';
|
|
@@ -253,7 +254,7 @@ Server options:
|
|
|
253
254
|
--log-file=PATH Daemon log file (default: ./.pmx-canvas/daemon-${port}.log)
|
|
254
255
|
--pid-file=PATH Optional daemon PID file (default: ./.pmx-canvas/daemon-${port}.pid)
|
|
255
256
|
--wait-ms=MS Health-check wait budget for daemon mode (default: 10000)
|
|
256
|
-
--theme=THEME Theme: dark (default), light, high-contrast, midnight, sepia, arctic, ember, forest
|
|
257
|
+
--theme=THEME Theme: dark (default), light, high-contrast, midnight, sepia, arctic, ember, forest, volt
|
|
257
258
|
--webview-automation Start a headless Bun.WebView automation session for /workbench
|
|
258
259
|
--webview-backend=BACKEND Bun.WebView backend: chrome or webkit
|
|
259
260
|
--webview-width=PX Automation WebView width (default: 1280)
|
|
@@ -343,6 +344,7 @@ Examples:
|
|
|
343
344
|
pmx-canvas watch --events context-pin,move-end Watch semantic deltas
|
|
344
345
|
pmx-canvas clear --dry-run Preview destructive op
|
|
345
346
|
pmx-canvas copilot install-extension --dry-run Preview Copilot adapter install
|
|
347
|
+
pmx-canvas skills sync --yes Refresh installed pmx-canvas skill copies in this workspace (--check to detect drift)
|
|
346
348
|
`);
|
|
347
349
|
process.exit(0);
|
|
348
350
|
}
|
package/src/cli/shared.ts
CHANGED
package/src/client/App.tsx
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
IconZoomIn,
|
|
60
60
|
IconZoomOut,
|
|
61
61
|
} from './icons';
|
|
62
|
+
import { clearThemeOverride } from './state/theme-override';
|
|
62
63
|
import { invalidateTokenCache } from './theme/tokens';
|
|
63
64
|
import { MOD_KEY } from './utils/platform';
|
|
64
65
|
import {
|
|
@@ -184,6 +185,9 @@ function Toolbar({
|
|
|
184
185
|
}, [openMenu]);
|
|
185
186
|
|
|
186
187
|
const applyTheme = (next: CanvasThemeName) => {
|
|
188
|
+
// An explicit pick ends any ?theme= session override (host default) and
|
|
189
|
+
// returns this client to the shared server-global theme it just saved.
|
|
190
|
+
clearThemeOverride();
|
|
187
191
|
document.documentElement.setAttribute('data-theme', next);
|
|
188
192
|
invalidateTokenCache();
|
|
189
193
|
canvasTheme.value = next;
|
|
@@ -307,18 +311,27 @@ function Toolbar({
|
|
|
307
311
|
<IconMinimap />
|
|
308
312
|
</button>
|
|
309
313
|
</ToolbarHint>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
314
|
+
{/* Anchor wrapper: the theme menu must open under THIS control, not at
|
|
315
|
+
the toolbar-group's right edge (0.4.2 report Finding T). */}
|
|
316
|
+
<span class="toolbar-menu-anchor">
|
|
317
|
+
<ToolbarHint label="Theme" detail={`Current theme: ${CANVAS_THEME_META[activeTheme].label}`}>
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
onClick={() => setOpenMenu(openMenu === 'theme' ? null : 'theme')}
|
|
321
|
+
aria-label="Choose theme"
|
|
322
|
+
aria-haspopup="menu"
|
|
323
|
+
aria-expanded={openMenu === 'theme'}
|
|
324
|
+
style={{ color: openMenu === 'theme' ? 'var(--c-accent)' : undefined }}
|
|
325
|
+
>
|
|
326
|
+
{canvasThemeScheme(activeTheme) === 'dark' ? <IconSun /> : <IconMoon />}
|
|
327
|
+
</button>
|
|
328
|
+
</ToolbarHint>
|
|
329
|
+
{openMenu === 'theme' && (
|
|
330
|
+
<div class="toolbar-menu" role="menu" aria-label="Theme">
|
|
331
|
+
{themeMenuItems}
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</span>
|
|
322
335
|
<ToolbarHint label="Snapshots" detail="Capture and restore canvas states" align="end">
|
|
323
336
|
<button
|
|
324
337
|
ref={snapshotBtnRef}
|
|
@@ -333,7 +346,7 @@ function Toolbar({
|
|
|
333
346
|
</span>
|
|
334
347
|
|
|
335
348
|
{/* Narrow screens: everything collapsed lives in this single menu. */}
|
|
336
|
-
<span class="toolbar-more">
|
|
349
|
+
<span class="toolbar-more toolbar-menu-anchor">
|
|
337
350
|
<ToolbarHint label="More" detail="All canvas actions" align="end">
|
|
338
351
|
<button
|
|
339
352
|
type="button"
|
|
@@ -348,6 +361,65 @@ function Toolbar({
|
|
|
348
361
|
</span>
|
|
349
362
|
</button>
|
|
350
363
|
</ToolbarHint>
|
|
364
|
+
{openMenu === 'more' && (
|
|
365
|
+
<div class="toolbar-menu" role="menu" aria-label="Canvas actions">
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
class="toolbar-menu-item"
|
|
369
|
+
onClick={menuAction(() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange()))}
|
|
370
|
+
>
|
|
371
|
+
<IconArrange />
|
|
372
|
+
<span>Arrange layout</span>
|
|
373
|
+
</button>
|
|
374
|
+
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleMinimap)}>
|
|
375
|
+
<IconMinimap />
|
|
376
|
+
<span>{minimapVisible ? 'Hide minimap' : 'Show minimap'}</span>
|
|
377
|
+
</button>
|
|
378
|
+
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleSnapshot)}>
|
|
379
|
+
<IconSnapshot />
|
|
380
|
+
<span>Snapshots</span>
|
|
381
|
+
</button>
|
|
382
|
+
<div class="toolbar-menu-separator" />
|
|
383
|
+
<button
|
|
384
|
+
type="button"
|
|
385
|
+
class="toolbar-menu-item"
|
|
386
|
+
onClick={menuAction(() => sendIntent('trace-toggle', { enabled: !isTraceOn }))}
|
|
387
|
+
>
|
|
388
|
+
<IconTrace />
|
|
389
|
+
<span>{isTraceOn ? 'Disable trace' : 'Enable trace'}</span>
|
|
390
|
+
</button>
|
|
391
|
+
{(isTraceOn || traceNodeCount > 0) && (
|
|
392
|
+
<button type="button" class="toolbar-menu-item" onClick={menuAction(() => sendIntent('trace-clear'))}>
|
|
393
|
+
<IconClearTrace />
|
|
394
|
+
<span>Clear trace</span>
|
|
395
|
+
</button>
|
|
396
|
+
)}
|
|
397
|
+
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleAnnotationMode)}>
|
|
398
|
+
<IconPen />
|
|
399
|
+
<span>{annotationTool === 'pen' ? 'Stop annotating' : 'Annotate canvas'}</span>
|
|
400
|
+
</button>
|
|
401
|
+
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleAnnotationEraser)}>
|
|
402
|
+
<IconEraser />
|
|
403
|
+
<span>{annotationTool === 'eraser' ? 'Stop erasing' : 'Erase annotations'}</span>
|
|
404
|
+
</button>
|
|
405
|
+
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleTextAnnotation)}>
|
|
406
|
+
<IconTextAnnotation />
|
|
407
|
+
<span>{annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}</span>
|
|
408
|
+
</button>
|
|
409
|
+
<div class="toolbar-menu-separator" />
|
|
410
|
+
<button type="button" class="toolbar-menu-item" onClick={menuAction(onOpenPalette)}>
|
|
411
|
+
<IconSearch />
|
|
412
|
+
<span>Search nodes and actions</span>
|
|
413
|
+
</button>
|
|
414
|
+
<button type="button" class="toolbar-menu-item" onClick={menuAction(onOpenShortcuts)}>
|
|
415
|
+
<IconShortcuts />
|
|
416
|
+
<span>Keyboard shortcuts</span>
|
|
417
|
+
</button>
|
|
418
|
+
<div class="toolbar-menu-separator" />
|
|
419
|
+
<div class="toolbar-menu-heading">Theme</div>
|
|
420
|
+
{themeMenuItems}
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
351
423
|
</span>
|
|
352
424
|
</div>
|
|
353
425
|
|
|
@@ -443,71 +515,6 @@ function Toolbar({
|
|
|
443
515
|
{countsLabel}
|
|
444
516
|
</span>
|
|
445
517
|
</div>
|
|
446
|
-
|
|
447
|
-
{openMenu === 'theme' && (
|
|
448
|
-
<div class="toolbar-menu" role="menu" aria-label="Theme">
|
|
449
|
-
{themeMenuItems}
|
|
450
|
-
</div>
|
|
451
|
-
)}
|
|
452
|
-
{openMenu === 'more' && (
|
|
453
|
-
<div class="toolbar-menu" role="menu" aria-label="Canvas actions">
|
|
454
|
-
<button
|
|
455
|
-
type="button"
|
|
456
|
-
class="toolbar-menu-item"
|
|
457
|
-
onClick={menuAction(() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange()))}
|
|
458
|
-
>
|
|
459
|
-
<IconArrange />
|
|
460
|
-
<span>Arrange layout</span>
|
|
461
|
-
</button>
|
|
462
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleMinimap)}>
|
|
463
|
-
<IconMinimap />
|
|
464
|
-
<span>{minimapVisible ? 'Hide minimap' : 'Show minimap'}</span>
|
|
465
|
-
</button>
|
|
466
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleSnapshot)}>
|
|
467
|
-
<IconSnapshot />
|
|
468
|
-
<span>Snapshots</span>
|
|
469
|
-
</button>
|
|
470
|
-
<div class="toolbar-menu-separator" />
|
|
471
|
-
<button
|
|
472
|
-
type="button"
|
|
473
|
-
class="toolbar-menu-item"
|
|
474
|
-
onClick={menuAction(() => sendIntent('trace-toggle', { enabled: !isTraceOn }))}
|
|
475
|
-
>
|
|
476
|
-
<IconTrace />
|
|
477
|
-
<span>{isTraceOn ? 'Disable trace' : 'Enable trace'}</span>
|
|
478
|
-
</button>
|
|
479
|
-
{(isTraceOn || traceNodeCount > 0) && (
|
|
480
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(() => sendIntent('trace-clear'))}>
|
|
481
|
-
<IconClearTrace />
|
|
482
|
-
<span>Clear trace</span>
|
|
483
|
-
</button>
|
|
484
|
-
)}
|
|
485
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleAnnotationMode)}>
|
|
486
|
-
<IconPen />
|
|
487
|
-
<span>{annotationTool === 'pen' ? 'Stop annotating' : 'Annotate canvas'}</span>
|
|
488
|
-
</button>
|
|
489
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleAnnotationEraser)}>
|
|
490
|
-
<IconEraser />
|
|
491
|
-
<span>{annotationTool === 'eraser' ? 'Stop erasing' : 'Erase annotations'}</span>
|
|
492
|
-
</button>
|
|
493
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onToggleTextAnnotation)}>
|
|
494
|
-
<IconTextAnnotation />
|
|
495
|
-
<span>{annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}</span>
|
|
496
|
-
</button>
|
|
497
|
-
<div class="toolbar-menu-separator" />
|
|
498
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onOpenPalette)}>
|
|
499
|
-
<IconSearch />
|
|
500
|
-
<span>Search nodes and actions</span>
|
|
501
|
-
</button>
|
|
502
|
-
<button type="button" class="toolbar-menu-item" onClick={menuAction(onOpenShortcuts)}>
|
|
503
|
-
<IconShortcuts />
|
|
504
|
-
<span>Keyboard shortcuts</span>
|
|
505
|
-
</button>
|
|
506
|
-
<div class="toolbar-menu-separator" />
|
|
507
|
-
<div class="toolbar-menu-heading">Theme</div>
|
|
508
|
-
{themeMenuItems}
|
|
509
|
-
</div>
|
|
510
|
-
)}
|
|
511
518
|
</div>
|
|
512
519
|
);
|
|
513
520
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { createNodeFromClient, saveCanvasTheme } from '../state/intent-bridge';
|
|
12
12
|
import { TYPE_LABELS, type CanvasNodeState } from '../types';
|
|
13
13
|
import { invalidateTokenCache } from '../theme/tokens';
|
|
14
|
+
import { clearThemeOverride } from '../state/theme-override';
|
|
14
15
|
|
|
15
16
|
import { MOD_KEY } from '../utils/platform';
|
|
16
17
|
|
|
@@ -174,6 +175,9 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
|
|
|
174
175
|
badge: 'THEME',
|
|
175
176
|
action: () => {
|
|
176
177
|
const next = canvasTheme.value === 'dark' ? 'light' : 'dark';
|
|
178
|
+
// An explicit pick ends any ?theme= session override, same as the
|
|
179
|
+
// toolbar picker — otherwise the session stays sticky-overridden.
|
|
180
|
+
clearThemeOverride();
|
|
177
181
|
document.documentElement.setAttribute('data-theme', next);
|
|
178
182
|
invalidateTokenCache();
|
|
179
183
|
canvasTheme.value = next;
|
|
@@ -528,9 +528,17 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
528
528
|
latestToolInputRef.current = toolInput;
|
|
529
529
|
latestToolResultRef.current = toolResult;
|
|
530
530
|
|
|
531
|
+
// Finding U (0.4.2 report): a stressed reconnect can leave sessionStatus
|
|
532
|
+
// 'error' (e.g. "MCP error -32001: Request timed out") while the cached app
|
|
533
|
+
// document still renders fine below. When content IS available, present the
|
|
534
|
+
// error as a degraded-interactivity notice, not a full-alarm banner over a
|
|
535
|
+
// working diagram.
|
|
536
|
+
const sessionErrorWithContent = sessionStatus === 'error' && typeof html === 'string' && html.length > 0;
|
|
531
537
|
const sessionUnavailableMessage =
|
|
532
538
|
sessionStatus === 'error'
|
|
533
|
-
?
|
|
539
|
+
? sessionErrorWithContent
|
|
540
|
+
? `Live session unavailable — showing saved content. Reopen the app to restore interactivity.${sessionError ? ` (${sessionError})` : ''}`
|
|
541
|
+
: (sessionError ?? 'Saved app session is unavailable. Reopen the app to restore interactivity.')
|
|
534
542
|
: 'Reconnecting saved app session...';
|
|
535
543
|
|
|
536
544
|
const flushToolResult = (bridge: AppBridge | null): Promise<void> | null => {
|
|
@@ -1040,9 +1048,12 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1040
1048
|
style={{
|
|
1041
1049
|
padding: '6px 10px',
|
|
1042
1050
|
fontSize: '11px',
|
|
1043
|
-
background:
|
|
1044
|
-
|
|
1045
|
-
|
|
1051
|
+
background:
|
|
1052
|
+
sessionStatus === 'error' && !sessionErrorWithContent ? 'var(--c-danger-12)' : 'var(--c-warn-10)',
|
|
1053
|
+
color: sessionStatus === 'error' && !sessionErrorWithContent ? 'var(--c-danger)' : 'var(--c-warn)',
|
|
1054
|
+
borderBottom: `1px solid ${
|
|
1055
|
+
sessionStatus === 'error' && !sessionErrorWithContent ? 'var(--c-danger-12)' : 'var(--c-warn-15)'
|
|
1056
|
+
}`,
|
|
1046
1057
|
}}
|
|
1047
1058
|
>
|
|
1048
1059
|
{sessionUnavailableMessage}
|
|
@@ -3,6 +3,7 @@ import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
|
|
|
3
3
|
import { canvasThemeScheme } from '../../shared/themes.js';
|
|
4
4
|
import type { CanvasNodeState } from '../types';
|
|
5
5
|
import { axSurfaceState, canvasTheme } from '../state/canvas-store';
|
|
6
|
+
import { iframeMode } from '../state/iframe-mode';
|
|
6
7
|
import { shouldContentFitIframeNode } from '../canvas/auto-fit';
|
|
7
8
|
import { ExtAppFrame } from './ExtAppFrame';
|
|
8
9
|
import { useAxSurfaceBridge } from './use-ax-surface-bridge';
|
|
@@ -185,7 +186,11 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
185
186
|
class="mcp-app-frame"
|
|
186
187
|
sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
187
188
|
allow="clipboard-read; clipboard-write"
|
|
188
|
-
loading
|
|
189
|
+
// Lazy loading only helps src mode (defers the HTTP request for
|
|
190
|
+
// off-screen nodes). In srcdoc mode the content is already inline —
|
|
191
|
+
// lazy would just delay PAINTING until the node scrolls near the
|
|
192
|
+
// viewport, which reads as "blank until opened" (Amp portal report).
|
|
193
|
+
loading={iframeMode.value === 'srcdoc' ? undefined : 'lazy'}
|
|
189
194
|
onLoad={pushAxState}
|
|
190
195
|
style={{ flex: 1, minHeight: 0, width: '100%' }}
|
|
191
196
|
title={`MCP App: ${sourceServer}`}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
workbenchConnectionEpoch,
|
|
27
27
|
} from './canvas-store';
|
|
28
28
|
import { fetchAxSurfaceState } from './intent-bridge';
|
|
29
|
+
import { initSessionThemeOverride, themeOverrideActive } from './theme-override';
|
|
29
30
|
import { DEFAULT_POSITIONS, makeNodeState } from './node-factory';
|
|
30
31
|
import { invalidateTokenCache } from '../theme/tokens';
|
|
31
32
|
import { resetAttentionBridge, syncAttentionFromSse } from './attention-bridge';
|
|
@@ -56,10 +57,17 @@ let pollSeq: number | null = null;
|
|
|
56
57
|
let pollGeneration = 0;
|
|
57
58
|
let sseFirstEventWatchdog: ReturnType<typeof setTimeout> | null = null;
|
|
58
59
|
|
|
59
|
-
function forcedTransport(): 'poll' | 'sse' | null {
|
|
60
|
-
if (typeof location === 'undefined') return null;
|
|
60
|
+
export function forcedTransport(): 'poll' | 'sse' | null {
|
|
61
|
+
if (typeof location === 'undefined' || typeof window === 'undefined') return null;
|
|
61
62
|
const value = new URLSearchParams(location.search).get('transport');
|
|
62
|
-
|
|
63
|
+
if (value === 'poll' || value === 'sse') return value;
|
|
64
|
+
// Amp orbs: the portal proxy buffers SSE, so auto mode burns the 3s
|
|
65
|
+
// watchdog before falling back — and on slow proxy days that trips the
|
|
66
|
+
// boot modal. The server stamps the orb flag into the page (canvasSpaHtml);
|
|
67
|
+
// go straight to the transport that works there. `?transport=sse` above
|
|
68
|
+
// still overrides for diagnosis.
|
|
69
|
+
if ((window as Window & { __PMX_AMP_ORB?: unknown }).__PMX_AMP_ORB === true) return 'poll';
|
|
70
|
+
return null;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
// Maps responseNodeId → thread prompt node ID so response deltas/completions
|
|
@@ -399,7 +407,9 @@ function handleConnected(data: Record<string, unknown>): void {
|
|
|
399
407
|
connectionStatus.value = 'connected';
|
|
400
408
|
// Reconnect marker for holders of server-minted URLs (Finding S).
|
|
401
409
|
workbenchConnectionEpoch.value += 1;
|
|
402
|
-
|
|
410
|
+
// A ?theme= session override (host-default theming) wins over the
|
|
411
|
+
// server-global theme for THIS client only.
|
|
412
|
+
if (typeof data.theme === 'string' && !themeOverrideActive()) {
|
|
403
413
|
applyCanvasTheme(data.theme);
|
|
404
414
|
}
|
|
405
415
|
if (data.ledgerSummary) {
|
|
@@ -890,7 +900,7 @@ function handleTraceState(data: Record<string, unknown>): void {
|
|
|
890
900
|
}
|
|
891
901
|
|
|
892
902
|
function handleThemeChanged(data: Record<string, unknown>): void {
|
|
893
|
-
if (typeof data.theme === 'string') {
|
|
903
|
+
if (typeof data.theme === 'string' && !themeOverrideActive()) {
|
|
894
904
|
applyCanvasTheme(data.theme);
|
|
895
905
|
}
|
|
896
906
|
}
|
|
@@ -1082,6 +1092,9 @@ function startPollingTransport(): () => void {
|
|
|
1082
1092
|
}
|
|
1083
1093
|
|
|
1084
1094
|
export function connectSSE(): () => void {
|
|
1095
|
+
// Host-default theming: a ?theme= param applies before the first server
|
|
1096
|
+
// frame so an embedding host panel never flashes the server-global theme.
|
|
1097
|
+
initSessionThemeOverride(applyCanvasTheme);
|
|
1085
1098
|
savedLayout = restoreLayout();
|
|
1086
1099
|
ensureStatusNode();
|
|
1087
1100
|
hasInitialServerLayout.value = false;
|
|
@@ -1118,6 +1131,10 @@ export function connectSSE(): () => void {
|
|
|
1118
1131
|
sseFirstEventWatchdog = null;
|
|
1119
1132
|
if (gotFirstEvent || eventSource !== source) return;
|
|
1120
1133
|
console.warn('[sse-bridge] No SSE event within watchdog window — switching to polling transport.');
|
|
1134
|
+
// Give the bootstrap card a fresh deadline for the fallback transport so
|
|
1135
|
+
// the "did not finish booting" modal doesn't fire while polling is
|
|
1136
|
+
// already recovering the boot (the inline bootstrap script listens).
|
|
1137
|
+
window.dispatchEvent(new Event('pmx-canvas-boot-extend'));
|
|
1121
1138
|
source.close();
|
|
1122
1139
|
eventSource = null;
|
|
1123
1140
|
startPollingTransport();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session theme override — host-default theming.
|
|
3
|
+
*
|
|
4
|
+
* Hosts that embed the workbench (the GitHub Copilot panel, the Claude Code
|
|
5
|
+
* desktop browser pane, ChatGPT-style app browsers, …) can open it with
|
|
6
|
+
* `?theme=<name>` or `?theme=auto` to give THEIR panel a fitting default
|
|
7
|
+
* without touching the server-global theme every other client sees. The
|
|
8
|
+
* override is client-local: server theme frames are ignored while it is
|
|
9
|
+
* active, and it ends the moment the user explicitly picks a theme from the
|
|
10
|
+
* picker (which then applies + saves globally as usual).
|
|
11
|
+
*
|
|
12
|
+
* `theme=auto` follows the host's prefers-color-scheme live, mapping to the
|
|
13
|
+
* built-in light/dark themes — so a light-mode host panel gets a light canvas
|
|
14
|
+
* by default and flips with the host appearance.
|
|
15
|
+
*/
|
|
16
|
+
import { type CanvasThemeName, isCanvasTheme } from '../../shared/themes.js';
|
|
17
|
+
|
|
18
|
+
let active = false;
|
|
19
|
+
// Latched when the user explicitly ends the override: the URL param survives
|
|
20
|
+
// reconnects (connectSSE re-inits on every transport drop), but the user's
|
|
21
|
+
// choice must stick for the page lifetime — only a reload re-activates.
|
|
22
|
+
let userEnded = false;
|
|
23
|
+
let schemeQuery: MediaQueryList | null = null;
|
|
24
|
+
let schemeListener: (() => void) | null = null;
|
|
25
|
+
|
|
26
|
+
export function themeOverrideActive(): boolean {
|
|
27
|
+
return active;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Test-only: reset the module state, including the page-lifetime latch. */
|
|
31
|
+
export function resetThemeOverrideForTests(): void {
|
|
32
|
+
active = false;
|
|
33
|
+
userEnded = false;
|
|
34
|
+
detachSchemeListener();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The user picked a theme explicitly — the session override ends for good. */
|
|
38
|
+
export function clearThemeOverride(): void {
|
|
39
|
+
active = false;
|
|
40
|
+
userEnded = true;
|
|
41
|
+
detachSchemeListener();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detachSchemeListener(): void {
|
|
45
|
+
if (schemeQuery && schemeListener) {
|
|
46
|
+
schemeQuery.removeEventListener('change', schemeListener);
|
|
47
|
+
}
|
|
48
|
+
schemeQuery = null;
|
|
49
|
+
schemeListener = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sessionThemeParam(): CanvasThemeName | 'auto' | null {
|
|
53
|
+
// DOM-less callers (server-side rendering of the module graph, unit tests
|
|
54
|
+
// that import the sse-bridge without a DOM) have no override.
|
|
55
|
+
if (typeof window === 'undefined' || !window.location) return null;
|
|
56
|
+
const value = new URLSearchParams(window.location.search).get('theme');
|
|
57
|
+
if (value === 'auto') return 'auto';
|
|
58
|
+
return isCanvasTheme(value) ? value : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Activate the override from the URL (no-op without a valid `?theme=`).
|
|
63
|
+
* `apply` is the canvas theme applier (client-side only — never saved).
|
|
64
|
+
*/
|
|
65
|
+
export function initSessionThemeOverride(apply: (theme: CanvasThemeName) => void): void {
|
|
66
|
+
if (userEnded) return;
|
|
67
|
+
const param = sessionThemeParam();
|
|
68
|
+
if (!param) return;
|
|
69
|
+
// Re-init happens on every reconnect — replace any prior listener instead
|
|
70
|
+
// of accumulating one per reconnect cycle.
|
|
71
|
+
detachSchemeListener();
|
|
72
|
+
active = true;
|
|
73
|
+
if (param !== 'auto') {
|
|
74
|
+
apply(param);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
schemeQuery = window.matchMedia('(prefers-color-scheme: light)');
|
|
78
|
+
schemeListener = () => {
|
|
79
|
+
if (active && schemeQuery) apply(schemeQuery.matches ? 'light' : 'dark');
|
|
80
|
+
};
|
|
81
|
+
schemeListener();
|
|
82
|
+
schemeQuery.addEventListener('change', schemeListener);
|
|
83
|
+
}
|
|
@@ -337,16 +337,16 @@
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
:root[data-theme="ember"] {
|
|
340
|
-
/* ── Core palette (ember · charcoal + vivid
|
|
341
|
-
--c-bg: #
|
|
342
|
-
--c-panel: #
|
|
343
|
-
--c-panel-soft: #
|
|
344
|
-
--c-line: #
|
|
345
|
-
--c-text: #
|
|
346
|
-
--c-text-soft: #
|
|
347
|
-
--c-muted: #
|
|
348
|
-
--c-dim: #
|
|
349
|
-
--c-accent: #
|
|
340
|
+
/* ── Core palette (ember · neutral charcoal + vivid orange-red) ─── */
|
|
341
|
+
--c-bg: #0B0B0C;
|
|
342
|
+
--c-panel: #161618;
|
|
343
|
+
--c-panel-soft: #101012;
|
|
344
|
+
--c-line: #29292C;
|
|
345
|
+
--c-text: #EDEBE6;
|
|
346
|
+
--c-text-soft: #CBC9C2;
|
|
347
|
+
--c-muted: #96948D;
|
|
348
|
+
--c-dim: #66655F;
|
|
349
|
+
--c-accent: #F34E1C;
|
|
350
350
|
--c-ok: #4CC38A;
|
|
351
351
|
--c-warn: #F5B944;
|
|
352
352
|
--c-warn-alt: #FFB300;
|
|
@@ -354,23 +354,23 @@
|
|
|
354
354
|
--c-purple: #A78BFA;
|
|
355
355
|
--c-thinking: #A78BFA;
|
|
356
356
|
--c-subagent: #38BDF8;
|
|
357
|
-
--c-input-bg: #
|
|
358
|
-
--c-dot-grid: rgba(
|
|
359
|
-
--c-panel-overlay: rgba(
|
|
360
|
-
--c-panel-glass: rgba(
|
|
357
|
+
--c-input-bg: #101012;
|
|
358
|
+
--c-dot-grid: rgba(243, 78, 28, 0.08);
|
|
359
|
+
--c-panel-overlay: rgba(11, 11, 12, 0.6);
|
|
360
|
+
--c-panel-glass: rgba(22, 22, 24, 0.92);
|
|
361
361
|
--c-shadow: rgba(0, 0, 0, 0.3);
|
|
362
362
|
--c-shadow-heavy: rgba(0, 0, 0, 0.5);
|
|
363
|
-
--c-glow-accent: rgba(
|
|
363
|
+
--c-glow-accent: rgba(243, 78, 28, 0.15);
|
|
364
364
|
--c-surface-hover: rgba(255, 255, 255, 0.06);
|
|
365
365
|
--c-surface-subtle: rgba(255, 255, 255, 0.03);
|
|
366
366
|
--c-surface-th: rgba(255, 255, 255, 0.04);
|
|
367
|
-
--c-accent-8: rgba(
|
|
368
|
-
--c-accent-10: rgba(
|
|
369
|
-
--c-accent-12: rgba(
|
|
370
|
-
--c-accent-15: rgba(
|
|
371
|
-
--c-accent-25: rgba(
|
|
372
|
-
--c-accent-30: rgba(
|
|
373
|
-
--c-accent-40: rgba(
|
|
367
|
+
--c-accent-8: rgba(243, 78, 28, 0.08);
|
|
368
|
+
--c-accent-10: rgba(243, 78, 28, 0.1);
|
|
369
|
+
--c-accent-12: rgba(243, 78, 28, 0.12);
|
|
370
|
+
--c-accent-15: rgba(243, 78, 28, 0.15);
|
|
371
|
+
--c-accent-25: rgba(243, 78, 28, 0.25);
|
|
372
|
+
--c-accent-30: rgba(243, 78, 28, 0.3);
|
|
373
|
+
--c-accent-40: rgba(243, 78, 28, 0.4);
|
|
374
374
|
--c-ok-10: rgba(76, 195, 138, 0.1);
|
|
375
375
|
--c-ok-12: rgba(76, 195, 138, 0.12);
|
|
376
376
|
--c-ok-20: rgba(76, 195, 138, 0.2);
|
|
@@ -383,12 +383,12 @@
|
|
|
383
383
|
--c-warn-30: rgba(245, 185, 68, 0.3);
|
|
384
384
|
--c-warn-60: rgba(245, 185, 68, 0.6);
|
|
385
385
|
--c-danger-12: rgba(240, 86, 106, 0.12);
|
|
386
|
-
--c-line-30: rgba(
|
|
387
|
-
--c-contrast-fg: #
|
|
388
|
-
--c-accent-hover: #
|
|
386
|
+
--c-line-30: rgba(41, 41, 44, 0.3);
|
|
387
|
+
--c-contrast-fg: #0B0B0C;
|
|
388
|
+
--c-accent-hover: #FF6B3A;
|
|
389
389
|
--c-warn-hover: #F7C868;
|
|
390
390
|
--c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
|
|
391
|
-
--c-annotation: #
|
|
391
|
+
--c-annotation: #F2F0EB;
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
:root[data-theme="forest"] {
|
|
@@ -446,6 +446,61 @@
|
|
|
446
446
|
--c-annotation: #EDF5EF;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
:root[data-theme="volt"] {
|
|
450
|
+
/* ── Core palette (volt · green-tinted charcoal + coral) ── */
|
|
451
|
+
--c-bg: #0B0D0B;
|
|
452
|
+
--c-panel: #171A17;
|
|
453
|
+
--c-panel-soft: #111311;
|
|
454
|
+
--c-line: #272A27;
|
|
455
|
+
--c-text: #F6FFF5;
|
|
456
|
+
--c-text-soft: #CBD3CA;
|
|
457
|
+
--c-muted: #9CA49C;
|
|
458
|
+
--c-dim: #626962;
|
|
459
|
+
--c-accent: #F34E3F;
|
|
460
|
+
--c-ok: #52C880;
|
|
461
|
+
--c-warn: #E4C465;
|
|
462
|
+
--c-warn-alt: #FFB300;
|
|
463
|
+
--c-danger: #D67670;
|
|
464
|
+
--c-purple: #BC8FB4;
|
|
465
|
+
--c-thinking: #BC8FB4;
|
|
466
|
+
--c-subagent: #6CA0E0;
|
|
467
|
+
--c-input-bg: #111311;
|
|
468
|
+
--c-dot-grid: rgba(243, 78, 63, 0.08);
|
|
469
|
+
--c-panel-overlay: rgba(11, 13, 11, 0.6);
|
|
470
|
+
--c-panel-glass: rgba(23, 26, 23, 0.92);
|
|
471
|
+
--c-shadow: rgba(0, 0, 0, 0.3);
|
|
472
|
+
--c-shadow-heavy: rgba(0, 0, 0, 0.5);
|
|
473
|
+
--c-glow-accent: rgba(243, 78, 63, 0.15);
|
|
474
|
+
--c-surface-hover: rgba(255, 255, 255, 0.06);
|
|
475
|
+
--c-surface-subtle: rgba(255, 255, 255, 0.03);
|
|
476
|
+
--c-surface-th: rgba(255, 255, 255, 0.04);
|
|
477
|
+
--c-accent-8: rgba(243, 78, 63, 0.08);
|
|
478
|
+
--c-accent-10: rgba(243, 78, 63, 0.1);
|
|
479
|
+
--c-accent-12: rgba(243, 78, 63, 0.12);
|
|
480
|
+
--c-accent-15: rgba(243, 78, 63, 0.15);
|
|
481
|
+
--c-accent-25: rgba(243, 78, 63, 0.25);
|
|
482
|
+
--c-accent-30: rgba(243, 78, 63, 0.3);
|
|
483
|
+
--c-accent-40: rgba(243, 78, 63, 0.4);
|
|
484
|
+
--c-ok-10: rgba(82, 200, 128, 0.1);
|
|
485
|
+
--c-ok-12: rgba(82, 200, 128, 0.12);
|
|
486
|
+
--c-ok-20: rgba(82, 200, 128, 0.2);
|
|
487
|
+
--c-ok-25: rgba(82, 200, 128, 0.25);
|
|
488
|
+
--c-warn-10: rgba(228, 196, 101, 0.1);
|
|
489
|
+
--c-warn-12: rgba(228, 196, 101, 0.12);
|
|
490
|
+
--c-warn-15: rgba(228, 196, 101, 0.15);
|
|
491
|
+
--c-warn-20: rgba(228, 196, 101, 0.2);
|
|
492
|
+
--c-warn-22: rgba(228, 196, 101, 0.22);
|
|
493
|
+
--c-warn-30: rgba(228, 196, 101, 0.3);
|
|
494
|
+
--c-warn-60: rgba(228, 196, 101, 0.6);
|
|
495
|
+
--c-danger-12: rgba(214, 118, 112, 0.12);
|
|
496
|
+
--c-line-30: rgba(39, 42, 39, 0.3);
|
|
497
|
+
--c-contrast-fg: #0B0D0B;
|
|
498
|
+
--c-accent-hover: #F8695B;
|
|
499
|
+
--c-warn-hover: #EBD087;
|
|
500
|
+
--c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
|
|
501
|
+
--c-annotation: #F1F7F0;
|
|
502
|
+
}
|
|
503
|
+
|
|
449
504
|
* {
|
|
450
505
|
box-sizing: border-box;
|
|
451
506
|
margin: 0;
|
|
@@ -944,7 +999,6 @@ body,
|
|
|
944
999
|
}
|
|
945
1000
|
|
|
946
1001
|
.toolbar-group {
|
|
947
|
-
position: relative;
|
|
948
1002
|
display: flex;
|
|
949
1003
|
flex-wrap: wrap;
|
|
950
1004
|
align-items: center;
|
|
@@ -953,6 +1007,14 @@ body,
|
|
|
953
1007
|
max-width: 100%;
|
|
954
1008
|
}
|
|
955
1009
|
|
|
1010
|
+
/* Positioning parent for a toolbar popover menu. Every menu MUST live inside
|
|
1011
|
+
one of these wrapped around its trigger — anchoring to a wider ancestor
|
|
1012
|
+
opens the menu far from the button (0.4.2 report Finding T). */
|
|
1013
|
+
.toolbar-menu-anchor {
|
|
1014
|
+
position: relative;
|
|
1015
|
+
display: inline-flex;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
956
1018
|
.canvas-toolbar button svg {
|
|
957
1019
|
display: block;
|
|
958
1020
|
}
|
|
@@ -1074,7 +1136,9 @@ body,
|
|
|
1074
1136
|
}
|
|
1075
1137
|
|
|
1076
1138
|
.toolbar-more {
|
|
1077
|
-
|
|
1139
|
+
/* inline-flex (NOT contents): this span is also the positioning anchor
|
|
1140
|
+
for the More menu — display:contents cannot anchor absolute children. */
|
|
1141
|
+
display: inline-flex;
|
|
1078
1142
|
}
|
|
1079
1143
|
}
|
|
1080
1144
|
|