cli-jaw 2.17.63 → 2.17.64

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 (52) hide show
  1. package/dist/src/code-mode/store.js +35 -6
  2. package/dist/src/code-mode/store.js.map +1 -1
  3. package/dist/src/routes/code-native.js +11 -2
  4. package/dist/src/routes/code-native.js.map +1 -1
  5. package/package.json +1 -1
  6. package/public/dist/.vite/manifest.json +21 -9
  7. package/public/dist/assets/CodeCanvas-c_omHvtV.js +3 -0
  8. package/public/dist/assets/FolderPanel-DOLT_syh.js +3 -0
  9. package/public/dist/assets/{FolderPanel-BHEYcjea.css → FolderPanel-DgoqN1cX.css} +1 -1
  10. package/public/dist/assets/icons-QgChTDzZ.js +1 -0
  11. package/public/dist/assets/manager-Bq8-tr-M.js +5 -0
  12. package/public/dist/assets/manager-C6UJc985.css +1 -0
  13. package/public/dist/manager/index.html +2 -2
  14. package/public/manager/src/App.tsx +3 -3
  15. package/public/manager/src/AppChrome.tsx +1 -1
  16. package/public/manager/src/background-tasks/BackgroundTaskMonitorPanel.tsx +41 -7
  17. package/public/manager/src/background-tasks/background-task-monitor.css +1 -4
  18. package/public/manager/src/code/CodeSessionList.tsx +95 -35
  19. package/public/manager/src/code/code-controller-runtime.ts +66 -8
  20. package/public/manager/src/code/code-controller-types.ts +4 -0
  21. package/public/manager/src/code/code-session-client.ts +1 -1
  22. package/public/manager/src/code/code-session-sidebar.css +32 -0
  23. package/public/manager/src/code/code.css +2 -4
  24. package/public/manager/src/code/session-order.ts +9 -0
  25. package/public/manager/src/components/CommandBar.tsx +0 -10
  26. package/public/manager/src/components/InstanceGroups.tsx +2 -0
  27. package/public/manager/src/components/InstanceListContent.tsx +2 -0
  28. package/public/manager/src/components/InstanceRow.tsx +146 -89
  29. package/public/manager/src/components/SidebarRail.tsx +6 -5
  30. package/public/manager/src/components/ThemeSwitch.tsx +46 -22
  31. package/public/manager/src/components/context-menu/ContextMenu.tsx +207 -0
  32. package/public/manager/src/components/context-menu/context-menu.css +111 -0
  33. package/public/manager/src/components/context-menu/icons.tsx +48 -0
  34. package/public/manager/src/folder-panel/FolderContextMenu.tsx +43 -17
  35. package/public/manager/src/folder-panel/FolderPanel.tsx +9 -1
  36. package/public/manager/src/folder-panel/FolderPanelOverlays.tsx +2 -0
  37. package/public/manager/src/folder-panel/FolderTreeRows.tsx +6 -1
  38. package/public/manager/src/folder-panel/folder-panel.css +0 -33
  39. package/public/manager/src/folder-panel/use-folder-context-menu.ts +6 -19
  40. package/public/manager/src/hooks/useDashboardRegistry.ts +24 -1
  41. package/public/manager/src/jaw-ceo/JawCeoConsolePanels.tsx +71 -9
  42. package/public/manager/src/jaw-ceo/jaw-ceo-console.css +5 -7
  43. package/public/manager/src/main.tsx +2 -0
  44. package/public/manager/src/manager-components.css +1 -34
  45. package/public/manager/src/manager-desktop-titlebar.css +117 -0
  46. package/public/manager/src/manager-layout.css +0 -1
  47. package/public/manager/src/manager-p0-1-1.css +0 -42
  48. package/public/manager/src/manager-polish.css +0 -88
  49. package/public/dist/assets/CodeCanvas-CVbdp0S-.js +0 -3
  50. package/public/dist/assets/FolderPanel-CJvwRwIV.js +0 -3
  51. package/public/dist/assets/manager-CCXm5Yfe.js +0 -5
  52. package/public/dist/assets/manager-D9rhKFKc.css +0 -1
