@towles/tool 0.0.109 → 0.0.111

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 (190) hide show
  1. package/package.json +9 -4
  2. package/{plugins/tt-agentboard → packages/agentboard}/README.md +1 -1
  3. package/{plugins/tt-agentboard → packages/agentboard}/apps/server/package.json +2 -1
  4. package/{plugins/tt-agentboard → packages/agentboard}/apps/server/src/main.ts +6 -20
  5. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/package.json +4 -0
  6. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DetailPanel.tsx +3 -2
  7. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/StatusBar.tsx +35 -0
  8. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/constants.ts +1 -0
  9. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/index.tsx +206 -226
  10. package/packages/agentboard/apps/tui/src/session-status.test.ts +70 -0
  11. package/packages/agentboard/apps/tui/src/session-status.ts +19 -0
  12. package/{plugins/tt-agentboard → packages/agentboard}/package.json +2 -6
  13. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/package.json +3 -0
  14. package/{plugins/tt-agentboard/packages/runtime/test → packages/agentboard/packages/runtime/src/agents}/tracker.test.ts +2 -2
  15. package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.test.ts +63 -0
  16. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/claude-code.ts +26 -2
  17. package/packages/agentboard/packages/runtime/src/config.test.ts +107 -0
  18. package/packages/agentboard/packages/runtime/src/config.ts +80 -0
  19. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/index.ts +1 -1
  20. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/plugins/loader.ts +1 -33
  21. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/git-info.ts +3 -2
  22. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/index.ts +23 -37
  23. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/launcher.ts +6 -18
  24. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/pane-scanner.ts +6 -0
  25. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/shared.ts +7 -2
  26. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/tsconfig.json +1 -1
  27. package/packages/shared/package.json +15 -0
  28. package/packages/shared/src/git/exec.ts +41 -0
  29. package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
  30. package/packages/shared/src/index.ts +8 -0
  31. package/packages/shared/tsconfig.json +16 -0
  32. package/src/cli.ts +1 -1
  33. package/src/commands/agentboard.ts +51 -67
  34. package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
  35. package/src/commands/auto-claude/config-init-helpers.ts +79 -0
  36. package/src/commands/auto-claude/config-init.test.ts +137 -0
  37. package/src/commands/auto-claude/config-init.ts +159 -0
  38. package/src/{lib → commands}/auto-claude/config.ts +4 -8
  39. package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
  40. package/src/commands/auto-claude/explain.test.ts +58 -0
  41. package/src/commands/auto-claude/explain.ts +97 -0
  42. package/src/commands/auto-claude/index.ts +37 -14
  43. package/src/{lib → commands}/auto-claude/labels.ts +1 -1
  44. package/src/commands/auto-claude/list.ts +5 -4
  45. package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
  46. package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
  47. package/src/commands/auto-claude/retry.test.ts +2 -2
  48. package/src/commands/auto-claude/retry.ts +5 -5
  49. package/src/commands/auto-claude/shell.ts +3 -0
  50. package/src/commands/auto-claude/status.test.ts +2 -2
  51. package/src/commands/auto-claude/status.ts +4 -4
  52. package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
  53. package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
  54. package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
  55. package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
  56. package/src/{lib → commands}/auto-claude/utils.ts +10 -4
  57. package/src/{lib/install → commands}/claude-settings.ts +1 -1
  58. package/src/commands/config/config.test.ts +129 -0
  59. package/src/commands/config/index.ts +11 -0
  60. package/src/commands/config/reset.ts +53 -0
  61. package/src/commands/config/schema.ts +19 -0
  62. package/src/commands/{config.ts → config/show.ts} +2 -2
  63. package/src/commands/config/validate.ts +51 -0
  64. package/src/commands/doctor/checks.ts +167 -0
  65. package/src/commands/doctor/format.test.ts +63 -0
  66. package/src/commands/doctor/format.ts +5 -0
  67. package/src/commands/doctor/history.test.ts +161 -0
  68. package/src/commands/doctor/history.ts +130 -0
  69. package/src/commands/doctor.ts +80 -151
  70. package/src/commands/gh/branch-clean.ts +4 -4
  71. package/src/commands/gh/branch.test.ts +4 -5
  72. package/src/commands/gh/branch.ts +10 -5
  73. package/src/commands/gh/pr.ts +6 -7
  74. package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
  75. package/src/commands/graph/format.test.ts +130 -0
  76. package/src/commands/graph/format.ts +94 -0
  77. package/src/commands/graph/index.ts +69 -41
  78. package/src/{lib → commands}/graph/labels.ts +4 -4
  79. package/src/{lib → commands}/graph/server.ts +2 -2
  80. package/src/{lib → commands}/graph/types.ts +2 -0
  81. package/src/commands/graph.test.ts +1 -1
  82. package/src/commands/install.ts +6 -6
  83. package/src/commands/journal/daily-notes.ts +4 -7
  84. package/src/{lib → commands}/journal/fs.ts +1 -1
  85. package/src/commands/journal/index.ts +2 -0
  86. package/src/commands/journal/list.test.ts +174 -0
  87. package/src/commands/journal/list.ts +213 -0
  88. package/src/commands/journal/meeting.ts +4 -7
  89. package/src/commands/journal/note.ts +4 -7
  90. package/src/{lib → commands}/journal/paths.ts +1 -1
  91. package/src/commands/journal/search.test.ts +156 -0
  92. package/src/commands/journal/search.ts +256 -0
  93. package/src/{lib → commands}/journal/templates.ts +1 -1
  94. package/src/config/settings.ts +35 -26
  95. package/plugins/tt-agentboard/bun.lock +0 -444
  96. package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
  97. package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
  98. package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
  99. package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
  100. package/plugins/tt-auto-claude/commands/list.md +0 -21
  101. package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
  102. package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
  103. package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
  104. package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
  105. package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
  106. package/src/commands/config.test.ts +0 -9
  107. package/src/lib/auto-claude/index.ts +0 -15
  108. package/src/lib/auto-claude/shell.ts +0 -6
  109. package/src/lib/graph/index.ts +0 -24
  110. package/src/lib/journal/index.ts +0 -11
  111. package/src/utils/git/exec.ts +0 -18
  112. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
  113. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
  114. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
  115. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
  116. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
  117. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
  118. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
  119. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
  120. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
  121. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
  122. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
  123. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
  124. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
  125. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
  126. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
  127. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
  128. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
  129. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
  130. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
  131. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
  132. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
  133. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
  134. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
  135. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
  136. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
  137. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
  138. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
  139. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
  140. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
  141. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
  142. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
  143. /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.json +0 -0
  144. /package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.json +0 -0
  145. /package/{plugins/tt-core → packages/core}/README.md +0 -0
  146. /package/{plugins/tt-core → packages/core}/commands/improve-architecture.md +0 -0
  147. /package/{plugins/tt-core → packages/core}/commands/interview-me.md +0 -0
  148. /package/{plugins/tt-core → packages/core}/commands/prd-to-issues.md +0 -0
  149. /package/{plugins/tt-core → packages/core}/commands/refine-text.md +0 -0
  150. /package/{plugins/tt-core → packages/core}/commands/task.md +0 -0
  151. /package/{plugins/tt-core → packages/core}/commands/tdd.md +0 -0
  152. /package/{plugins/tt-core → packages/core}/commands/write-prd.md +0 -0
  153. /package/{plugins/tt-core → packages/core}/skills/towles-tool/SKILL.md +0 -0
  154. /package/{src/utils → packages/shared/src}/date-utils.test.ts +0 -0
  155. /package/{src/utils → packages/shared/src}/date-utils.ts +0 -0
  156. /package/{src/utils → packages/shared/src}/fs.ts +0 -0
  157. /package/{src/utils → packages/shared/src}/git/branch-name.test.ts +0 -0
  158. /package/{src/utils → packages/shared/src}/git/branch-name.ts +0 -0
  159. /package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.test.ts +0 -0
  160. /package/{src/utils → packages/shared/src}/render.test.ts +0 -0
  161. /package/{src/utils → packages/shared/src}/render.ts +0 -0
  162. /package/src/{lib → commands}/auto-claude/config.test.ts +0 -0
  163. /package/src/{lib → commands}/auto-claude/labels.test.ts +0 -0
  164. /package/src/{lib → commands}/auto-claude/pipeline.test.ts +0 -0
  165. /package/src/{lib → commands}/auto-claude/prompt-templates/01_plan.prompt.md +0 -0
  166. /package/src/{lib → commands}/auto-claude/prompt-templates/02_implement.prompt.md +0 -0
  167. /package/src/{lib → commands}/auto-claude/prompt-templates/03_simplify.prompt.md +0 -0
  168. /package/src/{lib → commands}/auto-claude/prompt-templates/04_review.prompt.md +0 -0
  169. /package/src/{lib → commands}/auto-claude/prompt-templates/CLAUDE.md +0 -0
  170. /package/src/{lib → commands}/auto-claude/prompt-templates/index.test.ts +0 -0
  171. /package/src/{lib → commands}/auto-claude/prompt-templates/index.ts +0 -0
  172. /package/src/{lib → commands}/auto-claude/run-claude.test.ts +0 -0
  173. /package/src/{lib → commands}/auto-claude/spawn-claude.ts +0 -0
  174. /package/src/{lib → commands}/auto-claude/steps/simple-steps.ts +0 -0
  175. /package/src/{lib → commands}/auto-claude/steps/steps.test.ts +0 -0
  176. /package/src/{lib → commands}/auto-claude/stream-parser.test.ts +0 -0
  177. /package/src/{lib → commands}/auto-claude/stream-parser.ts +0 -0
  178. /package/src/{lib → commands}/auto-claude/templates.test.ts +0 -0
  179. /package/src/{lib → commands}/auto-claude/templates.ts +0 -0
  180. /package/src/{lib → commands}/auto-claude/test-helpers.ts +0 -0
  181. /package/src/{lib → commands}/auto-claude/utils.test.ts +0 -0
  182. /package/src/{lib → commands}/graph/analyzer.ts +0 -0
  183. /package/src/{lib → commands}/graph/graph-template.html +0 -0
  184. /package/src/{lib → commands}/graph/parser.test.ts +0 -0
  185. /package/src/{lib → commands}/graph/parser.ts +0 -0
  186. /package/src/{lib → commands}/graph/render.ts +0 -0
  187. /package/src/{lib → commands}/graph/sessions.ts +0 -0
  188. /package/src/{lib → commands}/graph/tools.ts +0 -0
  189. /package/src/{lib → commands}/graph/treemap.ts +0 -0
  190. /package/src/{lib → commands}/journal/editor.ts +0 -0
