@towles/tool 0.0.108 → 0.0.110

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 +204 -225
  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 +2 -0
  26. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/tsconfig.json +1 -1
  27. package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.json +1 -1
  28. package/packages/shared/package.json +15 -0
  29. package/packages/shared/src/git/exec.ts +41 -0
  30. package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
  31. package/packages/shared/src/index.ts +8 -0
  32. package/packages/shared/tsconfig.json +16 -0
  33. package/src/cli.ts +3 -2
  34. package/src/commands/agentboard.ts +42 -59
  35. package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
  36. package/src/commands/auto-claude/config-init-helpers.ts +79 -0
  37. package/src/commands/auto-claude/config-init.test.ts +137 -0
  38. package/src/commands/auto-claude/config-init.ts +159 -0
  39. package/src/{lib → commands}/auto-claude/config.ts +4 -8
  40. package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
  41. package/src/commands/auto-claude/explain.test.ts +58 -0
  42. package/src/commands/auto-claude/explain.ts +97 -0
  43. package/src/commands/auto-claude/index.ts +37 -14
  44. package/src/{lib → commands}/auto-claude/labels.ts +1 -1
  45. package/src/commands/auto-claude/list.ts +5 -4
  46. package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
  47. package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
  48. package/src/commands/auto-claude/retry.test.ts +2 -2
  49. package/src/commands/auto-claude/retry.ts +5 -5
  50. package/src/commands/auto-claude/shell.ts +3 -0
  51. package/src/commands/auto-claude/status.test.ts +2 -2
  52. package/src/commands/auto-claude/status.ts +4 -4
  53. package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
  54. package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
  55. package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
  56. package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
  57. package/src/{lib → commands}/auto-claude/utils.ts +10 -4
  58. package/src/{lib/install → commands}/claude-settings.ts +1 -1
  59. package/src/commands/config/config.test.ts +129 -0
  60. package/src/commands/config/index.ts +11 -0
  61. package/src/commands/config/reset.ts +53 -0
  62. package/src/commands/config/schema.ts +19 -0
  63. package/src/commands/{config.ts → config/show.ts} +2 -2
  64. package/src/commands/config/validate.ts +51 -0
  65. package/src/commands/doctor/checks.ts +167 -0
  66. package/src/commands/doctor/format.test.ts +63 -0
  67. package/src/commands/doctor/format.ts +5 -0
  68. package/src/commands/doctor/history.test.ts +161 -0
  69. package/src/commands/doctor/history.ts +130 -0
  70. package/src/commands/doctor.ts +80 -151
  71. package/src/commands/gh/branch-clean.ts +4 -4
  72. package/src/commands/gh/branch.test.ts +4 -5
  73. package/src/commands/gh/branch.ts +10 -5
  74. package/src/commands/gh/pr.ts +6 -7
  75. package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
  76. package/src/commands/graph/format.test.ts +130 -0
  77. package/src/commands/graph/format.ts +94 -0
  78. package/src/commands/graph/index.ts +69 -41
  79. package/src/{lib → commands}/graph/labels.ts +4 -4
  80. package/src/{lib → commands}/graph/server.ts +2 -2
  81. package/src/{lib → commands}/graph/types.ts +2 -0
  82. package/src/commands/graph.test.ts +1 -1
  83. package/src/commands/install.ts +6 -6
  84. package/src/commands/journal/daily-notes.ts +4 -7
  85. package/src/{lib → commands}/journal/fs.ts +1 -1
  86. package/src/commands/journal/index.ts +2 -0
  87. package/src/commands/journal/list.test.ts +174 -0
  88. package/src/commands/journal/list.ts +213 -0
  89. package/src/commands/journal/meeting.ts +4 -7
  90. package/src/commands/journal/note.ts +4 -7
  91. package/src/{lib → commands}/journal/paths.ts +1 -1
  92. package/src/commands/journal/search.test.ts +156 -0
  93. package/src/commands/journal/search.ts +256 -0
  94. package/src/{lib → commands}/journal/templates.ts +1 -1
  95. package/src/config/settings.ts +35 -26
  96. package/plugins/tt-agentboard/bun.lock +0 -444
  97. package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
  98. package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
  99. package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
  100. package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
  101. package/plugins/tt-auto-claude/commands/list.md +0 -21
  102. package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
  103. package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
  104. package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
  105. package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
  106. package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
  107. package/src/commands/config.test.ts +0 -9
  108. package/src/lib/auto-claude/index.ts +0 -15
  109. package/src/lib/auto-claude/shell.ts +0 -6
  110. package/src/lib/graph/index.ts +0 -24
  111. package/src/lib/journal/index.ts +0 -11
  112. package/src/utils/git/exec.ts +0 -18
  113. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
  114. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
  115. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
  116. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
  117. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
  118. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
  119. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
  120. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
  121. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
  122. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
  123. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
  124. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
  125. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
  126. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
  127. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
  128. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
  129. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
  130. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
  131. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
  132. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
  133. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
  134. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
  135. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
  136. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
  137. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
  138. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
  139. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
  140. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
  141. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
  142. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
  143. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
  144. /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.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);