@@ -0,0 +1,111 @@
1
+ .jaw-context-menu {
2
+ position: fixed;
3
+ box-sizing: border-box;
4
+ /* Above floating composer menus (1000), below the panel resize overlay. */
5
+ z-index: 1100;
6
+ min-width: 200px;
7
+ max-width: min(320px, calc(100vw - 16px));
8
+ max-height: calc(100vh - 16px);
9
+ overflow-y: auto;
10
+ padding: 5px;
11
+ border: 1px solid var(--border-subtle);
12
+ border-radius: 10px;
13
+ background: var(--bg-elevated);
14
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32);
15
+ font-family: var(--font-sans);
16
+ font-size: 13px;
17
+ }
18
+ .jaw-context-menu-item {
19
+ all: unset;
20
+ box-sizing: border-box;
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 9px;
24
+ width: 100%;
25
+ padding: 6px 9px;
26
+ border-radius: 6px;
27
+ color: var(--text-primary);
28
+ line-height: 1.25;
29
+ cursor: default;
30
+ }
31
+ .jaw-context-menu-item:hover:not(:disabled),
32
+ .jaw-context-menu-item:focus-visible {
33
+ background: var(--row-bg-hover);
34
+ }
35
+ .jaw-context-menu-item:disabled {
36
+ color: var(--text-dim);
37
+ opacity: 0.45;
38
+ }
39
+ .jaw-context-menu-item.is-danger { color: var(--danger, #e5484d); }
40
+ .jaw-context-menu-icon {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ width: 16px;
45
+ height: 16px;
46
+ flex: none;
47
+ color: var(--text-secondary);
48
+ }
49
+ .jaw-context-menu-item.is-danger .jaw-context-menu-icon { color: inherit; }
50
+ .jaw-context-menu-label {
51
+ flex: 1;
52
+ min-width: 0;
53
+ overflow: hidden;
54
+ text-overflow: ellipsis;
55
+ white-space: nowrap;
56
+ }
57
+ .jaw-context-menu-shortcut {
58
+ flex: none;
59
+ margin-left: 16px;
60
+ color: var(--text-dim);
61
+ font: inherit;
62
+ font-size: 12px;
63
+ }
64
+ .jaw-context-menu-separator {
65
+ height: 1px;
66
+ margin: 5px 4px;
67
+ background: var(--border-subtle);
68
+ }
69
+
70
+ /* Icon buttons that appear at the edge of a hovered/focused row, pairing with
71
+ a right-click ContextMenu. The hover selector works because :hover also
72
+ matches ancestors of the element under the pointer. */
73
+ .jaw-row-hover-actions {
74
+ display: inline-flex;
75
+ gap: 2px;
76
+ align-items: center;
77
+ opacity: 0;
78
+ pointer-events: none;
79
+ }
80
+ .jaw-row-hover-actions:focus-within,
81
+ *:hover > .jaw-row-hover-actions,
82
+ *:focus-within > .jaw-row-hover-actions {
83
+ opacity: 1;
84
+ pointer-events: auto;
85
+ }
86
+ @media (hover: none) {
87
+ .jaw-row-hover-actions {
88
+ opacity: 1;
89
+ pointer-events: auto;
90
+ }
91
+ }
92
+ .jaw-row-icon-btn {
93
+ all: unset;
94
+ box-sizing: border-box;
95
+ display: inline-flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ width: 22px;
99
+ height: 22px;
100
+ border-radius: 5px;
101
+ color: var(--text-secondary);
102
+ cursor: pointer;
103
+ }
104
+ .jaw-row-icon-btn:hover:not(:disabled),
105
+ .jaw-row-icon-btn:focus-visible {
106
+ background: var(--row-bg-hover);
107
+ color: var(--text-primary);
108
+ }
109
+ .jaw-row-icon-btn:disabled {
110
+ opacity: 0.4;
111
+ }
@@ -0,0 +1,48 @@
1
+ /* 16px stroke glyphs shared by context menus and row hover actions. */
2
+ const base = {
3
+ viewBox: '0 0 16 16', width: 15, height: 15, fill: 'none', stroke: 'currentColor', strokeWidth: 1.4,
4
+ strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, focusable: false,
5
+ } as const;
6
+
7
+ export function PencilGlyph() {
8
+ return <svg {...base}><path d="M10.8 2.7a1.6 1.6 0 0 1 2.3 2.3L5.6 12.5l-3 .8.8-3Z" /></svg>;
9
+ }
10
+ export function PinGlyph() {
11
+ return <svg {...base}><path d="M9.7 2.3 13.7 6.3M10.6 3.2 6.9 6.1l-2.5-.3-1 1 5.8 5.8 1-1-.3-2.5 2.9-3.7M5.9 10.1 2.5 13.5" /></svg>;
12
+ }
13
+ export function ArchiveGlyph() {
14
+ return <svg {...base}><rect x="2.3" y="3" width="11.4" height="3" rx="0.8" /><path d="M3.3 6v6.2a.8.8 0 0 0 .8.8h7.8a.8.8 0 0 0 .8-.8V6M6.5 8.8h3" /></svg>;
15
+ }
16
+ export function EyeGlyph() {
17
+ return <svg {...base}><path d="M1.8 8S4 3.8 8 3.8 14.2 8 14.2 8 12 12.2 8 12.2 1.8 8 1.8 8Z" /><circle cx="8" cy="8" r="1.8" /></svg>;
18
+ }
19
+ export function TrashGlyph() {
20
+ return <svg {...base}><path d="M2.8 4.3h10.4M6.3 4.3V3h3.4v1.3M4 4.3l.6 8.4a.8.8 0 0 0 .8.8h5.2a.8.8 0 0 0 .8-.8l.6-8.4" /></svg>;
21
+ }
22
+ export function CopyGlyph() {
23
+ return <svg {...base}><rect x="5.3" y="5.3" width="8" height="8" rx="1.2" /><path d="M10.7 5.3V3.5a.8.8 0 0 0-.8-.8H3.5a.8.8 0 0 0-.8.8v6.4a.8.8 0 0 0 .8.8h1.8" /></svg>;
24
+ }
25
+ export function FolderGlyph() {
26
+ return <svg {...base}><path d="M2.3 4.3a1 1 0 0 1 1-1h3l1.4 1.5h5a1 1 0 0 1 1 1v6.4a1 1 0 0 1-1 1H3.3a1 1 0 0 1-1-1Z" /></svg>;
27
+ }
28
+ export function ExternalGlyph() {
29
+ return <svg {...base}><path d="M6 3H3.5a.8.8 0 0 0-.8.8v8.7a.8.8 0 0 0 .8.8h8.7a.8.8 0 0 0 .8-.8V10M9 2.7h4.3V7M13.3 2.7 7.5 8.5" /></svg>;
30
+ }
31
+ export function PlayGlyph() {
32
+ return <svg {...base}><path d="M4.5 3v10l8-5Z" /></svg>;
33
+ }
34
+ export function StopGlyph() {
35
+ return <svg {...base}><rect x="3.5" y="3.5" width="9" height="9" rx="1.4" /></svg>;
36
+ }
37
+ export function RestartGlyph() {
38
+ return <svg {...base}><path d="M13 8a5 5 0 1 1-1.5-3.6M13 2.5v3h-3" /></svg>;
39
+ }
40
+ export function CheckGlyph() {
41
+ return <svg {...base}><path d="M3 8.8l3.2 3.2L13 5.2" /></svg>;
42
+ }
43
+ export function SpeakerGlyph() {
44
+ return <svg {...base}><path d="M8.7 3.2 5.3 6H3v4h2.3l3.4 2.8Z" /><path d="M11 6a3 3 0 0 1 0 4M12.9 4a5.4 5.4 0 0 1 0 8" /></svg>;
45
+ }
46
+ export function DocGlyph() {
47
+ return <svg {...base}><path d="M4.3 2.5H9l2.7 2.7v8.3H4.3Z" /><path d="M9 2.5v2.7h2.7M6.2 8.6h3.6M6.2 10.8h2.6" /></svg>;
48
+ }
@@ -1,3 +1,7 @@
1
+ import { useMemo } from 'react';
2
+ import { ContextMenu, type ContextMenuEntry, type ContextMenuState } from '../components/context-menu/ContextMenu';
3
+ import { CopyGlyph, DocGlyph, FolderGlyph, PencilGlyph, RestartGlyph } from '../components/context-menu/icons';
4
+ import { currentClientPlatform, isMacLikePlatform } from '../client-platform';
1
5
  import type { FolderPanelEntry } from './folder-sources';
2
6
 
3
7
  type FolderContextMenuProps = {
@@ -7,6 +11,7 @@ type FolderContextMenuProps = {
7
11
  canReveal: boolean;
8
12
  canRefresh: boolean;
9
13
  canMutate: boolean;
14
+ onClose: () => void;
10
15
  onCopyPath: () => void;
11
16
  onCopyRelativePath: () => void;
12
17
  onReveal: () => void;
@@ -17,23 +22,44 @@ type FolderContextMenuProps = {
17
22
  };
18
23
 
19
24
  export function FolderContextMenu(props: FolderContextMenuProps) {
25
+ const mac = isMacLikePlatform();
26
+ // ContextMenu's positioning effect keys on `state` identity — keep it stable.
27
+ const menuState = useMemo<ContextMenuState>(() => ({ x: props.x, y: props.y }), [props.x, props.y]);
28
+ const entries: ContextMenuEntry[] = [
29
+ { id: 'copy-path', label: 'Copy Path', icon: <CopyGlyph />, shortcut: mac ? '⇧⌘C' : 'Ctrl+Shift+C', onSelect: props.onCopyPath },
30
+ { id: 'copy-relative-path', label: 'Copy Relative Path', icon: <CopyGlyph />, shortcut: mac ? '⌘C' : 'Ctrl+C', onSelect: props.onCopyRelativePath },
31
+ { kind: 'separator', id: 'sep-reveal' },
32
+ {
33
+ id: 'reveal',
34
+ label: props.entry.kind === 'directory' ? 'Open Folder' : revealLabel(mac),
35
+ icon: <FolderGlyph />,
36
+ shortcut: mac ? '⌥⌘R' : 'Ctrl+Alt+R',
37
+ disabled: !props.canReveal,
38
+ onSelect: props.onReveal,
39
+ },
40
+ { kind: 'separator', id: 'sep-create' },
41
+ { id: 'new-file', label: 'New File', icon: <DocGlyph />, disabled: !props.canMutate, onSelect: props.onCreateFile },
42
+ { id: 'new-folder', label: 'New Folder', icon: <FolderGlyph />, disabled: !props.canMutate, onSelect: props.onCreateFolder },
43
+ { kind: 'separator', id: 'sep-rename' },
44
+ { id: 'rename', label: 'Rename', icon: <PencilGlyph />, disabled: !props.canMutate, onSelect: props.onRename },
45
+ ];
46
+ if (props.canRefresh) {
47
+ entries.push(
48
+ { kind: 'separator', id: 'sep-refresh' },
49
+ { id: 'refresh', label: 'Refresh', icon: <RestartGlyph />, onSelect: props.onRefresh },
50
+ );
51
+ }
20
52
  return (
21
- <div
22
- className="folder-context-menu"
23
- role="menu"
24
- style={{ left: props.x, top: props.y }}
25
- onPointerDown={event => event.stopPropagation()}
26
- onKeyDown={event => event.stopPropagation()}
27
- >
28
- <button type="button" role="menuitem" onClick={props.onCopyPath}>Copy Path</button>
29
- <button type="button" role="menuitem" onClick={props.onCopyRelativePath}>Copy Relative Path</button>
30
- <button type="button" role="menuitem" disabled={!props.canReveal} onClick={props.onReveal}>
31
- {props.entry.kind === 'directory' ? 'Open Folder' : 'Reveal in Finder'}
32
- </button>
33
- <button type="button" role="menuitem" disabled={!props.canMutate} onClick={props.onCreateFile}>New File</button>
34
- <button type="button" role="menuitem" disabled={!props.canMutate} onClick={props.onCreateFolder}>New Folder</button>
35
- <button type="button" role="menuitem" disabled={!props.canMutate} onClick={props.onRename}>Rename</button>
36
- {props.canRefresh && <button type="button" role="menuitem" onClick={props.onRefresh}>Refresh</button>}
37
- </div>
53
+ <ContextMenu
54
+ state={menuState}
55
+ entries={entries}
56
+ label={`Folder actions for ${props.entry.name}`}
57
+ onClose={props.onClose}
58
+ />
38
59
  );
39
60
  }
61
+
62
+ function revealLabel(mac: boolean): string {
63
+ if (mac) return 'Reveal in Finder';
64
+ return /win/i.test(currentClientPlatform()) ? 'Show in Explorer' : 'Show in File Manager';
65
+ }
@@ -56,6 +56,7 @@ export function FolderPanel(props: FolderPanelProps) {
56
56
  const [actionStatus, setActionStatus] = useState<string | null>(null);
57
57
  const [gitRefreshToken, setGitRefreshToken] = useState(0);
58
58
  const treeRef = useRef<HTMLDivElement | null>(null);
59
+ const wasContextMenuOpen = useRef(false);
59
60
  const restoredRootLoadRef = useRef<string | null>(null);
60
61
  const { folderChordActive, startFolderChord, cancelFolderChord } = useFolderChord();
61
62
  const onPreviewFile = props.onPreviewFile;
@@ -403,12 +404,18 @@ export function FolderPanel(props: FolderPanelProps) {
403
404
  }, [cancelFolderChord, copyEntryPath, copySelectedPath, expanded, folderChordActive, folderSelection, revealEntryPath, revealSelectedPath, selectEntry, startFolderChord, toggleEntryExpansion]);
404
405
 
405
406
  useEffect(() => {
407
+ const menuOpen = folderContextMenu.contextMenu !== null;
408
+ const menuJustClosed = wasContextMenuOpen.current && !menuOpen;
409
+ wasContextMenuOpen.current = menuOpen;
410
+ if (menuOpen) return;
411
+ if (menuJustClosed && document.activeElement !== document.body
412
+ && !document.activeElement?.matches('.folder-entry-btn[data-folder-path]')) return;
406
413
  const focusedPath = folderSelection.selection.focusedPath;
407
414
  if (!focusedPath) return;
408
415
  const buttons = treeRef.current?.querySelectorAll<HTMLButtonElement>('.folder-entry-btn[data-folder-path]');
409
416
  const nextButton = Array.from(buttons ?? []).find(button => button.dataset['folderPath'] === focusedPath);
410
417
  nextButton?.focus();
411
- }, [folderSelection.selection.focusedPath]);
418
+ }, [folderSelection.selection.focusedPath, folderContextMenu.contextMenu]);
412
419
 
413
420
  return (
414
421
  <div className="folder-panel">
@@ -498,6 +505,7 @@ export function FolderPanel(props: FolderPanelProps) {
498
505
  if (skipMoveConfirmChecked) setSkipInternalMoveConfirm(true);
499
506
  void executeMove(pendingMove);
500
507
  }}
508
+ onContextMenuClose={folderContextMenu.closeContextMenu}
501
509
  onCopyContextPath={() => { folderContextMenu.closeContextMenu(); void copySelectedPath('absolute'); }}
502
510
  onCopyContextRelativePath={() => { folderContextMenu.closeContextMenu(); void copySelectedPath('relative'); }}
503
511
  onRevealContextPath={() => { folderContextMenu.closeContextMenu(); void revealSelectedPath(); }}
@@ -13,6 +13,7 @@ type FolderPanelOverlaysProps = {
13
13
  onSkipMoveConfirmCheckedChange: (checked: boolean) => void;
14
14
  onCancelMove: () => void;
15
15
  onConfirmMove: () => void;
16
+ onContextMenuClose: () => void;
16
17
  onCopyContextPath: () => void;
17
18
  onCopyContextRelativePath: () => void;
18
19
  onRevealContextPath: () => void;
@@ -44,6 +45,7 @@ export function FolderPanelOverlays(props: FolderPanelOverlaysProps) {
44
45
  canReveal={props.canReveal}
45
46
  canRefresh={props.canRefresh}
46
47
  canMutate={props.canMutate}
48
+ onClose={props.onContextMenuClose}
47
49
  onCopyPath={props.onCopyContextPath}
48
50
  onCopyRelativePath={props.onCopyContextRelativePath}
49
51
  onReveal={props.onRevealContextPath}
@@ -155,7 +155,12 @@ export function FolderTreeRows(props: FolderTreeRowsProps) {
155
155
  onKeyDown={(event) => props.handleEntryKeyDown(event, entry)}
156
156
  onContextMenu={(event) => {
157
157
  event.preventDefault();
158
- props.openContextMenu(entry, event.clientX, event.clientY);
158
+ if (event.clientX !== 0 || event.clientY !== 0) {
159
+ props.openContextMenu(entry, event.clientX, event.clientY);
160
+ return;
161
+ }
162
+ const rect = event.currentTarget.getBoundingClientRect();
163
+ props.openContextMenu(entry, rect.left + 8, rect.bottom);
159
164
  }}
160
165
  onClick={(event) => props.selectEntry(entry, { range: event.shiftKey, toggle: isPlatformToggleClick(event), preview: false })}
161
166
  onDoubleClick={() => {
@@ -649,39 +649,6 @@
649
649
  justify-content: flex-end;
650
650
  gap: 6px;
651
651
  }
652
- .folder-context-menu {
653
- position: fixed;
654
- z-index: 80;
655
- min-width: 156px;
656
- max-width: min(240px, calc(100vw - 16px));
657
- padding: 4px;
658
- border: 1px solid var(--border-subtle);
659
- border-radius: 5px;
660
- background: var(--bg-elevated);
661
- box-shadow: 0 10px 24px rgba(0, 0, 0, 0.26);
662
- }
663
- .folder-context-menu button {
664
- all: unset;
665
- box-sizing: border-box;
666
- display: block;
667
- width: 100%;
668
- padding: 5px 8px;
669
- border-radius: 3px;
670
- color: var(--text-base);
671
- font-size: 12px;
672
- line-height: 1.2;
673
- overflow: hidden;
674
- text-overflow: ellipsis;
675
- white-space: nowrap;
676
- }
677
- .folder-context-menu button:hover:not(:disabled),
678
- .folder-context-menu button:focus-visible {
679
- background: var(--bg-hover);
680
- }
681
- .folder-context-menu button:disabled {
682
- color: var(--text-dim);
683
- opacity: 0.45;
684
- }
685
652
  .folder-empty {
686
653
  padding: 12px;
687
654
  color: var(--text-dim);
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useState } from 'react';
2
2
  import type { FolderPanelEntry } from './folder-panel-types';
3
3
 
4
4
  export type FolderContextMenuState = {
@@ -12,6 +12,11 @@ type UseFolderContextMenuInput = {
12
12
  selectOnlyPath: (path: string) => void;
13
13
  };
14
14
 
15
+ /**
16
+ * Owns which folder entry the context menu targets. Dismissal (outside
17
+ * pointerdown, Escape, blur, scroll) and focus restore live in the shared
18
+ * ContextMenu component.
19
+ */
15
20
  export function useFolderContextMenu(input: UseFolderContextMenuInput) {
16
21
  const [contextMenu, setContextMenu] = useState<FolderContextMenuState | null>(null);
17
22
  const closeContextMenu = useCallback(() => setContextMenu(null), []);
@@ -20,24 +25,6 @@ export function useFolderContextMenu(input: UseFolderContextMenuInput) {
20
25
  setContextMenu({ entry, x, y });
21
26
  }, [input]);
22
27
 
23
- useEffect(() => {
24
- if (!contextMenu) return;
25
- const closeOnEscape = (event: KeyboardEvent) => {
26
- if (event.key === 'Escape') setContextMenu(null);
27
- };
28
- const closeOnFrameFocus = () => {
29
- if (document.activeElement instanceof HTMLIFrameElement) setContextMenu(null);
30
- };
31
- window.addEventListener('pointerdown', closeContextMenu);
32
- window.addEventListener('keydown', closeOnEscape);
33
- window.addEventListener('blur', closeOnFrameFocus);
34
- return () => {
35
- window.removeEventListener('pointerdown', closeContextMenu);
36
- window.removeEventListener('keydown', closeOnEscape);
37
- window.removeEventListener('blur', closeOnFrameFocus);
38
- };
39
- }, [closeContextMenu, contextMenu]);
40
-
41
28
  return {
42
29
  contextMenu,
43
30
  closeContextMenu,
@@ -1,6 +1,8 @@
1
- import { useCallback, useState } from 'react';
1
+ import { useCallback, useRef, useState } from 'react';
2
2
  import { fetchRegistry, patchDashboardRegistry } from '../api';
3
+ import { publishInvalidation } from '../sync/invalidation-bus';
3
4
  import type {
5
+ DashboardInstance,
4
6
  DashboardRegistry,
5
7
  DashboardRegistryLoadResult,
6
8
  DashboardRegistryPatch,
@@ -53,3 +55,24 @@ export function useDashboardRegistry(): DashboardRegistryState {
53
55
 
54
56
  return { registry, status, saving, error, apply: applyResult, refresh, save };
55
57
  }
58
+
59
+ type FavoriteToggleDeps = {
60
+ save: (patch: DashboardRegistryPatch) => Promise<DashboardRegistryLoadResult | null>;
61
+ reload: () => Promise<void>;
62
+ };
63
+
64
+ export function useFavoriteToggle({ save, reload }: FavoriteToggleDeps): (instance: DashboardInstance) => void {
65
+ const pending = useRef(new Set<number>());
66
+ return useCallback((instance: DashboardInstance) => {
67
+ const port = instance.port;
68
+ if (pending.current.has(port)) return;
69
+ pending.current.add(port);
70
+ void save({ instances: { [String(port)]: { favorite: !instance.favorite } } })
71
+ .then(async result => {
72
+ if (!result) return;
73
+ await reload();
74
+ publishInvalidation({ topics: ['instances'], reason: 'instance:favorite-toggled', source: 'ui', sourceId: 'app' });
75
+ })
76
+ .finally(() => pending.current.delete(port));
77
+ }, [save, reload]);
78
+ }
@@ -1,4 +1,6 @@
1
- import { useCallback, useMemo, type ReactElement } from 'react';
1
+ import { useCallback, useMemo, useState, type ReactElement, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent } from 'react';
2
+ import { ContextMenu, useContextMenu, type ContextMenuEntry } from '../components/context-menu/ContextMenu';
3
+ import { CheckGlyph, DocGlyph, ExternalGlyph, PlayGlyph, SpeakerGlyph } from '../components/context-menu/icons';
2
4
  import type { JawCeoController } from './useJawCeo';
3
5
  import type { JawCeoVoiceController } from './useJawCeoVoice';
4
6
  import type { JawCeoAuditRecord, JawCeoCompletion, JawCeoResponseMode, JawCeoWatch } from './types';
@@ -184,6 +186,30 @@ function ActivityIcon(props: { tone: ActivityTone }) {
184
186
  );
185
187
  }
186
188
 
189
+ function resultMenuEntries(
190
+ completion: JawCeoCompletion,
191
+ props: {
192
+ model: JawCeoConsoleModel;
193
+ ceo: JawCeoController;
194
+ voice: JawCeoVoiceController;
195
+ onOpenWorker: (port: number, messageId?: number) => void;
196
+ },
197
+ ): ContextMenuEntry[] {
198
+ return [
199
+ { id: 'open', label: `Open worker :${completion.port}`, icon: <ExternalGlyph />, onSelect: () => props.onOpenWorker(completion.port, completion.messageId) },
200
+ { kind: 'separator', id: 'sep-result-actions' },
201
+ { id: 'summary', label: 'Summarize result', icon: <DocGlyph />, onSelect: () => void props.model.summarize(completion) },
202
+ { id: 'continue', label: 'Continue', icon: <PlayGlyph />, onSelect: () => void props.model.continueCompletion(completion) },
203
+ { id: 'speak', label: 'Speak result', icon: <SpeakerGlyph />, onSelect: () => void props.voice.speakCompletion(completion) },
204
+ { kind: 'separator', id: 'sep-result-ack' },
205
+ { id: 'ack', label: 'Acknowledge', icon: <CheckGlyph />, onSelect: () => void props.ceo.ackCompletion(completion.completionKey) },
206
+ ];
207
+ }
208
+
209
+ function isContextMenuKey(event: ReactKeyboardEvent<HTMLElement>): boolean {
210
+ return event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10');
211
+ }
212
+
187
213
  function ActivityGroup(props: {
188
214
  entries: ActivityEntry[];
189
215
  model: JawCeoConsoleModel;
@@ -191,6 +217,15 @@ function ActivityGroup(props: {
191
217
  voice: JawCeoVoiceController;
192
218
  onOpenWorker: (port: number, messageId?: number) => void;
193
219
  }) {
220
+ const menu = useContextMenu();
221
+ const [menuCompletion, setMenuCompletion] = useState<JawCeoCompletion | null>(null);
222
+ const openResultMenu = useCallback(
223
+ (event: ReactMouseEvent<HTMLElement> | ReactKeyboardEvent<HTMLElement>, completion: JawCeoCompletion) => {
224
+ setMenuCompletion(completion);
225
+ menu.openAt(event);
226
+ },
227
+ [menu],
228
+ );
194
229
  return (
195
230
  <section className="jaw-ceo-activity-shell" aria-label="CEO activity">
196
231
  <span className="jaw-ceo-activity-avatar" aria-hidden="true">CEO</span>
@@ -202,8 +237,16 @@ function ActivityGroup(props: {
202
237
  <div className="jaw-ceo-activity-list">
203
238
  {props.entries.map(entry => {
204
239
  const tone = activityTone(entry);
240
+ const completion = entry.kind === 'result' ? entry.completion : null;
205
241
  return (
206
- <article key={entry.id} className={`jaw-ceo-activity-row tone-${tone}${activityIsError(entry) ? ' is-error' : ''}`}>
242
+ <article
243
+ key={entry.id}
244
+ className={`jaw-ceo-activity-row tone-${tone}${activityIsError(entry) ? ' is-error' : ''}`}
245
+ tabIndex={completion ? 0 : undefined}
246
+ aria-label={completion ? `${activityTitle(entry)}: worker :${completion.port} result. Shift+F10 for actions` : undefined}
247
+ onContextMenu={completion ? event => openResultMenu(event, completion) : undefined}
248
+ onKeyDown={completion ? event => { if (isContextMenuKey(event)) openResultMenu(event, completion); } : undefined}
249
+ >
207
250
  <span className="jaw-ceo-activity-dot" aria-hidden="true" />
208
251
  <span className="jaw-ceo-activity-icon"><ActivityIcon tone={tone} /></span>
209
252
  <span className="jaw-ceo-activity-badge">{tone}</span>
@@ -212,13 +255,26 @@ function ActivityGroup(props: {
212
255
  <p>{activityPreview(entry)}</p>
213
256
  <small>{formatTime(activityTime(entry))}</small>
214
257
  </div>
215
- {entry.kind === 'result' ? (
216
- <div className="jaw-ceo-activity-actions">
217
- <button type="button" onClick={() => props.onOpenWorker(entry.completion.port, entry.completion.messageId)}>Open</button>
218
- <button type="button" onClick={() => void props.model.summarize(entry.completion)}>Summary</button>
219
- <button type="button" onClick={() => void props.model.continueCompletion(entry.completion)}>Continue</button>
220
- <button type="button" onClick={() => void props.voice.speakCompletion(entry.completion)}>Speak</button>
221
- <button type="button" onClick={() => void props.ceo.ackCompletion(entry.completion.completionKey)}>Ack</button>
258
+ {completion ? (
259
+ <div className="jaw-ceo-activity-actions jaw-row-hover-actions">
260
+ <button
261
+ type="button"
262
+ className="jaw-row-icon-btn"
263
+ title={`Open worker :${completion.port}`}
264
+ aria-label={`Open worker :${completion.port}`}
265
+ onClick={() => props.onOpenWorker(completion.port, completion.messageId)}
266
+ >
267
+ <ExternalGlyph />
268
+ </button>
269
+ <button
270
+ type="button"
271
+ className="jaw-row-icon-btn"
272
+ title="Acknowledge result"
273
+ aria-label={`Acknowledge worker :${completion.port} result`}
274
+ onClick={() => void props.ceo.ackCompletion(completion.completionKey)}
275
+ >
276
+ <CheckGlyph />
277
+ </button>
222
278
  </div>
223
279
  ) : <span className="jaw-ceo-trace-chip">Trace</span>}
224
280
  </article>
@@ -226,6 +282,12 @@ function ActivityGroup(props: {
226
282
  })}
227
283
  </div>
228
284
  </details>
285
+ <ContextMenu
286
+ state={menu.state}
287
+ entries={menuCompletion ? resultMenuEntries(menuCompletion, props) : []}
288
+ label={menuCompletion ? `Worker :${menuCompletion.port} result` : 'Worker result'}
289
+ onClose={menu.close}
290
+ />
229
291
  </section>
230
292
  );
231
293
  }
@@ -304,6 +304,10 @@
304
304
  align-items: start;
305
305
  padding: 4px 0;
306
306
  }
307
+ .jaw-ceo-activity-row:focus-visible {
308
+ outline: var(--focus-ring);
309
+ outline-offset: 2px;
310
+ }
307
311
  .jaw-ceo-activity-icon {
308
312
  width: 22px;
309
313
  height: 22px;
@@ -362,8 +366,7 @@
362
366
  text-overflow: ellipsis;
363
367
  white-space: nowrap;
364
368
  }
365
- .jaw-ceo-trace-chip,
366
- .jaw-ceo-activity-actions button {
369
+ .jaw-ceo-trace-chip {
367
370
  min-height: 24px;
368
371
  padding: 3px 7px;
369
372
  border: 1px solid var(--ceo-line);
@@ -372,17 +375,12 @@
372
375
  color: var(--ceo-ink);
373
376
  font-size: 10.5px;
374
377
  font-weight: 800;
375
- }
376
- .jaw-ceo-trace-chip {
377
378
  display: inline-flex;
378
379
  align-items: center;
379
380
  justify-content: center;
380
381
  }
381
382
  .jaw-ceo-activity-actions {
382
- display: flex;
383
- flex-wrap: wrap;
384
383
  justify-content: flex-end;
385
- gap: 5px;
386
384
  }
387
385
  .jaw-ceo-empty-state {
388
386
  min-height: 260px;
@@ -4,6 +4,7 @@ import './manager-tokens.css';
4
4
  import './styles.css';
5
5
  import './manager-layout.css';
6
6
  import './manager-components.css';
7
+ import './components/context-menu/context-menu.css';
7
8
  import './manager-emerging.css';
8
9
  import './manager-persistence.css';
9
10
  import './manager-profiles.css';
@@ -37,6 +38,7 @@ import './jaw-ceo/jaw-ceo.css';
37
38
  import './jaw-ceo/jaw-ceo-console.css';
38
39
  import './jaw-ceo/jaw-ceo-virtual.css';
39
40
  import './panels/panels.css';
41
+ import './manager-desktop-titlebar.css';
40
42
 
41
43
  const root = document.getElementById('manager-root');
42
44
  if (!root) throw new Error('manager-root not found');
@@ -674,23 +674,6 @@ button.instance-group-header[aria-expanded="false"] .instance-group-chevron {
674
674
  font-weight: 400;
675
675
  line-height: 1.4;
676
676
  }
677
- .instance-label-edit-button {
678
- position: absolute;
679
- top: 7px;
680
- right: 8px;
681
- width: 22px;
682
- min-width: 22px;
683
- height: 22px;
684
- min-height: 22px;
685
- padding: 0;
686
- border-radius: 6px;
687
- color: var(--text-secondary);
688
- font-size: 13px;
689
- line-height: 1;
690
- }
691
- .instance-label-edit-button:hover {
692
- color: var(--text-primary);
693
- }
694
677
  .instance-label-edit-form {
695
678
  display: grid;
696
679
  grid-template-columns: minmax(0, 1fr) auto auto;
@@ -718,22 +701,6 @@ button.instance-group-header[aria-expanded="false"] .instance-group-chevron {
718
701
  color: var(--status-error);
719
702
  font-size: 11px;
720
703
  }
721
- .instance-actions {
722
- display: grid;
723
- grid-auto-flow: column;
724
- grid-auto-columns: minmax(0, 1fr);
725
- gap: 6px;
726
- }
727
- .instance-actions button {
728
- min-width: 0;
729
- padding-inline: 6px;
730
- font-size: 12px;
731
- white-space: nowrap;
732
- }
733
- .instance-actions .action-start {
734
- border-color: color-mix(in srgb, var(--status-running) 40%, var(--border-subtle));
735
- color: var(--status-running);
736
- }
737
704
  .detail-panel {
738
705
  height: 100%;
739
706
  min-width: 0;
@@ -1303,5 +1270,5 @@ button.instance-group-header[aria-expanded="false"] .instance-group-chevron {
1303
1270
 
1304
1271
  @media (max-width: 639px) {
1305
1272
  .command-center .command-title { display: none; }
1306
- .command-center .command-actions :is(.desktop-open-button, .desktop-status-badge, .command-palette-trigger) { display: none; }
1273
+ .command-center .command-actions :is(.desktop-open-button, .desktop-status-badge) { display: none; }
1307
1274
  }