@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.
- package/package.json +9 -4
- package/{plugins/tt-agentboard → packages/agentboard}/README.md +1 -1
- package/{plugins/tt-agentboard → packages/agentboard}/apps/server/package.json +2 -1
- package/{plugins/tt-agentboard → packages/agentboard}/apps/server/src/main.ts +6 -20
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/package.json +4 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DetailPanel.tsx +3 -2
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/StatusBar.tsx +35 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/constants.ts +1 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/index.tsx +206 -226
- package/packages/agentboard/apps/tui/src/session-status.test.ts +70 -0
- package/packages/agentboard/apps/tui/src/session-status.ts +19 -0
- package/{plugins/tt-agentboard → packages/agentboard}/package.json +2 -6
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/package.json +3 -0
- package/{plugins/tt-agentboard/packages/runtime/test → packages/agentboard/packages/runtime/src/agents}/tracker.test.ts +2 -2
- package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.test.ts +63 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/claude-code.ts +26 -2
- package/packages/agentboard/packages/runtime/src/config.test.ts +107 -0
- package/packages/agentboard/packages/runtime/src/config.ts +80 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/index.ts +1 -1
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/plugins/loader.ts +1 -33
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/git-info.ts +3 -2
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/index.ts +23 -37
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/launcher.ts +6 -18
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/pane-scanner.ts +6 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/shared.ts +7 -2
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/tsconfig.json +1 -1
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/git/exec.ts +41 -0
- package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
- package/packages/shared/src/index.ts +8 -0
- package/packages/shared/tsconfig.json +16 -0
- package/src/cli.ts +1 -1
- package/src/commands/agentboard.ts +51 -67
- package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
- package/src/commands/auto-claude/config-init-helpers.ts +79 -0
- package/src/commands/auto-claude/config-init.test.ts +137 -0
- package/src/commands/auto-claude/config-init.ts +159 -0
- package/src/{lib → commands}/auto-claude/config.ts +4 -8
- package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
- package/src/commands/auto-claude/explain.test.ts +58 -0
- package/src/commands/auto-claude/explain.ts +97 -0
- package/src/commands/auto-claude/index.ts +37 -14
- package/src/{lib → commands}/auto-claude/labels.ts +1 -1
- package/src/commands/auto-claude/list.ts +5 -4
- package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
- package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
- package/src/commands/auto-claude/retry.test.ts +2 -2
- package/src/commands/auto-claude/retry.ts +5 -5
- package/src/commands/auto-claude/shell.ts +3 -0
- package/src/commands/auto-claude/status.test.ts +2 -2
- package/src/commands/auto-claude/status.ts +4 -4
- package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
- package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
- package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
- package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
- package/src/{lib → commands}/auto-claude/utils.ts +10 -4
- package/src/{lib/install → commands}/claude-settings.ts +1 -1
- package/src/commands/config/config.test.ts +129 -0
- package/src/commands/config/index.ts +11 -0
- package/src/commands/config/reset.ts +53 -0
- package/src/commands/config/schema.ts +19 -0
- package/src/commands/{config.ts → config/show.ts} +2 -2
- package/src/commands/config/validate.ts +51 -0
- package/src/commands/doctor/checks.ts +167 -0
- package/src/commands/doctor/format.test.ts +63 -0
- package/src/commands/doctor/format.ts +5 -0
- package/src/commands/doctor/history.test.ts +161 -0
- package/src/commands/doctor/history.ts +130 -0
- package/src/commands/doctor.ts +80 -151
- package/src/commands/gh/branch-clean.ts +4 -4
- package/src/commands/gh/branch.test.ts +4 -5
- package/src/commands/gh/branch.ts +10 -5
- package/src/commands/gh/pr.ts +6 -7
- package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
- package/src/commands/graph/format.test.ts +130 -0
- package/src/commands/graph/format.ts +94 -0
- package/src/commands/graph/index.ts +69 -41
- package/src/{lib → commands}/graph/labels.ts +4 -4
- package/src/{lib → commands}/graph/server.ts +2 -2
- package/src/{lib → commands}/graph/types.ts +2 -0
- package/src/commands/graph.test.ts +1 -1
- package/src/commands/install.ts +6 -6
- package/src/commands/journal/daily-notes.ts +4 -7
- package/src/{lib → commands}/journal/fs.ts +1 -1
- package/src/commands/journal/index.ts +2 -0
- package/src/commands/journal/list.test.ts +174 -0
- package/src/commands/journal/list.ts +213 -0
- package/src/commands/journal/meeting.ts +4 -7
- package/src/commands/journal/note.ts +4 -7
- package/src/{lib → commands}/journal/paths.ts +1 -1
- package/src/commands/journal/search.test.ts +156 -0
- package/src/commands/journal/search.ts +256 -0
- package/src/{lib → commands}/journal/templates.ts +1 -1
- package/src/config/settings.ts +35 -26
- package/plugins/tt-agentboard/bun.lock +0 -444
- package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
- package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
- package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
- package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
- package/plugins/tt-auto-claude/commands/list.md +0 -21
- package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
- package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
- package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
- package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
- package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
- package/src/commands/config.test.ts +0 -9
- package/src/lib/auto-claude/index.ts +0 -15
- package/src/lib/auto-claude/shell.ts +0 -6
- package/src/lib/graph/index.ts +0 -24
- package/src/lib/journal/index.ts +0 -11
- package/src/utils/git/exec.ts +0 -18
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.json +0 -0
- /package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.json +0 -0
- /package/{plugins/tt-core → packages/core}/README.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/improve-architecture.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/interview-me.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/prd-to-issues.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/refine-text.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/task.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/tdd.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/write-prd.md +0 -0
- /package/{plugins/tt-core → packages/core}/skills/towles-tool/SKILL.md +0 -0
- /package/{src/utils → packages/shared/src}/date-utils.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/date-utils.ts +0 -0
- /package/{src/utils → packages/shared/src}/fs.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/branch-name.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/branch-name.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/render.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/render.ts +0 -0
- /package/src/{lib → commands}/auto-claude/config.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/labels.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/pipeline.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/01_plan.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/02_implement.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/03_simplify.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/04_review.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/CLAUDE.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/index.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/index.ts +0 -0
- /package/src/{lib → commands}/auto-claude/run-claude.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/spawn-claude.ts +0 -0
- /package/src/{lib → commands}/auto-claude/steps/simple-steps.ts +0 -0
- /package/src/{lib → commands}/auto-claude/steps/steps.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/stream-parser.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/stream-parser.ts +0 -0
- /package/src/{lib → commands}/auto-claude/templates.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/templates.ts +0 -0
- /package/src/{lib → commands}/auto-claude/test-helpers.ts +0 -0
- /package/src/{lib → commands}/auto-claude/utils.test.ts +0 -0
- /package/src/{lib → commands}/graph/analyzer.ts +0 -0
- /package/src/{lib → commands}/graph/graph-template.html +0 -0
- /package/src/{lib → commands}/graph/parser.test.ts +0 -0
- /package/src/{lib → commands}/graph/parser.ts +0 -0
- /package/src/{lib → commands}/graph/render.ts +0 -0
- /package/src/{lib → commands}/graph/sessions.ts +0 -0
- /package/src/{lib → commands}/graph/tools.ts +0 -0
- /package/src/{lib → commands}/graph/treemap.ts +0 -0
- /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/
|
|
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:*",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
-
import { AgentTracker, instanceKey } from "
|
|
3
|
-
import type { AgentEvent } from "../
|
|
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)
|
|
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 >
|
|
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";
|
package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/plugins/loader.ts
RENAMED
|
@@ -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", "
|
|
116
|
+
configPath: join(home, ".config", "towles-tool", "towles-tool.settings.json"),
|
|
149
117
|
serverPort: SERVER_PORT,
|
|
150
118
|
};
|
|
151
119
|
}
|
package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/git-info.ts
RENAMED
|
@@ -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
|
-
|
|
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,
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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();
|
package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/launcher.ts
RENAMED
|
@@ -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
|
|
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
|
|