@@ -13,124 +13,72 @@ import {
13
13
  } from "solid-js";
14
14
  import type { Accessor } from "solid-js";
15
15
  import { createStore, reconcile } from "solid-js/store";
16
- import { TextAttributes } from "@opentui/core";
17
16
  import type { MouseEvent } from "@opentui/core";
18
17
 
19
- import {
20
- ensureServer,
21
- SERVER_PORT,
22
- SERVER_HOST,
23
- loadConfig,
24
- resolveTheme,
25
- saveConfig,
26
- } from "@tt-agentboard/runtime";
18
+ import { ensureServer, SERVER_PORT, SERVER_HOST, resolveTheme } from "@tt-agentboard/runtime";
27
19
  import type { ServerMessage, SessionData, ClientCommand, Theme } from "@tt-agentboard/runtime";
28
- import { TmuxClient } from "@tt-agentboard/mux-tmux";
29
20
  import { SessionCard } from "./components/SessionCard";
30
21
  import { DetailPanel } from "./components/DetailPanel";
31
22
  import { StatusBar } from "./components/StatusBar";
32
-
33
- // Detect tmux context (tmux only)
34
- type MuxContext = { type: "tmux"; sdk: TmuxClient; paneId: string } | { type: "none" };
35
-
36
- function detectMuxContext(): MuxContext {
37
- if (process.env.TMUX_PANE && process.env.TMUX) {
38
- return { type: "tmux", sdk: new TmuxClient(), paneId: process.env.TMUX_PANE };
39
- }
40
- return { type: "none" };
41
- }
23
+ import { computeSessionStatusCounts } from "./session-status";
24
+ import {
25
+ detectMuxContext,
26
+ refocusMainPane,
27
+ getClientTty,
28
+ getLocalSessionName,
29
+ } from "./mux-context";
30
+ import {
31
+ clampDetailPanelHeight,
32
+ getStoredDetailPanelHeight,
33
+ persistDetailPanelHeight,
34
+ } from "./detail-panel-height";
35
+ import {
36
+ SPINNERS,
37
+ BOLD,
38
+ DIM,
39
+ DEFAULT_DETAIL_PANEL_HEIGHT,
40
+ DIVIDER,
41
+ logResizeDebug,
42
+ } from "./constants";
42
43
 
