tmux-ide 2.9.0-beta.30 → 2.9.0-beta.32
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/bin/cli.js +3 -4
- package/package.json +1 -1
- package/packages/daemon/dist/command-center/tmux-servers.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +19 -12
- package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +48 -15
- package/packages/daemon/dist/tui/mirror/ui/badge.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/ui/detail-row.jsx +22 -0
- package/packages/daemon/dist/tui/mirror/ui/home-wordmark.js +11 -0
- package/packages/daemon/dist/tui/mirror/ui/key-hint.jsx +8 -2
- package/packages/daemon/dist/tui/mirror/ui/menu.jsx +8 -8
- package/packages/daemon/dist/tui/mirror/ui/overlay-frame.jsx +9 -7
- package/packages/daemon/dist/tui/mirror/ui/overlay-list-row.jsx +2 -2
- package/packages/daemon/dist/tui/mirror/ui/overlay-model.js +10 -0
- package/packages/daemon/dist/tui/mirror/ui/status-bar.jsx +2 -2
- package/packages/daemon/dist/tui/mirror/ui/surface.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/ui/tabs.jsx +1 -3
- package/packages/daemon/dist/tui/mirror/workspace/application-shortcuts.js +45 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +3 -2
- package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +3 -3
- package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +7 -7
- package/packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog.tsx +8 -1
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +5 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +29 -18
- package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +147 -73
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +3 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +86 -19
- package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +12 -2
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +3 -0
- package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +50 -86
- package/packages/daemon/src/tui/mirror/ui/badge.tsx +1 -1
- package/packages/daemon/src/tui/mirror/ui/detail-row.tsx +51 -0
- package/packages/daemon/src/tui/mirror/ui/home-wordmark.ts +12 -0
- package/packages/daemon/src/tui/mirror/ui/key-hint.tsx +17 -2
- package/packages/daemon/src/tui/mirror/ui/menu.tsx +16 -15
- package/packages/daemon/src/tui/mirror/ui/overlay-frame.tsx +19 -11
- package/packages/daemon/src/tui/mirror/ui/overlay-list-row.tsx +2 -2
- package/packages/daemon/src/tui/mirror/ui/overlay-model.ts +11 -0
- package/packages/daemon/src/tui/mirror/ui/status-bar.tsx +2 -2
- package/packages/daemon/src/tui/mirror/ui/surface.tsx +1 -1
- package/packages/daemon/src/tui/mirror/ui/tabs.tsx +1 -4
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +3 -3
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +20 -20
- package/packages/daemon/src/tui/mirror/workspace/application-shell-view.tsx +4 -1
- package/packages/daemon/src/tui/mirror/workspace/application-shortcuts.ts +52 -0
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.32",
|
|
11283
11283
|
description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
11284
11284
|
type: "module",
|
|
11285
11285
|
bin: {
|
|
@@ -71620,9 +71620,8 @@ function mountTmuxServerRoutes(app, options) {
|
|
|
71620
71620
|
(row) => row.sessionName === workspace.sessionName && row.liveSessionId === c.req.query("liveSessionId")
|
|
71621
71621
|
))
|
|
71622
71622
|
throw new TmuxServerScopeError("stale-generation");
|
|
71623
|
-
const snapshot2 = await owner.terminalInventoryRuntime.
|
|
71624
|
-
workspace.sessionName
|
|
71625
|
-
c.req.raw.signal
|
|
71623
|
+
const snapshot2 = await owner.terminalInventoryRuntime.discoverApplicationShellSession(
|
|
71624
|
+
workspace.sessionName
|
|
71626
71625
|
);
|
|
71627
71626
|
const currentSessions = await owner.catalog();
|
|
71628
71627
|
if (!currentSessions.some(
|
package/package.json
CHANGED
|
@@ -181,7 +181,9 @@ export function mountTmuxServerRoutes(app, options) {
|
|
|
181
181
|
if (!sessions.some((row) => row.sessionName === workspace.sessionName &&
|
|
182
182
|
row.liveSessionId === c.req.query("liveSessionId")))
|
|
183
183
|
throw new TmuxServerScopeError("stale-generation");
|
|
184
|
-
|
|
184
|
+
// Agent lists need the same status enrichment as pane headers. Raw
|
|
185
|
+
// terminal inventory omits these facts and falls back to process activity.
|
|
186
|
+
const snapshot = await owner.terminalInventoryRuntime.discoverApplicationShellSession(workspace.sessionName);
|
|
185
187
|
// Discovery can yield while a same-name session is replaced. Revalidate
|
|
186
188
|
// the captured incarnation before publishing agent metadata as current.
|
|
187
189
|
const currentSessions = await owner.catalog();
|
|
@@ -140,15 +140,15 @@ export function HomeAgentRoster(props) {
|
|
|
140
140
|
event.stopPropagation();
|
|
141
141
|
props.onMove(direction === "up" ? -1 : 1);
|
|
142
142
|
}} onMouseOut={() => setHovered(null)}>
|
|
143
|
+
<box height={searchRows()} flexShrink={0}/>
|
|
143
144
|
<box width={width()} height={1} flexShrink={0} flexDirection="row">
|
|
144
145
|
<Show when={props.snapshot.phase === "loading" || props.snapshot.loadingSessions > 0}>
|
|
145
146
|
<ActivityIndicator theme={props.theme} active={props.inputActive}/>
|
|
146
147
|
</Show>
|
|
147
|
-
<text height={1} flexGrow={1} fg={props.theme.roles.text.
|
|
148
|
+
<text height={1} flexGrow={1} fg={props.theme.roles.text.muted}>
|
|
148
149
|
{clipTerminal(title(), width())}
|
|
149
150
|
</text>
|
|
150
151
|
</box>
|
|
151
|
-
<box height={searchRows()} flexShrink={0}/>
|
|
152
152
|
<box width={width()} height={1} flexShrink={0} flexDirection="row">
|
|
153
153
|
<text width={Math.max(0, width() - (props.onToggleAttention ? 16 : 0))} height={1} fg={props.theme.roles.text.secondary} onMouseDown={(event) => {
|
|
154
154
|
if (!props.inputActive || event.button !== 0)
|
|
@@ -166,10 +166,17 @@ export function HomeAgentRoster(props) {
|
|
|
166
166
|
}}/>
|
|
167
167
|
</Show>
|
|
168
168
|
</box>
|
|
169
|
-
<Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() * rowHeight() + 1)} flexShrink={0}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
<Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() * rowHeight() + 1)} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
170
|
+
<Show when={props.snapshot.phase === "live" && !props.query}>
|
|
171
|
+
<text height={1} width={width()} fg={props.theme.roles.text.primary}>
|
|
172
|
+
{clipTerminal("Your next session starts here", width())}
|
|
173
|
+
</text>
|
|
174
|
+
<text height={1} width={width()} fg={props.theme.roles.text.muted}>
|
|
175
|
+
{clipTerminal("Open terminals, or use Commands to connect a machine.", width())}
|
|
176
|
+
</text>
|
|
177
|
+
</Show>
|
|
178
|
+
</box>}>
|
|
179
|
+
<box height={1} flexShrink={0}/>
|
|
173
180
|
<box height={visibleCount() * rowHeight()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
174
181
|
<For each={keys()}>
|
|
175
182
|
{(key) => {
|
|
@@ -185,10 +192,10 @@ export function HomeAgentRoster(props) {
|
|
|
185
192
|
props.onSelect(key);
|
|
186
193
|
props.onOpen(row(), "mouse");
|
|
187
194
|
}} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
|
|
188
|
-
<NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
<NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={rowHeight() > 1
|
|
196
|
+
? row().name
|
|
197
|
+
: padCells(row().name, columns().agent) +
|
|
198
|
+
padCells([row().machineLabel, row().serverLabel, row().sessionName]
|
|
192
199
|
.filter(Boolean)
|
|
193
200
|
.join(" / "), columns().session)} detail={padCells(`${!stale(row()) && row().attention ? "! " : ""}${stale(row()) ? "last seen" : homeAgentStatusLabel(row().activity).toLowerCase()}`, columns().status)} detailAlign="end" marker=" " selected={props.selection.selectedKey === key} focused={props.inputActive && !editing() && props.selection.selectedKey === key} hovered={hovered() === key} status={stale(row()) ? "unknown" : statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
|
|
194
201
|
if (!props.inputActive || row().paneId === null || stale(row()))
|
|
@@ -203,7 +210,7 @@ export function HomeAgentRoster(props) {
|
|
|
203
210
|
selected: props.selection.selectedKey === key,
|
|
204
211
|
disabled: row().paneId === null || stale(row()),
|
|
205
212
|
}).background}>
|
|
206
|
-
{clipTerminal(` ${
|
|
213
|
+
{clipTerminal(` ${[row().sessionName, row().machineLabel, row().serverLabel, row().harness].filter(Boolean).join(" · ")}`, width())}
|
|
207
214
|
</text>
|
|
208
215
|
</Show>
|
|
209
216
|
</box>);
|
|
@@ -212,7 +219,7 @@ export function HomeAgentRoster(props) {
|
|
|
212
219
|
</box>
|
|
213
220
|
</Show>
|
|
214
221
|
<Show when={props.onQueryChange}>
|
|
215
|
-
{(onQueryChange) => (<box position="absolute" top={
|
|
222
|
+
{(onQueryChange) => (<box position="absolute" top={0} left={0} width={width()} height={1}>
|
|
216
223
|
<HomeAgentSearch theme={props.theme} width={width()} query={props.query ?? ""} active={props.inputActive} onChange={onQueryChange()} onEdit={(event) => {
|
|
217
224
|
const action = applicationPaneRenameKeyAction(event, props.query ?? "");
|
|
218
225
|
if (action.kind === "update")
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { interactionReceiptTargetLabel } from "@tmux-ide/core";
|
|
2
2
|
import { For, Show } from "solid-js";
|
|
3
3
|
import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
|
|
4
|
+
import { DetailRow } from "../ui/detail-row.jsx";
|
|
4
5
|
import { TuiButton } from "../ui/button.jsx";
|
|
5
6
|
import { HomeAgentRoster } from "./application-home-agent-roster.jsx";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { APPLICATION_HOME_WORDMARK, APPLICATION_HOME_WORDMARK_WIDTH } from "../ui/home-wordmark.js";
|
|
8
|
+
/** Keep the marketing wordmark intact, falling back when the agent list needs the room. */
|
|
9
|
+
export function applicationHomeBrandVariant(width, height) {
|
|
10
|
+
return width >= APPLICATION_HOME_WORDMARK_WIDTH && height >= 28 ? "ascii" : "wordmark";
|
|
9
11
|
}
|
|
10
12
|
/** Presentation only: session data, commands, and keyboard admission stay with the shell. */
|
|
11
13
|
export function ApplicationHomeSurface(props) {
|
|
@@ -13,9 +15,15 @@ export function ApplicationHomeSurface(props) {
|
|
|
13
15
|
const height = () => Math.max(0, Math.floor(props.height));
|
|
14
16
|
const inset = () => Math.max(width() >= 40 ? 2 : width() >= 12 ? 1 : 0, props.branded ? Math.floor((width() - 96) / 2) : 0);
|
|
15
17
|
const bodyWidth = () => Math.max(0, width() - inset() * 2);
|
|
18
|
+
const showAscii = () => props.branded &&
|
|
19
|
+
props.agentRoster?.phase === "live" &&
|
|
20
|
+
props.agentRoster.rows.length === 0 &&
|
|
21
|
+
!props.agentQuery &&
|
|
22
|
+
applicationHomeBrandVariant(bodyWidth(), height()) === "ascii";
|
|
23
|
+
const brandRows = () => (showAscii() ? APPLICATION_HOME_WORDMARK.length : 1);
|
|
16
24
|
const spacious = () => height() >= 14;
|
|
17
25
|
const context = () => clipTerminal(props.branded && props.agentRoster
|
|
18
|
-
? "
|
|
26
|
+
? "Your agents, across your machines"
|
|
19
27
|
: `${props.session ?? "No session selected"} · ${props.status}`, bodyWidth());
|
|
20
28
|
const summary = () => {
|
|
21
29
|
if (!props.session || props.agents === undefined)
|
|
@@ -34,14 +42,31 @@ export function ApplicationHomeSurface(props) {
|
|
|
34
42
|
(props.onCycleTheme ? naturalButtonWidth(themeLabel()) + 2 : 0) +
|
|
35
43
|
(props.onOpenTutorial ? naturalButtonWidth(props.tutorialLabel ?? "Learn tmux-ide") + 2 : 0) +
|
|
36
44
|
2;
|
|
37
|
-
const reservedRows = () => (
|
|
45
|
+
const reservedRows = () => brandRows() -
|
|
46
|
+
1 +
|
|
47
|
+
(spacious() ? 4 : 2) +
|
|
38
48
|
(actionsInRow() ? 1 : 2 + (props.onCycleTheme ? 1 : 0) + (props.onOpenTutorial ? 1 : 0)) +
|
|
39
49
|
(spacious() ? 1 : 0) +
|
|
40
50
|
(props.note ? (spacious() ? 2 : 1) : 0);
|
|
41
|
-
const
|
|
42
|
-
|
|
51
|
+
const activityRows = () => (height() >= 24 && bodyWidth() >= 48 ? 2 : 1);
|
|
52
|
+
const selectedAgent = () => props.agentRoster?.rows.find((row) => row.key === props.agentSelection?.selectedKey);
|
|
53
|
+
// Receipts lack a fleet-wide machine identity. Never attribute a same-named
|
|
54
|
+
// workspace/pane collision to the selected agent.
|
|
55
|
+
const selectedActivity = () => {
|
|
56
|
+
const selected = selectedAgent();
|
|
57
|
+
if (!selected?.paneId || selected.daemonInstanceId !== props.activityDaemonId)
|
|
58
|
+
return [];
|
|
59
|
+
const matches = props.agentRoster?.rows.filter((row) => row.sessionName === selected.sessionName && row.paneId === selected.paneId);
|
|
60
|
+
if (matches?.length !== 1)
|
|
61
|
+
return [];
|
|
62
|
+
return (props.recentPaneActivity ?? []).filter((receipt) => receipt.workspaceName === selected.sessionName &&
|
|
63
|
+
receipt.target.kind === "pane" &&
|
|
64
|
+
receipt.target.semanticPaneId === selected.paneId);
|
|
65
|
+
};
|
|
66
|
+
const recentActivity = () => selectedActivity().slice(0, spacious()
|
|
67
|
+
? Math.max(0, Math.min(1, Math.floor((height() - reservedRows() - (props.agentRoster ? 12 : 2) - 2) / activityRows())))
|
|
43
68
|
: 0);
|
|
44
|
-
const activityHeight = () =>
|
|
69
|
+
const activityHeight = () => recentActivity().length > 0 ? recentActivity().length * activityRows() + 2 : 0;
|
|
45
70
|
const paneLabel = (paneId) => {
|
|
46
71
|
const matches = props.agentRoster?.rows.filter((row) => row.paneId === paneId) ?? [];
|
|
47
72
|
return matches.length === 1 ? matches[0].name : paneId;
|
|
@@ -65,9 +90,19 @@ export function ApplicationHomeSurface(props) {
|
|
|
65
90
|
: "timed out";
|
|
66
91
|
const rosterHeight = () => Math.max(0, height() - reservedRows() - activityHeight());
|
|
67
92
|
return (<box id="application-home" width={width()} height={height()} paddingLeft={inset()} paddingRight={inset()} paddingTop={spacious() ? 1 : 0} flexDirection="column" alignItems="flex-start" backgroundColor={props.theme.roles.surfaces.canvas} overflow="hidden">
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
93
|
+
<box height={brandRows()} width={bodyWidth()} flexShrink={0} flexDirection="column">
|
|
94
|
+
<Show when={showAscii()} fallback={<text width={bodyWidth()} height={1} fg={props.theme.roles.text.primary}>
|
|
95
|
+
<strong>
|
|
96
|
+
{clipTerminal(props.branded ? "tmux-ide" : props.project, bodyWidth())}
|
|
97
|
+
</strong>
|
|
98
|
+
</text>}>
|
|
99
|
+
<For each={APPLICATION_HOME_WORDMARK}>
|
|
100
|
+
{(line) => (<text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.primary}>
|
|
101
|
+
{" ".repeat(Math.max(0, Math.floor((bodyWidth() - APPLICATION_HOME_WORDMARK_WIDTH) / 2))) + line}
|
|
102
|
+
</text>)}
|
|
103
|
+
</For>
|
|
104
|
+
</Show>
|
|
105
|
+
</box>
|
|
71
106
|
<box height={spacious() ? 1 : 0} flexShrink={0}/>
|
|
72
107
|
<text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
|
|
73
108
|
{context()}
|
|
@@ -86,12 +121,10 @@ export function ApplicationHomeSurface(props) {
|
|
|
86
121
|
<Show when={activityHeight() > 0}>
|
|
87
122
|
<box width={bodyWidth()} height={activityHeight()} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
88
123
|
<text height={1} width={bodyWidth()} fg={props.theme.roles.text.secondary}>
|
|
89
|
-
{clipTerminal("
|
|
124
|
+
{clipTerminal(`${selectedAgent()?.name ?? "Agent"} · latest activity`, bodyWidth())}
|
|
90
125
|
</text>
|
|
91
126
|
<For each={recentActivity()}>
|
|
92
|
-
{(receipt) => (<
|
|
93
|
-
{clipTerminal(`${activityTime(receipt)} · ${interactionReceiptTargetLabel(receipt, paneLabel)} · ${activityPhase(receipt)}`, bodyWidth())}
|
|
94
|
-
</text>)}
|
|
127
|
+
{(receipt) => (<DetailRow theme={props.theme} width={bodyWidth()} label={interactionReceiptTargetLabel(receipt, paneLabel)} detail={activityPhase(receipt)} description={activityRows() === 2 ? activityTime(receipt) : undefined} attention={receipt.phase === "rejected" || receipt.phase === "timed-out"}/>)}
|
|
95
128
|
</For>
|
|
96
129
|
<text height={1} width={bodyWidth()} fg={props.theme.roles.text.muted}>
|
|
97
130
|
{clipTerminal("Activity reported through tmux-ide", bodyWidth())}
|
|
@@ -3,7 +3,7 @@ import { componentPalette } from "./state.js";
|
|
|
3
3
|
export function Badge(props) {
|
|
4
4
|
const palette = () => componentPalette(props.theme, props, props.tone);
|
|
5
5
|
const background = () => props.surface === "header" && palette().state === "base"
|
|
6
|
-
? props.theme.roles.surfaces.
|
|
6
|
+
? props.theme.roles.surfaces.panel
|
|
7
7
|
: palette().background;
|
|
8
8
|
const width = () => Math.max(1, Math.floor(props.width ?? terminalDisplayWidth(props.label) + 2));
|
|
9
9
|
const content = () => clipTerminal(props.presentation ?? ` ${props.marker ? `${props.marker} ` : ""}${props.label} `, width());
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
|
|
2
|
+
/** Read-only information row: primary label left, secondary detail aligned right. */
|
|
3
|
+
export function DetailRow(props) {
|
|
4
|
+
const detailWidth = () => props.detail
|
|
5
|
+
? Math.min(terminalDisplayWidth(props.detail), Math.floor(Math.max(0, props.width) / 2))
|
|
6
|
+
: 0;
|
|
7
|
+
const labelWidth = () => Math.max(0, props.width - detailWidth() - (detailWidth() ? 1 : 0));
|
|
8
|
+
return (<box width={props.width} height={props.description ? 2 : 1} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
9
|
+
<box width={props.width} height={1} flexShrink={0} flexDirection="row">
|
|
10
|
+
<text width={labelWidth()} height={1} fg={props.attention ? props.theme.colors.status.blocked : props.theme.roles.text.primary}>
|
|
11
|
+
{clipTerminal(props.label, labelWidth())}
|
|
12
|
+
</text>
|
|
13
|
+
<box flexGrow={1}/>
|
|
14
|
+
<text width={detailWidth()} height={1} fg={props.attention ? props.theme.colors.status.blocked : props.theme.roles.text.muted}>
|
|
15
|
+
{clipTerminal(props.detail ?? "", detailWidth())}
|
|
16
|
+
</text>
|
|
17
|
+
</box>
|
|
18
|
+
{props.description ? (<text width={props.width} height={1} fg={props.theme.roles.text.muted}>
|
|
19
|
+
{clipTerminal(props.description, props.width)}
|
|
20
|
+
</text>) : null}
|
|
21
|
+
</box>);
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Exact terminal glyphs from docs/public/ascii-wordmark.svg.
|
|
2
|
+
export const APPLICATION_HOME_WORDMARK = [
|
|
3
|
+
" ░██ ░██ ░██",
|
|
4
|
+
" ░██ ░██",
|
|
5
|
+
"░████████ ░█████████████ ░██ ░██ ░██ ░██ ░██ ░████████ ░███████",
|
|
6
|
+
" ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██░██ ░██ ░██ ░██",
|
|
7
|
+
" ░██ ░██ ░██ ░██ ░██ ░██ ░█████ ░██░██ ░██ ░█████████",
|
|
8
|
+
" ░██ ░██ ░██ ░██ ░██ ░███ ░██ ░██ ░██░██ ░███ ░██",
|
|
9
|
+
" ░████ ░██ ░██ ░██ ░█████░██ ░██ ░██ ░██ ░█████░██ ░███████",
|
|
10
|
+
];
|
|
11
|
+
export const APPLICATION_HOME_WORDMARK_WIDTH = 75;
|
|
@@ -6,7 +6,7 @@ import { useKeyboardRoute } from "./keyboard-router.jsx";
|
|
|
6
6
|
export function KeyHint(props) {
|
|
7
7
|
const palette = () => componentPalette(props.theme, props, "neutral");
|
|
8
8
|
const background = () => props.quiet && !props.focused && !props.hovered
|
|
9
|
-
? props.theme.roles.surfaces.
|
|
9
|
+
? props.theme.roles.surfaces.panel
|
|
10
10
|
: palette().background;
|
|
11
11
|
const foreground = () => props.quiet && !props.focused && !props.hovered
|
|
12
12
|
? props.theme.roles.text.muted
|
|
@@ -39,7 +39,13 @@ export function KeyHint(props) {
|
|
|
39
39
|
activate();
|
|
40
40
|
}}>
|
|
41
41
|
<text fg={foreground()} bg={background()}>
|
|
42
|
-
{props.focused || props.selected ? <strong>{content()}</strong> :
|
|
42
|
+
{props.focused || props.selected ? (<strong>{content()}</strong>) : props.quiet && !props.presentation ? (<>
|
|
43
|
+
{" "}
|
|
44
|
+
<span style={{ fg: props.theme.roles.text.primary }}>
|
|
45
|
+
{clipTerminal(props.keys, Math.max(0, width() - 1))}
|
|
46
|
+
</span>
|
|
47
|
+
{clipTerminal(props.label ? ` ${props.label} ` : " ", Math.max(0, width() - terminalDisplayWidth(props.keys) - 1))}
|
|
48
|
+
</>) : (content())}
|
|
43
49
|
</text>
|
|
44
50
|
</box>);
|
|
45
51
|
}
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
import { For } from "solid-js";
|
|
3
3
|
import { OverlayFrame } from "./overlay-frame.jsx";
|
|
4
4
|
import { OverlayListRow } from "./overlay-list-row.jsx";
|
|
5
|
-
import {
|
|
5
|
+
import { overlaySurfaceMetrics } from "./overlay-model.js";
|
|
6
6
|
export function Menu(props) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const innerWidth = () => Math.max(1, overlayFrameSize({
|
|
7
|
+
const preferredHeight = () => props.items.length + (props.title ? 1 : 0) + (props.footer ? 1 : 0) + 2;
|
|
8
|
+
const metrics = () => overlaySurfaceMetrics({
|
|
10
9
|
viewportWidth: props.viewportWidth ?? Math.max(props.width, props.left + props.width),
|
|
11
|
-
viewportHeight: props.viewportHeight ?? Math.max(
|
|
10
|
+
viewportHeight: props.viewportHeight ?? Math.max(preferredHeight(), props.top + preferredHeight()),
|
|
12
11
|
preferredWidth: props.width,
|
|
13
|
-
preferredHeight:
|
|
14
|
-
})
|
|
15
|
-
|
|
12
|
+
preferredHeight: preferredHeight(),
|
|
13
|
+
});
|
|
14
|
+
const innerWidth = () => metrics().contentWidth;
|
|
15
|
+
return (<OverlayFrame theme={props.theme} viewportWidth={props.viewportWidth ?? Math.max(props.width, props.left + props.width)} viewportHeight={props.viewportHeight ?? Math.max(preferredHeight(), props.top + preferredHeight())} width={props.width} height={preferredHeight()} placement="anchor" anchor={{ x: props.left, y: props.top }} zIndex={props.zIndex ?? 20} active={props.active} title={props.title} footer={props.footer} onDismiss={props.onDismiss}>
|
|
16
16
|
<For each={props.items.map((item) => item.id)}>
|
|
17
17
|
{(id) => {
|
|
18
18
|
const item = () => props.items.find((item) => item.id === id);
|
|
@@ -13,9 +13,9 @@ export function OverlayFrame(props) {
|
|
|
13
13
|
const width = () => size().width;
|
|
14
14
|
const height = () => size().height;
|
|
15
15
|
// Empty border sides below keep OpenTUI border styling from enabling a frame.
|
|
16
|
-
const bordered = () =>
|
|
16
|
+
const bordered = () => props.surface === false && width() >= 4 && height() >= 3;
|
|
17
17
|
const padding = () => overlaySurfacePadding(width(), height());
|
|
18
|
-
const innerWidth = () => Math.max(1, width() - (props.surface ? padding().horizontal * 2 : bordered() ? 2 : 0));
|
|
18
|
+
const innerWidth = () => Math.max(1, width() - (props.surface !== false ? padding().horizontal * 2 : bordered() ? 2 : 0));
|
|
19
19
|
const left = () => {
|
|
20
20
|
if (props.placement === "anchor")
|
|
21
21
|
return Math.max(0, Math.min(props.anchor?.x ?? 0, viewportWidth() - width()));
|
|
@@ -30,17 +30,19 @@ export function OverlayFrame(props) {
|
|
|
30
30
|
return Math.min(1, viewportHeight() - height());
|
|
31
31
|
return Math.max(0, Math.floor((viewportHeight() - height()) / 2));
|
|
32
32
|
};
|
|
33
|
-
const frame = () => (<box id="ui-overlay-frame" position="absolute" left={left()} top={top()} width={width()} height={height()} zIndex={props.zIndex ?? 100} border={bordered() ? true : []} borderStyle="rounded" borderColor={props.theme.roles.borders.focused} backgroundColor={props.surface
|
|
33
|
+
const frame = () => (<box id="ui-overlay-frame" position="absolute" left={left()} top={top()} width={width()} height={height()} zIndex={props.zIndex ?? 100} border={bordered() ? true : []} borderStyle="rounded" borderColor={props.theme.roles.borders.focused} backgroundColor={props.surface !== false
|
|
34
|
+
? props.theme.roles.surfaces.command
|
|
35
|
+
: props.theme.roles.surfaces.panelRaised} flexDirection="column" paddingLeft={props.surface !== false ? padding().horizontal : bordered() ? 1 : 0} paddingRight={props.surface !== false ? padding().horizontal : 0} paddingTop={props.surface !== false ? padding().vertical : 0} paddingBottom={props.surface !== false ? padding().vertical : 0} overflow="hidden" onMouseDown={(event) => {
|
|
34
36
|
event.preventDefault();
|
|
35
37
|
event.stopPropagation();
|
|
36
38
|
}}>
|
|
37
39
|
{props.title && height() >= 4 ? (<box width={innerWidth()} height={1} flexShrink={0} flexDirection="row">
|
|
38
|
-
<text width={Math.max(1, innerWidth() - (props.surface && innerWidth() >= 20 ? 4 : 0))} fg={props.theme.roles.text.primary} overflow="hidden">
|
|
40
|
+
<text width={Math.max(1, innerWidth() - (props.surface !== false && innerWidth() >= 20 ? 4 : 0))} fg={props.theme.roles.text.primary} overflow="hidden">
|
|
39
41
|
<strong>
|
|
40
|
-
{clipTerminal(props.title, Math.max(1, innerWidth() - (props.surface && innerWidth() >= 20 ? 4 : 0)))}
|
|
42
|
+
{clipTerminal(props.title, Math.max(1, innerWidth() - (props.surface !== false && innerWidth() >= 20 ? 4 : 0)))}
|
|
41
43
|
</strong>
|
|
42
44
|
</text>
|
|
43
|
-
{props.surface && innerWidth() >= 20 ? (<text width={4} fg={props.theme.roles.text.muted} onMouseDown={(event) => {
|
|
45
|
+
{props.surface !== false && innerWidth() >= 20 ? (<text width={4} fg={props.theme.roles.text.muted} onMouseDown={(event) => {
|
|
44
46
|
event.preventDefault();
|
|
45
47
|
event.stopPropagation();
|
|
46
48
|
if (event.button === 0 && props.active !== false)
|
|
@@ -51,7 +53,7 @@ export function OverlayFrame(props) {
|
|
|
51
53
|
</text>) : null}
|
|
52
54
|
</box>) : null}
|
|
53
55
|
{props.children}
|
|
54
|
-
{props.footer && height() >= 5 ? (<text width={innerWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.muted} bg={props.surface
|
|
56
|
+
{props.footer && height() >= 5 ? (<text width={innerWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.muted} bg={props.surface !== false
|
|
55
57
|
? props.theme.roles.surfaces.command
|
|
56
58
|
: props.theme.roles.surfaces.panelRaised} overflow="hidden">
|
|
57
59
|
{clipTerminal(props.footer, innerWidth())}
|
|
@@ -5,7 +5,7 @@ import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
|
|
|
5
5
|
import { overlayRowPalette } from "./overlay-surface.js";
|
|
6
6
|
import { componentPalette } from "./state.js";
|
|
7
7
|
export function OverlayListRow(props) {
|
|
8
|
-
const palette = () => props.surface
|
|
8
|
+
const palette = () => props.surface !== false
|
|
9
9
|
? overlayRowPalette(props.theme, {
|
|
10
10
|
selected: props.selected,
|
|
11
11
|
disabled: props.disabled,
|
|
@@ -18,7 +18,7 @@ export function OverlayListRow(props) {
|
|
|
18
18
|
? "● "
|
|
19
19
|
: " "
|
|
20
20
|
: props.selected
|
|
21
|
-
? props.surface
|
|
21
|
+
? props.surface !== false
|
|
22
22
|
? " "
|
|
23
23
|
: "› "
|
|
24
24
|
: props.reserveMarker
|
|
@@ -50,3 +50,13 @@ export function overlaySurfacePadding(width, height) {
|
|
|
50
50
|
vertical: height >= 8 ? 1 : 0,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
/** Content geometry for the default calm overlay, after viewport clamping. */
|
|
54
|
+
export function overlaySurfaceMetrics(input) {
|
|
55
|
+
const size = overlayFrameSize(input);
|
|
56
|
+
const padding = overlaySurfacePadding(size.width, size.height);
|
|
57
|
+
return {
|
|
58
|
+
...size,
|
|
59
|
+
contentWidth: Math.max(1, size.width - padding.horizontal * 2),
|
|
60
|
+
contentHeight: Math.max(0, size.height - padding.vertical * 2),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -2,7 +2,7 @@ import { clipTerminal } from "../terminal-text.js";
|
|
|
2
2
|
import { componentPalette } from "./state.js";
|
|
3
3
|
import { useKeyboardRoute } from "./keyboard-router.jsx";
|
|
4
4
|
export function StatusBar(props) {
|
|
5
|
-
return (<box id="ui-status-bar" width={props.width} height={1} flexDirection="row" justifyContent="space-between" backgroundColor={props.theme.roles.surfaces.
|
|
5
|
+
return (<box id="ui-status-bar" width={props.width} height={1} flexDirection="row" justifyContent="space-between" backgroundColor={props.theme.roles.surfaces.panel} overflow="hidden">
|
|
6
6
|
{props.children}
|
|
7
7
|
</box>);
|
|
8
8
|
}
|
|
@@ -58,7 +58,7 @@ export function StatusSegment(props) {
|
|
|
58
58
|
loading: props.loading,
|
|
59
59
|
empty: props.empty,
|
|
60
60
|
}, props.tone);
|
|
61
|
-
const background = () => palette().state === "base" ? props.theme.roles.surfaces.
|
|
61
|
+
const background = () => palette().state === "base" ? props.theme.roles.surfaces.panel : palette().background;
|
|
62
62
|
const foreground = () => {
|
|
63
63
|
if (palette().state !== "base")
|
|
64
64
|
return palette().foreground;
|
|
@@ -35,7 +35,7 @@ export function Surface(props) {
|
|
|
35
35
|
case "raised":
|
|
36
36
|
return local.theme.roles.surfaces.panelRaised;
|
|
37
37
|
case "header":
|
|
38
|
-
return local.theme.roles.surfaces.
|
|
38
|
+
return local.theme.roles.surfaces.panel;
|
|
39
39
|
case "panel":
|
|
40
40
|
default:
|
|
41
41
|
return local.theme.roles.surfaces.panel;
|
|
@@ -19,9 +19,7 @@ export function Tabs(props) {
|
|
|
19
19
|
const addWidth = () => (props.onAdd ? terminalDisplayWidth(props.addLabel ?? "+") + 2 : 0);
|
|
20
20
|
const equalWidth = () => Math.max(1, Math.min(32, Math.floor(((props.width ?? 0) - addWidth()) / Math.max(1, props.items.length))));
|
|
21
21
|
const itemWidth = (item) => props.fit === "equal" && props.width ? equalWidth() : terminalDisplayWidth(tabText(item));
|
|
22
|
-
const background = () => props.
|
|
23
|
-
? props.theme.roles.surfaces.header
|
|
24
|
-
: props.theme.roles.surfaces.panel;
|
|
22
|
+
const background = () => props.theme.roles.surfaces.panel;
|
|
25
23
|
useKeyboardRoute((event) => {
|
|
26
24
|
if (!props.focused || event.eventType !== "press")
|
|
27
25
|
return false;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { applicationCommandDescription } from "./application-command-description.js";
|
|
2
|
+
/** Display metadata only. Context-specific keyboard owners remain authoritative. */
|
|
3
|
+
export const APPLICATION_SHORTCUTS = [
|
|
4
|
+
...["home", "terminals", "switch-session"].map((command) => {
|
|
5
|
+
const description = applicationCommandDescription(command);
|
|
6
|
+
return { category: "Application", label: description.label, keys: description.shortcut };
|
|
7
|
+
}),
|
|
8
|
+
{ category: "Application", label: "Commands", keys: "F5" },
|
|
9
|
+
{ category: "Application", label: "Agent attention", keys: "F7" },
|
|
10
|
+
{ category: "Application", label: "Sidebar / Sessions when hidden", keys: "Ctrl+G" },
|
|
11
|
+
{ category: "Home", label: "Find an agent or workspace", keys: "/" },
|
|
12
|
+
{ category: "Home", label: "Cycle machine filter", keys: "f" },
|
|
13
|
+
{ category: "Home", label: "Toggle attention filter", keys: "a" },
|
|
14
|
+
{ category: "Home", label: "Open selected agent", keys: "Enter" },
|
|
15
|
+
{ category: "Command and session menus", label: "Search actions or sessions", keys: "Type" },
|
|
16
|
+
{ category: "Command and session menus", label: "Choose a result", keys: "↑ / ↓" },
|
|
17
|
+
{ category: "Command and session menus", label: "Activate selected result", keys: "Enter" },
|
|
18
|
+
{ category: "Command and session menus", label: "Search / navigation mode", keys: "Ctrl+Space" },
|
|
19
|
+
{ category: "Menu navigation mode", label: "Move selection", keys: "j / k" },
|
|
20
|
+
{ category: "Menu navigation mode", label: "First / last result", keys: "g / G" },
|
|
21
|
+
{ category: "Menu navigation mode", label: "Return to search", keys: "i" },
|
|
22
|
+
{
|
|
23
|
+
category: "Command and session menus",
|
|
24
|
+
label: "Previous / next page",
|
|
25
|
+
keys: "PageUp / PageDown",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
category: "Command and session menus",
|
|
29
|
+
label: "Previous / next half page",
|
|
30
|
+
keys: "Ctrl+U / Ctrl+D",
|
|
31
|
+
},
|
|
32
|
+
{ category: "Sessions", label: "Local / all hosts", keys: "Ctrl+H" },
|
|
33
|
+
{ category: "Sessions", label: "Favorite session", keys: "Ctrl+F" },
|
|
34
|
+
{ category: "Sessions", label: "Browse windows without activating", keys: "Ctrl+← / Ctrl+→" },
|
|
35
|
+
{ category: "Sessions", label: "Show / hide preview", keys: "Ctrl+P" },
|
|
36
|
+
{ category: "Sessions", label: "Expand / restore preview", keys: "Ctrl+E" },
|
|
37
|
+
{ category: "Sessions", label: "New session", keys: "Ctrl+N" },
|
|
38
|
+
{ category: "Sessions", label: "Confirm close session", keys: "Ctrl+X" },
|
|
39
|
+
{ category: "Sessions", label: "Retry selected host", keys: "Ctrl+R" },
|
|
40
|
+
...["shortcuts", "whats-new"].map((command) => {
|
|
41
|
+
const description = applicationCommandDescription(command);
|
|
42
|
+
return { category: "Help in menus", label: description.label, keys: description.shortcut };
|
|
43
|
+
}),
|
|
44
|
+
{ category: "Help in menus", label: "Back / dismiss", keys: "Esc" },
|
|
45
|
+
];
|
|
@@ -258,9 +258,10 @@ export function mountTmuxServerRoutes(app: Hono, options: TmuxServerRoutesOption
|
|
|
258
258
|
)
|
|
259
259
|
)
|
|
260
260
|
throw new TmuxServerScopeError("stale-generation");
|
|
261
|
-
|
|
261
|
+
// Agent lists need the same status enrichment as pane headers. Raw
|
|
262
|
+
// terminal inventory omits these facts and falls back to process activity.
|
|
263
|
+
const snapshot = await owner.terminalInventoryRuntime.discoverApplicationShellSession(
|
|
262
264
|
workspace.sessionName,
|
|
263
|
-
c.req.raw.signal,
|
|
264
265
|
);
|
|
265
266
|
// Discovery can yield while a same-name session is replaced. Revalidate
|
|
266
267
|
// the captured incarnation before publishing agent metadata as current.
|
|
@@ -24,7 +24,7 @@ exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s compact shell
|
|
|
24
24
|
│ │
|
|
25
25
|
│ │
|
|
26
26
|
└──────────────────────────────────────────────────────────┘
|
|
27
|
-
F5 · ^q quit
|
|
27
|
+
F5 · ^q quit F6 Sessions F7 Attention F5 Commands"
|
|
28
28
|
`;
|
|
29
29
|
|
|
30
30
|
exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s standard shell chrome 1`] = `
|
|
@@ -67,7 +67,7 @@ exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s standard shell
|
|
|
67
67
|
│ │
|
|
68
68
|
│ │
|
|
69
69
|
└──────────────────────────────────────────────────────────────────────────────────────────┘
|
|
70
|
-
F5 palette · ^q quit
|
|
70
|
+
F5 palette · ^q quit F6 Sessions F7 Attention F5 Commands"
|
|
71
71
|
`;
|
|
72
72
|
|
|
73
73
|
exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s wide shell chrome 1`] = `
|
|
@@ -130,5 +130,5 @@ exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s wide shell chr
|
|
|
130
130
|
│ │
|
|
131
131
|
│ │
|
|
132
132
|
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
133
|
-
F5 palette · ^q quit
|
|
133
|
+
F5 palette · ^q quit F6 Sessions F7 Attention F5 Commands"
|
|
134
134
|
`;
|
|
@@ -13,7 +13,7 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
|
|
|
13
13
|
|
|
14
14
|
…
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
F6 Sessions F7 Attention F5 Commands"
|
|
17
17
|
`;
|
|
18
18
|
|
|
19
19
|
exports[`production ApplicationShellView retains shell, window strip, pane chrome, and canvas at %sx%s 2`] = `
|
|
@@ -29,7 +29,7 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
|
|
|
29
29
|
|
|
30
30
|
…
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
F6 Sessions F7 Attention F5 Commands"
|
|
33
33
|
`;
|
|
34
34
|
|
|
35
35
|
exports[`production ApplicationShellView retains shell, window strip, pane chrome, and canvas at %sx%s 3`] = `
|
|
@@ -45,24 +45,24 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
|
|
|
45
45
|
|
|
46
46
|
…
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
F6 Sessions F7 Attention F5 Commands"
|
|
49
49
|
`;
|
|
50
50
|
|
|
51
51
|
exports[`production ApplicationShellView reacts palette geometry through narrow live resizes 1`] = `
|
|
52
52
|
" ⌂ ●❯ live
|
|
53
|
-
|
|
53
|
+
t Command palette esc
|
|
54
54
|
m Search commands…
|
|
55
55
|
2 Home F1
|
|
56
56
|
C Terminals F2 …
|
|
57
57
|
New terminal wind…
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
↑↓ Select F5 Commands"
|
|
61
61
|
`;
|
|
62
62
|
|
|
63
63
|
exports[`production ApplicationShellView reacts palette geometry through narrow live resizes 2`] = `
|
|
64
64
|
" ⌂ ●❯ live
|
|
65
|
-
|
|
65
|
+
tCommand palette
|
|
66
66
|
m …rch commands…
|
|
67
67
|
2 Home F1w
|
|
68
68
|
C …
|
|
@@ -83,5 +83,5 @@ exports[`production ApplicationShellView preserves canonical attention while pri
|
|
|
83
83
|
|
|
84
84
|
…
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
F6 Sessions F7 Attention F5 Commands"
|
|
87
87
|
`;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Show } from "solid-js";
|
|
3
3
|
import type { SemanticThemeSnapshot } from "../theme.ts";
|
|
4
4
|
import { clipTerminal } from "../terminal-text.ts";
|
|
5
|
+
import { overlaySurfaceMetrics } from "../ui/overlay-model.ts";
|
|
5
6
|
import { Dialog } from "../ui/dialog.tsx";
|
|
6
7
|
import { TuiButton } from "../ui/button.tsx";
|
|
7
8
|
|
|
@@ -20,7 +21,13 @@ export interface ApplicationAddMachineDialogProps {
|
|
|
20
21
|
/** Collects an SSH target only; the owner validates and opens the connection. */
|
|
21
22
|
export function ApplicationAddMachineDialog(props: ApplicationAddMachineDialogProps) {
|
|
22
23
|
const width = () => Math.max(1, Math.min(58, props.width - (props.width >= 8 ? 4 : 0)));
|
|
23
|
-
const contentWidth = () =>
|
|
24
|
+
const contentWidth = () =>
|
|
25
|
+
overlaySurfaceMetrics({
|
|
26
|
+
viewportWidth: props.width,
|
|
27
|
+
viewportHeight: props.height,
|
|
28
|
+
preferredWidth: width(),
|
|
29
|
+
preferredHeight: 10,
|
|
30
|
+
}).contentWidth;
|
|
24
31
|
return (
|
|
25
32
|
<Show when={props.open}>
|
|
26
33
|
<Dialog
|