tmux-ide 2.9.0-beta.36 → 2.9.0-beta.38

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 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.36",
11282
+ version: "2.9.0-beta.38",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.36",
3
+ "version": "2.9.0-beta.38",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,7 @@ import { fleetConnectionMessage, } from "@tmux-ide/daemon-client/fleet-connectio
6
6
  import { terminalAgentStatusLabel } from "./application-terminal-workspace-policy.js";
7
7
  import { For, Show, createEffect, createMemo, createSignal, } from "solid-js";
8
8
  import { clipTerminal, friendlySessionLabel } from "../terminal-text.js";
9
+ import { KeyHint } from "../ui/key-hint.jsx";
9
10
  import { NavigationRow } from "../ui/navigation-row.jsx";
10
11
  import { Surface } from "../ui/surface.jsx";
11
12
  import { useKeyboardRoute } from "../ui/keyboard-router.jsx";
@@ -302,10 +303,14 @@ export function ApplicationMachineSidebar(props) {
302
303
  return true;
303
304
  });
304
305
  return (<Surface id="application-machine-sidebar" theme={props.theme} variant="panel" width={props.width} height={props.height} flexShrink={0} flexDirection="column" overflow="hidden">
305
- <text height={1} fg={props.theme.roles.text.secondary}>
306
- {" "}
307
- {props.model.tabs?.().length ? "Machines · F9 tabs · ?" : "Machines · ? help"}
308
- </text>
306
+ <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
307
+ <text fg={props.theme.roles.text.secondary}>{" Machines"}</text>
308
+ <box flexGrow={1}/>
309
+ <Show when={props.model.tabs?.().length && props.width >= 28}>
310
+ <KeyHint theme={props.theme} keys="F9" label="Tabs" quiet/>
311
+ </Show>
312
+ <KeyHint theme={props.theme} keys="?" label={props.width >= 20 ? "Help" : undefined} quiet button onPress={() => setShowHelp(!showHelp())}/>
313
+ </box>
309
314
  <For each={visibleTabs()}>
310
315
  {(tab) => (<box height={1} flexDirection="row">
311
316
  <NavigationRow theme={props.theme} width={Math.max(1, props.width - 4)} id={`fleet-tab:${tab.key}`} label={`${tab.hostLabel} / ${tab.label}`} marker={tab.active ? "●" : "○"} detail={tab.available ? "" : "offline"} focused={false} onActivate={() => props.model.onOpenTab?.(tab.key)}/>
@@ -417,10 +422,10 @@ export function ApplicationMachineSidebar(props) {
417
422
  </text>
418
423
  </Show>
419
424
  <Show when={props.model.onOpenSwitcher}>
420
- <NavigationRow theme={props.theme} id="machine:switcher" label="Sessions" detail="F6" marker=" " width={props.width} onActivate={() => props.model.onOpenSwitcher?.()}/>
425
+ <KeyHint theme={props.theme} keys="F6" label="Sessions" width={props.width} quiet button onPress={() => props.model.onOpenSwitcher?.()}/>
421
426
  </Show>
422
427
  <Show when={props.model.onOpenAttention}>
423
- <NavigationRow theme={props.theme} id="machine:attention" label={`Attention (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)}) · F7`} marker="!" width={props.width} onActivate={() => props.model.onOpenAttention?.()}/>
428
+ <KeyHint theme={props.theme} keys="F7" label={`Attention (${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?.()}/>
424
429
  </Show>
425
430
  <Show when={controlsHeight() > 0 && controlGroup()}>
426
431
  {(group) => (<>
@@ -429,12 +434,12 @@ export function ApplicationMachineSidebar(props) {
429
434
  ? fleetConnectionMessage(group().diagnostic)
430
435
  : connectionDetail(group())}
431
436
  </text>
432
- <NavigationRow theme={props.theme} id="machine:retry" label="Retry connection (R)" marker="↻" width={props.width} onActivate={() => props.model.onRetryMachine?.(group().id)}/>
433
- <NavigationRow theme={props.theme} id="machine:disconnect" label="Disconnect (D)" marker="×" width={props.width} onActivate={() => props.model.onDisconnectMachine?.(group().id)}/>
437
+ <KeyHint theme={props.theme} keys="R" label="Retry connection" width={props.width} quiet button onPress={() => props.model.onRetryMachine?.(group().id)}/>
438
+ <KeyHint theme={props.theme} keys="D" label="Disconnect" width={props.width} quiet button onPress={() => props.model.onDisconnectMachine?.(group().id)}/>
434
439
  </>)}
435
440
  </Show>
436
441
  <For each={props.model.onAddMachine ? [props.model.onAddMachine] : []}>
437
- {(add) => (<NavigationRow theme={props.theme} id="machine:add" label="Add machine" detail="A" marker=" " width={props.width} onActivate={add}/>)}
442
+ {(add) => (<KeyHint theme={props.theme} keys="A" label="Add machine" width={props.width} quiet button onPress={add}/>)}
438
443
  </For>
439
444
  </Surface>);
440
445
  }
@@ -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,
@@ -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 || /^(?:ready|live|connected)$/iu.test(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 activityLabel = transient ?? notification ?? connectionLabel;
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
  ? [
@@ -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
- " ⌂ !❯ ▤ ± ◆ ready ⧉ web !blocked F5 palette
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 F5 Commands"
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 ready ⧉ web !blocked F5 palette
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 F5 Commands"
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 ready ⧉ web !blocked F5 palette
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 F5 Commands"
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
- " ⌂ ●❯ live
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 F5 Commands"
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 live
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 F5 Commands"
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 live
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 F5 Commands"
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
- " ⌂ ●❯ live
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
- " ⌂ ●❯ live
64
+ " ⌂ ●❯
65
65
  tCommand palette
66
66
  m …rch commands…
67
67
  2 Home F1w
@@ -71,7 +71,7 @@ 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 live
74
+ " tmux-ide ⌂ Home ●❯ Terminals Local · main
75
75
  tmux-ide ! main ! workin… + New window
76
76
  ● main ● Codex ● working ⋯ ! Scout ! idle ⋯
77
77
  CANONICAL-CELL CANONICAL-CELL
@@ -83,5 +83,5 @@ exports[`production ApplicationShellView preserves canonical attention while pri
83
83
 
84
84
  …
85
85
 
86
- F6 Sessions F7 Attention F5 Commands"
86
+ F6 Sessions F7 Attention F10 Sidebar F5 Commands"
87
87
  `;
@@ -21,6 +21,7 @@ import {
21
21
  } from "solid-js";
22
22
  import { clipTerminal, friendlySessionLabel } from "../terminal-text.ts";
23
23
  import type { SemanticThemeSnapshot } from "../theme.ts";
24
+ import { KeyHint } from "../ui/key-hint.tsx";
24
25
  import { NavigationRow } from "../ui/navigation-row.tsx";
25
26
  import { Surface } from "../ui/surface.tsx";
26
27
  import { useKeyboardRoute } from "../ui/keyboard-router.tsx";
@@ -458,10 +459,21 @@ export function ApplicationMachineSidebar(props: {
458
459
  flexDirection="column"
459
460
  overflow="hidden"
460
461
  >
461
- <text height={1} fg={props.theme.roles.text.secondary}>
462
- {" "}
463
- {props.model.tabs?.().length ? "Machines · F9 tabs · ?" : "Machines · ? help"}
464
- </text>
462
+ <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
463
+ <text fg={props.theme.roles.text.secondary}>{" Machines"}</text>
464
+ <box flexGrow={1} />
465
+ <Show when={props.model.tabs?.().length && props.width >= 28}>
466
+ <KeyHint theme={props.theme} keys="F9" label="Tabs" quiet />
467
+ </Show>
468
+ <KeyHint
469
+ theme={props.theme}
470
+ keys="?"
471
+ label={props.width >= 20 ? "Help" : undefined}
472
+ quiet
473
+ button
474
+ onPress={() => setShowHelp(!showHelp())}
475
+ />
476
+ </box>
465
477
  <For each={visibleTabs()}>
466
478
  {(tab) => (
467
479
  <box height={1} flexDirection="row">
@@ -653,24 +665,25 @@ export function ApplicationMachineSidebar(props: {
653
665
  </text>
654
666
  </Show>
655
667
  <Show when={props.model.onOpenSwitcher}>
656
- <NavigationRow
668
+ <KeyHint
657
669
  theme={props.theme}
658
- id="machine:switcher"
670
+ keys="F6"
659
671
  label="Sessions"
660
- detail="F6"
661
- marker=" "
662
672
  width={props.width}
663
- onActivate={() => props.model.onOpenSwitcher?.()}
673
+ quiet
674
+ button
675
+ onPress={() => props.model.onOpenSwitcher?.()}
664
676
  />
665
677
  </Show>
666
678
  <Show when={props.model.onOpenAttention}>
667
- <NavigationRow
679
+ <KeyHint
668
680
  theme={props.theme}
669
- id="machine:attention"
670
- label={`Attention (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)}) · F7`}
671
- marker="!"
681
+ keys="F7"
682
+ label={`Attention (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`}
672
683
  width={props.width}
673
- onActivate={() => props.model.onOpenAttention?.()}
684
+ quiet
685
+ button
686
+ onPress={() => props.model.onOpenAttention?.()}
674
687
  />
675
688
  </Show>
676
689
  <Show when={controlsHeight() > 0 && controlGroup()}>
@@ -681,35 +694,37 @@ export function ApplicationMachineSidebar(props: {
681
694
  ? fleetConnectionMessage(group().diagnostic!)
682
695
  : connectionDetail(group())}
683
696
  </text>
684
- <NavigationRow
697
+ <KeyHint
685
698
  theme={props.theme}
686
- id="machine:retry"
687
- label="Retry connection (R)"
688
- marker="↻"
699
+ keys="R"
700
+ label="Retry connection"
689
701
  width={props.width}
690
- onActivate={() => props.model.onRetryMachine?.(group().id)}
702
+ quiet
703
+ button
704
+ onPress={() => props.model.onRetryMachine?.(group().id)}
691
705
  />
692
- <NavigationRow
706
+ <KeyHint
693
707
  theme={props.theme}
694
- id="machine:disconnect"
695
- label="Disconnect (D)"
696
- marker="×"
708
+ keys="D"
709
+ label="Disconnect"
697
710
  width={props.width}
698
- onActivate={() => props.model.onDisconnectMachine?.(group().id)}
711
+ quiet
712
+ button
713
+ onPress={() => props.model.onDisconnectMachine?.(group().id)}
699
714
  />
700
715
  </>
701
716
  )}
702
717
  </Show>
703
718
  <For each={props.model.onAddMachine ? [props.model.onAddMachine] : []}>
704
719
  {(add) => (
705
- <NavigationRow
720
+ <KeyHint
706
721
  theme={props.theme}
707
- id="machine:add"
722
+ keys="A"
708
723
  label="Add machine"
709
- detail="A"
710
- marker=" "
711
724
  width={props.width}
712
- onActivate={add}
725
+ quiet
726
+ button
727
+ onPress={add}
713
728
  />
714
729
  )}
715
730
  </For>
@@ -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;
@@ -394,15 +395,24 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
394
395
  }}
395
396
  >
396
397
  <ApplicationShell
398
+ onFooterAction={(key) => {
399
+ props.machineSidebar?.onBlur?.();
400
+ if (key === "F6") props.machineSidebar?.onOpenSwitcher?.();
401
+ else if (key === "F7") props.machineSidebar?.onOpenAttention?.();
402
+ else
403
+ props.onPaletteActivate?.(
404
+ props.sidebarVisible === false ? "show-sidebar" : "hide-sidebar",
405
+ "mouse",
406
+ );
407
+ }}
397
408
  footerContext={props.surface() === "home" ? "home" : "terminals"}
409
+ scrollback={props.surface() === "terminals" && scrollback()}
398
410
  rightChips={
399
- props.machineLabel
411
+ props.surface() === "terminals" && props.dimensions().width >= 60
400
412
  ? [
401
413
  {
402
- id: "machine",
403
- label: `SSH ${props.machineLabel}`,
404
- context: true,
405
- textColor: props.machineColor,
414
+ id: "workspace-context",
415
+ label: ` ${props.machineLabel ?? "Local"} · ${friendlySessionLabel(shell.activeSession)} `,
406
416
  },
407
417
  ]
408
418
  : undefined
@@ -413,6 +423,11 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
413
423
  onHelp={() => props.onSetPaletteOpen(true, "mouse")}
414
424
  note={
415
425
  props.bootstrapNote() ??
426
+ (props.copyFeedback
427
+ ? props.copyFeedback.copied
428
+ ? "Copied"
429
+ : "Copy unavailable"
430
+ : null) ??
416
431
  (props.generationStatus() === "read-only"
417
432
  ? `Read-only input · select a pane to request control · shared tmux ${props.layout().current?.cols ?? "?"}×${props.layout().current?.rows ?? "?"}`
418
433
  : props.generationStatus())
@@ -517,6 +532,7 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
517
532
  >
518
533
  {(source) => (
519
534
  <ApplicationTerminalWorkspace
535
+ onScrollbackChange={setScrollback}
520
536
  layout={props.layout}
521
537
  adapter={source.adapter}
522
538
  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
  ? {
@@ -31,7 +31,13 @@ export function createApplicationTransientNoteOwner(options: {
31
31
  publish(note) {
32
32
  clear();
33
33
  options.write(note);
34
- if (!note) return;
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 each={props.note ? [props.note] : []}>
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="accent"
128
+ tone="warning"
121
129
  />
122
130
  )}
123
131
  </For>
124
132
  <For each={props.rightChips ?? []}>
125
133
  {(chip) => (
126
- <Badge
127
- theme={props.theme}
128
- label={chip.label.trim()}
129
- textColor={chip.textColor}
130
- width={Math.max(1, terminalDisplayWidth(chip.label) + (chip.context ? 1 : 0))}
131
- presentation={`${chip.label}${chip.context ? " " : ""}`}
132
- surface="header"
133
- hovered={chip.hovered}
134
- context={chip.context}
135
- attention={chip.attention}
136
- tone={chip.attention ? "warning" : chip.context ? "accent" : "neutral"}
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,8 @@ export interface ShellStatusStripProps {
152
164
  tool?: string | null;
153
165
  dockMode?: string | null;
154
166
  focus?: string | null;
167
+ onFooterAction?: (key: "F6" | "F7" | "F10") => void;
168
+ scrollback?: boolean;
155
169
  notification: string | null;
156
170
  transient?: string | null;
157
171
  connectionState?: "connected" | "reconnecting" | "disconnected" | "recovering";
@@ -178,16 +192,19 @@ export function ContextStatusBar(props: ShellStatusStripProps) {
178
192
  const activity = () => presentation().activity;
179
193
  const showMessage = () => !/^(?:Live|\d+ sessions? live)$/iu.test(activity().label);
180
194
  const hints = () => {
181
- const candidates = /^home$/iu.test(props.mode)
182
- ? [
183
- { keys: "↑↓", label: "Select" },
184
- { keys: "Enter", label: "Open" },
185
- { keys: "/", label: "Search" },
186
- ]
187
- : [
188
- { keys: "F6", label: "Sessions" },
189
- { keys: "F7", label: "Attention" },
190
- ];
195
+ const candidates = props.scrollback
196
+ ? [{ keys: "Esc", label: "Back to live" }]
197
+ : /^home$/iu.test(props.mode)
198
+ ? [
199
+ { keys: "↑↓", label: "Select" },
200
+ { keys: "Enter", label: "Open" },
201
+ { keys: "/", label: "Search" },
202
+ ]
203
+ : [
204
+ { keys: "F6", label: "Sessions" },
205
+ { keys: "F7", label: "Attention" },
206
+ { keys: "F10", label: "Sidebar" },
207
+ ];
191
208
  let remaining = available();
192
209
  return candidates.filter((hint) => {
193
210
  const width = terminalDisplayWidth(`${hint.keys} ${hint.label}`) + 2;
@@ -203,7 +220,26 @@ export function ContextStatusBar(props: ShellStatusStripProps) {
203
220
  when={showMessage()}
204
221
  fallback={
205
222
  <For each={hints()}>
206
- {(hint) => <KeyHint theme={props.theme} keys={hint.keys} label={hint.label} quiet />}
223
+ {(hint) => {
224
+ const action = () =>
225
+ hint.keys === "F6" || hint.keys === "F7" || hint.keys === "F10"
226
+ ? hint.keys
227
+ : null;
228
+ return (
229
+ <KeyHint
230
+ theme={props.theme}
231
+ keys={hint.keys}
232
+ label={hint.label}
233
+ quiet
234
+ button={action() !== null}
235
+ onPress={
236
+ action() && props.onFooterAction
237
+ ? () => props.onFooterAction?.(action()!)
238
+ : undefined
239
+ }
240
+ />
241
+ );
242
+ }}
207
243
  </For>
208
244
  }
209
245
  >
@@ -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 (!value || /^(?:ready|live|connected)$/iu.test(value)) return null;
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 activityLabel = transient ?? notification ?? connectionLabel;
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"
@@ -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
- " ⌂ ●❯ production application ro…
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 production application root
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 production application root
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,8 @@ import type { SemanticThemeSnapshot } from "../theme.ts";
11
11
  import type { ApplicationShellProjection } from "./application-shell.ts";
12
12
 
13
13
  export interface ApplicationShellProps {
14
+ onFooterAction?: (key: "F6" | "F7" | "F10") => void;
15
+ scrollback?: boolean;
14
16
  footerContext?: "home" | "terminals";
15
17
  theme: SemanticThemeSnapshot;
16
18
  projection: ApplicationShellProjection;
@@ -144,6 +146,8 @@ export function ApplicationShell(props: ApplicationShellProps) {
144
146
  props.showToolStatus === false ? null : props.projection.semantic.bottomDock.mode
145
147
  }
146
148
  focus={props.focusLabel ?? applicationShellFocusLabel(props.projection)}
149
+ onFooterAction={props.onFooterAction}
150
+ scrollback={props.scrollback}
147
151
  notification={props.projection.semantic.statusStrip.message}
148
152
  transient={props.note}
149
153
  connectionState={props.projection.semantic.statusStrip.state}