@1agh/maude 0.34.0 → 0.36.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.
@@ -449,10 +449,11 @@ const STICONS = {
449
449
  <polyline points="3 12.8 3 13.6 13 13.6 13 12.8" />
450
450
  </>
451
451
  ),
452
+ // lucide `rotate-cw`, scaled from the 24px source into our 16px viewBox.
452
453
  reload: (
453
454
  <>
454
- <path d="M3.2 8a5 5 0 1 1 1.4 3.5" />
455
- <polyline points="3.2 11.4 3.2 8 6.6 8" />
455
+ <path d="M14 8a6 6 0 1 1-2-4.47L14 5.33" />
456
+ <path d="M14 2v3.33h-3.33" />
456
457
  </>
457
458
  ),
458
459
  help: (
@@ -1092,10 +1093,23 @@ function FileRow({ file, activePath, onOpen, onDelete, openCount: oc, depth, kin
1092
1093
  // Delete only real canvases in a deletable group (onDelete is undefined for the
1093
1094
  // DS group + runtime files); the server enforces the rest.
1094
1095
  const canDelete = isCanvas && typeof onDelete === 'function' && kind !== 'runtime';
1096
+ // Stable hook for the desktop E2E harness (data-testid convention — see the
1097
+ // `desktop-e2e` skill): canvas rows only, slug derived from the relative path
1098
+ // (e.g. `ui/Smoke.tsx` → `canvas-row-ui-smoke`).
1099
+ const testId = isCanvas
1100
+ ? 'canvas-row-' +
1101
+ file.path
1102
+ .replace(/^\.[^/]+\//, '') // strip the leading designRoot dot-folder (.design/)
1103
+ .replace(CANVAS_EXT_RE, '')
1104
+ .replace(/[^a-z0-9]+/gi, '-')
1105
+ .toLowerCase()
1106
+ .replace(/^-+|-+$/g, '')
1107
+ : undefined;
1095
1108
  const row = (
1096
1109
  <button
1097
1110
  type="button"
1098
1111
  role="treeitem"
1112
+ data-testid={testId}
1099
1113
  aria-selected={isSel}
1100
1114
  aria-disabled={inert ? 'true' : undefined}
1101
1115
  tabIndex={isSel ? 0 : -1}
@@ -1383,6 +1397,8 @@ function Sidebar({
1383
1397
  onToggleSection,
1384
1398
  onNewBoard,
1385
1399
  onDeleteBoard,
1400
+ onRefresh,
1401
+ refreshing,
1386
1402
  collapsed,
1387
1403
  onCollapse,
1388
1404
  width,
@@ -1457,6 +1473,19 @@ function Sidebar({
1457
1473
  >
1458
1474
  <StIcon name="plus" size={15} />
1459
1475
  </button>
1476
+ {onRefresh && (
1477
+ <button
1478
+ type="button"
1479
+ className={'st-iconbtn st-refresh' + (refreshing ? ' is-spinning' : '')}
1480
+ data-tip="Refresh files · ⇧⌘R"
1481
+ aria-label="Refresh files"
1482
+ aria-busy={refreshing || undefined}
1483
+ disabled={refreshing}
1484
+ onClick={() => onRefresh()}
1485
+ >
1486
+ <StIcon name="reload" size={15} />
1487
+ </button>
1488
+ )}
1460
1489
  <span
1461
1490
  className="st-live"
1462
1491
  data-tip={wsConnected ? 'live · file index synced' : 'reconnecting…'}
@@ -1553,7 +1582,7 @@ function Sidebar({
1553
1582
  </div>
1554
1583
  </div>
1555
1584
 
1556
- <div className="st-tree" role="tree" aria-label="Project file tree">
1585
+ <div className="st-tree" role="tree" aria-label="Project file tree" data-testid="canvas-list">
1557
1586
  {filteredGroups.map((g) => {
1558
1587
  // Hide gitignored runtime / orphan-only project sections by default.
1559
1588
  // Active search overrides — if the user typed a query, they want hits
@@ -2748,6 +2777,7 @@ function Viewport({
2748
2777
  src={canvasUrl(t.path, cfg)}
2749
2778
  className={t.path === activePath ? 'active' : ''}
2750
2779
  data-path={t.path}
2780
+ data-testid={t.path === activePath ? 'canvas-frame' : undefined}
2751
2781
  onLoad={() => onIframeLoad?.(t.path)}
2752
2782
  // T2 (9.1-A) — only sandbox + delegate clipboard when the canvas is
2753
2783
  // served cross-origin (canvasOrigin present = the split is on). In
@@ -3136,6 +3166,7 @@ function StatusBar({
3136
3166
  (changesCount > 0 ? ' has-changes' : unpushed > 0 ? ' has-unpushed' : '')
3137
3167
  }
3138
3168
  onClick={onOpenChanges}
3169
+ data-testid="open-changes"
3139
3170
  data-tip="Open Changes · ⌘⇧G"
3140
3171
  data-tip-pos="top"
3141
3172
  aria-label="Open Changes panel"
@@ -6303,6 +6334,50 @@ function App() {
6303
6334
 
6304
6335
  const reloadTree = useCallback(() => loadTree(), [loadTree]);
6305
6336
 
6337
+ // User-facing tree refresh with a visible spin. The header button and ⌘⇧R call
6338
+ // this so the reload icon spins for at least one beat — even when /_index-data
6339
+ // returns instantly — so the action registers visually ("something is
6340
+ // happening"). The min-duration race keeps the spin from flashing for one
6341
+ // frame on a fast read; the ref guard ignores re-entrant clicks. The passive
6342
+ // focus backstop below uses the plain reloadTree (no icon to animate).
6343
+ const [treeRefreshing, setTreeRefreshing] = useState(false);
6344
+ const treeRefreshingRef = useRef(false);
6345
+ const refreshTree = useCallback(async () => {
6346
+ if (treeRefreshingRef.current) return;
6347
+ treeRefreshingRef.current = true;
6348
+ setTreeRefreshing(true);
6349
+ try {
6350
+ await Promise.all([loadTree(), new Promise((r) => setTimeout(r, 550))]);
6351
+ } finally {
6352
+ treeRefreshingRef.current = false;
6353
+ setTreeRefreshing(false);
6354
+ }
6355
+ }, [loadTree]);
6356
+
6357
+ // Backstop for the desktop sidecar: re-list the tree whenever the window
6358
+ // regains focus. The fs-watch → canvas-list-update auto-refresh can drop
6359
+ // events in a `bun --compile` standalone binary (recursive fs.watch is
6360
+ // unreliable there) and across a sidecar respawn / WS reconnect, leaving a
6361
+ // stale tree after a canvas was created from the ACP chat or a terminal.
6362
+ // `/_index-data` is a cheap read and people tab away to the agent and back, so
6363
+ // this turns "switch projects to force a refresh" into "just come back to the
6364
+ // window". Debounced so a rapid blur/focus burst coalesces to one re-read.
6365
+ useEffect(() => {
6366
+ let t = null;
6367
+ const onFocus = () => {
6368
+ if (t) clearTimeout(t);
6369
+ t = setTimeout(() => {
6370
+ t = null;
6371
+ reloadTree();
6372
+ }, 150);
6373
+ };
6374
+ window.addEventListener('focus', onFocus);
6375
+ return () => {
6376
+ window.removeEventListener('focus', onFocus);
6377
+ if (t) clearTimeout(t);
6378
+ };
6379
+ }, [reloadTree]);
6380
+
6306
6381
  // Phase 22 — create a blank brief board from the tree header. POSTs to the
6307
6382
  // main-origin-only /_api/canvas (the untrusted canvas iframe can't reach it),
6308
6383
  // then refreshes the tree and opens the new board so it's immediately the
@@ -6784,6 +6859,15 @@ function App() {
6784
6859
  setPaletteOpen((v) => !v);
6785
6860
  return;
6786
6861
  }
6862
+ // Cmd+Shift+R — refresh the FILES tree (re-read /_index-data). The fs-watch
6863
+ // → canvas-list-update auto-refresh can miss events in the compiled desktop
6864
+ // sidecar (recursive fs.watch is unreliable in a bun --compile binary), and
6865
+ // ⌘R is taken by canvas-iframe reload, so this is the manual escape hatch.
6866
+ if (meta && e.shiftKey && (e.key === 'r' || e.key === 'R')) {
6867
+ e.preventDefault();
6868
+ refreshTree();
6869
+ return;
6870
+ }
6787
6871
  // Cmd+R — reload active iframe (override browser reload)
6788
6872
  if (meta && (e.key === 'r' || e.key === 'R')) {
6789
6873
  e.preventDefault();
@@ -6933,6 +7017,7 @@ function App() {
6933
7017
  return () => window.removeEventListener('keydown', onKey);
6934
7018
  }, [
6935
7019
  reloadActive,
7020
+ refreshTree,
6936
7021
  selected,
6937
7022
  activePath,
6938
7023
  focusedCommentId,
@@ -7216,6 +7301,8 @@ function App() {
7216
7301
  onToggleSection={toggleSection}
7217
7302
  onNewBoard={createBoard}
7218
7303
  onDeleteBoard={deleteBoard}
7304
+ onRefresh={refreshTree}
7305
+ refreshing={treeRefreshing}
7219
7306
  collapsed={!sidebarOpen}
7220
7307
  onCollapse={() => setSidebarOpen(false)}
7221
7308
  width={sbSize.w}
@@ -411,6 +411,7 @@ export default function GitPanel({
411
411
  <button
412
412
  type="button"
413
413
  className="btn btn--primary gp-publish"
414
+ data-testid="git-publish"
414
415
  data-tour="publish"
415
416
  disabled={!!busy}
416
417
  onClick={() =>
@@ -435,6 +436,7 @@ export default function GitPanel({
435
436
  className={'st-rpanel gp-panel' + (resizing ? ' is-resizing' : '')}
436
437
  style={width ? { width, flexBasis: width } : undefined}
437
438
  aria-label="Changes"
439
+ data-testid="git-panel"
438
440
  >
439
441
  <div className="gp-head">
440
442
  <div className="gp-panel-hd">
@@ -494,6 +496,7 @@ export default function GitPanel({
494
496
  <button
495
497
  type="button"
496
498
  className="btn btn--sm"
499
+ data-testid="git-get-latest"
497
500
  disabled={!!busy}
498
501
  onClick={() =>
499
502
  run('getLatest', onGetLatest, {
@@ -559,6 +562,7 @@ export default function GitPanel({
559
562
  <button
560
563
  type="button"
561
564
  className="btn btn--ghost btn--sm"
565
+ data-testid="git-get-latest"
562
566
  data-tour="pull"
563
567
  disabled={!!busy}
564
568
  onClick={() =>
@@ -639,6 +643,7 @@ export default function GitPanel({
639
643
  </label>
640
644
  <textarea
641
645
  className="gp-msg"
646
+ data-testid="git-commit-message"
642
647
  placeholder="Describe what changed in this version…"
643
648
  aria-label="Describe what changed in this version"
644
649
  rows={2}
@@ -665,6 +670,7 @@ export default function GitPanel({
665
670
  <button
666
671
  type="button"
667
672
  className="btn btn--ghost btn--sm"
673
+ data-testid="git-save-all"
668
674
  disabled={!message.trim() || !!busy}
669
675
  title="Save every change"
670
676
  onClick={async () => {