tmux-ide 2.9.0-beta.39 → 2.9.0-beta.41
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/README.md +5 -0
- package/bin/cli.js +6 -3
- package/package.json +6 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-fleet-tabs.js +29 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +10 -13
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +1 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +45 -7
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +44 -44
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +2 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-input.js +1 -0
- package/packages/daemon/dist/tui/mirror/ui/agent-status-marker.js +6 -5
- package/packages/daemon/dist/tui/mirror/ui/icons.js +9 -0
- package/packages/daemon/dist/tui/mirror/ui/index.js +3 -0
- package/packages/daemon/dist/tui/mirror/ui/session-row.jsx +71 -0
- package/packages/daemon/dist/tui/mirror/ui/working-sessions.jsx +61 -0
- package/packages/daemon/dist/tui/mirror/workspace/application-action-descriptions.js +51 -0
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +10 -3
- package/packages/daemon/dist/tui/mirror/workspace/application-shortcuts.js +15 -7
- package/packages/daemon/dist/tui/mirror/workspace/palette-list-layout.js +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +34 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +14 -18
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +44 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +70 -80
- package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +5 -3
- package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +80 -5
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +6 -2
- package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +6 -5
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +6 -2
- package/packages/daemon/src/tui/mirror/ui/agent-status-marker.ts +7 -5
- package/packages/daemon/src/tui/mirror/ui/icons.ts +9 -0
- package/packages/daemon/src/tui/mirror/ui/index.ts +4 -0
- package/packages/daemon/src/tui/mirror/ui/session-row.tsx +122 -0
- package/packages/daemon/src/tui/mirror/ui/working-sessions.tsx +99 -0
- package/packages/daemon/src/tui/mirror/workspace/application-action-descriptions.ts +52 -0
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +12 -3
- package/packages/daemon/src/tui/mirror/workspace/application-shortcuts.ts +19 -7
- package/packages/daemon/src/tui/mirror/workspace/palette-list-layout.ts +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Show } from "solid-js";
|
|
2
|
+
import { clipTerminal } from "../terminal-text.js";
|
|
3
|
+
import { NavigationRow } from "./navigation-row.jsx";
|
|
4
|
+
import { TuiButton } from "./button.jsx";
|
|
5
|
+
import { componentPalette } from "./state.js";
|
|
6
|
+
import { createAgentStatusMarker } from "./agent-status-marker.js";
|
|
7
|
+
import { UI_ICONS } from "./icons.js";
|
|
8
|
+
export function sessionRowStatus(row) {
|
|
9
|
+
if (!row.available)
|
|
10
|
+
return "offline";
|
|
11
|
+
if (row.attention || row.activity === "waiting")
|
|
12
|
+
return "needs input";
|
|
13
|
+
if (row.activity === "failed")
|
|
14
|
+
return "failed";
|
|
15
|
+
if (row.activity === "running")
|
|
16
|
+
return "working";
|
|
17
|
+
if (row.unread)
|
|
18
|
+
return "new result";
|
|
19
|
+
if (!row.activity || row.activity === "disconnected")
|
|
20
|
+
return "unknown";
|
|
21
|
+
return "idle";
|
|
22
|
+
}
|
|
23
|
+
/** Shared two-line session identity. Pure presentation: opening/closing belongs to its owner. */
|
|
24
|
+
export function SessionRow(props) {
|
|
25
|
+
const marker = createAgentStatusMarker({
|
|
26
|
+
theme: () => props.theme,
|
|
27
|
+
status: () => (sessionRowStatus(props.row) === "idle" ? "idle" : props.row.activity),
|
|
28
|
+
attention: () => !!props.row.attention,
|
|
29
|
+
unavailable: () => !props.row.available,
|
|
30
|
+
});
|
|
31
|
+
const tone = () => {
|
|
32
|
+
const status = sessionRowStatus(props.row);
|
|
33
|
+
return status === "needs input" || status === "failed"
|
|
34
|
+
? "blocked"
|
|
35
|
+
: status === "working"
|
|
36
|
+
? "working"
|
|
37
|
+
: status === "new result"
|
|
38
|
+
? "done"
|
|
39
|
+
: "neutral";
|
|
40
|
+
};
|
|
41
|
+
const glyph = () => !props.row.available
|
|
42
|
+
? UI_ICONS.offline
|
|
43
|
+
: sessionRowStatus(props.row) === "new result"
|
|
44
|
+
? UI_ICONS.newResult
|
|
45
|
+
: marker();
|
|
46
|
+
const palette = () => componentPalette(props.theme, {
|
|
47
|
+
selected: props.selected,
|
|
48
|
+
focused: props.focused,
|
|
49
|
+
disabled: !props.row.available,
|
|
50
|
+
});
|
|
51
|
+
const detail = () => `${props.row.hostLabel} · ${props.row.serverLabel ?? "default"}${props.row.active ? " · current" : ""}`;
|
|
52
|
+
return (<box id={`session-row:${props.row.key}`} width={props.width} height={2} flexDirection="column" overflow="hidden">
|
|
53
|
+
<box height={1} flexDirection="row">
|
|
54
|
+
<NavigationRow theme={props.theme} id={`fleet-tab:${props.row.key}`} width={Math.max(1, props.width - (props.onClose ? 3 : 0))} label={props.row.label} tone={tone()} marker={glyph()} detail={sessionRowStatus(props.row)} selected={props.selected} focused={props.focused} disabled={!props.row.available} onActivate={props.onOpen}/>
|
|
55
|
+
<Show when={props.onClose}>
|
|
56
|
+
<TuiButton theme={props.theme} label={UI_ICONS.close} size="compact" width={3} onPress={props.onClose}/>
|
|
57
|
+
</Show>
|
|
58
|
+
</box>
|
|
59
|
+
<box height={1} backgroundColor={palette().background} onMouseDown={(event) => {
|
|
60
|
+
if (event.button !== 0 || !props.row.available)
|
|
61
|
+
return;
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
props.onOpen();
|
|
65
|
+
}}>
|
|
66
|
+
<text fg={props.selected || props.focused ? palette().foreground : props.theme.roles.text.muted}>
|
|
67
|
+
{clipTerminal(` ${detail()}`, props.width)}
|
|
68
|
+
</text>
|
|
69
|
+
</box>
|
|
70
|
+
</box>);
|
|
71
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
|
|
3
|
+
import { SessionRow } from "./session-row.jsx";
|
|
4
|
+
import { useKeyboardRoute } from "./keyboard-router.jsx";
|
|
5
|
+
/** Bounded working-set list, independent of fleet discovery and terminal ownership. */
|
|
6
|
+
export function WorkingSessions(props) {
|
|
7
|
+
const [selected, setSelected] = createSignal(null);
|
|
8
|
+
const index = () => Math.max(0, props.rows.findIndex((row) => row.key === selected()));
|
|
9
|
+
const capacity = () => Math.max(0, Math.floor((props.height - 1) / 3));
|
|
10
|
+
const start = () => Math.max(0, index() - capacity() + 1);
|
|
11
|
+
const visible = () => props.rows.slice(start(), start() + capacity()).map((row) => row.key);
|
|
12
|
+
createEffect(() => {
|
|
13
|
+
if (!props.rows.some((row) => row.key === selected()))
|
|
14
|
+
setSelected((props.rows.find((row) => row.active) ?? props.rows[0])?.key ?? null);
|
|
15
|
+
if (!props.focused)
|
|
16
|
+
setSelected((props.rows.find((row) => row.active) ?? props.rows[index()])?.key ?? null);
|
|
17
|
+
});
|
|
18
|
+
const open = (key) => {
|
|
19
|
+
setSelected(key);
|
|
20
|
+
props.onFocus?.();
|
|
21
|
+
if (props.rows.find((row) => row.key === key)?.available)
|
|
22
|
+
props.onOpen(key);
|
|
23
|
+
};
|
|
24
|
+
useKeyboardRoute((event) => {
|
|
25
|
+
if (!props.focused || event.eventType !== "press" || event.ctrl || event.meta)
|
|
26
|
+
return false;
|
|
27
|
+
const key = event.name.toLowerCase();
|
|
28
|
+
if (!["up", "down", "j", "k", "home", "end", "enter", "return", "space"].includes(key))
|
|
29
|
+
return false;
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
event.stopPropagation();
|
|
32
|
+
if (!props.rows.length || !capacity())
|
|
33
|
+
return true;
|
|
34
|
+
if (["enter", "return", "space"].includes(key))
|
|
35
|
+
open(props.rows[index()].key);
|
|
36
|
+
else {
|
|
37
|
+
const next = key === "home"
|
|
38
|
+
? 0
|
|
39
|
+
: key === "end"
|
|
40
|
+
? props.rows.length - 1
|
|
41
|
+
: Math.max(0, Math.min(props.rows.length - 1, index() + (["up", "k"].includes(key) ? -1 : 1)));
|
|
42
|
+
setSelected(props.rows[next].key);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
return (<box width={props.width} height={props.height} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
47
|
+
<text height={1} fg={props.theme.roles.text.secondary}>
|
|
48
|
+
{props.focused ? <strong> Working sessions</strong> : " Working sessions"}
|
|
49
|
+
</text>
|
|
50
|
+
<Show when={props.rows.length} fallback={<text fg={props.theme.roles.text.muted}> No working sessions yet.</text>}>
|
|
51
|
+
<For each={visible()}>
|
|
52
|
+
{(key) => {
|
|
53
|
+
const row = createMemo((previous) => props.rows.find((row) => row.key === key) ?? previous, props.rows.find((row) => row.key === key));
|
|
54
|
+
return (<box height={3} flexShrink={0} flexDirection="column">
|
|
55
|
+
<SessionRow theme={props.theme} row={row()} width={props.width} selected={props.focused ? selected() === key : row().active} onOpen={() => open(key)} onClose={props.onClose ? () => props.onClose?.(key) : undefined}/>
|
|
56
|
+
</box>);
|
|
57
|
+
}}
|
|
58
|
+
</For>
|
|
59
|
+
</Show>
|
|
60
|
+
</box>);
|
|
61
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Shared presentation/binding metadata. Context owners still decide availability and execution. */
|
|
2
|
+
export const HOME_ACTIONS = {
|
|
3
|
+
search: { key: "/", keys: "/", label: "Search", description: "Find an agent or workspace" },
|
|
4
|
+
machine: { key: "f", keys: "f", label: "Machines", description: "Cycle machine filter" },
|
|
5
|
+
all: { key: "0", keys: "0", label: "All", description: "Show all agent activity", value: "all" },
|
|
6
|
+
working: {
|
|
7
|
+
key: "w",
|
|
8
|
+
keys: "w",
|
|
9
|
+
label: "Working",
|
|
10
|
+
description: "Show working agents",
|
|
11
|
+
value: "working",
|
|
12
|
+
},
|
|
13
|
+
attention: {
|
|
14
|
+
key: "a",
|
|
15
|
+
keys: "a",
|
|
16
|
+
label: "Needs attention",
|
|
17
|
+
description: "Toggle attention filter",
|
|
18
|
+
value: "attention",
|
|
19
|
+
},
|
|
20
|
+
open: { key: "enter", keys: "Enter", label: "open", description: "Open selected agent" },
|
|
21
|
+
};
|
|
22
|
+
export const HOME_ACTIVITY_ACTIONS = [
|
|
23
|
+
HOME_ACTIONS.all,
|
|
24
|
+
HOME_ACTIONS.working,
|
|
25
|
+
HOME_ACTIONS.attention,
|
|
26
|
+
];
|
|
27
|
+
export const CHROME_ACTIONS = {
|
|
28
|
+
home: { keys: "F1", label: "Home" },
|
|
29
|
+
terminals: { keys: "F2", label: "Terminals" },
|
|
30
|
+
commands: { keys: "F5", label: "Commands" },
|
|
31
|
+
sessions: { keys: "F6", label: "Sessions" },
|
|
32
|
+
attention: { keys: "F7", label: "Attention" },
|
|
33
|
+
tabs: { keys: "F9", label: "Tabs" },
|
|
34
|
+
sidebar: { keys: "F10", label: "Show / hide sidebar" },
|
|
35
|
+
};
|
|
36
|
+
export const SIDEBAR_ACTIONS = {
|
|
37
|
+
help: { key: "?", keys: "?", label: "Help", description: "Using tmux-ide" },
|
|
38
|
+
retry: {
|
|
39
|
+
key: "r",
|
|
40
|
+
keys: "R",
|
|
41
|
+
label: "Retry connection",
|
|
42
|
+
description: "Retry selected machine connection",
|
|
43
|
+
},
|
|
44
|
+
disconnect: {
|
|
45
|
+
key: "d",
|
|
46
|
+
keys: "D",
|
|
47
|
+
label: "Disconnect",
|
|
48
|
+
description: "Disconnect selected machine",
|
|
49
|
+
},
|
|
50
|
+
add: { key: "a", keys: "A", label: "Add machine", description: "Add an SSH machine" },
|
|
51
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CHROME_ACTIONS } from "./application-action-descriptions.js";
|
|
1
2
|
import { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
|
|
2
3
|
export { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
|
|
3
4
|
import { PANE_ACTION_MENU_ITEMS } from "./pane-action-menu-model.js";
|
|
@@ -7,18 +8,20 @@ export const PALETTE_REFERENCE_COMMANDS = {
|
|
|
7
8
|
};
|
|
8
9
|
/** Presentation only: execution remains in the existing application owners. */
|
|
9
10
|
export function applicationCommandDescription(command) {
|
|
11
|
+
if (command === "help")
|
|
12
|
+
return { id: command, label: "Using tmux-ide", detail: "Help and getting started" };
|
|
10
13
|
if (command === "hide-sidebar" || command === "show-sidebar")
|
|
11
14
|
return {
|
|
12
15
|
id: command,
|
|
13
16
|
label: command === "hide-sidebar" ? "Hide sidebar" : "Show sidebar",
|
|
14
17
|
detail: "Terminal layout",
|
|
15
|
-
shortcut:
|
|
18
|
+
shortcut: CHROME_ACTIONS.sidebar.keys,
|
|
16
19
|
};
|
|
17
20
|
if (command === "switch-session")
|
|
18
21
|
return {
|
|
19
22
|
id: command,
|
|
20
23
|
label: "Switch session",
|
|
21
|
-
shortcut:
|
|
24
|
+
shortcut: CHROME_ACTIONS.sessions.keys,
|
|
22
25
|
detail: "Sessions across machines",
|
|
23
26
|
};
|
|
24
27
|
if (command === "shortcuts" || command === "whats-new")
|
|
@@ -71,7 +74,11 @@ export function applicationCommandDescription(command) {
|
|
|
71
74
|
? "Zoom / unzoom pane"
|
|
72
75
|
: undefined) ??
|
|
73
76
|
(command === "home" ? "Home" : command === "terminals" ? "Terminals" : "New terminal window"),
|
|
74
|
-
shortcut: command === "home"
|
|
77
|
+
shortcut: command === "home"
|
|
78
|
+
? CHROME_ACTIONS.home.keys
|
|
79
|
+
: command === "terminals"
|
|
80
|
+
? CHROME_ACTIONS.terminals.keys
|
|
81
|
+
: undefined,
|
|
75
82
|
detail: command === "home"
|
|
76
83
|
? "sessions and agent state"
|
|
77
84
|
: command === "terminals"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HOME_ACTIONS, CHROME_ACTIONS, SIDEBAR_ACTIONS, } from "./application-action-descriptions.js";
|
|
1
2
|
import { applicationCommandDescription } from "./application-command-description.js";
|
|
2
3
|
/** Display metadata only. Context-specific keyboard owners remain authoritative. */
|
|
3
4
|
export const APPLICATION_SHORTCUTS = [
|
|
@@ -5,16 +6,23 @@ export const APPLICATION_SHORTCUTS = [
|
|
|
5
6
|
const description = applicationCommandDescription(command);
|
|
6
7
|
return { category: "Application", label: description.label, keys: description.shortcut };
|
|
7
8
|
}),
|
|
8
|
-
{ category: "Application",
|
|
9
|
-
{ category: "Application",
|
|
10
|
-
{ category: "Application",
|
|
9
|
+
{ category: "Application", ...CHROME_ACTIONS.commands },
|
|
10
|
+
{ category: "Application", ...CHROME_ACTIONS.sidebar },
|
|
11
|
+
{ category: "Application", ...CHROME_ACTIONS.attention },
|
|
11
12
|
{ category: "Application", label: "Sidebar / Sessions when hidden", keys: "Ctrl+G" },
|
|
12
13
|
{ category: "Terminals", label: "Open link", keys: "Shift+click" },
|
|
13
14
|
{ category: "Terminals", label: "Select and copy text", keys: "Shift+drag" },
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
...Object.values(HOME_ACTIONS).map((action) => ({
|
|
16
|
+
category: "Home",
|
|
17
|
+
label: action.description,
|
|
18
|
+
keys: action.keys,
|
|
19
|
+
})),
|
|
20
|
+
{ category: "Sidebar (when focused)", label: "Switch working sessions / machines", keys: "Tab" },
|
|
21
|
+
...Object.values(SIDEBAR_ACTIONS).map((action) => ({
|
|
22
|
+
category: "Sidebar (when focused)",
|
|
23
|
+
label: action.description,
|
|
24
|
+
keys: action.keys,
|
|
25
|
+
})),
|
|
18
26
|
{ category: "Command and session menus", label: "Search actions or sessions", keys: "Type" },
|
|
19
27
|
{ category: "Command and session menus", label: "Choose a result", keys: "↑ / ↓" },
|
|
20
28
|
{ category: "Command and session menus", label: "Activate selected result", keys: "Enter" },
|
|
@@ -13,7 +13,7 @@ export function paletteSection(command) {
|
|
|
13
13
|
return "Appearance";
|
|
14
14
|
if (command === "appearance")
|
|
15
15
|
return "Appearance";
|
|
16
|
-
if (command === "shortcuts" || command === "whats-new")
|
|
16
|
+
if (command === "help" || command === "shortcuts" || command === "whats-new")
|
|
17
17
|
return "Help";
|
|
18
18
|
return "Panes";
|
|
19
19
|
}
|
|
@@ -6,6 +6,7 @@ export interface FleetTabTarget {
|
|
|
6
6
|
readonly server?: TmuxServerScope;
|
|
7
7
|
readonly label: string;
|
|
8
8
|
readonly hostLabel: string;
|
|
9
|
+
readonly serverLabel?: string;
|
|
9
10
|
}
|
|
10
11
|
/** Retained targets, with at most one active terminal owner. Suspended tabs own no streams. */
|
|
11
12
|
export function createFleetTabs(options: {
|
|
@@ -16,19 +17,45 @@ export function createFleetTabs(options: {
|
|
|
16
17
|
unavailable(): void;
|
|
17
18
|
}) {
|
|
18
19
|
const tabs = new Map<string, FleetTabTarget>();
|
|
20
|
+
const unread = new Set<string>();
|
|
21
|
+
const observations = new Map<string, Map<string, string>>();
|
|
19
22
|
let active: string | null = null;
|
|
20
23
|
let pending: string | null = null;
|
|
21
24
|
let epoch = 0;
|
|
22
25
|
let disposed = false;
|
|
23
26
|
return {
|
|
24
|
-
snapshot: () => ({ tabs: [...tabs.values()], active }),
|
|
27
|
+
snapshot: () => ({ tabs: [...tabs.values()], active, unread: [...unread] }),
|
|
28
|
+
observe(key: string, agents: readonly { id: string; activity: string }[] | null) {
|
|
29
|
+
if (disposed || !tabs.has(key)) return;
|
|
30
|
+
if (!agents) {
|
|
31
|
+
observations.delete(key);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const previous = observations.get(key);
|
|
35
|
+
observations.set(key, new Map(agents.map((agent) => [agent.id, agent.activity])));
|
|
36
|
+
if (
|
|
37
|
+
active !== key &&
|
|
38
|
+
!unread.has(key) &&
|
|
39
|
+
agents.some(
|
|
40
|
+
(agent) => agent.activity === "complete" && previous?.get(agent.id) === "running",
|
|
41
|
+
)
|
|
42
|
+
) {
|
|
43
|
+
unread.add(key);
|
|
44
|
+
options.publish();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
25
47
|
remember(target: FleetTabTarget) {
|
|
26
48
|
if (disposed) return;
|
|
27
49
|
if (!tabs.has(target.key) && tabs.size >= 8) {
|
|
28
50
|
const removable = [...tabs.keys()].find((key) => key !== active);
|
|
29
|
-
if (removable)
|
|
51
|
+
if (removable) {
|
|
52
|
+
tabs.delete(removable);
|
|
53
|
+
unread.delete(removable);
|
|
54
|
+
observations.delete(removable);
|
|
55
|
+
}
|
|
30
56
|
}
|
|
31
57
|
tabs.set(target.key, Object.freeze({ ...target }));
|
|
58
|
+
unread.delete(target.key);
|
|
32
59
|
active = target.key;
|
|
33
60
|
options.publish();
|
|
34
61
|
},
|
|
@@ -52,6 +79,7 @@ export function createFleetTabs(options: {
|
|
|
52
79
|
options.unavailable();
|
|
53
80
|
return false;
|
|
54
81
|
}
|
|
82
|
+
unread.delete(key);
|
|
55
83
|
active = key;
|
|
56
84
|
options.publish();
|
|
57
85
|
return true;
|
|
@@ -65,6 +93,8 @@ export function createFleetTabs(options: {
|
|
|
65
93
|
pending = null;
|
|
66
94
|
}
|
|
67
95
|
tabs.delete(key);
|
|
96
|
+
unread.delete(key);
|
|
97
|
+
observations.delete(key);
|
|
68
98
|
options.publish();
|
|
69
99
|
},
|
|
70
100
|
suspend() {
|
|
@@ -80,6 +110,8 @@ export function createFleetTabs(options: {
|
|
|
80
110
|
disposed = true;
|
|
81
111
|
epoch++;
|
|
82
112
|
tabs.clear();
|
|
113
|
+
unread.clear();
|
|
114
|
+
observations.clear();
|
|
83
115
|
active = null;
|
|
84
116
|
},
|
|
85
117
|
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import {
|
|
3
|
+
HOME_ACTIONS,
|
|
4
|
+
HOME_ACTIVITY_ACTIONS,
|
|
5
|
+
} from "../workspace/application-action-descriptions.ts";
|
|
2
6
|
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
|
|
3
7
|
import type { SemanticThemeSnapshot } from "../theme.ts";
|
|
4
8
|
import { clipTerminal, terminalDisplayWidth } from "../terminal-text.ts";
|
|
@@ -151,13 +155,13 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
|
|
|
151
155
|
const key = event.name.toLowerCase();
|
|
152
156
|
const filter =
|
|
153
157
|
!editing() &&
|
|
154
|
-
(key ===
|
|
158
|
+
(key === HOME_ACTIONS.machine.key
|
|
155
159
|
? props.onCycleMachine
|
|
156
|
-
: key ===
|
|
160
|
+
: key === HOME_ACTIONS.attention.key
|
|
157
161
|
? props.onToggleAttention
|
|
158
|
-
: key ===
|
|
162
|
+
: key === HOME_ACTIONS.working.key && props.onSetActivityFilter
|
|
159
163
|
? () => props.onSetActivityFilter?.("working")
|
|
160
|
-
: key ===
|
|
164
|
+
: key === HOME_ACTIONS.all.key && props.onSetActivityFilter
|
|
161
165
|
? () => props.onSetActivityFilter?.("all")
|
|
162
166
|
: undefined);
|
|
163
167
|
if (filter) {
|
|
@@ -232,7 +236,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
|
|
|
232
236
|
>
|
|
233
237
|
<KeyHint
|
|
234
238
|
theme={props.theme}
|
|
235
|
-
keys=
|
|
239
|
+
keys={HOME_ACTIONS.machine.keys}
|
|
236
240
|
label={props.filterLabel?.split(" · ")[0] ?? "All machines"}
|
|
237
241
|
width={
|
|
238
242
|
props.onSetActivityFilter && filterRows() === 1
|
|
@@ -259,7 +263,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
|
|
|
259
263
|
fallback={
|
|
260
264
|
<KeyHint
|
|
261
265
|
theme={props.theme}
|
|
262
|
-
keys=
|
|
266
|
+
keys={HOME_ACTIONS.attention.keys}
|
|
263
267
|
label="Attention"
|
|
264
268
|
quiet
|
|
265
269
|
button
|
|
@@ -269,19 +273,11 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
|
|
|
269
273
|
/>
|
|
270
274
|
}
|
|
271
275
|
>
|
|
272
|
-
<For
|
|
273
|
-
each={
|
|
274
|
-
[
|
|
275
|
-
{ key: "0", label: "All", value: "all" },
|
|
276
|
-
{ key: "w", label: "Working", value: "working" },
|
|
277
|
-
{ key: "a", label: "Needs attention", value: "attention" },
|
|
278
|
-
] as const
|
|
279
|
-
}
|
|
280
|
-
>
|
|
276
|
+
<For each={HOME_ACTIVITY_ACTIONS}>
|
|
281
277
|
{(filter) => (
|
|
282
278
|
<KeyHint
|
|
283
279
|
theme={props.theme}
|
|
284
|
-
keys={filter.
|
|
280
|
+
keys={filter.keys}
|
|
285
281
|
label={filter.value === "attention" && width() < 47 ? "Attention" : filter.label}
|
|
286
282
|
quiet
|
|
287
283
|
button
|
|
@@ -463,8 +459,8 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
|
|
|
463
459
|
<Show when={footer().endsWith("Enter open") && width() >= 20}>
|
|
464
460
|
<KeyHint
|
|
465
461
|
theme={props.theme}
|
|
466
|
-
keys=
|
|
467
|
-
label=
|
|
462
|
+
keys={HOME_ACTIONS.open.keys}
|
|
463
|
+
label={HOME_ACTIONS.open.label}
|
|
468
464
|
quiet
|
|
469
465
|
button
|
|
470
466
|
onPress={() => openSelected("mouse")}
|
|
@@ -307,6 +307,7 @@ export function createApplicationHomeNavigationOwner(options: {
|
|
|
307
307
|
command === "appearance" ||
|
|
308
308
|
command === "hide-sidebar" ||
|
|
309
309
|
command === "show-sidebar" ||
|
|
310
|
+
command === "help" ||
|
|
310
311
|
command === "shortcuts" ||
|
|
311
312
|
command === "whats-new" ||
|
|
312
313
|
command === "switch-session"
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
createApplicationMachineAgents,
|
|
10
10
|
type ApplicationMachineAgent,
|
|
11
11
|
} from "./application-machine-agents.ts";
|
|
12
|
-
import { createSignal, onCleanup } from "solid-js";
|
|
12
|
+
import { createEffect, createSignal, onCleanup } from "solid-js";
|
|
13
13
|
import { applicationMachineAuthorityManager as manager } from "./application-machine-authority.ts";
|
|
14
14
|
import { createApplicationMachineCatalog } from "./application-machine-catalog.ts";
|
|
15
15
|
import { ephemeralMachineProfile } from "./application-machine-startup.ts";
|
|
@@ -195,7 +195,8 @@ export function createApplicationMachineNavigation(options: {
|
|
|
195
195
|
machineId: id,
|
|
196
196
|
liveSessionId: session.liveSessionId,
|
|
197
197
|
label: session.name,
|
|
198
|
-
hostLabel:
|
|
198
|
+
hostLabel: manager.getMachine(id)?.label ?? id,
|
|
199
|
+
serverLabel: session.serverLabel ?? "default",
|
|
199
200
|
});
|
|
200
201
|
preferences.change({ type: "visit", key: session.id });
|
|
201
202
|
if (remember && history[historyIndex] !== session.id) {
|
|
@@ -246,6 +247,27 @@ export function createApplicationMachineNavigation(options: {
|
|
|
246
247
|
const isDefaultSession = (machineId: string, session: { server?: TmuxServerScope }) =>
|
|
247
248
|
!session.server ||
|
|
248
249
|
session.server.generation === manager.getMachine(machineId)?.read()?.instanceId;
|
|
250
|
+
const tabAgents = (tab: FleetTabTarget) => {
|
|
251
|
+
const group = agentGroups().find((value) => value.machineId === tab.machineId);
|
|
252
|
+
// This observer covers only the default tmux server; never guess for other scopes.
|
|
253
|
+
if (!resolveTab(tab) || !group?.available || !isDefaultSession(tab.machineId, tab)) return null;
|
|
254
|
+
const observation = group.observation;
|
|
255
|
+
if (
|
|
256
|
+
observation &&
|
|
257
|
+
(observation.loadingSessions > 0 ||
|
|
258
|
+
observation.unavailableSessions > 0 ||
|
|
259
|
+
observation.truncatedSessions > 0 ||
|
|
260
|
+
observation.refreshingSessionKeys.length > 0)
|
|
261
|
+
)
|
|
262
|
+
return null;
|
|
263
|
+
const rows = group.agents.filter((agent) => agent.liveSessionId === tab.liveSessionId);
|
|
264
|
+
if (rows.some((agent) => agent.disabled)) return null;
|
|
265
|
+
return rows;
|
|
266
|
+
};
|
|
267
|
+
createEffect(() => {
|
|
268
|
+
tabRevision();
|
|
269
|
+
for (const tab of tabs.snapshot().tabs) tabs.observe(tab.key, tabAgents(tab));
|
|
270
|
+
});
|
|
249
271
|
const sidebar: ApplicationMachineSidebarModel = {
|
|
250
272
|
groups: () =>
|
|
251
273
|
snapshot().groups.map((group) => ({
|
|
@@ -269,11 +291,26 @@ export function createApplicationMachineNavigation(options: {
|
|
|
269
291
|
tabs: () => {
|
|
270
292
|
tabRevision();
|
|
271
293
|
const value = tabs.snapshot();
|
|
272
|
-
return value.tabs.map((tab) =>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
294
|
+
return value.tabs.map((tab) => {
|
|
295
|
+
const agents = tabAgents(tab);
|
|
296
|
+
const activity = !agents
|
|
297
|
+
? undefined
|
|
298
|
+
: agents.some((agent) => agent.activity === "failed")
|
|
299
|
+
? ("failed" as const)
|
|
300
|
+
: agents.some((agent) => agent.attention || agent.activity === "waiting")
|
|
301
|
+
? ("waiting" as const)
|
|
302
|
+
: agents.some((agent) => agent.activity === "running")
|
|
303
|
+
? ("running" as const)
|
|
304
|
+
: ("idle" as const);
|
|
305
|
+
return {
|
|
306
|
+
...tab,
|
|
307
|
+
active: value.active === tab.key,
|
|
308
|
+
available: !!resolveTab(tab),
|
|
309
|
+
activity,
|
|
310
|
+
attention: agents?.some((agent) => agent.attention || agent.activity === "waiting"),
|
|
311
|
+
unread: value.unread.includes(tab.key),
|
|
312
|
+
};
|
|
313
|
+
});
|
|
277
314
|
},
|
|
278
315
|
onOpenTab: (key) => {
|
|
279
316
|
void tabs.activate(key);
|