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.
- package/dist/src/code-mode/store.js +35 -6
- package/dist/src/code-mode/store.js.map +1 -1
- package/dist/src/routes/code-native.js +11 -2
- package/dist/src/routes/code-native.js.map +1 -1
- package/package.json +1 -1
- package/public/dist/.vite/manifest.json +21 -9
- package/public/dist/assets/CodeCanvas-c_omHvtV.js +3 -0
- package/public/dist/assets/FolderPanel-DOLT_syh.js +3 -0
- package/public/dist/assets/{FolderPanel-BHEYcjea.css → FolderPanel-DgoqN1cX.css} +1 -1
- package/public/dist/assets/icons-QgChTDzZ.js +1 -0
- package/public/dist/assets/manager-Bq8-tr-M.js +5 -0
- package/public/dist/assets/manager-C6UJc985.css +1 -0
- package/public/dist/manager/index.html +2 -2
- package/public/manager/src/App.tsx +3 -3
- package/public/manager/src/AppChrome.tsx +1 -1
- package/public/manager/src/background-tasks/BackgroundTaskMonitorPanel.tsx +41 -7
- package/public/manager/src/background-tasks/background-task-monitor.css +1 -4
- package/public/manager/src/code/CodeSessionList.tsx +95 -35
- package/public/manager/src/code/code-controller-runtime.ts +66 -8
- package/public/manager/src/code/code-controller-types.ts +4 -0
- package/public/manager/src/code/code-session-client.ts +1 -1
- package/public/manager/src/code/code-session-sidebar.css +32 -0
- package/public/manager/src/code/code.css +2 -4
- package/public/manager/src/code/session-order.ts +9 -0
- package/public/manager/src/components/CommandBar.tsx +0 -10
- package/public/manager/src/components/InstanceGroups.tsx +2 -0
- package/public/manager/src/components/InstanceListContent.tsx +2 -0
- package/public/manager/src/components/InstanceRow.tsx +146 -89
- package/public/manager/src/components/SidebarRail.tsx +6 -5
- package/public/manager/src/components/ThemeSwitch.tsx +46 -22
- package/public/manager/src/components/context-menu/ContextMenu.tsx +207 -0
- package/public/manager/src/components/context-menu/context-menu.css +111 -0
- package/public/manager/src/components/context-menu/icons.tsx +48 -0
- package/public/manager/src/folder-panel/FolderContextMenu.tsx +43 -17
- package/public/manager/src/folder-panel/FolderPanel.tsx +9 -1
- package/public/manager/src/folder-panel/FolderPanelOverlays.tsx +2 -0
- package/public/manager/src/folder-panel/FolderTreeRows.tsx +6 -1
- package/public/manager/src/folder-panel/folder-panel.css +0 -33
- package/public/manager/src/folder-panel/use-folder-context-menu.ts +6 -19
- package/public/manager/src/hooks/useDashboardRegistry.ts +24 -1
- package/public/manager/src/jaw-ceo/JawCeoConsolePanels.tsx +71 -9
- package/public/manager/src/jaw-ceo/jaw-ceo-console.css +5 -7
- package/public/manager/src/main.tsx +2 -0
- package/public/manager/src/manager-components.css +1 -34
- package/public/manager/src/manager-desktop-titlebar.css +117 -0
- package/public/manager/src/manager-layout.css +0 -1
- package/public/manager/src/manager-p0-1-1.css +0 -42
- package/public/manager/src/manager-polish.css +0 -88
- package/public/dist/assets/CodeCanvas-CVbdp0S-.js +0 -3
- package/public/dist/assets/FolderPanel-CJvwRwIV.js +0 -3
- package/public/dist/assets/manager-CCXm5Yfe.js +0 -5
- 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
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
{
|
|
216
|
-
<div className="jaw-ceo-activity-actions">
|
|
217
|
-
<button
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
1273
|
+
.command-center .command-actions :is(.desktop-open-button, .desktop-status-badge) { display: none; }
|
|
1307
1274
|
}
|