tmux-ide 2.9.0-beta.40 → 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 +43 -31
- 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 +67 -63
- package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +4 -2
- package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +32 -8
- package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +2 -1
- 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
package/README.md
CHANGED
|
@@ -146,6 +146,11 @@ environment.
|
|
|
146
146
|
|
|
147
147
|
Regenerate the production-renderer demo with `pnpm demo:tui`.
|
|
148
148
|
|
|
149
|
+
Explore real Home, sidebar, Help, pane-header and footer components with `pnpm gallery:tui`. The
|
|
150
|
+
development gallery uses fixture data and local actions, so it does not connect
|
|
151
|
+
to your daemon or change live tmux sessions. See the
|
|
152
|
+
[gallery controls and checks](scripts/tui-gallery/README.md).
|
|
153
|
+
|
|
149
154
|
- [Documentation](https://github.com/wavyrai/tmux-ide/tree/main/docs)
|
|
150
155
|
- [Contributing](CONTRIBUTING.md)
|
|
151
156
|
- [Release checklist](RELEASE.md)
|
package/bin/cli.js
CHANGED
|
@@ -11279,7 +11279,7 @@ var require_package = __commonJS({
|
|
|
11279
11279
|
"package.json"(exports, module) {
|
|
11280
11280
|
module.exports = {
|
|
11281
11281
|
name: "tmux-ide",
|
|
11282
|
-
version: "2.9.0-beta.
|
|
11282
|
+
version: "2.9.0-beta.41",
|
|
11283
11283
|
description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
11284
11284
|
type: "module",
|
|
11285
11285
|
bin: {
|
|
@@ -11339,7 +11339,7 @@ var require_package = __commonJS({
|
|
|
11339
11339
|
"test:pack-installed": "node scripts/pack-check-run.mjs",
|
|
11340
11340
|
"release:opentui:check": "node scripts/opentui-release-check.mjs",
|
|
11341
11341
|
"check:native-deps": "node packages/daemon/scripts/check-native-deps.mjs",
|
|
11342
|
-
check: "pnpm run lint:workspace && pnpm run check:control-bytes && pnpm run format:check && pnpm run typecheck:workspace && pnpm run test:postinstall && pnpm run test:portable-release-contract && pnpm run test:benchmark-comparative && pnpm run test:contracts-initializer-purity && pnpm run test:unit && pnpm run test:tui-testdrive && pnpm run test:product-test-rig && pnpm run test:daemon-bun && pnpm run test:tui-renderer && pnpm run test:workbench-dock-package && pnpm run test:pane-frame-package && pnpm run docs:build && pnpm run pack:check && pnpm run test:pack-installed && pnpm run check:native-deps && pnpm run smoke:desktop",
|
|
11342
|
+
check: "pnpm run lint:workspace && pnpm run check:control-bytes && pnpm run format:check && pnpm run typecheck:workspace && pnpm run test:postinstall && pnpm run test:portable-release-contract && pnpm run test:benchmark-comparative && pnpm run test:contracts-initializer-purity && pnpm run test:unit && pnpm run test:tui-testdrive && pnpm run test:product-test-rig && pnpm run test:daemon-bun && pnpm run test:tui-renderer && pnpm run typecheck:tui-gallery && pnpm run test:tui-gallery && pnpm run test:workbench-dock-package && pnpm run test:pane-frame-package && pnpm run docs:build && pnpm run pack:check && pnpm run test:pack-installed && pnpm run check:native-deps && pnpm run smoke:desktop",
|
|
11343
11343
|
postinstall: "node scripts/postinstall.js",
|
|
11344
11344
|
docs: "turbo run dev --filter=@tmux-ide/docs",
|
|
11345
11345
|
"demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
|
|
@@ -11374,7 +11374,10 @@ var require_package = __commonJS({
|
|
|
11374
11374
|
"test:postinstall": "node --test scripts/postinstall-daemon-upgrade.test.mjs",
|
|
11375
11375
|
"test:development-isolation": "tsx scripts/lib/development-isolation-qualification.ts",
|
|
11376
11376
|
"test:development-isolation-unit": "node --test scripts/lib/development-isolation-resources.test.mjs",
|
|
11377
|
-
"test:development-ci": "node --test scripts/lib/development-ci.test.mjs"
|
|
11377
|
+
"test:development-ci": "node --test scripts/lib/development-ci.test.mjs",
|
|
11378
|
+
"gallery:tui": "bun --preload @opentui/solid/preload scripts/tui-gallery/index.tsx",
|
|
11379
|
+
"test:tui-gallery": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./scripts/tui-gallery/gallery.test.tsx",
|
|
11380
|
+
"typecheck:tui-gallery": "tsc --noEmit -p scripts/tsconfig.tui-gallery.json"
|
|
11378
11381
|
},
|
|
11379
11382
|
keywords: [
|
|
11380
11383
|
"tmux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-ide",
|
|
3
|
-
"version": "2.9.0-beta.
|
|
3
|
+
"version": "2.9.0-beta.41",
|
|
4
4
|
"description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"test:pack-installed": "node scripts/pack-check-run.mjs",
|
|
61
61
|
"release:opentui:check": "node scripts/opentui-release-check.mjs",
|
|
62
62
|
"check:native-deps": "node packages/daemon/scripts/check-native-deps.mjs",
|
|
63
|
-
"check": "pnpm run lint:workspace && pnpm run check:control-bytes && pnpm run format:check && pnpm run typecheck:workspace && pnpm run test:postinstall && pnpm run test:portable-release-contract && pnpm run test:benchmark-comparative && pnpm run test:contracts-initializer-purity && pnpm run test:unit && pnpm run test:tui-testdrive && pnpm run test:product-test-rig && pnpm run test:daemon-bun && pnpm run test:tui-renderer && pnpm run test:workbench-dock-package && pnpm run test:pane-frame-package && pnpm run docs:build && pnpm run pack:check && pnpm run test:pack-installed && pnpm run check:native-deps && pnpm run smoke:desktop",
|
|
63
|
+
"check": "pnpm run lint:workspace && pnpm run check:control-bytes && pnpm run format:check && pnpm run typecheck:workspace && pnpm run test:postinstall && pnpm run test:portable-release-contract && pnpm run test:benchmark-comparative && pnpm run test:contracts-initializer-purity && pnpm run test:unit && pnpm run test:tui-testdrive && pnpm run test:product-test-rig && pnpm run test:daemon-bun && pnpm run test:tui-renderer && pnpm run typecheck:tui-gallery && pnpm run test:tui-gallery && pnpm run test:workbench-dock-package && pnpm run test:pane-frame-package && pnpm run docs:build && pnpm run pack:check && pnpm run test:pack-installed && pnpm run check:native-deps && pnpm run smoke:desktop",
|
|
64
64
|
"postinstall": "node scripts/postinstall.js",
|
|
65
65
|
"docs": "turbo run dev --filter=@tmux-ide/docs",
|
|
66
66
|
"demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
|
|
@@ -95,7 +95,10 @@
|
|
|
95
95
|
"test:postinstall": "node --test scripts/postinstall-daemon-upgrade.test.mjs",
|
|
96
96
|
"test:development-isolation": "tsx scripts/lib/development-isolation-qualification.ts",
|
|
97
97
|
"test:development-isolation-unit": "node --test scripts/lib/development-isolation-resources.test.mjs",
|
|
98
|
-
"test:development-ci": "node --test scripts/lib/development-ci.test.mjs"
|
|
98
|
+
"test:development-ci": "node --test scripts/lib/development-ci.test.mjs",
|
|
99
|
+
"gallery:tui": "bun --preload @opentui/solid/preload scripts/tui-gallery/index.tsx",
|
|
100
|
+
"test:tui-gallery": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./scripts/tui-gallery/gallery.test.tsx",
|
|
101
|
+
"typecheck:tui-gallery": "tsc --noEmit -p scripts/tsconfig.tui-gallery.json"
|
|
99
102
|
},
|
|
100
103
|
"keywords": [
|
|
101
104
|
"tmux",
|
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
/** Retained targets, with at most one active terminal owner. Suspended tabs own no streams. */
|
|
2
2
|
export function createFleetTabs(options) {
|
|
3
3
|
const tabs = new Map();
|
|
4
|
+
const unread = new Set();
|
|
5
|
+
const observations = new Map();
|
|
4
6
|
let active = null;
|
|
5
7
|
let pending = null;
|
|
6
8
|
let epoch = 0;
|
|
7
9
|
let disposed = false;
|
|
8
10
|
return {
|
|
9
|
-
snapshot: () => ({ tabs: [...tabs.values()], active }),
|
|
11
|
+
snapshot: () => ({ tabs: [...tabs.values()], active, unread: [...unread] }),
|
|
12
|
+
observe(key, agents) {
|
|
13
|
+
if (disposed || !tabs.has(key))
|
|
14
|
+
return;
|
|
15
|
+
if (!agents) {
|
|
16
|
+
observations.delete(key);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const previous = observations.get(key);
|
|
20
|
+
observations.set(key, new Map(agents.map((agent) => [agent.id, agent.activity])));
|
|
21
|
+
if (active !== key &&
|
|
22
|
+
!unread.has(key) &&
|
|
23
|
+
agents.some((agent) => agent.activity === "complete" && previous?.get(agent.id) === "running")) {
|
|
24
|
+
unread.add(key);
|
|
25
|
+
options.publish();
|
|
26
|
+
}
|
|
27
|
+
},
|
|
10
28
|
remember(target) {
|
|
11
29
|
if (disposed)
|
|
12
30
|
return;
|
|
13
31
|
if (!tabs.has(target.key) && tabs.size >= 8) {
|
|
14
32
|
const removable = [...tabs.keys()].find((key) => key !== active);
|
|
15
|
-
if (removable)
|
|
33
|
+
if (removable) {
|
|
16
34
|
tabs.delete(removable);
|
|
35
|
+
unread.delete(removable);
|
|
36
|
+
observations.delete(removable);
|
|
37
|
+
}
|
|
17
38
|
}
|
|
18
39
|
tabs.set(target.key, Object.freeze({ ...target }));
|
|
40
|
+
unread.delete(target.key);
|
|
19
41
|
active = target.key;
|
|
20
42
|
options.publish();
|
|
21
43
|
},
|
|
@@ -41,6 +63,7 @@ export function createFleetTabs(options) {
|
|
|
41
63
|
options.unavailable();
|
|
42
64
|
return false;
|
|
43
65
|
}
|
|
66
|
+
unread.delete(key);
|
|
44
67
|
active = key;
|
|
45
68
|
options.publish();
|
|
46
69
|
return true;
|
|
@@ -55,6 +78,8 @@ export function createFleetTabs(options) {
|
|
|
55
78
|
pending = null;
|
|
56
79
|
}
|
|
57
80
|
tabs.delete(key);
|
|
81
|
+
unread.delete(key);
|
|
82
|
+
observations.delete(key);
|
|
58
83
|
options.publish();
|
|
59
84
|
},
|
|
60
85
|
suspend() {
|
|
@@ -70,6 +95,8 @@ export function createFleetTabs(options) {
|
|
|
70
95
|
disposed = true;
|
|
71
96
|
epoch++;
|
|
72
97
|
tabs.clear();
|
|
98
|
+
unread.clear();
|
|
99
|
+
observations.clear();
|
|
73
100
|
active = null;
|
|
74
101
|
},
|
|
75
102
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { HOME_ACTIONS, HOME_ACTIVITY_ACTIONS, } from "../workspace/application-action-descriptions.js";
|
|
2
3
|
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
|
|
3
4
|
import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
|
|
4
5
|
import { NavigationRow } from "../ui/navigation-row.jsx";
|
|
@@ -100,13 +101,13 @@ export function HomeAgentRoster(props) {
|
|
|
100
101
|
return false;
|
|
101
102
|
const key = event.name.toLowerCase();
|
|
102
103
|
const filter = !editing() &&
|
|
103
|
-
(key ===
|
|
104
|
+
(key === HOME_ACTIONS.machine.key
|
|
104
105
|
? props.onCycleMachine
|
|
105
|
-
: key ===
|
|
106
|
+
: key === HOME_ACTIONS.attention.key
|
|
106
107
|
? props.onToggleAttention
|
|
107
|
-
: key ===
|
|
108
|
+
: key === HOME_ACTIONS.working.key && props.onSetActivityFilter
|
|
108
109
|
? () => props.onSetActivityFilter?.("working")
|
|
109
|
-
: key ===
|
|
110
|
+
: key === HOME_ACTIONS.all.key && props.onSetActivityFilter
|
|
110
111
|
? () => props.onSetActivityFilter?.("all")
|
|
111
112
|
: undefined);
|
|
112
113
|
if (filter) {
|
|
@@ -162,7 +163,7 @@ export function HomeAgentRoster(props) {
|
|
|
162
163
|
</text>
|
|
163
164
|
</box>
|
|
164
165
|
<box width={width()} height={filterRows()} flexShrink={0} flexDirection={filterRows() > 1 ? "column" : "row"} overflow="hidden">
|
|
165
|
-
<KeyHint theme={props.theme} keys=
|
|
166
|
+
<KeyHint theme={props.theme} keys={HOME_ACTIONS.machine.keys} label={props.filterLabel?.split(" · ")[0] ?? "All machines"} width={props.onSetActivityFilter && filterRows() === 1
|
|
166
167
|
? Math.max(1, width() - 47)
|
|
167
168
|
: props.onSetActivityFilter
|
|
168
169
|
? Math.min(width(), 28)
|
|
@@ -171,16 +172,12 @@ export function HomeAgentRoster(props) {
|
|
|
171
172
|
props.onCycleMachine?.();
|
|
172
173
|
}}/>
|
|
173
174
|
<box height={1} flexShrink={0} flexDirection="row" overflow="hidden" width={Math.min(width(), 47)}>
|
|
174
|
-
<Show when={props.onSetActivityFilter} fallback={<KeyHint theme={props.theme} keys=
|
|
175
|
+
<Show when={props.onSetActivityFilter} fallback={<KeyHint theme={props.theme} keys={HOME_ACTIONS.attention.keys} label="Attention" quiet button onPress={() => {
|
|
175
176
|
if (props.inputActive)
|
|
176
177
|
props.onToggleAttention?.();
|
|
177
178
|
}}/>}>
|
|
178
|
-
<For each={
|
|
179
|
-
|
|
180
|
-
{ key: "w", label: "Working", value: "working" },
|
|
181
|
-
{ key: "a", label: "Needs attention", value: "attention" },
|
|
182
|
-
]}>
|
|
183
|
-
{(filter) => (<KeyHint theme={props.theme} keys={filter.key} label={filter.value === "attention" && width() < 47 ? "Attention" : filter.label} quiet button selected={(props.activityFilter ?? "all") === filter.value} onPress={() => {
|
|
179
|
+
<For each={HOME_ACTIVITY_ACTIONS}>
|
|
180
|
+
{(filter) => (<KeyHint theme={props.theme} keys={filter.keys} label={filter.value === "attention" && width() < 47 ? "Attention" : filter.label} quiet button selected={(props.activityFilter ?? "all") === filter.value} onPress={() => {
|
|
184
181
|
if (props.inputActive)
|
|
185
182
|
props.onSetActivityFilter?.(filter.value);
|
|
186
183
|
}}/>)}
|
|
@@ -267,7 +264,7 @@ export function HomeAgentRoster(props) {
|
|
|
267
264
|
{clipTerminal(footer().replace(/ Enter open$/u, ""), width())}
|
|
268
265
|
</text>
|
|
269
266
|
<Show when={footer().endsWith("Enter open") && width() >= 20}>
|
|
270
|
-
<KeyHint theme={props.theme} keys=
|
|
267
|
+
<KeyHint theme={props.theme} keys={HOME_ACTIONS.open.keys} label={HOME_ACTIONS.open.label} quiet button onPress={() => openSelected("mouse")}/>
|
|
271
268
|
</Show>
|
|
272
269
|
</box>
|
|
273
270
|
<Show when={showRecovery()}>
|
|
@@ -215,6 +215,7 @@ export function createApplicationHomeNavigationOwner(options) {
|
|
|
215
215
|
command === "appearance" ||
|
|
216
216
|
command === "hide-sidebar" ||
|
|
217
217
|
command === "show-sidebar" ||
|
|
218
|
+
command === "help" ||
|
|
218
219
|
command === "shortcuts" ||
|
|
219
220
|
command === "whats-new" ||
|
|
220
221
|
command === "switch-session")
|
|
@@ -3,7 +3,7 @@ import { createFleetTabs } from "./application-fleet-tabs.js";
|
|
|
3
3
|
import { saveMachineProfiles } from "../../../lib/local-fleet-request.js";
|
|
4
4
|
import { createApplicationFleetPreferences } from "./application-fleet-preferences.js";
|
|
5
5
|
import { createApplicationMachineAgents, } from "./application-machine-agents.js";
|
|
6
|
-
import { createSignal, onCleanup } from "solid-js";
|
|
6
|
+
import { createEffect, createSignal, onCleanup } from "solid-js";
|
|
7
7
|
import { applicationMachineAuthorityManager as manager } from "./application-machine-authority.js";
|
|
8
8
|
import { createApplicationMachineCatalog } from "./application-machine-catalog.js";
|
|
9
9
|
import { ephemeralMachineProfile } from "./application-machine-startup.js";
|
|
@@ -159,7 +159,8 @@ export function createApplicationMachineNavigation(options) {
|
|
|
159
159
|
machineId: id,
|
|
160
160
|
liveSessionId: session.liveSessionId,
|
|
161
161
|
label: session.name,
|
|
162
|
-
hostLabel:
|
|
162
|
+
hostLabel: manager.getMachine(id)?.label ?? id,
|
|
163
|
+
serverLabel: session.serverLabel ?? "default",
|
|
163
164
|
});
|
|
164
165
|
preferences.change({ type: "visit", key: session.id });
|
|
165
166
|
if (remember && history[historyIndex] !== session.id) {
|
|
@@ -198,6 +199,28 @@ export function createApplicationMachineNavigation(options) {
|
|
|
198
199
|
});
|
|
199
200
|
const isDefaultSession = (machineId, session) => !session.server ||
|
|
200
201
|
session.server.generation === manager.getMachine(machineId)?.read()?.instanceId;
|
|
202
|
+
const tabAgents = (tab) => {
|
|
203
|
+
const group = agentGroups().find((value) => value.machineId === tab.machineId);
|
|
204
|
+
// This observer covers only the default tmux server; never guess for other scopes.
|
|
205
|
+
if (!resolveTab(tab) || !group?.available || !isDefaultSession(tab.machineId, tab))
|
|
206
|
+
return null;
|
|
207
|
+
const observation = group.observation;
|
|
208
|
+
if (observation &&
|
|
209
|
+
(observation.loadingSessions > 0 ||
|
|
210
|
+
observation.unavailableSessions > 0 ||
|
|
211
|
+
observation.truncatedSessions > 0 ||
|
|
212
|
+
observation.refreshingSessionKeys.length > 0))
|
|
213
|
+
return null;
|
|
214
|
+
const rows = group.agents.filter((agent) => agent.liveSessionId === tab.liveSessionId);
|
|
215
|
+
if (rows.some((agent) => agent.disabled))
|
|
216
|
+
return null;
|
|
217
|
+
return rows;
|
|
218
|
+
};
|
|
219
|
+
createEffect(() => {
|
|
220
|
+
tabRevision();
|
|
221
|
+
for (const tab of tabs.snapshot().tabs)
|
|
222
|
+
tabs.observe(tab.key, tabAgents(tab));
|
|
223
|
+
});
|
|
201
224
|
const sidebar = {
|
|
202
225
|
groups: () => snapshot().groups.map((group) => ({
|
|
203
226
|
...group,
|
|
@@ -214,11 +237,26 @@ export function createApplicationMachineNavigation(options) {
|
|
|
214
237
|
tabs: () => {
|
|
215
238
|
tabRevision();
|
|
216
239
|
const value = tabs.snapshot();
|
|
217
|
-
return value.tabs.map((tab) =>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
240
|
+
return value.tabs.map((tab) => {
|
|
241
|
+
const agents = tabAgents(tab);
|
|
242
|
+
const activity = !agents
|
|
243
|
+
? undefined
|
|
244
|
+
: agents.some((agent) => agent.activity === "failed")
|
|
245
|
+
? "failed"
|
|
246
|
+
: agents.some((agent) => agent.attention || agent.activity === "waiting")
|
|
247
|
+
? "waiting"
|
|
248
|
+
: agents.some((agent) => agent.activity === "running")
|
|
249
|
+
? "running"
|
|
250
|
+
: "idle";
|
|
251
|
+
return {
|
|
252
|
+
...tab,
|
|
253
|
+
active: value.active === tab.key,
|
|
254
|
+
available: !!resolveTab(tab),
|
|
255
|
+
activity,
|
|
256
|
+
attention: agents?.some((agent) => agent.attention || agent.activity === "waiting"),
|
|
257
|
+
unread: value.unread.includes(tab.key),
|
|
258
|
+
};
|
|
259
|
+
});
|
|
222
260
|
},
|
|
223
261
|
onOpenTab: (key) => {
|
|
224
262
|
void tabs.activate(key);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { CHROME_ACTIONS, SIDEBAR_ACTIONS } from "../workspace/application-action-descriptions.js";
|
|
1
2
|
import { createAgentStatusMarker } from "../ui/agent-status-marker.js";
|
|
2
3
|
import { fleetHostColor, summarizeFleetActivity } from "./fleet-presentation.js";
|
|
3
|
-
import {
|
|
4
|
+
import { WorkingSessions } from "../ui/working-sessions.jsx";
|
|
4
5
|
/* @jsxImportSource @opentui/solid */
|
|
5
6
|
import { fleetConnectionMessage, } from "@tmux-ide/daemon-client/fleet-connection-status";
|
|
6
7
|
import { terminalAgentStatusLabel } from "./application-terminal-workspace-policy.js";
|
|
@@ -23,13 +24,12 @@ export function ApplicationMachineSidebar(props) {
|
|
|
23
24
|
: 0;
|
|
24
25
|
const controlsHeight = () => (props.height >= 8 && controlGroup() ? 4 : 0);
|
|
25
26
|
const searchHeight = () => (props.model.onOpenSwitcher ? 1 : 0) + (props.model.onOpenAttention ? 1 : 0);
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
27
|
+
const [workingFocused, setWorkingFocused] = createSignal(true);
|
|
28
|
+
const tabHeight = () => (props.model.tabs?.().length ?? 0) > 0 && props.height >= 12
|
|
29
|
+
? 1 +
|
|
30
|
+
Math.min(props.model.tabs().length, Math.max(1, Math.floor((props.height / 2 - 1) / 3))) *
|
|
31
|
+
3
|
|
32
|
+
: 0;
|
|
33
33
|
const machineHeight = () => Math.max(0, props.height -
|
|
34
34
|
1 -
|
|
35
35
|
(props.model.onAddMachine ? 1 : 0) -
|
|
@@ -133,6 +133,7 @@ export function ApplicationMachineSidebar(props) {
|
|
|
133
133
|
props.model.onCollapse?.(preferenceKey(group), value);
|
|
134
134
|
};
|
|
135
135
|
const activate = (row, source) => {
|
|
136
|
+
setWorkingFocused(false);
|
|
136
137
|
setSelectedKey(row.key);
|
|
137
138
|
setLocalFocused(true);
|
|
138
139
|
props.model.onFocus?.();
|
|
@@ -169,7 +170,7 @@ export function ApplicationMachineSidebar(props) {
|
|
|
169
170
|
: 0;
|
|
170
171
|
const rowHeight = (row) => (row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1) + sectionGap(row);
|
|
171
172
|
const revealFocusedRow = () => {
|
|
172
|
-
if (!focused() || !scroll)
|
|
173
|
+
if (!focused() || (workingFocused() && tabHeight()) || !scroll)
|
|
173
174
|
return;
|
|
174
175
|
const y = rows()
|
|
175
176
|
.slice(0, index())
|
|
@@ -191,6 +192,14 @@ export function ApplicationMachineSidebar(props) {
|
|
|
191
192
|
if (!focused() || event.eventType !== "press" || event.meta)
|
|
192
193
|
return false;
|
|
193
194
|
let key = event.name.toLowerCase();
|
|
195
|
+
if (key === "tab" && tabHeight()) {
|
|
196
|
+
setWorkingFocused((value) => !value);
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (workingFocused() && tabHeight() && !["escape", "?", "question", "/", "a"].includes(key))
|
|
202
|
+
return false;
|
|
194
203
|
if (event.ctrl && key === "d")
|
|
195
204
|
key = "halfdown";
|
|
196
205
|
else if (event.ctrl && key === "u")
|
|
@@ -204,7 +213,7 @@ export function ApplicationMachineSidebar(props) {
|
|
|
204
213
|
l: "right",
|
|
205
214
|
g: event.shift ? "end" : "home",
|
|
206
215
|
"/": "search",
|
|
207
|
-
|
|
216
|
+
[SIDEBAR_ACTIONS.help.key]: "help",
|
|
208
217
|
question: "help",
|
|
209
218
|
}[key] ?? key;
|
|
210
219
|
if (event.ctrl && !["halfdown", "halfup"].includes(key))
|
|
@@ -226,9 +235,9 @@ export function ApplicationMachineSidebar(props) {
|
|
|
226
235
|
"return",
|
|
227
236
|
"space",
|
|
228
237
|
"escape",
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
238
|
+
SIDEBAR_ACTIONS.add.key,
|
|
239
|
+
SIDEBAR_ACTIONS.retry.key,
|
|
240
|
+
SIDEBAR_ACTIONS.disconnect.key,
|
|
232
241
|
"f",
|
|
233
242
|
].includes(key))
|
|
234
243
|
return false;
|
|
@@ -247,7 +256,7 @@ export function ApplicationMachineSidebar(props) {
|
|
|
247
256
|
props.model.onBlur?.();
|
|
248
257
|
return true;
|
|
249
258
|
}
|
|
250
|
-
if (key ===
|
|
259
|
+
if (key === SIDEBAR_ACTIONS.add.key) {
|
|
251
260
|
props.model.onAddMachine?.();
|
|
252
261
|
return true;
|
|
253
262
|
}
|
|
@@ -260,9 +269,9 @@ export function ApplicationMachineSidebar(props) {
|
|
|
260
269
|
props.model.onFavorite?.(row.session.id, !props.model.favorites?.().includes(row.session.id));
|
|
261
270
|
return true;
|
|
262
271
|
}
|
|
263
|
-
if (key ===
|
|
272
|
+
if (key === SIDEBAR_ACTIONS.retry.key || key === SIDEBAR_ACTIONS.disconnect.key) {
|
|
264
273
|
if (row.group.id !== "local") {
|
|
265
|
-
if (key ===
|
|
274
|
+
if (key === SIDEBAR_ACTIONS.retry.key)
|
|
266
275
|
props.model.onRetryMachine?.(row.group.id);
|
|
267
276
|
else
|
|
268
277
|
props.model.onDisconnectMachine?.(row.group.id);
|
|
@@ -297,19 +306,22 @@ export function ApplicationMachineSidebar(props) {
|
|
|
297
306
|
});
|
|
298
307
|
return (<Surface id="application-machine-sidebar" theme={props.theme} variant="panel" width={props.width} height={props.height} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
299
308
|
<box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
|
|
300
|
-
<text fg={props.theme.roles.text.secondary}>
|
|
309
|
+
<text fg={props.theme.roles.text.secondary}>
|
|
310
|
+
{tabHeight() ? " Workspace" : " Machines"}
|
|
311
|
+
</text>
|
|
301
312
|
<box flexGrow={1}/>
|
|
302
313
|
<Show when={props.model.tabs?.().length && props.width >= 28}>
|
|
303
|
-
<KeyHint theme={props.theme} keys=
|
|
314
|
+
<KeyHint theme={props.theme} keys={CHROME_ACTIONS.tabs.keys} label={CHROME_ACTIONS.tabs.label} quiet/>
|
|
304
315
|
</Show>
|
|
305
|
-
<KeyHint theme={props.theme} keys=
|
|
316
|
+
<KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.help.keys} label={props.width >= 20 ? SIDEBAR_ACTIONS.help.label : undefined} quiet button onPress={() => props.onHelp?.("mouse")}/>
|
|
306
317
|
</box>
|
|
307
|
-
<
|
|
308
|
-
{(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
318
|
+
<Show when={tabHeight() > 0}>
|
|
319
|
+
<WorkingSessions theme={props.theme} rows={props.model.tabs?.() ?? []} width={props.width} height={tabHeight()} focused={focused() && workingFocused()} onFocus={() => {
|
|
320
|
+
setWorkingFocused(true);
|
|
321
|
+
setLocalFocused(true);
|
|
322
|
+
props.model.onFocus?.();
|
|
323
|
+
}} onOpen={(key) => props.model.onOpenTab?.(key)} onClose={props.model.onCloseTab}/>
|
|
324
|
+
</Show>
|
|
313
325
|
<scrollbox ref={(value) => {
|
|
314
326
|
scroll = value;
|
|
315
327
|
const resized = value.content.onSizeChange;
|
|
@@ -394,7 +406,7 @@ export function ApplicationMachineSidebar(props) {
|
|
|
394
406
|
: `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
|
|
395
407
|
: row.group.agents?.length && row.group.state === "ready"
|
|
396
408
|
? activity(row).label
|
|
397
|
-
: `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && row.key === selectedKey())} status={(row.agent?.attention ?? activity(row).kind === "attention")
|
|
409
|
+
: `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && !(workingFocused() && tabHeight()) && row.key === selectedKey())} status={(row.agent?.attention ?? activity(row).kind === "attention")
|
|
398
410
|
? "blocked"
|
|
399
411
|
: undefined} onActivate={(source) => activate(row, source)}/>
|
|
400
412
|
<Show when={row.agent}>
|
|
@@ -410,10 +422,10 @@ export function ApplicationMachineSidebar(props) {
|
|
|
410
422
|
</scrollbox>)}
|
|
411
423
|
</For>
|
|
412
424
|
<Show when={props.model.onOpenSwitcher}>
|
|
413
|
-
<KeyHint theme={props.theme} keys=
|
|
425
|
+
<KeyHint theme={props.theme} keys={CHROME_ACTIONS.sessions.keys} label="Browse all sessions" width={props.width} quiet button onPress={() => props.model.onOpenSwitcher?.()}/>
|
|
414
426
|
</Show>
|
|
415
427
|
<Show when={props.model.onOpenAttention}>
|
|
416
|
-
<KeyHint theme={props.theme} keys=
|
|
428
|
+
<KeyHint theme={props.theme} keys={CHROME_ACTIONS.attention.keys} label={`${CHROME_ACTIONS.attention.label} (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`} width={props.width} quiet button onPress={() => props.model.onOpenAttention?.()}/>
|
|
417
429
|
</Show>
|
|
418
430
|
<Show when={controlsHeight() > 0 && controlGroup()}>
|
|
419
431
|
{(group) => (<>
|
|
@@ -422,12 +434,12 @@ export function ApplicationMachineSidebar(props) {
|
|
|
422
434
|
? fleetConnectionMessage(group().diagnostic)
|
|
423
435
|
: connectionDetail(group())}
|
|
424
436
|
</text>
|
|
425
|
-
<KeyHint theme={props.theme} keys=
|
|
426
|
-
<KeyHint theme={props.theme} keys=
|
|
437
|
+
<KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.retry.keys} label={SIDEBAR_ACTIONS.retry.label} width={props.width} quiet button onPress={() => props.model.onRetryMachine?.(group().id)}/>
|
|
438
|
+
<KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.disconnect.keys} label={SIDEBAR_ACTIONS.disconnect.label} width={props.width} quiet button onPress={() => props.model.onDisconnectMachine?.(group().id)}/>
|
|
427
439
|
</>)}
|
|
428
440
|
</Show>
|
|
429
441
|
<For each={props.model.onAddMachine ? [props.model.onAddMachine] : []}>
|
|
430
|
-
{(add) => (<KeyHint theme={props.theme} keys=
|
|
442
|
+
{(add) => (<KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.add.keys} label={SIDEBAR_ACTIONS.add.label} width={props.width} quiet button onPress={add}/>)}
|
|
431
443
|
</For>
|
|
432
444
|
</Surface>);
|
|
433
445
|
}
|
|
@@ -27,9 +27,9 @@ export function createApplicationPaletteCommandOwner(options) {
|
|
|
27
27
|
const activate = (command, source, confirmed = false) => {
|
|
28
28
|
if (busy())
|
|
29
29
|
return;
|
|
30
|
-
if (command === "shortcuts" || command === "whats-new") {
|
|
30
|
+
if (command === "help" || command === "shortcuts" || command === "whats-new") {
|
|
31
31
|
setCloseArmed(false);
|
|
32
|
-
setReferencePage(command === "shortcuts" ? "shortcuts" : "changes");
|
|
32
|
+
setReferencePage(command === "help" ? "help" : command === "shortcuts" ? "shortcuts" : "changes");
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
if (command === "switch-session") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { batch, createEffect, createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { UI_ICONS } from "./icons.js";
|
|
2
3
|
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
4
|
const listeners = new Set();
|
|
4
5
|
let timer;
|
|
@@ -37,25 +38,25 @@ export function createAgentStatusMarker(options) {
|
|
|
37
38
|
});
|
|
38
39
|
return () => {
|
|
39
40
|
if (options.unavailable?.())
|
|
40
|
-
return
|
|
41
|
+
return UI_ICONS.unknown;
|
|
41
42
|
if (options.attention?.())
|
|
42
|
-
return
|
|
43
|
+
return UI_ICONS.attention;
|
|
43
44
|
switch (options.status()) {
|
|
44
45
|
case "running":
|
|
45
46
|
case "working":
|
|
46
47
|
return animated() ? FRAMES[current()] : options.theme().glyphs.active;
|
|
47
48
|
case "waiting":
|
|
48
49
|
case "blocked":
|
|
49
|
-
return
|
|
50
|
+
return UI_ICONS.attention;
|
|
50
51
|
case "complete":
|
|
51
52
|
case "done":
|
|
52
53
|
return options.theme().glyphs.check;
|
|
53
54
|
case "failed":
|
|
54
|
-
return
|
|
55
|
+
return UI_ICONS.failed;
|
|
55
56
|
case "idle":
|
|
56
57
|
return options.theme().glyphs.inactive;
|
|
57
58
|
default:
|
|
58
|
-
return
|
|
59
|
+
return UI_ICONS.unknown;
|
|
59
60
|
}
|
|
60
61
|
};
|
|
61
62
|
}
|
|
@@ -14,3 +14,6 @@ export { componentPalette, } from "./state.js";
|
|
|
14
14
|
export { Tabs } from "./tabs.jsx";
|
|
15
15
|
export { Surface } from "./surface.jsx";
|
|
16
16
|
export { StatusBar, StatusBarAction, StatusBarGroup, StatusBarSegment, StatusSegment, } from "./status-bar.jsx";
|
|
17
|
+
export { UI_ICONS } from "./icons.js";
|
|
18
|
+
export { SessionRow, sessionRowStatus } from "./session-row.jsx";
|
|
19
|
+
export { WorkingSessions } from "./working-sessions.jsx";
|
|
@@ -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
|
+
}
|