@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
@@ -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
+ });
@@ -0,0 +1,19 @@
1
+ import type { SessionData } from "@tt-agentboard/runtime";
2
+ import type { SessionStatusCounts } from "./components/StatusBar";
3
+
4
+ export function computeSessionStatusCounts(sessions: SessionData[]): SessionStatusCounts {
5
+ let active = 0;
6
+ let error = 0;
7
+ let idle = 0;
8
+ for (const s of sessions) {
9
+ const status = s.agentState?.status;
10
+ if (status === "running" || status === "waiting") {
11
+ active++;
12
+ } else if (status === "error") {
13
+ error++;
14
+ } else {
15
+ idle++;
16
+ }
17
+ }
18
+ return { active, error, idle };
19
+ }
@@ -2,18 +2,14 @@
2
2
  "name": "@towles/tt-agentboard",
3
3
  "version": "0.0.1",
4
4
  "private": true,
5
- "workspaces": [
6
- "apps/*",
7
- "packages/*"
8
- ],
9
5
  "type": "module",
10
6
  "scripts": {
11
7
  "server": "bun run apps/server/src/main.ts",
12
8
  "tui": "bun run apps/tui/src/index.tsx",
13
9
  "dev:server": "bun --watch run apps/server/src/main.ts",
14
10
  "dev:tui": "bun --watch run apps/tui/src/index.tsx",
15
- "test": "bun test packages/runtime/test",
16
- "typecheck": "tsc --noEmit"
11
+ "test": "bun test packages/runtime/src",
12
+ "typecheck:runtime": "tsc --noEmit -p packages/runtime/tsconfig.json"
17
13
  },