@@ -428,6 +395,9 @@ function App() {
428
395
  }
429
396
  } else if (msg.type === "re-identify") {
430
397
  reIdentify();
398
+ } else if (msg.type === "quit") {
399
+ if (ws) ws.close();
400
+ renderer.destroy();
431
401
  }
432
402
  });
433
403
 
@@ -443,17 +413,6 @@ function App() {
443
413
  };
444
414
 
445
415
  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
416
  });
458
417
 
459
418
  const hasRunning = createMemo(() => sessions.some((s) => s.agentState?.status === "running"));
@@ -470,19 +429,21 @@ function App() {
470
429
  const sessionName = detailPanelSessionName();
471
430
  if (!sessionName) return;
472
431
  const storedHeight = getStoredDetailPanelHeight(sessionName);
473
- logResizeDebug("loadStoredDetailPanelHeight", {
474
- session: sessionName,
475
- storedHeight,
476
- });
432
+ if (TUI_DEBUG)
433
+ logResizeDebug("loadStoredDetailPanelHeight", {
434
+ session: sessionName,
435
+ storedHeight,
436
+ });
477
437
  setDetailPanelHeight(storedHeight);
478
438
  });
479
439
 
480
440
  createEffect(() => {
481
- logResizeDebug("detailPanelHeight:changed", {
482
- height: detailPanelHeight(),
483
- session: detailPanelSessionName(),
484
- isResizing: isDetailResizing(),
485
- });
441
+ if (TUI_DEBUG)
442
+ logResizeDebug("detailPanelHeight:changed", {
443
+ height: detailPanelHeight(),
444
+ session: detailPanelSessionName(),
445
+ isResizing: isDetailResizing(),
446
+ });
486
447
  });
487
448
 