43
44
  const muxCtx = detectMuxContext();
44
45
 
45
- const SPINNERS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
46
- const BOLD = TextAttributes.BOLD;
47
- const DIM = TextAttributes.DIM;
48
- const DEFAULT_DETAIL_PANEL_HEIGHT = 10;
49
- const MIN_DETAIL_PANEL_HEIGHT = 4;
50
- const DIVIDER = "─".repeat(200);
51
- const RESIZE_DEBUG_LOG = "/tmp/agentboard-tui-resize.log";
52
-
53
46
  const TUI_DEBUG = !!process.env.TT_AGENTBOARD_DEBUG;
54
47
 
55
- function logResizeDebug(message: string, data?: Record<string, unknown>): void {
56
- if (!TUI_DEBUG) return;
57
- const ts = new Date().toISOString();
58
- const extra = data ? ` ${JSON.stringify(data)}` : "";
59
- try {
60
- appendFileSync(RESIZE_DEBUG_LOG, `[${ts}] [pid:${process.pid}] ${message}${extra}\n`);
61
- } catch {}
62
- }
63
-
64
- function clampDetailPanelHeight(height: number): number {
65
- return Math.max(MIN_DETAIL_PANEL_HEIGHT, Math.round(height));
66
- }
67
-
68
- function getStoredDetailPanelHeight(sessionName: string): number {
69
- const stored = loadConfig().detailPanelHeights?.[sessionName];
70
- return typeof stored === "number" ? clampDetailPanelHeight(stored) : DEFAULT_DETAIL_PANEL_HEIGHT;
71
- }
72
-
73
- function persistDetailPanelHeight(sessionName: string, height: number): void {
74
- const config = loadConfig();
75
- saveConfig({
76
- detailPanelHeights: {
77
- ...(config.detailPanelHeights ?? {}),
78
- [sessionName]: clampDetailPanelHeight(height),
79
- },
80
- });
81
- }
82
-
83
- /** Refocus the main (non-sidebar) pane after TUI capability detection finishes.
84
- * This must happen from the TUI process — doing it from the server races with
85
- * capability query responses and leaks escape sequences to the main pane. */
86
- function refocusMainPane() {
87
- if (muxCtx.type === "tmux") {
88
- try {
89
- // Use the TUI's own pane ID to find its current window (handles stash restore
90
- // where the pane may have moved to a different window than the original).
91
- const windowId =
92
- process.env.REFOCUS_WINDOW ||
93
- Bun.spawnSync(["tmux", "display-message", "-t", muxCtx.paneId, "-p", "#{window_id}"], {
94
- stdout: "pipe",
95
- stderr: "pipe",
96
- })
97
- .stdout.toString()
98
- .trim();
99
- if (!windowId) return;
100
- const r = Bun.spawnSync(
101
- ["tmux", "list-panes", "-t", windowId, "-F", "#{pane_id} #{pane_title}"],
102
- { stdout: "pipe", stderr: "pipe" },
103
- );
104
- const lines = r.stdout.toString().trim().split("\n");
105
- const main = lines.find((l) => !l.includes("agentboard-sidebar"));
106
- if (main) {
107
- const paneId = main.split(" ")[0];
108
- Bun.spawnSync(["tmux", "select-pane", "-t", paneId], { stdout: "pipe", stderr: "pipe" });
109
- }
110
- } catch {}
111
- }
112
- }
113
-
114
- function getClientTty(): string {
115
- if (muxCtx.type === "tmux") {
116
- const { sdk, paneId } = muxCtx;
117
- const sessName = sdk.display("#{session_name}", { target: paneId });
118
- if (sessName) {
119
- const clients = sdk.listClients();
120
- const client = clients.find((c) => c.sessionName === sessName);
121
- if (client) return client.tty;
48
+ function KeyHints(props: {
49
+ hints: [string, string][];
50
+ palette: Accessor<Theme["palette"]>;
51
+ cols?: number;
52
+ }) {
53
+ const cols = () => props.cols ?? 2;
54
+ const rows = () => {
55
+ const pairs: [string, string][][] = [];
56
+ for (let i = 0; i < props.hints.length; i += cols()) {
57
+ pairs.push(props.hints.slice(i, i + cols()));
122
58
  }
123
- return sdk.getClientTty();
124
- }
125
- return "";
126
- }
59
+ return pairs;
60
+ };
127
61
 
128
- function getLocalSessionName(): string | null {
129
- if (muxCtx.type === "tmux") {
130
- const sessionName = muxCtx.sdk.display("#{session_name}", { target: muxCtx.paneId });
131
- return sessionName || null;
132
- }
133
- return null;
62
+ return (
63
+ <box flexDirection="column">
64
+ <For each={rows()}>
65
+ {(row) => (
66
+ <box flexDirection="row">
67
+ <For each={row}>
68
+ {([key, label]) => (
69
+ <box width={16} flexShrink={0}>
70
+ <text>
71
+ <span style={{ fg: props.palette().overlay0 }}>{key}</span>
72
+ <span style={{ fg: props.palette().overlay1 }}>{` ${label}`}</span>
73
+ </text>
74
+ </box>
75
+ )}
76
+ </For>
77
+ </box>
78
+ )}
79
+ </For>
80
+ </box>
81
+ );
134
82
  }
135
83
 
136
84
  function App() {
@@ -148,6 +96,7 @@ function App() {
148
96
  const [connected, setConnected] = createSignal(false);
149
97
  const [spinIdx, setSpinIdx] = createSignal(0);
150
98
  const [detailPanelHeight, setDetailPanelHeight] = createSignal(DEFAULT_DETAIL_PANEL_HEIGHT);
99
+ const [preferredEditor, setPreferredEditor] = createSignal("code");
151
100
  const [isDetailResizeHover, setIsDetailResizeHover] = createSignal(false);
152
101
  const [isDetailResizing, setIsDetailResizing] = createSignal(false);
153
102
  const detailPanelSessionName = createMemo(() => focusedSession() ?? mySession());
@@ -161,12 +110,12 @@ function App() {
161
110
  const [modal, setModal] = createSignal<"none" | "confirm-kill" | "help">("none");
162
111
  const [killTarget, setKillTarget] = createSignal<string | null>(null);
163
112
 
164
- const [clientTty, setClientTty] = createSignal(getClientTty());
113
+ const [clientTty, setClientTty] = createSignal(getClientTty(muxCtx));
165
114
  let ws: WebSocket | null = null;
166
115
  let startupFocusSynced = false;
167
116
  let detailResizeStartY = 0;
168
117
  let detailResizeStartHeight = DEFAULT_DETAIL_PANEL_HEIGHT;
169
- const startupSessionName = getLocalSessionName();
118
+ const startupSessionName = getLocalSessionName(muxCtx);
170
119
 
171
120
  const focusedData = createMemo(() => sessions.find((s) => s.name === focusedSession()) ?? null);
172
121
 
@@ -186,7 +135,7 @@ function App() {
186
135
  }
187
136
 
188
137
  function reIdentify() {
189
- const sessionName = getLocalSessionName();
138
+ const sessionName = getLocalSessionName(muxCtx);
190
139
  if (!sessionName) return;
191
140
 
192
141
  if (muxCtx.type === "tmux") {
@@ -274,14 +223,15 @@ function App() {
274
223
  }
275
224
 
276
225
  function beginDetailResize(event: MouseEvent) {
277
- logResizeDebug("beginDetailResize", {
278
- button: event.button,
279
- x: event.x,
280
- y: event.y,
281
- currentHeight: detailPanelHeight(),
282
- session: detailPanelSessionName(),
283
- target: event.target?.id ?? null,
284
- });
226
+ if (TUI_DEBUG)
227
+ logResizeDebug("beginDetailResize", {
228
+ button: event.button,
229
+ x: event.x,
230
+ y: event.y,
231
+ currentHeight: detailPanelHeight(),
232
+ session: detailPanelSessionName(),
233
+ target: event.target?.id ?? null,
234
+ });
285
235
  if (event.button !== 0) return;
286
236
  (renderer as any).setCapturedRenderable?.(event.target ?? undefined);
287
237
  detailResizeStartY = event.y;
@@ -291,36 +241,39 @@ function App() {
291
241
  }
292
242
 
293
243
  function handleDetailResizeDrag(event: MouseEvent) {
294
- logResizeDebug("handleDetailResizeDrag", {
295
- x: event.x,
296
- y: event.y,
297
- isResizing: isDetailResizing(),
298
- startY: detailResizeStartY,
299
- startHeight: detailResizeStartHeight,
300
- currentHeight: detailPanelHeight(),
301
- session: detailPanelSessionName(),
302
- });
244
+ if (TUI_DEBUG)
245
+ logResizeDebug("handleDetailResizeDrag", {
246
+ x: event.x,
247
+ y: event.y,
248
+ isResizing: isDetailResizing(),
249
+ startY: detailResizeStartY,
250
+ startHeight: detailResizeStartHeight,
251
+ currentHeight: detailPanelHeight(),
252
+ session: detailPanelSessionName(),
253
+ });
303
254
  if (!isDetailResizing()) return;
304
255
  const delta = detailResizeStartY - event.y;
305
256
  const nextHeight = clampDetailPanelHeight(detailResizeStartHeight + delta);
306
257
  setDetailPanelHeight(nextHeight);
307
- logResizeDebug("handleDetailResizeDrag:applied", {
308
- delta,
309
- nextHeight,
310
- session: detailPanelSessionName(),
311
- });
258
+ if (TUI_DEBUG)
259
+ logResizeDebug("handleDetailResizeDrag:applied", {
260
+ delta,
261
+ nextHeight,
262
+ session: detailPanelSessionName(),
263
+ });
312
264
  event.stopPropagation();
313
265
  }
314
266
 
315
267
  function endDetailResize(event?: MouseEvent) {
316
- logResizeDebug("endDetailResize", {
317
- x: event?.x,
318
- y: event?.y,
319
- isResizing: isDetailResizing(),
320
- currentHeight: detailPanelHeight(),
321
- session: detailPanelSessionName(),
322
- target: event?.target?.id ?? null,
323
- });
268
+ if (TUI_DEBUG)
269
+ logResizeDebug("endDetailResize", {
270
+ x: event?.x,
271
+ y: event?.y,
272
+ isResizing: isDetailResizing(),
273
+ currentHeight: detailPanelHeight(),
274
+ session: detailPanelSessionName(),
275
+ target: event?.target?.id ?? null,
276
+ });
324
277
  if (!isDetailResizing()) return;
325
278
  (renderer as any).setCapturedRenderable?.(undefined);
326
279
  setIsDetailResizing(false);
@@ -329,10 +282,11 @@ function App() {
329
282
  const sessionName = detailPanelSessionName();
330
283
  if (sessionName) {
331
284
  persistDetailPanelHeight(sessionName, detailPanelHeight());
332
- logResizeDebug("endDetailResize:persisted", {
333
- session: sessionName,
334
- height: detailPanelHeight(),
335
- });
285
+ if (TUI_DEBUG)
286
+ logResizeDebug("endDetailResize:persisted", {
287
+ session: sessionName,
288
+ height: detailPanelHeight(),
289
+ });
336
290
  }
337
291
 
338
292
  event?.stopPropagation();
@@ -353,13 +307,25 @@ function App() {
353
307
  });
354
308
  }
355
309
 
356
- onMount(() => {
357
- logResizeDebug("mount", {
358
- startupSessionName,
359
- localSessionName: getLocalSessionName(),
360
- muxType: muxCtx.type,
361
- tmuxPane: process.env.TMUX_PANE ?? null,
310
+ function openInEditor() {
311
+ const data = focusedData();
312
+ if (!data?.dir) return;
313
+ const editor = preferredEditor();
314
+ Bun.spawn([editor, data.dir], {
315
+ stdout: "ignore",
316
+ stderr: "ignore",
317
+ stdin: "ignore",
362
318
  });
319
+ }
320
+
321
+ onMount(() => {
322
+ if (TUI_DEBUG)
323
+ logResizeDebug("mount", {
324
+ startupSessionName,
325
+ localSessionName: getLocalSessionName(muxCtx),
326
+ muxType: muxCtx.type,
327
+ tmuxPane: process.env.TMUX_PANE ?? null,
328
+ });
363
329
  // Refocus the main pane once terminal capability detection finishes.
364
330
  // This avoids the race where the server refocuses too early and capability
365
331
  // responses leak as garbage text into the main pane.
@@ -367,7 +333,7 @@ function App() {
367
333
  const doStartupRefocus = () => {
368
334
  if (startupRefocused) return;
369
335
  startupRefocused = true;
370
- refocusMainPane();
336
+ refocusMainPane(muxCtx);
371
337
  };
372
338
  renderer.on("capabilities", doStartupRefocus);
373
339
  // Fallback: if no capability response arrives within 2s, refocus anyway
@@ -412,6 +378,7 @@ function App() {
412
378
  setFocusedSession(startupFocus);
413
379
  setCurrentSession(msg.currentSession);
414
380
  setTheme(resolveTheme(msg.theme));
381
+ if (msg.preferredEditor) setPreferredEditor(msg.preferredEditor);
415
382
  } else if (msg.type === "focus") {
416
383
  setFocusedSession(msg.focusedSession);
417
384
  setCurrentSession(msg.currentSession);
@@ -421,13 +388,17 @@ function App() {
421
388
 
422
389
  if (!startupFocusSynced && sessions.some((session) => session.name === msg.name)) {
423
390
  startupFocusSynced = true;
391
+ const oldFocus = focusedSession();
424
392
  setFocusedSession(msg.name);
425
- if (focusedSession() !== msg.name) {
393
+ if (oldFocus !== msg.name) {
426
394
  startupFocusToPublish = msg.name;
427
395
  }
428
396
  }
429
397
  } else if (msg.type === "re-identify") {
430
398
  reIdentify();
399
+ } else if (msg.type === "quit") {
400
+ if (ws) ws.close();
401
+ renderer.destroy();
431
402
  }
432
403
  });
433
404
 
@@ -443,17 +414,6 @@ function App() {
443
414
  };
444
415
 
445
416
  onCleanup(() => socket.close());
446
-
447
- // Listen for quit messages from server
448
- socket.addEventListener("message", (event) => {
449
- try {
450
- const msg = JSON.parse(event.data as string);
451
- if (msg.type === "quit") {
452
- if (ws) ws.close();
453
- renderer.destroy();
454
- }
455
- } catch {}
456
- });
457
417
  });
458
418
 
459
419
  const hasRunning = createMemo(() => sessions.some((s) => s.agentState?.status === "running"));
@@ -470,19 +430,21 @@ function App() {
470
430
  const sessionName = detailPanelSessionName();
471
431
  if (!sessionName) return;
472
432
  const storedHeight = getStoredDetailPanelHeight(sessionName);
473
- logResizeDebug("loadStoredDetailPanelHeight", {
474
- session: sessionName,
475
- storedHeight,
476
- });
433
+ if (TUI_DEBUG)
434
+ logResizeDebug("loadStoredDetailPanelHeight", {
435
+ session: sessionName,
436
+ storedHeight,
437
+ });
477
438
  setDetailPanelHeight(storedHeight);
478
439
  });
479
440
 
480
441
  createEffect(() => {
481
- logResizeDebug("detailPanelHeight:changed", {
482
- height: detailPanelHeight(),
483
- session: detailPanelSessionName(),
484
- isResizing: isDetailResizing(),
485
- });
442
+ if (TUI_DEBUG)
443
+ logResizeDebug("detailPanelHeight:changed", {
444
+ height: detailPanelHeight(),
445
+ session: detailPanelSessionName(),
446
+ isResizing: isDetailResizing(),
447
+ });
486
448
  });
487
449
 
488
450
  useKeyboard((key) => {
@@ -581,9 +543,6 @@ function App() {
581
543
  case "r":
582
544
  send({ type: "refresh" });
583
545
  break;
584
- case "t":
585
- // reserved — was theme picker
586
- break;
587
546
  case "u":
588
547
  send({ type: "show-all-sessions" });
589
548
  break;
@@ -608,8 +567,10 @@ function App() {
608
567
  }
609
568
  break;
610
569
  }
570
+ case "e":
571
+ openInEditor();
572
+ break;
611
573
  case "n":
612
- case "c":
613
574
  createNewSession();
614
575
  break;
615
576
  case "?":
@@ -635,6 +596,7 @@ function App() {
635
596
  );
636
597
 
637
598
  const unseenCount = createMemo(() => sessions.filter((s) => s.unseen).length);
599
+ const sessionStatusCounts = createMemo(() => computeSessionStatusCounts(sessions));
638
600
 
639
601
  const isFocused = createSelector(focusedSession);
640
602
 
@@ -646,6 +608,7 @@ function App() {
646
608
  runningCount={runningAgentCount()}
647
609
  errorCount={errorAgentCount()}
648
610
  unseenCount={unseenCount()}
611
+ sessionStatusCounts={sessionStatusCounts()}
649
612
  theme={theme}
650
613
  />
651
614
 
@@ -722,30 +685,32 @@ function App() {
722
685
  <Show
723
686
  when={panelFocus() === "sessions"}
724
687
  fallback={
725
- <text>
726
- <span style={{ fg: P().overlay0 }}>{"←"}</span>
727
- <span style={{ fg: P().overlay1 }}>{" back "}</span>
728
- <span style={{ fg: P().overlay0 }}>{""}</span>
729
- <span style={{ fg: P().overlay1 }}>{" focus "}</span>
730
- <span style={{ fg: P().overlay0 }}>{"d"}</span>
731
- <span style={{ fg: P().overlay1 }}>{" dismiss "}</span>
732
- <span style={{ fg: P().overlay0 }}>{"x"}</span>
733
- <span style={{ fg: P().overlay1 }}>{" kill"}</span>
734
- </text>
688
+ <KeyHints
689
+ palette={P}
690
+ hints={[
691
+ ["←", "back"],
692
+ ["⏎", "focus"],
693
+ ["d", "dismiss"],
694
+ ["x", "kill"],
695
+ ]}
696
+ />
735
697
  }
736
698
  >
737
- <text>
738
- <span style={{ fg: P().overlay0 }}>{"⇥"}</span>
739
- <span style={{ fg: P().overlay1 }}>{" cycle "}</span>
740
- <span style={{ fg: P().overlay0 }}>{""}</span>
741
- <span style={{ fg: P().overlay1 }}>{" go "}</span>
742
- <span style={{ fg: P().overlay0 }}>{"→"}</span>
743
- <span style={{ fg: P().overlay1 }}>{" detail "}</span>
744
- <span style={{ fg: P().overlay0 }}>{"d"}</span>
745
- <span style={{ fg: P().overlay1 }}>{" hide "}</span>
746
- <span style={{ fg: P().overlay0 }}>{"x"}</span>
747
- <span style={{ fg: P().overlay1 }}>{" kill"}</span>
748
- </text>
699
+ <KeyHints
700
+ palette={P}
701
+ hints={[
702
+ ["⇥", "cycle"],
703
+ ["⏎", "go"],
704
+ ["→", "select"],
705
+ ["n", "new"],
706
+ ["e", "edit"],
707
+ ["d", "hide"],
708
+ ["x", "kill"],
709
+ ["r", "refresh"],
710
+ ["q", "quit"],
711
+ ["?", "help"],
712
+ ]}
713
+ />
749
714
  </Show>
750
715
  </box>
751
716
 
@@ -796,24 +761,31 @@ function App() {
796
761
 
797
762
  // --- Help Overlay ---
798
763
 
764
+ const HELP_KEYS: [string, string][] = [
765
+ ["j/k ↑↓", "Move focus"],
766
+ ["Enter", "Switch to session"],
767
+ ["1-9", "Jump to session"],
768
+ ["Tab", "Cycle sessions"],
769
+ ["n", "New session"],
770
+ ["e", "Open in editor"],
771
+ ["d", "Hide session"],
772
+ ["x", "Kill session"],
773
+ ["r", "Refresh"],
774
+ ["u", "Show all sessions"],
775
+ ["→/l", "Select panel"],
776
+ ["←/h/Esc", "Back to sessions"],
777
+ ["Alt+↑↓", "Reorder sessions"],
778
+ ["q", "Quit"],
779
+ ];
780
+
781
+ const HELP_COLS = 2;
782
+ const HELP_ROWS = Math.ceil(HELP_KEYS.length / HELP_COLS);
783
+ const HELP_COLUMNS = Array.from({ length: HELP_COLS }, (_, c) =>
784
+ HELP_KEYS.slice(c * HELP_ROWS, (c + 1) * HELP_ROWS),
785
+ );
786
+
799
787
  function HelpOverlay(props: { palette: Accessor<Theme["palette"]>; onClose: () => void }) {
800
788
  const P = () => props.palette();
801
- const keys: [string, string][] = [
802
- ["j/k ↑↓", "Move focus"],
803
- ["Enter", "Switch to session"],
804
- ["1-9", "Jump to session"],
805
- ["Tab", "Cycle sessions"],
806
- ["n/c", "New session"],
807
- ["d", "Hide session"],
808
- ["x", "Kill session"],
809
- ["r", "Refresh"],
810
- ["u", "Show all sessions"],
811
- ["→/l", "Detail panel"],
812
- ["←/h/Esc", "Back to sessions"],
813
- ["Alt+↑↓", "Reorder sessions"],
814
- ["q", "Quit"],
815
- ];
816
-
817
789
  return (
818
790
  <box
819
791
  position="absolute"
@@ -832,7 +804,7 @@ function HelpOverlay(props: { palette: Accessor<Theme["palette"]>; onClose: () =
832
804
  backgroundColor={P().mantle}
833
805
  padding={1}
834
806
  flexDirection="column"
835
- width={30}
807
+ width={56}
836
808
  >
837
809
  <text>
838
810
  <span style={{ fg: P().blue, attributes: BOLD }}>Keybindings</span>
@@ -840,20 +812,28 @@ function HelpOverlay(props: { palette: Accessor<Theme["palette"]>; onClose: () =
840
812
  <box height={1}>
841
813
  <text style={{ fg: P().surface2 }}>{DIVIDER}</text>
842
814
  </box>
843
- <For each={keys}>
844
- {([key, desc]) => (
845
- <box flexDirection="row" paddingLeft={1}>
846
- <box width={12} flexShrink={0}>
847
- <text>
848
- <span style={{ fg: P().sky }}>{key}</span>
849
- </text>
815
+ <box flexDirection="row">
816
+ <For each={HELP_COLUMNS}>
817
+ {(col) => (
818
+ <box flexDirection="column" flexGrow={1}>
819
+ <For each={col}>
820
+ {([key, desc]) => (
821
+ <box flexDirection="row" paddingLeft={1}>
822
+ <box width={12} flexShrink={0}>
823
+ <text>
824
+ <span style={{ fg: P().sky }}>{key}</span>
825
+ </text>
826
+ </box>
827
+ <text truncate>
828
+ <span style={{ fg: P().subtext0 }}>{desc}</span>
829
+ </text>
830
+ </box>
831
+ )}
832
+ </For>
850
833
  </box>
851
- <text truncate>
852
- <span style={{ fg: P().subtext0 }}>{desc}</span>
853
- </text>
854
- </box>
855
- )}
856
- </For>
834
+ )}
835
+ </For>
836
+ </box>
857
837
  <box height={1}>
858
838
  <text style={{ fg: P().surface2 }}>{DIVIDER}</text>
859
839
  </box>