18
14
  "dependencies": {
19
15
  "@tt-agentboard/mux-tmux": "workspace:*",
@@ -7,6 +7,9 @@
7
7
  "scripts": {
8
8
  "test": "bun test"
9
9
  },
10
+ "dependencies": {
11
+ "consola": "^3.4.2"
12
+ },
10
13
  "devDependencies": {
11
14
  "@types/bun": "latest",
12
15
  "typescript": "^5"
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from "bun:test";
2
- import { AgentTracker, instanceKey } from "../src/agents/tracker";
3
- import type { AgentEvent } from "../src/contracts/agent";
2
+ import { AgentTracker, instanceKey } from "./tracker";
3
+ import type { AgentEvent } from "../contracts/agent";
4
4
 
5
5
  function makeEvent(overrides: Partial<AgentEvent> = {}): AgentEvent {
6
6
  return {
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { determineStatus } from "./claude-code";
3
+
4
+ describe("determineStatus", () => {
5
+ it('returns "idle" when no message', () => {
6
+ expect(determineStatus({})).toBe("idle");
7
+ expect(determineStatus({ message: undefined })).toBe("idle");
8
+ });
9
+
10
+ it('returns "idle" when message has no role', () => {
11
+ expect(determineStatus({ message: { content: "hi" } })).toBe("idle");
12
+ });
13
+
14
+ it('returns "running" for user messages', () => {
15
+ expect(determineStatus({ message: { role: "user", content: "hello" } })).toBe("running");
16
+ });
17
+
18
+ it('returns "running" for assistant messages with tool_use', () => {
19
+ expect(
20
+ determineStatus({
21
+ message: {
22
+ role: "assistant",
23
+ content: [{ type: "text", text: "Let me check that." }, { type: "tool_use" }],
24
+ },
25
+ }),
26
+ ).toBe("running");
27
+ });
28
+
29
+ it('returns "done" for assistant messages without tool_use', () => {
30
+ expect(
31
+ determineStatus({
32
+ message: {
33
+ role: "assistant",
34
+ content: [{ type: "text", text: "Here is the answer." }],
35
+ },
36
+ }),
37
+ ).toBe("done");
38
+ });
39
+
40
+ it('returns "done" for assistant messages with string content', () => {
41
+ expect(
42
+ determineStatus({
43
+ message: { role: "assistant", content: "plain text response" },
44
+ }),
45
+ ).toBe("done");
46
+ });
47
+
48
+ it('returns "done" for assistant messages with empty content', () => {
49
+ expect(
50
+ determineStatus({
51
+ message: { role: "assistant", content: [] },
52
+ }),
53
+ ).toBe("done");
54
+ });
55
+
56
+ it('returns "idle" for unknown roles', () => {
57
+ expect(
58
+ determineStatus({
59
+ message: { role: "system", content: "system message" },
60
+ }),
61
+ ).toBe("idle");
62
+ });
63
+ });
@@ -18,6 +18,7 @@ import { join, basename } from "node:path";
18
18
  import { homedir } from "node:os";
19
19
  import type { AgentStatus } from "../../contracts/agent";
20
20
  import type { AgentWatcher, AgentWatcherContext } from "../../contracts/agent-watcher";
21
+ import { JOURNAL_IDLE_TIMEOUT_MS } from "../../shared";
21
22
 
22
23
  // --- Types ---
23
24
 
@@ -145,7 +146,30 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
145
146
  const threadId = basename(filePath, ".jsonl");
146
147
  const prev = this.sessions.get(threadId);
147
148
 
148
- if (prev && size === prev.fileSize) return;
149
+ if (prev && size === prev.fileSize) {
150
+ // Post-seed: if status is "running" but journal hasn't been written to
151
+ // in >2min, the process likely exited — downgrade to "idle".
152
+ if (this.seeded && prev.status === "running") {
153
+ try {
154
+ const mtime = (await stat(filePath)).mtimeMs;
155
+ if (Date.now() - mtime > JOURNAL_IDLE_TIMEOUT_MS) {
156
+ prev.status = "idle";
157
+ const session = prev.projectDir ? this.ctx?.resolveSession(prev.projectDir) : undefined;
158
+ if (session) {
159
+ this.ctx?.emit({
160
+ agent: "claude-code",
161
+ session,
162
+ status: "idle",
163
+ ts: Date.now(),
164
+ threadId,
165
+ threadName: prev.threadName,
166
+ });
167
+ }
168
+ }
169
+ } catch {}
170
+ }
171
+ return;
172
+ }
149
173
 
150
174
  // Seed mode: read last entry to capture real status for post-seed emit
151
175
  if (!this.seeded) {
@@ -178,7 +202,7 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
178
202
  if (latestStatus === "running") {
179
203
  try {
180
204
  const mtime = (await stat(filePath)).mtimeMs;
181
- if (Date.now() - mtime > 10_000) latestStatus = "idle";
205
+ if (Date.now() - mtime > JOURNAL_IDLE_TIMEOUT_MS) latestStatus = "idle";
182
206
  } catch {}
183
207
  }
184
208
 
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { loadConfig, saveConfig, loadPreferredEditor } from "./config";
5
+
6
+ const TEST_HOME = join(import.meta.dir, ".test-home");
7
+ const CONFIG_DIR = join(TEST_HOME, ".config", "towles-tool");
8
+ const SETTINGS_FILE = join(CONFIG_DIR, "towles-tool.settings.json");
9
+
10
+ describe("config", () => {
11
+ beforeEach(() => {
12
+ rmSync(TEST_HOME, { recursive: true, force: true });
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(TEST_HOME, { recursive: true, force: true });
17
+ });
18
+
19
+ describe("loadConfig", () => {
20
+ it("returns defaults when no settings file exists", () => {
21
+ const config = loadConfig(TEST_HOME);
22
+ expect(config.port).toBeUndefined();
23
+ expect(config.theme).toBeUndefined();
24
+ expect(config.sidebarWidth).toBeUndefined();
25
+ });
26
+
27
+ it("reads agentboard config from settings file", () => {
28
+ mkdirSync(CONFIG_DIR, { recursive: true });
29
+ writeFileSync(
30
+ SETTINGS_FILE,
31
+ JSON.stringify({
32
+ preferredEditor: "cursor",
33
+ agentboard: {
34
+ port: 4201,
35
+ theme: "tokyo-night",
36
+ sidebarWidth: 30,
37
+ },
38
+ }),
39
+ );
40
+
41
+ const config = loadConfig(TEST_HOME);
42
+ expect(config.port).toBe(4201);
43
+ expect(config.theme).toBe("tokyo-night");
44
+ expect(config.sidebarWidth).toBe(30);
45
+ });
46
+
47
+ it("handles malformed JSON gracefully", () => {
48
+ mkdirSync(CONFIG_DIR, { recursive: true });
49
+ writeFileSync(SETTINGS_FILE, "not json {{{");
50
+
51
+ const config = loadConfig(TEST_HOME);
52
+ expect(config.port).toBeUndefined();
53
+ });
54
+
55
+ it("handles missing agentboard key", () => {
56
+ mkdirSync(CONFIG_DIR, { recursive: true });
57
+ writeFileSync(SETTINGS_FILE, JSON.stringify({ preferredEditor: "code" }));
58
+
59
+ const config = loadConfig(TEST_HOME);
60
+ expect(config.port).toBeUndefined();
61
+ });
62
+ });
63
+
64
+ describe("saveConfig", () => {
65
+ it("creates settings file with agentboard key", () => {
66
+ saveConfig({ theme: "gruvbox-dark" }, TEST_HOME);
67
+
68
+ expect(existsSync(SETTINGS_FILE)).toBe(true);
69
+ const saved = JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
70
+ expect(saved.agentboard.theme).toBe("gruvbox-dark");
71
+ });
72
+
73
+ it("merges with existing agentboard config", () => {
74
+ mkdirSync(CONFIG_DIR, { recursive: true });
75
+ writeFileSync(
76
+ SETTINGS_FILE,
77
+ JSON.stringify({
78
+ preferredEditor: "cursor",
79
+ agentboard: {
80
+ theme: "nord",
81
+ sidebarWidth: 28,
82
+ },
83
+ }),
84
+ );
85
+
86
+ saveConfig({ sidebarWidth: 32 }, TEST_HOME);
87
+
88
+ const saved = JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
89
+ expect(saved.agentboard.theme).toBe("nord");
90
+ expect(saved.agentboard.sidebarWidth).toBe(32);
91
+ expect(saved.preferredEditor).toBe("cursor");
92
+ });
93
+ });
94
+
95
+ describe("loadPreferredEditor", () => {
96
+ it("returns 'code' when no settings file exists", () => {
97
+ expect(loadPreferredEditor(TEST_HOME)).toBe("code");
98
+ });
99
+
100
+ it("reads preferredEditor from settings file", () => {
101
+ mkdirSync(CONFIG_DIR, { recursive: true });
102
+ writeFileSync(SETTINGS_FILE, JSON.stringify({ preferredEditor: "cursor" }));
103
+
104
+ expect(loadPreferredEditor(TEST_HOME)).toBe("cursor");
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,80 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ import type { PartialTheme } from "./themes";
5
+
6
+ export interface AgentboardConfig {
7
+ /** Explicit mux provider name (overrides auto-detect) */
8
+ mux?: string;
9
+ /** Custom server port */
10
+ port?: number;
11
+ /** Theme: builtin name (e.g. "catppuccin-latte") or partial inline theme object */
12
+ theme?: string | PartialTheme;
13
+ /** Sidebar column width (default 26) */
14
+ sidebarWidth?: number;
15
+ /** Sidebar position relative to the terminal window (default "left") */
16
+ sidebarPosition?: "left" | "right";
17
+ /** Tmux prefix key for sidebar toggle (default "s") */
18
+ keybinding?: string;
19
+ /** Persisted detail panel heights keyed by mux session name */
20
+ detailPanelHeights?: Record<string, number>;
21
+ }
22
+
23
+ const DEFAULTS: AgentboardConfig = {};
24
+
25
+ function settingsPath(homeDir?: string): string {
26
+ const home = homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
27
+ return join(home, ".config", "towles-tool", "towles-tool.settings.json");
28
+ }
29
+
30
+ function readSettingsFile(homeDir?: string): Record<string, unknown> {
31
+ try {
32
+ return JSON.parse(readFileSync(settingsPath(homeDir), "utf-8")) as Record<string, unknown>;
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Load agentboard config from ~/.config/towles-tool/towles-tool.settings.json
40
+ * under the "agentboard" key.
41
+ * @param homeDir — override home directory (for testing)
42
+ */
43
+ export function loadConfig(homeDir?: string): AgentboardConfig {
44
+ const settings = readSettingsFile(homeDir);
45
+ const agentboard = settings.agentboard;
46
+
47
+ if (!agentboard || typeof agentboard !== "object") {
48
+ return { ...DEFAULTS };
49
+ }
50
+
51
+ return { ...DEFAULTS, ...(agentboard as Partial<AgentboardConfig>) };
52
+ }
53
+
54
+ /**
55
+ * Save partial agentboard config updates to the main settings file.
56
+ * Merges with existing agentboard config to preserve fields.
57
+ * @param updates — partial config fields to write
58
+ * @param homeDir — override home directory (for testing)
59
+ */
60
+ export function saveConfig(updates: Partial<AgentboardConfig>, homeDir?: string): void {
61
+ const filePath = settingsPath(homeDir);
62
+ const settings = readSettingsFile(homeDir);
63
+ const existing = (settings.agentboard ?? {}) as Partial<AgentboardConfig>;
64
+ const merged = { ...existing, ...updates };
65
+
66
+ settings.agentboard = merged;
67
+
68
+ mkdirSync(dirname(filePath), { recursive: true });
69
+ writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n");
70
+ }
71
+
72
+ /**
73
+ * Load preferredEditor from the main settings file.
74
+ * @param homeDir — override home directory (for testing)
75
+ */
76
+ export function loadPreferredEditor(homeDir?: string): string {
77
+ const settings = readSettingsFile(homeDir);
78
+ const editor = settings.preferredEditor;
79
+ return typeof editor === "string" && editor.length > 0 ? editor : "code";
80
+ }
@@ -37,7 +37,7 @@ export {
37
37
  SERVER_ERR_LOG,
38
38
  INSTALL_LOG,
39
39
  } from "./debug";
40
- export { loadConfig, saveConfig } from "./config";
40
+ export { loadConfig, saveConfig, loadPreferredEditor } from "./config";
41
41
  export type { AgentboardConfig } from "./config";
42
42
  export { resolveTheme, BUILTIN_THEMES, DEFAULT_THEME } from "./themes";
43
43
  export type { Theme, ThemePalette, PartialTheme } from "./themes";
@@ -89,38 +89,6 @@ export class PluginLoader {
89
89
  return loaded;
90
90
  }
91
91
 
92
- /**
93
- * Load community plugins from npm package names.
94
- * Each package should `export default function(api: PluginAPI) { ... }`
95
- * or have a package.json "agentboard" field pointing to the entry file.
96
- *
97
- * Returns names of successfully loaded packages.
98
- */
99
- loadPackages(packageNames: string[]): string[] {
100
- const loaded: string[] = [];
101
- const api = this.createAPI();
102
-
103
- for (const pkg of packageNames) {
104
- try {
105
- const mod = require(pkg);
106
- const factory: PluginFactory | undefined =
107
- typeof mod.default === "function"
108
- ? mod.default
109
- : typeof mod === "function"
110
- ? mod
111
- : undefined;
112
- if (factory) {
113
- factory(api);
114
- loaded.push(pkg);
115
- }
116
- } catch {
117
- // Package not installed or broken — skip
118
- }
119
- }
120
-
121
- return loaded;
122
- }
123
-
124
92
  /**
125
93
  * Load a single factory from a file path.
126
94
  */
@@ -145,7 +113,7 @@ export class PluginLoader {
145
113
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
146
114
  return {
147
115
  registeredMuxProviders: this.registry.list(),
148
- configPath: join(home, ".config", "towles-tool", "agentboard", "config.json"),
116
+ configPath: join(home, ".config", "towles-tool", "towles-tool.settings.json"),
149
117
  serverPort: SERVER_PORT,
150
118
  };
151
119
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, watch } from "node:fs";
2
2
  import type { FSWatcher } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import consola from "consola";
4
5
  import type { SessionData } from "../shared";
5
6
 
6
7
  // --- Shell helper (for git commands only) ---
@@ -151,8 +152,8 @@ export function syncGitWatchers(sessions: SessionData[], broadcastFn: () => void
151
152
  try {
152
153
  const watcher = watch(headPath, () => onGitHeadChange(broadcastFn));
153
154
  gitHeadWatchers.set(dir, watcher);
154
- } catch {
155
- /* ignore */
155
+ } catch (err) {
156
+ consola.debug(`Failed to watch git HEAD at ${headPath}:`, err);
156
157
  }
157
158
  }
158
159
  }
@@ -1,18 +1,19 @@
1
- import { readFileSync, statSync, unlinkSync, writeFileSync, appendFileSync } from "node:fs";
1
+ import { readFileSync, unlinkSync, writeFileSync, appendFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import type { MuxProvider } from "../contracts/mux";
5
5
  import { isFullSidebarCapable, isBatchCapable } from "../contracts/mux";
6
6
  import type { AgentEvent } from "../contracts/agent";
7
+ import { TERMINAL_STATUSES } from "../contracts/agent";
7
8
  import type { AgentWatcher, AgentWatcherContext } from "../contracts/agent-watcher";
8
9
  import { AgentTracker, instanceKey } from "../agents/tracker";
9
10
  import { SessionOrder } from "./session-order";
10
11
  import { SessionMetadataStore } from "./metadata-store";
11
- import { loadConfig, saveConfig } from "../config";
12
+ import { loadConfig, saveConfig, loadPreferredEditor } from "../config";
12
13
  import { resolveSidebarWidthFromResizeContext, snapshotSidebarWindows } from "./sidebar-width-sync";
13
14
  import type { SidebarResizeContext, SidebarResizeSuppression } from "./sidebar-width-sync";
14
15
  import { shell, getGitInfo, syncGitWatchers, teardownGitWatchers } from "./git-info";
15
- import { refreshPortSnapshot, getSessionPorts } from "./port-scanner";
16
+ import { refreshPortSnapshot, getSessionPorts, startPortPoll } from "./port-scanner";
16
17
  import {
17
18
  SERVER_PORT,
18
19
  SERVER_HOST,
@@ -73,8 +74,9 @@ export function startServer(
73
74
  const config = loadConfig();
74
75
  let currentTheme: string | undefined =
75
76
  typeof config.theme === "string" ? config.theme : undefined;
76
- let sidebarWidth = config.sidebarWidth ?? 26;
77
+ let sidebarWidth = config.sidebarWidth ?? 35;
77
78
  let sidebarPosition: "left" | "right" = config.sidebarPosition ?? "left";
79
+ let preferredEditor = loadPreferredEditor();
78
80
  let sidebarVisible = false;
79
81
 
80
82
  log("server", "config loaded", {
@@ -196,19 +198,24 @@ export function startServer(
196
198
  if (!paneAgents || paneAgents.size === 0) return watcherAgents;
197
199
 
198
200
  const result = [...watcherAgents];
199
- // Build a set of tracked agent:threadId keys for matching
200
- const trackedByKey = new Set(watcherAgents.map((a) => instanceKey(a.agent, a.threadId)));
201
- // Also track which agent names + threadIds are covered by watchers
202
- const trackedThreadIds = new Set(
203
- watcherAgents.filter((a) => a.threadId).map((a) => `${a.agent}:${a.threadId}`),
204
- );
201
+ const trackedByKey = new Map(result.map((a, i) => [instanceKey(a.agent, a.threadId), i]));
205
202
 
206
203
  for (const [, presence] of paneAgents) {
207
- // If the pane scanner resolved a threadId, check if watcher already tracks it
208
- if (presence.threadId && trackedThreadIds.has(`${presence.agent}:${presence.threadId}`))
204
+ const key = instanceKey(presence.agent, presence.threadId);
205
+ const trackedIdx = trackedByKey.get(key);
206
+
207
+ if (trackedIdx != null) {
208
+ // Watcher already tracks this agent — correct terminal statuses
209
+ // using pane liveness (process is confirmed alive, so terminal
210
+ // journal status is a between-turn artifact).
211
+ const tracked = result[trackedIdx]!;
212
+ if (TERMINAL_STATUSES.has(tracked.status)) {
213
+ tracked.status = "running";
214
+ tracked.paneId = presence.paneId;
215
+ }
209
216
  continue;
210
- // Check by instanceKey as well
211
- if (trackedByKey.has(instanceKey(presence.agent, presence.threadId))) continue;
217
+ }
218
+
212
219
  // If we have no threadId from pane scan and watcher tracks any instance of this agent, skip
213
220
  if (!presence.threadId && watcherAgents.some((a) => a.agent === presence.agent)) continue;
214
221
 
@@ -319,6 +326,7 @@ export function startServer(
319
326
  currentSession,
320
327
  theme: currentTheme,
321
328
  sidebarWidth,
329
+ preferredEditor,
322
330
  ts: Date.now(),
323
331
  };
324
332
  }
@@ -1128,15 +1136,6 @@ export function startServer(
1128
1136
  }
1129
1137
  }
1130
1138
 
1131
- // If status is "running" but journal hasn't been written to recently,
1132
- // the Claude process likely exited — downgrade to "idle".
1133
- if (lastStatus === "running") {
1134
- try {
1135
- const mtime = statSync(filePath).mtimeMs;
1136
- if (Date.now() - mtime > 10_000) lastStatus = "idle";
1137
- } catch {}
1138
- }
1139
-
1140
1139
  return { threadName, status: lastStatus };
1141
1140
  } catch {
1142
1141
  continue;
@@ -1445,21 +1444,8 @@ export function startServer(
1445
1444
 
1446
1445
  // --- Port polling (detect new/stopped listeners every 10s) ---
1447
1446
 
1448
- const PORT_POLL_INTERVAL_MS = 10_000;
1449
1447
  let portPollTimer: ReturnType<typeof setInterval> | null = null;
1450
1448
 
1451
- function startPortPoll() {
1452
- // Run initial snapshot immediately so first broadcast has ports
1453
- if (lastState) {
1454
- refreshPortSnapshot(lastState.sessions.map((s) => s.name));
1455
- }
1456
- portPollTimer = setInterval(() => {
1457
- if (!lastState || clientCount === 0) return;
1458
- const changed = refreshPortSnapshot(lastState.sessions.map((s) => s.name));
1459
- if (changed) broadcastState();
1460
- }, PORT_POLL_INTERVAL_MS);
1461
- }
1462
-
1463
1449
  function cleanup() {
1464
1450
  for (const w of allWatchers) w.stop();
1465
1451
  if (watcherBroadcastTimer) clearTimeout(watcherBroadcastTimer);
@@ -1728,7 +1714,7 @@ export function startServer(
1728
1714
  refreshPortSnapshot(allMuxSessions);
1729
1715
  }
1730
1716
  broadcastState();
1731
- startPortPoll();
1717
+ portPollTimer = startPortPoll({ lastState, clientCount, broadcastState });
1732
1718
  startPaneScan();
1733
1719
  // Run initial pane scan
1734
1720
  refreshPaneAgents();
@@ -1,5 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
2
  import { connect } from "node:net";
4
3
  import { SERVER_PORT, SERVER_HOST, PID_FILE } from "../shared";
5
4
  import { SERVER_ERR_LOG } from "../debug";
@@ -32,31 +31,20 @@ async function isPortOpen(host: string, port: number, timeoutMs = 200): Promise<
32
31
  });
33
32
  }
34
33
 
35
- function resolveAgentboardDir(): string {
36
- if (process.env.TT_AGENTBOARD_DIR) return process.env.TT_AGENTBOARD_DIR;
37
- // Walk up from packages/runtime/src/server/ to the plugin root
38
- return new URL("../../../..", import.meta.url).pathname;
39
- }
40
-
41
- function resolveServerEntryPath(pluginDir: string): string {
42
- return join(pluginDir, "apps", "server", "src", "main.ts");
43
- }
44
-
45
34
  export async function ensureServer(): Promise<void> {
46
35
  if (existsSync(PID_FILE)) {
47
36
  const pid = Number.parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
48
37
  if (!Number.isNaN(pid) && isProcessAlive(pid) && (await isPortOpen(SERVER_HOST, SERVER_PORT))) {
49
38
  return;
50
39
  }
40
+ // Stale PID file — remove before spawning a new server
41
+ try {
42
+ unlinkSync(PID_FILE);
43
+ } catch {}
51
44
  }
52
45
 
53
- const pluginDir = resolveAgentboardDir();
54
- const serverPath = resolveServerEntryPath(pluginDir);
55
-
56
- const proc = Bun.spawn([process.execPath, "run", serverPath], {
46
+ const proc = Bun.spawn(["tt", "agentboard", "server"], {
57
47
  stdio: ["ignore", "ignore", Bun.file(SERVER_ERR_LOG)],
58
- cwd: pluginDir,
59
- env: { ...process.env, TT_AGENTBOARD_DIR: pluginDir },
60
48
  });
61
49
  proc.unref();
62
50