488
449
  useKeyboard((key) => {
@@ -581,9 +542,6 @@ function App() {
581
542
  case "r":
582
543
  send({ type: "refresh" });
583
544
  break;
584
- case "t":
585
- // reserved — was theme picker
586
- break;
587
545
  case "u":
588
546
  send({ type: "show-all-sessions" });
589
547
  break;
@@ -608,8 +566,10 @@ function App() {
608
566
  }
609
567
  break;
610
568
  }
569
+ case "e":
570
+ openInEditor();
571
+ break;
611
572
  case "n":
612
- case "c":
613
573
  createNewSession();
614
574
  break;
615
575
  case "?":
@@ -635,6 +595,7 @@ function App() {
635
595
  );
636
596
 
637
597
  const unseenCount = createMemo(() => sessions.filter((s) => s.unseen).length);
598
+ const sessionStatusCounts = createMemo(() => computeSessionStatusCounts(sessions));
638
599
 
639
600
  const isFocused = createSelector(focusedSession);
640
601
 
@@ -646,6 +607,7 @@ function App() {
646
607
  runningCount={runningAgentCount()}
647
608
  errorCount={errorAgentCount()}
648
609
  unseenCount={unseenCount()}
610
+ sessionStatusCounts={sessionStatusCounts()}
649
611
  theme={theme}
650
612
  />
651
613
 
@@ -722,30 +684,32 @@ function App() {
722
684
  <Show
723
685
  when={panelFocus() === "sessions"}
724
686
  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>
687
+ <KeyHints
688
+ palette={P}
689
+ hints={[
690
+ ["←", "back"],
691
+ ["⏎", "focus"],
692
+ ["d", "dismiss"],
693
+ ["x", "kill"],
694
+ ]}
695
+ />
735
696
  }
736
697
  >
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>
698
+ <KeyHints
699
+ palette={P}
700
+ hints={[
701
+ ["⇥", "cycle"],
702
+ ["⏎", "go"],
703
+ ["→", "select"],
704
+ ["n", "new"],
705
+ ["e", "edit"],
706
+ ["d", "hide"],
707
+ ["x", "kill"],
708
+ ["r", "refresh"],
709
+ ["q", "quit"],
710
+ ["?", "help"],
711
+ ]}
712
+ />
749
713
  </Show>
750
714
  </box>
751
715
 
@@ -796,24 +760,31 @@ function App() {
796
760
 
797
761
  // --- Help Overlay ---
798
762
 
763
+ const HELP_KEYS: [string, string][] = [
764
+ ["j/k ↑↓", "Move focus"],
765
+ ["Enter", "Switch to session"],
766
+ ["1-9", "Jump to session"],
767
+ ["Tab", "Cycle sessions"],
768
+ ["n", "New session"],
769
+ ["e", "Open in editor"],
770
+ ["d", "Hide session"],
771
+ ["x", "Kill session"],
772
+ ["r", "Refresh"],
773
+ ["u", "Show all sessions"],
774
+ ["→/l", "Select panel"],
775
+ ["←/h/Esc", "Back to sessions"],
776
+ ["Alt+↑↓", "Reorder sessions"],
777
+ ["q", "Quit"],
778
+ ];
779
+
780
+ const HELP_COLS = 2;
781
+ const HELP_ROWS = Math.ceil(HELP_KEYS.length / HELP_COLS);
782
+ const HELP_COLUMNS = Array.from({ length: HELP_COLS }, (_, c) =>
783
+ HELP_KEYS.slice(c * HELP_ROWS, (c + 1) * HELP_ROWS),
784
+ );
785
+
799
786
  function HelpOverlay(props: { palette: Accessor<Theme["palette"]>; onClose: () => void }) {
800
787
  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
788
  return (
818
789
  <box
819
790
  position="absolute"
@@ -832,7 +803,7 @@ function HelpOverlay(props: { palette: Accessor<Theme["palette"]>; onClose: () =
832
803
  backgroundColor={P().mantle}
833
804
  padding={1}
834
805
  flexDirection="column"
835
- width={30}
806
+ width={56}
836
807
  >
837
808
  <text>
838
809
  <span style={{ fg: P().blue, attributes: BOLD }}>Keybindings</span>
@@ -840,20 +811,28 @@ function HelpOverlay(props: { palette: Accessor<Theme["palette"]>; onClose: () =
840
811
  <box height={1}>
841
812
  <text style={{ fg: P().surface2 }}>{DIVIDER}</text>
842
813
  </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>
814
+ <box flexDirection="row">
815
+ <For each={HELP_COLUMNS}>
816
+ {(col) => (
817
+ <box flexDirection="column" flexGrow={1}>
818
+ <For each={col}>
819
+ {([key, desc]) => (
820
+ <box flexDirection="row" paddingLeft={1}>
821
+ <box width={12} flexShrink={0}>
822
+ <text>
823
+ <span style={{ fg: P().sky }}>{key}</span>
824
+ </text>
825
+ </box>
826
+ <text truncate>
827
+ <span style={{ fg: P().subtext0 }}>{desc}</span>
828
+ </text>
829
+ </box>
830
+ )}
831
+ </For>
850
832
  </box>
851
- <text truncate>
852
- <span style={{ fg: P().subtext0 }}>{desc}</span>
853
- </text>
854
- </box>
855
- )}
856
- </For>
833
+ )}
834
+ </For>
835
+ </box>
857
836
  <box height={1}>
858
837
  <text style={{ fg: P().surface2 }}>{DIVIDER}</text>
859
838
  </box>
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type { SessionData, AgentEvent } from "@tt-agentboard/runtime";
3
+ import { computeSessionStatusCounts } from "./session-status";
4
+
5
+ function makeSession(agentStatus?: AgentEvent["status"]): SessionData {
6
+ return {
7
+ name: "test",
8
+ createdAt: Date.now(),
9
+ dir: "/tmp",
10
+ branch: "main",
11
+ dirty: false,
12
+ isWorktree: false,
13
+ filesChanged: 0,
14
+ linesAdded: 0,
15
+ linesRemoved: 0,
16
+ commitsDelta: 0,
17
+ unseen: false,
18
+ panes: 1,
19
+ ports: [],
20
+ windows: 1,
21
+ uptime: "0s",
22
+ agentState: agentStatus
23
+ ? { agent: "claude", session: "test", status: agentStatus, ts: Date.now() }
24
+ : null,
25
+ agents: [],
26
+ eventTimestamps: [],
27
+ };
28
+ }
29
+
30
+ describe("computeSessionStatusCounts", () => {
31
+ it("returns all zeros for empty sessions", () => {
32
+ expect(computeSessionStatusCounts([])).toEqual({ active: 0, error: 0, idle: 0 });
33
+ });
34
+
35
+ it("counts running sessions as active", () => {
36
+ const sessions = [makeSession("running"), makeSession("running")];
37
+ expect(computeSessionStatusCounts(sessions)).toEqual({ active: 2, error: 0, idle: 0 });
38
+ });
39
+
40
+ it("counts waiting sessions as active", () => {
41
+ const sessions = [makeSession("waiting")];
42
+ expect(computeSessionStatusCounts(sessions)).toEqual({ active: 1, error: 0, idle: 0 });
43
+ });
44
+
45
+ it("counts error sessions", () => {
46
+ const sessions = [makeSession("error")];
47
+ expect(computeSessionStatusCounts(sessions)).toEqual({ active: 0, error: 1, idle: 0 });
48
+ });
49
+
50
+ it("counts idle, done, interrupted, and null agentState as idle", () => {
51
+ const sessions = [
52
+ makeSession("idle"),
53
+ makeSession("done"),
54
+ makeSession("interrupted"),
55
+ makeSession(undefined),
56
+ ];
57
+ expect(computeSessionStatusCounts(sessions)).toEqual({ active: 0, error: 0, idle: 4 });
58
+ });
59
+
60
+ it("counts mixed statuses correctly", () => {
61
+ const sessions = [
62
+ makeSession("running"),
63
+ makeSession("error"),
64
+ makeSession("idle"),
65
+ makeSession("waiting"),
66
+ makeSession("done"),
67
+ ];
68
+ expect(computeSessionStatusCounts(sessions)).toEqual({ active: 2, error: 1, idle: 2 });
69
+ });
70
+ });