tmux-ide 2.9.0-beta.35 → 2.9.0-beta.37
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 +1 -1
- package/package.json +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +7 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-transient-note-owner.js +2 -1
- package/packages/daemon/dist/tui/mirror/shell-chrome.js +9 -3
- package/packages/daemon/dist/tui/mirror/theme.js +2 -0
- package/packages/daemon/dist/tui/mirror/ui/button.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/ui/menu.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/ui/overlay-frame.jsx +20 -5
- package/packages/daemon/dist/tui/mirror/workspace/pane-action-menu.jsx +1 -1
- package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +18 -15
- package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +6 -6
- package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +11 -11
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +12 -6
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +9 -0
- package/packages/daemon/src/tui/mirror/runtime/application-transient-note-owner.ts +7 -1
- package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +40 -24
- package/packages/daemon/src/tui/mirror/shell-chrome.ts +14 -3
- package/packages/daemon/src/tui/mirror/theme.ts +3 -0
- package/packages/daemon/src/tui/mirror/ui/button.tsx +2 -2
- package/packages/daemon/src/tui/mirror/ui/menu.tsx +2 -0
- package/packages/daemon/src/tui/mirror/ui/overlay-frame.tsx +43 -7
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +3 -3
- package/packages/daemon/src/tui/mirror/workspace/application-shell-view.tsx +2 -0
- package/packages/daemon/src/tui/mirror/workspace/pane-action-menu.tsx +2 -0
- package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +53 -42
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.37",
|
|
11283
11283
|
description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
11284
11284
|
type: "module",
|
|
11285
11285
|
bin: {
|
package/package.json
CHANGED
|
@@ -91,6 +91,11 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
91
91
|
? clampTerminalViewportOrigin(snapshot, { cols: frame.width, rows: frame.contentHeight }, origin, snapshot.history.length)
|
|
92
92
|
: origin;
|
|
93
93
|
}, (paneId) => props.adapter.retainPaneView?.(paneId) ?? null);
|
|
94
|
+
createEffect(() => {
|
|
95
|
+
const paneId = props.focusedPane;
|
|
96
|
+
props.onScrollbackChange?.(paneId !== null && scrollback.offset(paneId) > 0);
|
|
97
|
+
});
|
|
98
|
+
onCleanup(() => props.onScrollbackChange?.(false));
|
|
94
99
|
const selectionViewport = (paneId, frame) => props.adapter.renderSource.supportsViewportOrigin
|
|
95
100
|
? {
|
|
96
101
|
cols: frame.width,
|
|
@@ -1318,10 +1323,10 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
1318
1323
|
</box>)}
|
|
1319
1324
|
</For>
|
|
1320
1325
|
<Show when={windowLinkMenu()}>
|
|
1321
|
-
<Menu theme={props.theme} title="Window link" left={0} top={1} width={Math.min(36, props.width)} viewportWidth={props.width} viewportHeight={props.height} selectedId="unlink" items={[{ id: "unlink", label: "Unlink this tab", shortcut: "U" }]} footer="Other links keep their panes" onDismiss={() => setWindowLinkMenu(null)} onSelect={unlinkWindowTab}/>
|
|
1326
|
+
<Menu theme={props.theme} viewportOrigin={{ x: props.originX ?? 0, y: props.originY ?? 0 }} title="Window link" left={0} top={1} width={Math.min(36, props.width)} viewportWidth={props.width} viewportHeight={props.height} selectedId="unlink" items={[{ id: "unlink", label: "Unlink this tab", shortcut: "U" }]} footer="Other links keep their panes" onDismiss={() => setWindowLinkMenu(null)} onSelect={unlinkWindowTab}/>
|
|
1322
1327
|
</Show>
|
|
1323
1328
|
<Show when={paneContextMenu()}>
|
|
1324
|
-
{(menu) => (<PaneActionMenu theme={props.theme} left={menu().left} top={menu().top} width={paneContextMenuWidth()} viewportWidth={props.width} viewportHeight={props.height + topOffset()} paneTitle={menu().displayName} active onDismiss={paneMenu.dismiss} selectedId={menu().selectedId} closeArmed={menu().closeArmed} onHighlight={paneMenu.highlight} onActionIntent={paneMenu.activate}/>)}
|
|
1329
|
+
{(menu) => (<PaneActionMenu theme={props.theme} left={menu().left} top={menu().top} width={paneContextMenuWidth()} viewportWidth={props.width} viewportHeight={props.height + topOffset()} viewportOrigin={{ x: props.originX ?? 0, y: props.originY ?? 0 }} paneTitle={menu().displayName} active onDismiss={paneMenu.dismiss} selectedId={menu().selectedId} closeArmed={menu().closeArmed} onHighlight={paneMenu.highlight} onActionIntent={paneMenu.activate}/>)}
|
|
1325
1330
|
</Show>
|
|
1326
1331
|
<For each={[
|
|
1327
1332
|
...new Set([
|
|
@@ -16,7 +16,8 @@ export function createApplicationTransientNoteOwner(options) {
|
|
|
16
16
|
publish(note) {
|
|
17
17
|
clear();
|
|
18
18
|
options.write(note);
|
|
19
|
-
if (!note
|
|
19
|
+
if (!note ||
|
|
20
|
+
/\b(?:failed|failure|error|unavailable|disconnected|reconnecting|recovering|read.only|denied)\b/iu.test(note))
|
|
20
21
|
return;
|
|
21
22
|
timer = clock.setTimeout(() => {
|
|
22
23
|
timer = null;
|
|
@@ -222,9 +222,10 @@ export function shellStatusLine(variant, input, width) {
|
|
|
222
222
|
}
|
|
223
223
|
return `${out}…`;
|
|
224
224
|
}
|
|
225
|
-
function meaningfulStatusMessage(message) {
|
|
225
|
+
export function meaningfulStatusMessage(message) {
|
|
226
226
|
const value = message?.trim();
|
|
227
|
-
if (!value ||
|
|
227
|
+
if (!value ||
|
|
228
|
+
/^(?:ready|live|connected|live tmux sessions? discovered|\d+ sessions? live)$/iu.test(value))
|
|
228
229
|
return null;
|
|
229
230
|
return value;
|
|
230
231
|
}
|
|
@@ -264,7 +265,12 @@ export function contextStatusPresentation(input) {
|
|
|
264
265
|
: input.connectionState === "recovering"
|
|
265
266
|
? "Recovering"
|
|
266
267
|
: "Disconnected";
|
|
267
|
-
const
|
|
268
|
+
const persistentProblem = notification && contextStatusTone(notification, "connected").tone === "blocked";
|
|
269
|
+
const activityLabel = input.connectionState !== "connected"
|
|
270
|
+
? (notification ?? connectionLabel)
|
|
271
|
+
: persistentProblem
|
|
272
|
+
? notification
|
|
273
|
+
: (transient ?? notification ?? connectionLabel);
|
|
268
274
|
const activityTone = contextStatusTone(activityLabel, input.connectionState);
|
|
269
275
|
const location = input.variant === "wide"
|
|
270
276
|
? [
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* verbatim, aliasing @opentui/core to a browser shim that only exposes RGBA.
|
|
11
11
|
*/
|
|
12
12
|
import { RGBA } from "@opentui/core";
|
|
13
|
+
/** Shared scrim for modal surfaces across light and dark themes. */
|
|
14
|
+
export const MODAL_BACKDROP = RGBA.fromInts(0, 0, 0, 150);
|
|
13
15
|
import { BUILTIN_VISUAL_THEMES, findVisualThemePreset, contrastRatio, deriveAttentionBlend, deriveFocusedHeader, mixSrgbColors, readableForeground, resolveVisualTheme, } from "@tmux-ide/contracts";
|
|
14
16
|
import { LEGACY_THEME_OVERRIDE_PROVENANCE, } from "../../lib/legacy-theme-compat.js";
|
|
15
17
|
import { parseTerminalHostColor, terminalHostMode } from "../../lib/terminal-host-color.js";
|
|
@@ -24,7 +24,7 @@ export function TuiButton(props) {
|
|
|
24
24
|
const inset = size() === "compact" ? 1 : 1;
|
|
25
25
|
return clipTerminal(`${" ".repeat(inset)}${marker}${props.label}${shortcut} `, width());
|
|
26
26
|
};
|
|
27
|
-
const background = () => variant() === "ghost"
|
|
27
|
+
const background = () => palette().state === "base" && (variant() === "ghost" || variant() === "secondary")
|
|
28
28
|
? (props.background ?? props.theme.roles.surfaces.panel)
|
|
29
29
|
: palette().background;
|
|
30
30
|
const activate = () => {
|
|
@@ -12,7 +12,7 @@ export function Menu(props) {
|
|
|
12
12
|
preferredHeight: preferredHeight(),
|
|
13
13
|
});
|
|
14
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}>
|
|
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()} viewportOrigin={props.viewportOrigin} 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);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { Portal, useTerminalDimensions } from "@opentui/solid";
|
|
3
|
+
import { MODAL_BACKDROP } from "../theme.js";
|
|
1
4
|
import { clipTerminal } from "../terminal-text.js";
|
|
2
5
|
import { overlayFrameSize, overlaySurfacePadding } from "./overlay-model.js";
|
|
3
6
|
/** Owns modal geometry and pointer capture without owning commands or renderer lifecycle. */
|
|
4
7
|
export function OverlayFrame(props) {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
8
|
+
const dimensions = useTerminalDimensions();
|
|
9
|
+
const viewportWidth = () => Math.max(1, Math.floor(props.viewportOrigin ? dimensions().width : props.viewportWidth));
|
|
10
|
+
const viewportHeight = () => Math.max(1, Math.floor(props.viewportOrigin ? dimensions().height : props.viewportHeight));
|
|
7
11
|
const size = () => overlayFrameSize({
|
|
8
12
|
viewportWidth: viewportWidth(),
|
|
9
13
|
viewportHeight: viewportHeight(),
|
|
@@ -18,14 +22,14 @@ export function OverlayFrame(props) {
|
|
|
18
22
|
const innerWidth = () => Math.max(1, width() - (props.surface !== false ? padding().horizontal * 2 : bordered() ? 2 : 0));
|
|
19
23
|
const left = () => {
|
|
20
24
|
if (props.placement === "anchor")
|
|
21
|
-
return Math.max(0, Math.min(props.anchor?.x ?? 0, viewportWidth() - width()));
|
|
25
|
+
return Math.max(0, Math.min((props.anchor?.x ?? 0) + (props.viewportOrigin?.x ?? 0), viewportWidth() - width()));
|
|
22
26
|
if (props.placement === "top-right")
|
|
23
27
|
return Math.max(0, viewportWidth() - width() - 1);
|
|
24
28
|
return Math.max(0, Math.floor((viewportWidth() - width()) / 2));
|
|
25
29
|
};
|
|
26
30
|
const top = () => {
|
|
27
31
|
if (props.placement === "anchor")
|
|
28
|
-
return Math.max(0, Math.min(props.anchor?.y ?? 0, viewportHeight() - height()));
|
|
32
|
+
return Math.max(0, Math.min((props.anchor?.y ?? 0) + (props.viewportOrigin?.y ?? 0), viewportHeight() - height()));
|
|
29
33
|
if (props.placement === "top-right")
|
|
30
34
|
return Math.min(1, viewportHeight() - height());
|
|
31
35
|
return Math.max(0, Math.floor((viewportHeight() - height()) / 2));
|
|
@@ -61,7 +65,7 @@ export function OverlayFrame(props) {
|
|
|
61
65
|
</box>);
|
|
62
66
|
if (props.modal === false)
|
|
63
67
|
return frame();
|
|
64
|
-
|
|
68
|
+
const host = () => (<box id="ui-overlay-frame-host" position="absolute" left={0} top={0} width={viewportWidth()} height={viewportHeight()} zIndex={props.zIndex ?? 100} backgroundColor={props.active === false ? undefined : MODAL_BACKDROP} onMouseDown={(event) => {
|
|
65
69
|
event.preventDefault();
|
|
66
70
|
event.stopPropagation();
|
|
67
71
|
if (props.active !== false && props.dismissOnOutsidePress !== false)
|
|
@@ -69,4 +73,15 @@ export function OverlayFrame(props) {
|
|
|
69
73
|
}}>
|
|
70
74
|
{frame()}
|
|
71
75
|
</box>);
|
|
76
|
+
return props.viewportOrigin ? (<Portal ref={(node) => {
|
|
77
|
+
const container = node;
|
|
78
|
+
container.position = "absolute";
|
|
79
|
+
container.left = 0;
|
|
80
|
+
container.top = 0;
|
|
81
|
+
container.width = "100%";
|
|
82
|
+
container.height = "100%";
|
|
83
|
+
container.zIndex = props.zIndex ?? 100;
|
|
84
|
+
}}>
|
|
85
|
+
{host()}
|
|
86
|
+
</Portal>) : (host());
|
|
72
87
|
}
|
|
@@ -11,5 +11,5 @@ function presentation(closeArmed) {
|
|
|
11
11
|
}
|
|
12
12
|
/** Pane-scoped menu compound. Command execution remains owned by the workspace runtime. */
|
|
13
13
|
export function PaneActionMenu(props) {
|
|
14
|
-
return (<Menu theme={props.theme} left={props.left} top={props.top} width={props.width} viewportWidth={props.viewportWidth} viewportHeight={props.viewportHeight} active={props.active} title={props.paneTitle} footer={props.closeArmed ? "X / Enter confirm · Esc cancel" : "↑↓ choose · Enter run · Esc back"} selectedId={props.selectedId} items={presentation(props.closeArmed)} onDismiss={props.onDismiss} onHighlight={(id) => props.onHighlight?.(id)} onSelect={(id) => props.onActionIntent(id)}/>);
|
|
14
|
+
return (<Menu theme={props.theme} left={props.left} top={props.top} width={props.width} viewportWidth={props.viewportWidth} viewportHeight={props.viewportHeight} viewportOrigin={props.viewportOrigin} active={props.active} title={props.paneTitle} footer={props.closeArmed ? "X / Enter confirm · Esc cancel" : "↑↓ choose · Enter run · Esc back"} selectedId={props.selectedId} items={presentation(props.closeArmed)} onDismiss={props.onDismiss} onHighlight={(id) => props.onHighlight?.(id)} onSelect={(id) => props.onActionIntent(id)}/>);
|
|
15
15
|
}
|
|
@@ -31,6 +31,7 @@ function badgeWidth(status) {
|
|
|
31
31
|
*/
|
|
32
32
|
export function PaneTitleBar(props) {
|
|
33
33
|
const [pointerInside, setPointerInside] = createSignal(false);
|
|
34
|
+
const [menuHovered, setMenuHovered] = createSignal(false);
|
|
34
35
|
const hovered = () => props.hovered ?? pointerInside();
|
|
35
36
|
const safeWidth = () => Math.max(1, Math.floor(props.width));
|
|
36
37
|
const status = () => agentStatus(props.activity);
|
|
@@ -41,10 +42,16 @@ export function PaneTitleBar(props) {
|
|
|
41
42
|
: status();
|
|
42
43
|
const presence = () => (props.interaction ? paneInteractionPresence(props.interaction) : null);
|
|
43
44
|
const activityLabel = () => presence()
|
|
44
|
-
? `${presence().badge}${props.interaction?.origin === "external" ? " ·
|
|
45
|
+
? `${presence().badge[0]}${presence().badge.slice(1).toLowerCase()}${props.interaction?.origin === "external" ? " · tmux" : ""}`
|
|
45
46
|
: "";
|
|
46
47
|
const activityWidth = () => {
|
|
47
|
-
const available = safeWidth() -
|
|
48
|
+
const available = safeWidth() -
|
|
49
|
+
markerGutterWidth() -
|
|
50
|
+
markerWidth() -
|
|
51
|
+
actionWidth() -
|
|
52
|
+
zoomWidth() -
|
|
53
|
+
badgeWidth(statusLabel()) -
|
|
54
|
+
12;
|
|
48
55
|
if (!presence() || available < presence().badge.length + 2)
|
|
49
56
|
return 0;
|
|
50
57
|
return Math.min(available, terminalDisplayWidth(activityLabel()) + 2);
|
|
@@ -56,12 +63,6 @@ export function PaneTitleBar(props) {
|
|
|
56
63
|
// Reserve these cells even when the control is quiet or unavailable. Hover
|
|
57
64
|
// and menu lifetime must never change title clipping or terminal geometry.
|
|
58
65
|
const actionWidth = () => (safeWidth() >= 7 ? 3 : 0);
|
|
59
|
-
const actionsVisible = () => props.selected ||
|
|
60
|
-
props.keyboardFocused ||
|
|
61
|
-
props.terminalFocused ||
|
|
62
|
-
hovered() ||
|
|
63
|
-
props.menuFocused ||
|
|
64
|
-
props.menuOpen;
|
|
65
66
|
const zoomLabel = () => !props.zoomed
|
|
66
67
|
? ""
|
|
67
68
|
: safeWidth() >= 32
|
|
@@ -146,16 +147,18 @@ export function PaneTitleBar(props) {
|
|
|
146
147
|
}}>
|
|
147
148
|
{clipTerminal(zoomLabel(), zoomWidth())}
|
|
148
149
|
</text>) : null}
|
|
149
|
-
<
|
|
150
|
-
|
|
150
|
+
<box id={`pane-header-status:${props.paneId}`} width={activityWidth() + (showBadge() ? badgeWidth(statusLabel()) : 0)} height={1} flexShrink={0} flexDirection="row" backgroundColor={props.theme.roles.surfaces.panel}>
|
|
151
|
+
<Show when={activityBadge()} keyed>
|
|
152
|
+
{(activity) => (<Badge theme={props.theme} label={activity.label} width={activity.width} tone={activity.presence.tone === "danger"
|
|
151
153
|
? "destructive"
|
|
152
154
|
: activity.presence.tone === "info"
|
|
153
155
|
? "accent"
|
|
154
|
-
: "done"}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
: "done"}/>)}
|
|
157
|
+
</Show>
|
|
158
|
+
{showBadge() ? (<AgentBadge theme={props.theme} label={statusLabel()} status={status()} activity={props.activity} width={badgeWidth(statusLabel())} attention={props.attention}/>) : null}
|
|
159
|
+
</box>
|
|
160
|
+
{actionWidth() > 0 ? (<box width={actionWidth()} height={1} flexShrink={0} onMouseOver={() => setMenuHovered(true)} onMouseOut={() => setMenuHovered(false)}>
|
|
161
|
+
<IconButton theme={props.theme} icon="⋯" label="Pane actions" variant="secondary" width={actionWidth()} hovered={menuHovered()} focused={props.menuFocused} selected={props.menuOpen} disabled={props.menuDisabled} background={props.theme.roles.surfaces.panelRaised} onPress={() => activateMenu()}/>
|
|
159
162
|
</box>) : null}
|
|
160
163
|
</box>);
|
|
161
164
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
2
|
|
|
3
3
|
exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s compact shell chrome 1`] = `
|
|
4
|
-
" ⌂ !❯ ▤ ± ◆
|
|
4
|
+
" ⌂ !❯ ▤ ± ◆ ⧉ web !blocked F5 palette
|
|
5
5
|
tmux ┌──────────────────────────────────────────────────────────┐
|
|
6
6
|
● web │ › compact workspace │
|
|
7
7
|
○ api │● Active view and keyboard focus are selected terminal│
|
|
@@ -24,11 +24,11 @@ exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s compact shell
|
|
|
24
24
|
│ │
|
|
25
25
|
│ │
|
|
26
26
|
└──────────────────────────────────────────────────────────┘
|
|
27
|
-
F5 · ^q quit F6 Sessions F7 Attention
|
|
27
|
+
F5 · ^q quit F6 Sessions F7 Attention F10 Sidebar F5 Commands"
|
|
28
28
|
`;
|
|
29
29
|
|
|
30
30
|
exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s standard shell chrome 1`] = `
|
|
31
|
-
" ▸ tmux-ide ⌂ Home !❯ Terminal ▤ Files ± Diff ◆ Missions
|
|
31
|
+
" ▸ tmux-ide ⌂ Home !❯ Terminal ▤ Files ± Diff ◆ Missions ⧉ web !blocked F5 palette
|
|
32
32
|
tmux-ide ┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
|
33
33
|
● web │ › standard workspace │
|
|
34
34
|
○ api │● Active view and keyboard focus are selected terminal│
|
|
@@ -67,11 +67,11 @@ exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s standard shell
|
|
|
67
67
|
│ │
|
|
68
68
|
│ │
|
|
69
69
|
└──────────────────────────────────────────────────────────────────────────────────────────┘
|
|
70
|
-
F5 palette · ^q quit F6 Sessions F7 Attention
|
|
70
|
+
F5 palette · ^q quit F6 Sessions F7 Attention F10 Sidebar F5 Commands"
|
|
71
71
|
`;
|
|
72
72
|
|
|
73
73
|
exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s wide shell chrome 1`] = `
|
|
74
|
-
" ▸ tmux-ide F1 ⌂ Home F2 !❯ Terminal F3 ▤ Files F4 ± Diff F6 ◆ Missions
|
|
74
|
+
" ▸ tmux-ide F1 ⌂ Home F2 !❯ Terminal F3 ▤ Files F4 ± Diff F6 ◆ Missions ⧉ web !blocked F5 palette
|
|
75
75
|
tmux-ide ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
76
76
|
● web │ › wide workspace │
|
|
77
77
|
○ api │● Active view and keyboard focus are selected terminal│
|
|
@@ -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 F6 Sessions F7 Attention
|
|
133
|
+
F5 palette · ^q quit F6 Sessions F7 Attention F10 Sidebar F5 Commands"
|
|
134
134
|
`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
2
|
|
|
3
3
|
exports[`production ApplicationShellView retains shell, window strip, pane chrome, and canvas at %sx%s 1`] = `
|
|
4
|
-
" ⌂ ●❯
|
|
4
|
+
" ⌂ ●❯ Local · main
|
|
5
5
|
tmux ● main ● workin… +
|
|
6
6
|
● main ● Codex ● working ⋯
|
|
7
7
|
○ website CANONICAL-CELL
|
|
@@ -13,11 +13,11 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
|
|
|
13
13
|
|
|
14
14
|
…
|
|
15
15
|
|
|
16
|
-
F6 Sessions F7 Attention
|
|
16
|
+
F6 Sessions F7 Attention F10 Sidebar F5 Commands"
|
|
17
17
|
`;
|
|
18
18
|
|
|
19
19
|
exports[`production ApplicationShellView retains shell, window strip, pane chrome, and canvas at %sx%s 2`] = `
|
|
20
|
-
" tmux-ide ⌂ Home ●❯ Terminals
|
|
20
|
+
" tmux-ide ⌂ Home ●❯ Terminals Local · main
|
|
21
21
|
tmux-ide ● main ● workin… + New window
|
|
22
22
|
● main ● Codex ● working ⋯
|
|
23
23
|
○ website CANONICAL-CELL
|
|
@@ -29,11 +29,11 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
|
|
|
29
29
|
|
|
30
30
|
…
|
|
31
31
|
|
|
32
|
-
F6 Sessions F7 Attention
|
|
32
|
+
F6 Sessions F7 Attention F10 Sidebar F5 Commands"
|
|
33
33
|
`;
|
|
34
34
|
|
|
35
35
|
exports[`production ApplicationShellView retains shell, window strip, pane chrome, and canvas at %sx%s 3`] = `
|
|
36
|
-
" tmux-ide F1 ⌂ Home F2 ●❯ Terminals
|
|
36
|
+
" tmux-ide F1 ⌂ Home F2 ●❯ Terminals Local · main
|
|
37
37
|
tmux-ide ● main ● workin… + New window
|
|
38
38
|
● main ● Codex ● working ⋯
|
|
39
39
|
○ website CANONICAL-CELL
|
|
@@ -45,11 +45,11 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
|
|
|
45
45
|
|
|
46
46
|
…
|
|
47
47
|
|
|
48
|
-
F6 Sessions F7 Attention
|
|
48
|
+
F6 Sessions F7 Attention F10 Sidebar F5 Commands"
|
|
49
49
|
`;
|
|
50
50
|
|
|
51
51
|
exports[`production ApplicationShellView reacts palette geometry through narrow live resizes 1`] = `
|
|
52
|
-
" ⌂ ●❯
|
|
52
|
+
" ⌂ ●❯
|
|
53
53
|
t Command palette esc
|
|
54
54
|
m Search commands…
|
|
55
55
|
2 Home F1
|
|
@@ -61,7 +61,7 @@ exports[`production ApplicationShellView reacts palette geometry through narrow
|
|
|
61
61
|
`;
|
|
62
62
|
|
|
63
63
|
exports[`production ApplicationShellView reacts palette geometry through narrow live resizes 2`] = `
|
|
64
|
-
" ⌂ ●❯
|
|
64
|
+
" ⌂ ●❯
|
|
65
65
|
tCommand palette
|
|
66
66
|
m …rch commands…
|
|
67
67
|
2 Home F1w
|
|
@@ -71,9 +71,9 @@ exports[`production ApplicationShellView reacts palette geometry through narrow
|
|
|
71
71
|
`;
|
|
72
72
|
|
|
73
73
|
exports[`production ApplicationShellView preserves canonical attention while prioritizing mixed window activity 1`] = `
|
|
74
|
-
" tmux-ide ⌂ Home ●❯ Terminals
|
|
74
|
+
" tmux-ide ⌂ Home ●❯ Terminals Local · main
|
|
75
75
|
tmux-ide ! main ! workin… + New window
|
|
76
|
-
● main ● Codex ● working ⋯ ! Scout ! idle
|
|
76
|
+
● main ● Codex ● working ⋯ ! Scout ! idle ⋯
|
|
77
77
|
CANONICAL-CELL CANONICAL-CELL
|
|
78
78
|
Agents
|
|
79
79
|
● Codex [WORKING]
|
|
@@ -83,5 +83,5 @@ exports[`production ApplicationShellView preserves canonical attention while pri
|
|
|
83
83
|
|
|
84
84
|
…
|
|
85
85
|
|
|
86
|
-
F6 Sessions F7 Attention
|
|
86
|
+
F6 Sessions F7 Attention F10 Sidebar F5 Commands"
|
|
87
87
|
`;
|
|
@@ -10,7 +10,7 @@ import type { ApplicationAppearanceOwner } from "./application-appearance-owner.
|
|
|
10
10
|
/* @jsxImportSource @opentui/solid */
|
|
11
11
|
import type { ApplicationShellProjectionV1 } from "@tmux-ide/contracts";
|
|
12
12
|
import type { Accessor, ComponentProps, JSX } from "solid-js";
|
|
13
|
-
import { Show, createMemo } from "solid-js";
|
|
13
|
+
import { Show, createMemo, createSignal } from "solid-js";
|
|
14
14
|
|
|
15
15
|
import { friendlySessionLabel } from "../terminal-text.ts";
|
|
16
16
|
import { ApplicationShell } from "../workspace/application-shell-view.tsx";
|
|
@@ -163,6 +163,7 @@ export interface ApplicationShellViewProps {
|
|
|
163
163
|
|
|
164
164
|
/** Pure Solid composition over the WorkspaceClient semantic projection. */
|
|
165
165
|
export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Element {
|
|
166
|
+
const [scrollback, setScrollback] = createSignal(false);
|
|
166
167
|
const projection = createMemo(() => {
|
|
167
168
|
const semantic = props.semantic();
|
|
168
169
|
if (!semantic) return null;
|
|
@@ -395,14 +396,13 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
|
|
|
395
396
|
>
|
|
396
397
|
<ApplicationShell
|
|
397
398
|
footerContext={props.surface() === "home" ? "home" : "terminals"}
|
|
399
|
+
scrollback={props.surface() === "terminals" && scrollback()}
|
|
398
400
|
rightChips={
|
|
399
|
-
props.
|
|
401
|
+
props.surface() === "terminals" && props.dimensions().width >= 60
|
|
400
402
|
? [
|
|
401
403
|
{
|
|
402
|
-
id: "
|
|
403
|
-
label: `
|
|
404
|
-
context: true,
|
|
405
|
-
textColor: props.machineColor,
|
|
404
|
+
id: "workspace-context",
|
|
405
|
+
label: ` ${props.machineLabel ?? "Local"} · ${friendlySessionLabel(shell.activeSession)} `,
|
|
406
406
|
},
|
|
407
407
|
]
|
|
408
408
|
: undefined
|
|
@@ -413,6 +413,11 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
|
|
|
413
413
|
onHelp={() => props.onSetPaletteOpen(true, "mouse")}
|
|
414
414
|
note={
|
|
415
415
|
props.bootstrapNote() ??
|
|
416
|
+
(props.copyFeedback
|
|
417
|
+
? props.copyFeedback.copied
|
|
418
|
+
? "Copied"
|
|
419
|
+
: "Copy unavailable"
|
|
420
|
+
: null) ??
|
|
416
421
|
(props.generationStatus() === "read-only"
|
|
417
422
|
? `Read-only input · select a pane to request control · shared tmux ${props.layout().current?.cols ?? "?"}×${props.layout().current?.rows ?? "?"}`
|
|
418
423
|
: props.generationStatus())
|
|
@@ -517,6 +522,7 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
|
|
|
517
522
|
>
|
|
518
523
|
{(source) => (
|
|
519
524
|
<ApplicationTerminalWorkspace
|
|
525
|
+
onScrollbackChange={setScrollback}
|
|
520
526
|
layout={props.layout}
|
|
521
527
|
adapter={source.adapter}
|
|
522
528
|
rendererEpoch={source.rendererEpoch}
|
|
@@ -148,6 +148,7 @@ export function beginApplicationMouseIngress(
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
export interface ApplicationTerminalWorkspaceProps {
|
|
151
|
+
readonly onScrollbackChange?: (active: boolean) => void;
|
|
151
152
|
readonly paneInteractions?: Accessor<ReadonlyMap<string, PaneInteractionProjection>>;
|
|
152
153
|
readonly layout: Accessor<OpenTuiWorkspaceLayoutSnapshot>;
|
|
153
154
|
readonly adapter: PaneScopedTerminalAdapter;
|
|
@@ -318,6 +319,12 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
318
319
|
},
|
|
319
320
|
(paneId) => props.adapter.retainPaneView?.(paneId) ?? null,
|
|
320
321
|
);
|
|
322
|
+
|
|
323
|
+
createEffect(() => {
|
|
324
|
+
const paneId = props.focusedPane;
|
|
325
|
+
props.onScrollbackChange?.(paneId !== null && scrollback.offset(paneId) > 0);
|
|
326
|
+
});
|
|
327
|
+
onCleanup(() => props.onScrollbackChange?.(false));
|
|
321
328
|
const selectionViewport = (paneId: string, frame: OpenTuiPaneFrame) =>
|
|
322
329
|
props.adapter.renderSource.supportsViewportOrigin
|
|
323
330
|
? {
|
|
@@ -1880,6 +1887,7 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1880
1887
|
<Show when={windowLinkMenu()}>
|
|
1881
1888
|
<Menu
|
|
1882
1889
|
theme={props.theme}
|
|
1890
|
+
viewportOrigin={{ x: props.originX ?? 0, y: props.originY ?? 0 }}
|
|
1883
1891
|
title="Window link"
|
|
1884
1892
|
left={0}
|
|
1885
1893
|
top={1}
|
|
@@ -1902,6 +1910,7 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1902
1910
|
width={paneContextMenuWidth()}
|
|
1903
1911
|
viewportWidth={props.width}
|
|
1904
1912
|
viewportHeight={props.height + topOffset()}
|
|
1913
|
+
viewportOrigin={{ x: props.originX ?? 0, y: props.originY ?? 0 }}
|
|
1905
1914
|
paneTitle={menu().displayName}
|
|
1906
1915
|
active
|
|
1907
1916
|
onDismiss={paneMenu.dismiss}
|
|
@@ -31,7 +31,13 @@ export function createApplicationTransientNoteOwner(options: {
|
|
|
31
31
|
publish(note) {
|
|
32
32
|
clear();
|
|
33
33
|
options.write(note);
|
|
34
|
-
if (
|
|
34
|
+
if (
|
|
35
|
+
!note ||
|
|
36
|
+
/\b(?:failed|failure|error|unavailable|disconnected|reconnecting|recovering|read.only|denied)\b/iu.test(
|
|
37
|
+
note,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
return;
|
|
35
41
|
timer = clock.setTimeout(() => {
|
|
36
42
|
timer = null;
|
|
37
43
|
if (options.read() === note) options.write(null);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { For, Show } from "solid-js";
|
|
3
3
|
import {
|
|
4
4
|
contextStatusPresentation,
|
|
5
|
+
meaningfulStatusMessage,
|
|
5
6
|
developmentChromeLabel,
|
|
6
7
|
shellNavigationPresentation,
|
|
7
8
|
shellSurfaceTabs,
|
|
@@ -106,7 +107,14 @@ export function ShellTabBar(props: ShellTabBarProps) {
|
|
|
106
107
|
/>
|
|
107
108
|
)}
|
|
108
109
|
</For>
|
|
109
|
-
<For
|
|
110
|
+
<For
|
|
111
|
+
each={
|
|
112
|
+
meaningfulStatusMessage(props.note) &&
|
|
113
|
+
/read.only|reconnect|disconnect|recover|unavailable|failed/iu.test(props.note ?? "")
|
|
114
|
+
? [props.note!]
|
|
115
|
+
: []
|
|
116
|
+
}
|
|
117
|
+
>
|
|
110
118
|
{(note) => (
|
|
111
119
|
<Badge
|
|
112
120
|
theme={props.theme}
|
|
@@ -117,24 +125,28 @@ export function ShellTabBar(props: ShellTabBarProps) {
|
|
|
117
125
|
1,
|
|
118
126
|
Math.min(Math.floor(props.width / 3), terminalDisplayWidth(note) + 1),
|
|
119
127
|
)}
|
|
120
|
-
tone="
|
|
128
|
+
tone="warning"
|
|
121
129
|
/>
|
|
122
130
|
)}
|
|
123
131
|
</For>
|
|
124
132
|
<For each={props.rightChips ?? []}>
|
|
125
133
|
{(chip) => (
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
<text
|
|
135
|
+
width={Math.min(
|
|
136
|
+
Math.max(1, Math.floor(props.width / 3)),
|
|
137
|
+
terminalDisplayWidth(chip.label),
|
|
138
|
+
)}
|
|
139
|
+
flexShrink={0}
|
|
140
|
+
fg={
|
|
141
|
+
chip.attention
|
|
142
|
+
? props.theme.roles.statusTone.warning
|
|
143
|
+
: (chip.textColor ?? props.theme.roles.text.muted)
|
|
144
|
+
}
|
|
145
|
+
bg={props.theme.roles.surfaces.panel}
|
|
146
|
+
overflow="hidden"
|
|
147
|
+
>
|
|
148
|
+
{clipTerminal(chip.label, Math.max(1, Math.floor(props.width / 3)))}
|
|
149
|
+
</text>
|
|
138
150
|
)}
|
|
139
151
|
</For>
|
|
140
152
|
</Surface>
|
|
@@ -152,6 +164,7 @@ export interface ShellStatusStripProps {
|
|
|
152
164
|
tool?: string | null;
|
|
153
165
|
dockMode?: string | null;
|
|
154
166
|
focus?: string | null;
|
|
167
|
+
scrollback?: boolean;
|
|
155
168
|
notification: string | null;
|
|
156
169
|
transient?: string | null;
|
|
157
170
|
connectionState?: "connected" | "reconnecting" | "disconnected" | "recovering";
|
|
@@ -178,16 +191,19 @@ export function ContextStatusBar(props: ShellStatusStripProps) {
|
|
|
178
191
|
const activity = () => presentation().activity;
|
|
179
192
|
const showMessage = () => !/^(?:Live|\d+ sessions? live)$/iu.test(activity().label);
|
|
180
193
|
const hints = () => {
|
|
181
|
-
const candidates =
|
|
182
|
-
? [
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
const candidates = props.scrollback
|
|
195
|
+
? [{ keys: "Esc", label: "Back to live" }]
|
|
196
|
+
: /^home$/iu.test(props.mode)
|
|
197
|
+
? [
|
|
198
|
+
{ keys: "↑↓", label: "Select" },
|
|
199
|
+
{ keys: "Enter", label: "Open" },
|
|
200
|
+
{ keys: "/", label: "Search" },
|
|
201
|
+
]
|
|
202
|
+
: [
|
|
203
|
+
{ keys: "F6", label: "Sessions" },
|
|
204
|
+
{ keys: "F7", label: "Attention" },
|
|
205
|
+
{ keys: "F10", label: "Sidebar" },
|
|
206
|
+
];
|
|
191
207
|
let remaining = available();
|
|
192
208
|
return candidates.filter((hint) => {
|
|
193
209
|
const width = terminalDisplayWidth(`${hint.keys} ${hint.label}`) + 2;
|
|
@@ -385,9 +385,13 @@ export interface ContextStatusPresentation {
|
|
|
385
385
|
readonly hints: readonly ContextStatusHint[];
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
function meaningfulStatusMessage(message: string | null | undefined): string | null {
|
|
388
|
+
export function meaningfulStatusMessage(message: string | null | undefined): string | null {
|
|
389
389
|
const value = message?.trim();
|
|
390
|
-
if (
|
|
390
|
+
if (
|
|
391
|
+
!value ||
|
|
392
|
+
/^(?:ready|live|connected|live tmux sessions? discovered|\d+ sessions? live)$/iu.test(value)
|
|
393
|
+
)
|
|
394
|
+
return null;
|
|
391
395
|
return value;
|
|
392
396
|
}
|
|
393
397
|
|
|
@@ -445,7 +449,14 @@ export function contextStatusPresentation(input: {
|
|
|
445
449
|
: input.connectionState === "recovering"
|
|
446
450
|
? "Recovering"
|
|
447
451
|
: "Disconnected";
|
|
448
|
-
const
|
|
452
|
+
const persistentProblem =
|
|
453
|
+
notification && contextStatusTone(notification, "connected").tone === "blocked";
|
|
454
|
+
const activityLabel =
|
|
455
|
+
input.connectionState !== "connected"
|
|
456
|
+
? (notification ?? connectionLabel)
|
|
457
|
+
: persistentProblem
|
|
458
|
+
? notification
|
|
459
|
+
: (transient ?? notification ?? connectionLabel);
|
|
449
460
|
const activityTone = contextStatusTone(activityLabel, input.connectionState);
|
|
450
461
|
const location: ContextStatusLocationSegment[] =
|
|
451
462
|
input.variant === "wide"
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* verbatim, aliasing @opentui/core to a browser shim that only exposes RGBA.
|
|
11
11
|
*/
|
|
12
12
|
import { RGBA } from "@opentui/core";
|
|
13
|
+
|
|
14
|
+
/** Shared scrim for modal surfaces across light and dark themes. */
|
|
15
|
+
export const MODAL_BACKDROP = RGBA.fromInts(0, 0, 0, 150);
|
|
13
16
|
import {
|
|
14
17
|
BUILTIN_VISUAL_THEMES,
|
|
15
18
|
findVisualThemePreset,
|
|
@@ -16,7 +16,7 @@ export interface TuiButtonProps extends ComponentInteractionState {
|
|
|
16
16
|
variant?: ButtonVariant;
|
|
17
17
|
size?: ButtonSize;
|
|
18
18
|
width?: number;
|
|
19
|
-
/** Opaque surface used by neutral
|
|
19
|
+
/** Opaque base surface used by neutral controls embedded in custom chrome. */
|
|
20
20
|
background?: RGBA;
|
|
21
21
|
onPress?: () => void;
|
|
22
22
|
}
|
|
@@ -55,7 +55,7 @@ export function TuiButton(props: TuiButtonProps) {
|
|
|
55
55
|
return clipTerminal(`${" ".repeat(inset)}${marker}${props.label}${shortcut} `, width());
|
|
56
56
|
};
|
|
57
57
|
const background = () =>
|
|
58
|
-
variant() === "ghost"
|
|
58
|
+
palette().state === "base" && (variant() === "ghost" || variant() === "secondary")
|
|
59
59
|
? (props.background ?? props.theme.roles.surfaces.panel)
|
|
60
60
|
: palette().background;
|
|
61
61
|
const activate = () => {
|
|
@@ -24,6 +24,7 @@ export interface MenuProps {
|
|
|
24
24
|
width: number;
|
|
25
25
|
viewportWidth?: number;
|
|
26
26
|
viewportHeight?: number;
|
|
27
|
+
viewportOrigin?: Readonly<{ x: number; y: number }>;
|
|
27
28
|
zIndex?: number;
|
|
28
29
|
active?: boolean;
|
|
29
30
|
footer?: string;
|
|
@@ -53,6 +54,7 @@ export function Menu(props: MenuProps) {
|
|
|
53
54
|
}
|
|
54
55
|
width={props.width}
|
|
55
56
|
height={preferredHeight()}
|
|
57
|
+
viewportOrigin={props.viewportOrigin}
|
|
56
58
|
placement="anchor"
|
|
57
59
|
anchor={{ x: props.left, y: props.top }}
|
|
58
60
|
zIndex={props.zIndex ?? 20}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* @jsxImportSource @opentui/solid */
|
|
2
|
-
import type
|
|
2
|
+
import { Portal, useTerminalDimensions, type JSX } from "@opentui/solid";
|
|
3
|
+
import { type BoxRenderable } from "@opentui/core";
|
|
3
4
|
|
|
4
|
-
import type
|
|
5
|
+
import { MODAL_BACKDROP, type SemanticThemeSnapshot } from "../theme.ts";
|
|
5
6
|
import { clipTerminal } from "../terminal-text.ts";
|
|
6
7
|
import { overlayFrameSize, overlaySurfacePadding, type OverlayPlacement } from "./overlay-model.ts";
|
|
7
8
|
|
|
@@ -17,6 +18,8 @@ export interface OverlayFrameProps {
|
|
|
17
18
|
footer?: string;
|
|
18
19
|
placement?: OverlayPlacement;
|
|
19
20
|
anchor?: Readonly<{ x: number; y: number }>;
|
|
21
|
+
/** Lift an anchored workspace menu into the full application overlay plane. */
|
|
22
|
+
viewportOrigin?: Readonly<{ x: number; y: number }>;
|
|
20
23
|
modal?: boolean;
|
|
21
24
|
active?: boolean;
|
|
22
25
|
zIndex?: number;
|
|
@@ -27,8 +30,11 @@ export interface OverlayFrameProps {
|
|
|
27
30
|
|
|
28
31
|
/** Owns modal geometry and pointer capture without owning commands or renderer lifecycle. */
|
|
29
32
|
export function OverlayFrame(props: OverlayFrameProps) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
33
|
+
const dimensions = useTerminalDimensions();
|
|
34
|
+
const viewportWidth = () =>
|
|
35
|
+
Math.max(1, Math.floor(props.viewportOrigin ? dimensions().width : props.viewportWidth));
|
|
36
|
+
const viewportHeight = () =>
|
|
37
|
+
Math.max(1, Math.floor(props.viewportOrigin ? dimensions().height : props.viewportHeight));
|
|
32
38
|
const size = () =>
|
|
33
39
|
overlayFrameSize({
|
|
34
40
|
viewportWidth: viewportWidth(),
|
|
@@ -48,13 +54,25 @@ export function OverlayFrame(props: OverlayFrameProps) {
|
|
|
48
54
|
);
|
|
49
55
|
const left = () => {
|
|
50
56
|
if (props.placement === "anchor")
|
|
51
|
-
return Math.max(
|
|
57
|
+
return Math.max(
|
|
58
|
+
0,
|
|
59
|
+
Math.min(
|
|
60
|
+
(props.anchor?.x ?? 0) + (props.viewportOrigin?.x ?? 0),
|
|
61
|
+
viewportWidth() - width(),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
52
64
|
if (props.placement === "top-right") return Math.max(0, viewportWidth() - width() - 1);
|
|
53
65
|
return Math.max(0, Math.floor((viewportWidth() - width()) / 2));
|
|
54
66
|
};
|
|
55
67
|
const top = () => {
|
|
56
68
|
if (props.placement === "anchor")
|
|
57
|
-
return Math.max(
|
|
69
|
+
return Math.max(
|
|
70
|
+
0,
|
|
71
|
+
Math.min(
|
|
72
|
+
(props.anchor?.y ?? 0) + (props.viewportOrigin?.y ?? 0),
|
|
73
|
+
viewportHeight() - height(),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
58
76
|
if (props.placement === "top-right") return Math.min(1, viewportHeight() - height());
|
|
59
77
|
return Math.max(0, Math.floor((viewportHeight() - height()) / 2));
|
|
60
78
|
};
|
|
@@ -139,7 +157,7 @@ export function OverlayFrame(props: OverlayFrameProps) {
|
|
|
139
157
|
</box>
|
|
140
158
|
);
|
|
141
159
|
if (props.modal === false) return frame();
|
|
142
|
-
|
|
160
|
+
const host = () => (
|
|
143
161
|
<box
|
|
144
162
|
id="ui-overlay-frame-host"
|
|
145
163
|
position="absolute"
|
|
@@ -148,6 +166,7 @@ export function OverlayFrame(props: OverlayFrameProps) {
|
|
|
148
166
|
width={viewportWidth()}
|
|
149
167
|
height={viewportHeight()}
|
|
150
168
|
zIndex={props.zIndex ?? 100}
|
|
169
|
+
backgroundColor={props.active === false ? undefined : MODAL_BACKDROP}
|
|
151
170
|
onMouseDown={(event) => {
|
|
152
171
|
event.preventDefault();
|
|
153
172
|
event.stopPropagation();
|
|
@@ -157,4 +176,21 @@ export function OverlayFrame(props: OverlayFrameProps) {
|
|
|
157
176
|
{frame()}
|
|
158
177
|
</box>
|
|
159
178
|
);
|
|
179
|
+
return props.viewportOrigin ? (
|
|
180
|
+
<Portal
|
|
181
|
+
ref={(node) => {
|
|
182
|
+
const container = node as BoxRenderable;
|
|
183
|
+
container.position = "absolute";
|
|
184
|
+
container.left = 0;
|
|
185
|
+
container.top = 0;
|
|
186
|
+
container.width = "100%";
|
|
187
|
+
container.height = "100%";
|
|
188
|
+
container.zIndex = props.zIndex ?? 100;
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{host()}
|
|
192
|
+
</Portal>
|
|
193
|
+
) : (
|
|
194
|
+
host()
|
|
195
|
+
);
|
|
160
196
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
2
|
|
|
3
3
|
exports[`production ApplicationShell → WorkbenchShell OpenTUI renderer records the %sx%s collapsed dock acceptance baseline 1`] = `
|
|
4
|
-
" ⌂ ●❯
|
|
4
|
+
" ⌂ ●❯
|
|
5
5
|
tmux ▌› Native terminal canvas terminals · canvas
|
|
6
6
|
● main
|
|
7
7
|
○ website
|
|
@@ -28,7 +28,7 @@ exports[`production ApplicationShell → WorkbenchShell OpenTUI renderer records
|
|
|
28
28
|
`;
|
|
29
29
|
|
|
30
30
|
exports[`production ApplicationShell → WorkbenchShell OpenTUI renderer records the %sx%s open dock acceptance baseline 1`] = `
|
|
31
|
-
" tmux-ide ⌂ Home ●❯ Terminals
|
|
31
|
+
" tmux-ide ⌂ Home ●❯ Terminals
|
|
32
32
|
tmux-ide ▌› Native terminal canvas terminals · canvas
|
|
33
33
|
● main
|
|
34
34
|
○ website
|
|
@@ -71,7 +71,7 @@ exports[`production ApplicationShell → WorkbenchShell OpenTUI renderer records
|
|
|
71
71
|
`;
|
|
72
72
|
|
|
73
73
|
exports[`production ApplicationShell → WorkbenchShell OpenTUI renderer records the %sx%s maximized dock acceptance baseline 1`] = `
|
|
74
|
-
" tmux-ide F1 ⌂ Home F2 ●❯ Terminals
|
|
74
|
+
" tmux-ide F1 ⌂ Home F2 ●❯ Terminals
|
|
75
75
|
tmux-ide ▸ tools F3 ▤ Files F4 ± Changes F6 ●◆ Missions F9 ◷ Activity ▾ close ▣ restore
|
|
76
76
|
● main ▌○ Native Missions surface maximized
|
|
77
77
|
○ website
|
|
@@ -11,6 +11,7 @@ import type { SemanticThemeSnapshot } from "../theme.ts";
|
|
|
11
11
|
import type { ApplicationShellProjection } from "./application-shell.ts";
|
|
12
12
|
|
|
13
13
|
export interface ApplicationShellProps {
|
|
14
|
+
scrollback?: boolean;
|
|
14
15
|
footerContext?: "home" | "terminals";
|
|
15
16
|
theme: SemanticThemeSnapshot;
|
|
16
17
|
projection: ApplicationShellProjection;
|
|
@@ -144,6 +145,7 @@ export function ApplicationShell(props: ApplicationShellProps) {
|
|
|
144
145
|
props.showToolStatus === false ? null : props.projection.semantic.bottomDock.mode
|
|
145
146
|
}
|
|
146
147
|
focus={props.focusLabel ?? applicationShellFocusLabel(props.projection)}
|
|
148
|
+
scrollback={props.scrollback}
|
|
147
149
|
notification={props.projection.semantic.statusStrip.message}
|
|
148
150
|
transient={props.note}
|
|
149
151
|
connectionState={props.projection.semantic.statusStrip.state}
|
|
@@ -12,6 +12,7 @@ export interface PaneActionMenuProps {
|
|
|
12
12
|
readonly width: number;
|
|
13
13
|
readonly viewportWidth?: number;
|
|
14
14
|
readonly viewportHeight?: number;
|
|
15
|
+
readonly viewportOrigin?: Readonly<{ x: number; y: number }>;
|
|
15
16
|
readonly active?: boolean;
|
|
16
17
|
readonly selectedId: PaneMenuActionId;
|
|
17
18
|
readonly closeArmed: boolean;
|
|
@@ -39,6 +40,7 @@ export function PaneActionMenu(props: PaneActionMenuProps) {
|
|
|
39
40
|
width={props.width}
|
|
40
41
|
viewportWidth={props.viewportWidth}
|
|
41
42
|
viewportHeight={props.viewportHeight}
|
|
43
|
+
viewportOrigin={props.viewportOrigin}
|
|
42
44
|
active={props.active}
|
|
43
45
|
title={props.paneTitle}
|
|
44
46
|
footer={
|
|
@@ -84,6 +84,7 @@ function badgeWidth(status: string | undefined): number {
|
|
|
84
84
|
*/
|
|
85
85
|
export function PaneTitleBar(props: PaneTitleBarProps) {
|
|
86
86
|
const [pointerInside, setPointerInside] = createSignal(false);
|
|
87
|
+
const [menuHovered, setMenuHovered] = createSignal(false);
|
|
87
88
|
const hovered = () => props.hovered ?? pointerInside();
|
|
88
89
|
const safeWidth = () => Math.max(1, Math.floor(props.width));
|
|
89
90
|
const status = () => agentStatus(props.activity);
|
|
@@ -96,11 +97,17 @@ export function PaneTitleBar(props: PaneTitleBarProps) {
|
|
|
96
97
|
const presence = () => (props.interaction ? paneInteractionPresence(props.interaction) : null);
|
|
97
98
|
const activityLabel = () =>
|
|
98
99
|
presence()
|
|
99
|
-
? `${presence()!.badge}${props.interaction?.origin === "external" ? " ·
|
|
100
|
+
? `${presence()!.badge[0]}${presence()!.badge.slice(1).toLowerCase()}${props.interaction?.origin === "external" ? " · tmux" : ""}`
|
|
100
101
|
: "";
|
|
101
102
|
const activityWidth = () => {
|
|
102
103
|
const available =
|
|
103
|
-
safeWidth() -
|
|
104
|
+
safeWidth() -
|
|
105
|
+
markerGutterWidth() -
|
|
106
|
+
markerWidth() -
|
|
107
|
+
actionWidth() -
|
|
108
|
+
zoomWidth() -
|
|
109
|
+
badgeWidth(statusLabel()) -
|
|
110
|
+
12;
|
|
104
111
|
if (!presence() || available < presence()!.badge.length + 2) return 0;
|
|
105
112
|
return Math.min(available, terminalDisplayWidth(activityLabel()) + 2);
|
|
106
113
|
};
|
|
@@ -111,13 +118,6 @@ export function PaneTitleBar(props: PaneTitleBarProps) {
|
|
|
111
118
|
// Reserve these cells even when the control is quiet or unavailable. Hover
|
|
112
119
|
// and menu lifetime must never change title clipping or terminal geometry.
|
|
113
120
|
const actionWidth = () => (safeWidth() >= 7 ? 3 : 0);
|
|
114
|
-
const actionsVisible = () =>
|
|
115
|
-
props.selected ||
|
|
116
|
-
props.keyboardFocused ||
|
|
117
|
-
props.terminalFocused ||
|
|
118
|
-
hovered() ||
|
|
119
|
-
props.menuFocused ||
|
|
120
|
-
props.menuOpen;
|
|
121
121
|
const zoomLabel = () =>
|
|
122
122
|
!props.zoomed
|
|
123
123
|
? ""
|
|
@@ -267,49 +267,60 @@ export function PaneTitleBar(props: PaneTitleBarProps) {
|
|
|
267
267
|
{clipTerminal(zoomLabel(), zoomWidth())}
|
|
268
268
|
</text>
|
|
269
269
|
) : null}
|
|
270
|
-
<
|
|
271
|
-
{
|
|
272
|
-
|
|
270
|
+
<box
|
|
271
|
+
id={`pane-header-status:${props.paneId}`}
|
|
272
|
+
width={activityWidth() + (showBadge() ? badgeWidth(statusLabel()) : 0)}
|
|
273
|
+
height={1}
|
|
274
|
+
flexShrink={0}
|
|
275
|
+
flexDirection="row"
|
|
276
|
+
backgroundColor={props.theme.roles.surfaces.panel}
|
|
277
|
+
>
|
|
278
|
+
<Show when={activityBadge()} keyed>
|
|
279
|
+
{(activity) => (
|
|
280
|
+
<Badge
|
|
281
|
+
theme={props.theme}
|
|
282
|
+
label={activity.label}
|
|
283
|
+
width={activity.width}
|
|
284
|
+
tone={
|
|
285
|
+
activity.presence.tone === "danger"
|
|
286
|
+
? "destructive"
|
|
287
|
+
: activity.presence.tone === "info"
|
|
288
|
+
? "accent"
|
|
289
|
+
: "done"
|
|
290
|
+
}
|
|
291
|
+
/>
|
|
292
|
+
)}
|
|
293
|
+
</Show>
|
|
294
|
+
{showBadge() ? (
|
|
295
|
+
<AgentBadge
|
|
273
296
|
theme={props.theme}
|
|
274
|
-
label={
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
: activity.presence.tone === "info"
|
|
280
|
-
? "accent"
|
|
281
|
-
: "done"
|
|
282
|
-
}
|
|
283
|
-
selected={props.selected}
|
|
284
|
-
focused={props.keyboardFocused || props.terminalFocused}
|
|
297
|
+
label={statusLabel()!}
|
|
298
|
+
status={status()!}
|
|
299
|
+
activity={props.activity}
|
|
300
|
+
width={badgeWidth(statusLabel())}
|
|
301
|
+
attention={props.attention}
|
|
285
302
|
/>
|
|
286
|
-
)}
|
|
287
|
-
</
|
|
288
|
-
{showBadge() ? (
|
|
289
|
-
<AgentBadge
|
|
290
|
-
theme={props.theme}
|
|
291
|
-
label={statusLabel()!}
|
|
292
|
-
status={status()!}
|
|
293
|
-
activity={props.activity}
|
|
294
|
-
width={badgeWidth(statusLabel())}
|
|
295
|
-
selected={props.selected}
|
|
296
|
-
focused={props.keyboardFocused || props.terminalFocused}
|
|
297
|
-
hovered={hovered()}
|
|
298
|
-
attention={props.attention}
|
|
299
|
-
/>
|
|
300
|
-
) : null}
|
|
303
|
+
) : null}
|
|
304
|
+
</box>
|
|
301
305
|
{actionWidth() > 0 ? (
|
|
302
|
-
<box
|
|
306
|
+
<box
|
|
307
|
+
width={actionWidth()}
|
|
308
|
+
height={1}
|
|
309
|
+
flexShrink={0}
|
|
310
|
+
onMouseOver={() => setMenuHovered(true)}
|
|
311
|
+
onMouseOut={() => setMenuHovered(false)}
|
|
312
|
+
>
|
|
303
313
|
<IconButton
|
|
304
314
|
theme={props.theme}
|
|
305
|
-
icon=
|
|
315
|
+
icon="⋯"
|
|
306
316
|
label="Pane actions"
|
|
307
|
-
variant="
|
|
317
|
+
variant="secondary"
|
|
308
318
|
width={actionWidth()}
|
|
319
|
+
hovered={menuHovered()}
|
|
309
320
|
focused={props.menuFocused}
|
|
310
321
|
selected={props.menuOpen}
|
|
311
322
|
disabled={props.menuDisabled}
|
|
312
|
-
background={
|
|
323
|
+
background={props.theme.roles.surfaces.panelRaised}
|
|
313
324
|
onPress={() => activateMenu()}
|
|
314
325
|
/>
|
|
315
326
|
</box>
|