tmux-ide 2.9.0-beta.20 → 2.9.0-beta.21

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.
Files changed (49) hide show
  1. package/bin/cli.js +100 -8
  2. package/package.json +3 -2
  3. package/packages/daemon/dist/command-center/diagnostics.js +65 -0
  4. package/packages/daemon/dist/command-center/log-stream.js +9 -0
  5. package/packages/daemon/dist/command-center/server.js +7 -0
  6. package/packages/daemon/dist/lib/app-config.js +4 -2
  7. package/packages/daemon/dist/lib/soak-diagnostics.js +124 -0
  8. package/packages/daemon/dist/lib/soak-verdict.js +185 -35
  9. package/packages/daemon/dist/lib/terminal-host-color.js +33 -0
  10. package/packages/daemon/dist/tui/mirror/automatic-contrast.js +161 -0
  11. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +9 -3
  12. package/packages/daemon/dist/tui/mirror/pane-surface.jsx +7 -5
  13. package/packages/daemon/dist/tui/mirror/resize-transaction.js +48 -20
  14. package/packages/daemon/dist/tui/mirror/runtime/application-appearance-owner.js +40 -6
  15. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +63 -42
  16. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +138 -67
  17. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-palette-owner.js +44 -28
  18. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +49 -11
  19. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +140 -2
  20. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +3 -1
  21. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +2 -1
  22. package/packages/daemon/dist/tui/mirror/theme.js +4 -31
  23. package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +15 -8
  24. package/packages/daemon/src/command-center/diagnostics.ts +75 -0
  25. package/packages/daemon/src/command-center/log-stream.ts +8 -0
  26. package/packages/daemon/src/command-center/server.ts +8 -0
  27. package/packages/daemon/src/lib/app-config.ts +11 -3
  28. package/packages/daemon/src/lib/soak-diagnostics.ts +183 -0
  29. package/packages/daemon/src/lib/soak-verdict.ts +314 -23
  30. package/packages/daemon/src/lib/terminal-host-color.ts +43 -0
  31. package/packages/daemon/src/tui/mirror/automatic-contrast.ts +180 -0
  32. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +26 -5
  33. package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -8
  34. package/packages/daemon/src/tui/mirror/resize-transaction.ts +46 -22
  35. package/packages/daemon/src/tui/mirror/runtime/application-appearance-owner.ts +45 -6
  36. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +99 -75
  37. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -0
  38. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +9 -2
  39. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
  40. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +148 -69
  41. package/packages/daemon/src/tui/mirror/runtime/application-terminal-palette-owner.ts +55 -28
  42. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +59 -17
  43. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +151 -3
  44. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +3 -1
  45. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +2 -1
  46. package/packages/daemon/src/tui/mirror/theme.ts +4 -39
  47. package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +21 -8
  48. package/packages/daemon-client/src/terminal-fast-lane.test.ts +18 -0
  49. package/packages/daemon-client/src/terminal-fast-lane.ts +7 -2
@@ -12,6 +12,7 @@ function nonEmpty(value, label) {
12
12
  }
13
13
  function sameTarget(state, observation) {
14
14
  return (state.operationId === observation.operationId &&
15
+ state.authorityGeneration === observation.authorityGeneration &&
15
16
  state.workspaceName === observation.workspaceName &&
16
17
  state.semanticPaneId === observation.semanticPaneId &&
17
18
  state.axis === observation.axis);
@@ -19,9 +20,9 @@ function sameTarget(state, observation) {
19
20
  /**
20
21
  * Renderer-local pane-resize transaction.
21
22
  *
22
- * Pointer motion only changes the preview state. Release authors exactly one
23
- * semantic intent and keeps that preview visible until the matching observed
24
- * operation settles or rejects. The controller owns no timers, transport, or
23
+ * Pointer motion submits the first target immediately, then retains only the
24
+ * latest desired size while receipt and canonical layout settle the pending
25
+ * operation. Release closes the gesture and flushes that latest target. The controller owns no timers, transport, or
25
26
  * rendering effects beyond the functions supplied by its caller.
26
27
  */
27
28
  export class ResizeTransactionController {
@@ -35,6 +36,8 @@ export class ResizeTransactionController {
35
36
  #projection = null;
36
37
  #revision = 0;
37
38
  #disposed = false;
39
+ #dragOpen = false;
40
+ #submittedCells = null;
38
41
  constructor(options) {
39
42
  if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
40
43
  throw new RangeError("resize transaction timeout must be positive");
@@ -54,6 +57,8 @@ export class ResizeTransactionController {
54
57
  const workspaceName = nonEmpty(input.workspaceName, "workspaceName");
55
58
  const semanticPaneId = nonEmpty(input.semanticPaneId, "semanticPaneId");
56
59
  const canonicalCells = positiveCells(input.canonicalCells, "canonicalCells");
60
+ this.#dragOpen = true;
61
+ this.#submittedCells = null;
57
62
  this.#projection = createOptimisticProjection({
58
63
  generation: authorityGeneration,
59
64
  revision: this.#revision,
@@ -71,30 +76,36 @@ export class ResizeTransactionController {
71
76
  });
72
77
  return true;
73
78
  }
74
- /** Purely local preview update. It never calls submit or schedules work. */
79
+ /** First/latest coalescing: at most one mutation awaits canonical settlement. */
75
80
  move(previewCells) {
76
- if (this.#state.phase !== "dragging")
81
+ if (!this.#dragOpen || this.#state.phase === "idle")
77
82
  return false;
78
83
  const next = positiveCells(previewCells, "previewCells");
79
84
  if (next === this.#state.previewCells)
80
85
  return false;
81
86
  this.#emit({ ...this.#state, previewCells: next });
87
+ if (this.#state.phase === "dragging")
88
+ this.#submit();
82
89
  return true;
83
90
  }
84
- /**
85
- * Submit once. Duplicate physical release/up/drop events return the existing
86
- * operation id without authoring another mutation.
87
- */
91
+ /** Duplicate releases never author another mutation. */
88
92
  release() {
93
+ this.#dragOpen = false;
89
94
  if (this.#state.phase === "pending")
90
95
  return this.#state.operationId;
96
+ if (this.#state.phase !== "dragging")
97
+ return null;
98
+ const canonicalCells = this.#state.canonicalCells;
99
+ this.#emit({ phase: "idle", canonicalCells, outcome: null });
100
+ return null;
101
+ }
102
+ #submit() {
91
103
  if (this.#state.phase !== "dragging")
92
104
  return null;
93
105
  const dragging = this.#state;
94
- if (dragging.previewCells === dragging.canonicalCells) {
95
- this.#emit({ phase: "idle", canonicalCells: dragging.canonicalCells, outcome: null });
106
+ if (dragging.previewCells === dragging.canonicalCells)
96
107
  return null;
97
- }
108
+ this.#submittedCells = dragging.previewCells;
98
109
  const operationId = nonEmpty(this.#options.operationId(), "operationId");
99
110
  const submittedAt = this.#options.now();
100
111
  if (!this.#projection)
@@ -146,10 +157,15 @@ export class ResizeTransactionController {
146
157
  message: rejection.message,
147
158
  });
148
159
  }
149
- /** Escape/cancel is only meaningful before release. */
160
+ /** Discard queued motion; an already dispatched mutation cannot be undone. */
150
161
  cancelDrag() {
151
- if (this.#state.phase !== "dragging")
162
+ if (!this.#dragOpen || this.#state.phase === "idle")
152
163
  return false;
164
+ this.#dragOpen = false;
165
+ if (this.#state.phase === "pending") {
166
+ this.#emit({ ...this.#state, previewCells: this.#submittedCells });
167
+ return true;
168
+ }
153
169
  const canonicalCells = this.#state.canonicalCells;
154
170
  this.#emit({ phase: "idle", canonicalCells, outcome: null });
155
171
  return true;
@@ -160,6 +176,8 @@ export class ResizeTransactionController {
160
176
  }
161
177
  /** Retire one runtime lane without permanently disposing the controller. */
162
178
  retire() {
179
+ this.#dragOpen = false;
180
+ this.#submittedCells = null;
163
181
  this.#clearTimeout();
164
182
  this.#projection = null;
165
183
  if (this.#state.phase !== "idle" || this.#state.canonicalCells !== null) {
@@ -172,7 +190,8 @@ export class ResizeTransactionController {
172
190
  !this.#projection?.pending.some((operation) => operation.operationId === observation.operationId))
173
191
  return false;
174
192
  const cells = positiveCells(observation.cells, "observed cells");
175
- const operationId = this.#state.operationId;
193
+ const pending = this.#state;
194
+ const operationId = pending.operationId;
176
195
  if (!this.#projection)
177
196
  return false;
178
197
  this.#revision += 1;
@@ -182,11 +201,19 @@ export class ResizeTransactionController {
182
201
  value: cells,
183
202
  }, { observedOperationIds: [operationId], nowMs: this.#options.now() });
184
203
  this.#clearTimeout();
185
- this.#emit({
186
- phase: "idle",
187
- canonicalCells: cells,
188
- outcome: { kind: "settled", operationId, source, cells },
189
- });
204
+ const hasLatest = pending.previewCells !== this.#submittedCells && pending.previewCells !== cells;
205
+ if (this.#dragOpen || hasLatest) {
206
+ this.#emit({ ...pending, phase: "dragging", canonicalCells: cells });
207
+ if (hasLatest)
208
+ this.#submit();
209
+ }
210
+ else {
211
+ this.#emit({
212
+ phase: "idle",
213
+ canonicalCells: cells,
214
+ outcome: { kind: "settled", operationId, source, cells },
215
+ });
216
+ }
190
217
  return true;
191
218
  }
192
219
  #revertTimedOut(operationId) {
@@ -202,6 +229,7 @@ export class ResizeTransactionController {
202
229
  this.#projection = reconcileOptimisticOperation(this.#projection, operationId, reason.kind === "timed-out" ? "timed-out" : "rejected");
203
230
  }
204
231
  const canonicalCells = this.#state.canonicalCells;
232
+ this.#dragOpen = false;
205
233
  this.#clearTimeout();
206
234
  this.#emit({
207
235
  phase: "idle",
@@ -1,14 +1,32 @@
1
+ import { createAutomaticContrastPass } from "../automatic-contrast.js";
1
2
  import { VISUAL_THEME_PRESETS, findVisualThemePreset } from "@tmux-ide/contracts";
2
3
  import { batch, createSignal } from "solid-js";
3
4
  import { updateAppConfig } from "../../../lib/app-config.js";
4
5
  import { createSemanticThemeStore, createTerminalPaletteProjection, deriveSystemVisualHostDefaults, } from "../theme.js";
5
6
  import { createApplicationTransientNoteOwner } from "./application-transient-note-owner.js";
6
7
  function hostDefaults(snapshot) {
7
- if (snapshot.availability !== "available")
8
- return null;
9
- return deriveSystemVisualHostDefaults(snapshot);
8
+ return (deriveSystemVisualHostDefaults(snapshot) ?? { appearance: snapshot.detectedMode, overrides: {} });
10
9
  }
11
10
  export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
11
+ const [automaticContrast, setAutomaticContrast] = createSignal(config.theme.automaticContrast ?? true);
12
+ const correct = createAutomaticContrastPass();
13
+ const postProcess = (buffer) => {
14
+ if (automaticContrast())
15
+ correct(buffer);
16
+ };
17
+ renderer.addPostProcessFn?.(postProcess);
18
+ const setContrast = (enabled) => {
19
+ if (enabled === automaticContrast())
20
+ return;
21
+ setAutomaticContrast(enabled);
22
+ renderer.requestRender?.();
23
+ };
24
+ const toggleAutomaticContrast = () => {
25
+ if (pickerOpen()) {
26
+ setContrast(!automaticContrast());
27
+ setPickerError(null);
28
+ }
29
+ };
12
30
  const initialHostPalette = terminalPaletteOwner.getSnapshot();
13
31
  const store = createSemanticThemeStore(config.theme, {
14
32
  rendererMode: renderer.themeMode,
@@ -65,10 +83,18 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
65
83
  const apply = (id) => {
66
84
  const preset = findVisualThemePreset(id);
67
85
  const mode = preset?.appearance ?? id;
68
- store.configure({ ...config.theme, mode, preset: preset?.id });
86
+ const wasSystem = theme().setting === "system";
87
+ store.configure({
88
+ ...config.theme,
89
+ mode,
90
+ preset: preset?.id,
91
+ });
92
+ if (mode === "system" && !wasSystem)
93
+ void terminalPaletteOwner.refresh();
69
94
  setPickerSelection(id);
70
95
  };
71
96
  let originalSelection = pickerSelection();
97
+ let originalContrast = automaticContrast();
72
98
  const preview = (id) => {
73
99
  if (!pickerOpen())
74
100
  return;
@@ -79,6 +105,7 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
79
105
  const cancelPicker = () => {
80
106
  if (!pickerOpen())
81
107
  return;
108
+ setContrast(originalContrast);
82
109
  apply(originalSelection);
83
110
  setPickerOpen(false);
84
111
  setPickerError(null);
@@ -89,6 +116,7 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
89
116
  try {
90
117
  updateAppConfig({
91
118
  theme: {
119
+ automaticContrast: automaticContrast(),
92
120
  mode: theme().setting,
93
121
  preset: findVisualThemePreset(pickerSelection())?.id ?? "",
94
122
  },
@@ -105,7 +133,7 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
105
133
  const order = ["dark", "light", "system"];
106
134
  const next = order[(order.indexOf(theme().setting) + 1) % order.length];
107
135
  apply(next);
108
- updateAppConfig({ theme: { mode: next, preset: "" } });
136
+ updateAppConfig({ theme: { mode: next, preset: "", automaticContrast: automaticContrast() } });
109
137
  notice.publish(`theme → ${next}`);
110
138
  };
111
139
  return {
@@ -116,6 +144,8 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
116
144
  palette,
117
145
  hostPalette,
118
146
  setTransientNote: notice.publish,
147
+ automaticContrast,
148
+ toggleAutomaticContrast,
119
149
  pickerOpen,
120
150
  pickerError,
121
151
  pickerQuery,
@@ -125,6 +155,7 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
125
155
  if (pickerOpen())
126
156
  return;
127
157
  originalSelection = pickerSelection();
158
+ originalContrast = automaticContrast();
128
159
  setPickerQuery("");
129
160
  setPickerError(null);
130
161
  setPickerOpen(true);
@@ -139,7 +170,9 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
139
170
  return true;
140
171
  const name = event.name.toLowerCase();
141
172
  const choices = pickerOptions();
142
- if (name === "escape")
173
+ if (event.ctrl && name === "a")
174
+ toggleAutomaticContrast();
175
+ else if (name === "escape")
143
176
  cancelPicker();
144
177
  else if (name === "return" || name === "enter")
145
178
  savePicker();
@@ -166,6 +199,7 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
166
199
  },
167
200
  cycleTheme,
168
201
  dispose() {
202
+ renderer.removePostProcessFn?.(postProcess);
169
203
  stopTheme();
170
204
  stopRendererTheme();
171
205
  stopHostPalette();
@@ -68,6 +68,7 @@ export function ApplicationMachineSidebar(props) {
68
68
  }))
69
69
  : []),
70
70
  ]));
71
+ const rowsByKey = createMemo(() => new Map(rows().map((row) => [row.key, row])));
71
72
  const index = () => Math.max(0, rows().findIndex((row) => row.key === selectedKey()));
72
73
  const controlGroup = () => {
73
74
  const group = rows()[index()]?.group;
@@ -281,49 +282,69 @@ export function ApplicationMachineSidebar(props) {
281
282
  revealFocusedRow();
282
283
  };
283
284
  }} height={machineHeight()} width={props.width} scrollX={false} scrollY={true} horizontalScrollbarOptions={{ visible: false }}>
284
- <For each={rows()}>
285
- {(row) => (<box width={Math.max(1, props.width - 1)} height={row.agent ? (row.agentHeading ? 3 : 2) : 1} flexShrink={0} flexDirection="column">
286
- <Show when={row.agentHeading}>
287
- <text height={1} fg={props.theme.roles.text.secondary}>
288
- {" "}
289
- Agents
290
- </text>
291
- </Show>
292
- <NavigationRow theme={props.theme} id={`machine:${row.key}`} width={Math.max(1, props.width - 1)} labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined} label={row.agent
293
- ? row.agent.name
294
- : row.session
295
- ? friendlySessionLabel(row.session.name)
296
- : row.group.label} marker={row.agent
297
- ? active(row)
298
- ? row.agent.attention
299
- ? "›!"
300
- : "›"
301
- : row.agent.attention
302
- ? "!"
303
- : "•"
304
- : row.session
305
- ? props.model.favorites?.().includes(row.session.id)
306
- ? " ★"
307
- : active(row)
308
- ? " ›"
309
- : " "
310
- : collapsed().has(preferenceKey(row.group))
311
- ? "▸"
312
- : "▾"} detail={row.agent
313
- ? row.group.state !== "ready" || row.agent.disabled
314
- ? "unavailable"
315
- : `[${terminalAgentStatusLabel(row.agent.activity)}]`
316
- : row.session
317
- ? row.group.state !== "ready" || row.session.disabled
285
+ <For each={rows().map((row) => row.key)}>
286
+ {(key) => {
287
+ // The semantic key owns the native row; fresh snapshots update its
288
+ // props instead of destroying and recreating identical renderables.
289
+ const current = createMemo((previous) => rowsByKey().get(key) ?? previous, rowsByKey().get(key));
290
+ const row = {
291
+ key,
292
+ get group() {
293
+ return current().group;
294
+ },
295
+ get session() {
296
+ return current().session;
297
+ },
298
+ get agent() {
299
+ return current().agent;
300
+ },
301
+ get agentHeading() {
302
+ return current().agentHeading;
303
+ },
304
+ };
305
+ return (<box width={Math.max(1, props.width - 1)} height={row.agent ? (row.agentHeading ? 3 : 2) : 1} flexShrink={0} flexDirection="column">
306
+ <Show when={row.agentHeading}>
307
+ <text height={1} fg={props.theme.roles.text.secondary}>
308
+ {" "}
309
+ Agents
310
+ </text>
311
+ </Show>
312
+ <NavigationRow theme={props.theme} id={`machine:${row.key}`} width={Math.max(1, props.width - 1)} labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined} label={row.agent
313
+ ? row.agent.name
314
+ : row.session
315
+ ? friendlySessionLabel(row.session.name)
316
+ : row.group.label} marker={row.agent
317
+ ? active(row)
318
+ ? row.agent.attention
319
+ ? "›!"
320
+ : "›"
321
+ : row.agent.attention
322
+ ? "!"
323
+ : "•"
324
+ : row.session
325
+ ? props.model.favorites?.().includes(row.session.id)
326
+ ? " ★"
327
+ : active(row)
328
+ ? " ›"
329
+ : " "
330
+ : collapsed().has(preferenceKey(row.group))
331
+ ? "▸"
332
+ : "▾"} detail={row.agent
333
+ ? row.group.state !== "ready" || row.agent.disabled
318
334
  ? "unavailable"
319
- : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
320
- : row.group.agents?.length && row.group.state === "ready"
321
- ? activity(row).label
322
- : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && row.key === selectedKey())} attention={row.agent?.attention ?? activity(row).kind === "attention"} onActivate={(source) => activate(row, source)}/>
323
- <Show when={row.agent}>
324
- {(agent) => (<text height={1} fg={props.theme.roles.text.muted} content={clipTerminal(` ${friendlySessionLabel(agent().sessionName)}`, Math.max(1, props.width - 1))}/>)}
325
- </Show>
326
- </box>)}
335
+ : `[${terminalAgentStatusLabel(row.agent.activity)}]`
336
+ : row.session
337
+ ? row.group.state !== "ready" || row.session.disabled
338
+ ? "unavailable"
339
+ : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
340
+ : row.group.agents?.length && row.group.state === "ready"
341
+ ? activity(row).label
342
+ : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && row.key === selectedKey())} attention={row.agent?.attention ?? activity(row).kind === "attention"} onActivate={(source) => activate(row, source)}/>
343
+ <Show when={row.agent}>
344
+ {(agent) => (<text height={1} fg={props.theme.roles.text.muted} content={clipTerminal(` ${friendlySessionLabel(agent().sessionName)}`, Math.max(1, props.width - 1))}/>)}
345
+ </Show>
346
+ </box>);
347
+ }}
327
348
  </For>
328
349
  </scrollbox>
329
350
  <For each={agentHeight() > 0 ? [true] : []}>