@towles/tool 0.0.107 → 0.0.108

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 (76) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/plugins/tt-agentboard/README.md +160 -0
  4. package/plugins/tt-agentboard/apps/server/package.json +20 -0
  5. package/plugins/tt-agentboard/apps/server/src/main.ts +60 -0
  6. package/plugins/tt-agentboard/apps/tui/build.ts +11 -0
  7. package/plugins/tt-agentboard/apps/tui/bunfig.toml +1 -0
  8. package/plugins/tt-agentboard/apps/tui/package.json +23 -0
  9. package/plugins/tt-agentboard/apps/tui/scripts/sessionizer.sh +36 -0
  10. package/plugins/tt-agentboard/apps/tui/src/components/DetailPanel.tsx +350 -0
  11. package/plugins/tt-agentboard/apps/tui/src/components/DiffStats.tsx +33 -0
  12. package/plugins/tt-agentboard/apps/tui/src/components/SessionCard.tsx +177 -0
  13. package/plugins/tt-agentboard/apps/tui/src/components/StatusBar.tsx +49 -0
  14. package/plugins/tt-agentboard/apps/tui/src/constants.ts +46 -0
  15. package/plugins/tt-agentboard/apps/tui/src/detail-panel-height.ts +21 -0
  16. package/plugins/tt-agentboard/apps/tui/src/index.tsx +880 -0
  17. package/plugins/tt-agentboard/apps/tui/src/mux-context.ts +61 -0
  18. package/plugins/tt-agentboard/apps/tui/tsconfig.json +15 -0
  19. package/plugins/tt-agentboard/bun.lock +444 -0
  20. package/plugins/tt-agentboard/package.json +26 -0
  21. package/plugins/tt-agentboard/packages/mux-tmux/package.json +14 -0
  22. package/plugins/tt-agentboard/packages/mux-tmux/src/client.ts +550 -0
  23. package/plugins/tt-agentboard/packages/mux-tmux/src/index.ts +18 -0
  24. package/plugins/tt-agentboard/packages/mux-tmux/src/provider.ts +259 -0
  25. package/plugins/tt-agentboard/packages/mux-tmux/tsconfig.json +13 -0
  26. package/plugins/tt-agentboard/packages/runtime/package.json +14 -0
  27. package/plugins/tt-agentboard/packages/runtime/src/agents/tracker.ts +233 -0
  28. package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/amp.ts +316 -0
  29. package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/claude-code.ts +374 -0
  30. package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/codex.ts +364 -0
  31. package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/opencode.ts +249 -0
  32. package/plugins/tt-agentboard/packages/runtime/src/config.ts +70 -0
  33. package/plugins/tt-agentboard/packages/runtime/src/contracts/agent-watcher.ts +38 -0
  34. package/plugins/tt-agentboard/packages/runtime/src/contracts/agent.ts +16 -0
  35. package/plugins/tt-agentboard/packages/runtime/src/contracts/index.ts +3 -0
  36. package/plugins/tt-agentboard/packages/runtime/src/contracts/mux.ts +148 -0
  37. package/plugins/tt-agentboard/packages/runtime/src/debug.ts +19 -0
  38. package/plugins/tt-agentboard/packages/runtime/src/index.ts +69 -0
  39. package/plugins/tt-agentboard/packages/runtime/src/mux/detect.ts +20 -0
  40. package/plugins/tt-agentboard/packages/runtime/src/mux/registry.ts +45 -0
  41. package/plugins/tt-agentboard/packages/runtime/src/plugins/loader.ts +152 -0
  42. package/plugins/tt-agentboard/packages/runtime/src/server/context.ts +112 -0
  43. package/plugins/tt-agentboard/packages/runtime/src/server/git-info.ts +164 -0
  44. package/plugins/tt-agentboard/packages/runtime/src/server/index.ts +1753 -0
  45. package/plugins/tt-agentboard/packages/runtime/src/server/launcher.ts +71 -0
  46. package/plugins/tt-agentboard/packages/runtime/src/server/metadata-store.ts +86 -0
  47. package/plugins/tt-agentboard/packages/runtime/src/server/pane-scanner.ts +327 -0
  48. package/plugins/tt-agentboard/packages/runtime/src/server/port-scanner.ts +155 -0
  49. package/plugins/tt-agentboard/packages/runtime/src/server/session-order.ts +127 -0
  50. package/plugins/tt-agentboard/packages/runtime/src/server/sidebar-manager.ts +232 -0
  51. package/plugins/tt-agentboard/packages/runtime/src/server/sidebar-width-sync.ts +66 -0
  52. package/plugins/tt-agentboard/packages/runtime/src/shared.ts +179 -0
  53. package/plugins/tt-agentboard/packages/runtime/src/themes.ts +750 -0
  54. package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +83 -0
  55. package/plugins/tt-agentboard/packages/runtime/test/tracker.test.ts +172 -0
  56. package/plugins/tt-agentboard/packages/runtime/tsconfig.json +13 -0
  57. package/plugins/tt-agentboard/tsconfig.json +19 -0
  58. package/plugins/tt-auto-claude/.claude-plugin/plugin.json +8 -0
  59. package/plugins/tt-auto-claude/commands/create-issue.md +20 -0
  60. package/plugins/tt-auto-claude/commands/list.md +21 -0
  61. package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +71 -0
  62. package/plugins/tt-core/.claude-plugin/plugin.json +8 -0
  63. package/plugins/tt-core/README.md +18 -0
  64. package/plugins/tt-core/commands/improve-architecture.md +66 -0
  65. package/plugins/tt-core/commands/interview-me.md +38 -0
  66. package/plugins/tt-core/commands/prd-to-issues.md +49 -0
  67. package/plugins/tt-core/commands/refine-text.md +30 -0
  68. package/plugins/tt-core/commands/task.md +37 -0
  69. package/plugins/tt-core/commands/tdd.md +69 -0
  70. package/plugins/tt-core/commands/write-prd.md +69 -0
  71. package/plugins/tt-core/promptfooconfig.interview-me.yaml +155 -0
  72. package/plugins/tt-core/promptfooconfig.refine-text.yaml +242 -0
  73. package/plugins/tt-core/promptfooconfig.tdd.yaml +144 -0
  74. package/plugins/tt-core/promptfooconfig.write-prd.yaml +145 -0
  75. package/plugins/tt-core/skills/towles-tool/SKILL.md +35 -0
  76. package/src/commands/agentboard.ts +19 -2
@@ -0,0 +1,127 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ interface PersistedSessionOrder {
5
+ order?: unknown;
6
+ hidden?: unknown;
7
+ }
8
+
9
+ /**
10
+ * Maintains custom session ordering for reorder-session commands.
11
+ * Stores an ordered list of session names. The `apply` method takes
12
+ * the natural session list and returns it sorted by the custom order.
13
+ *
14
+ * When a `persistPath` is provided, the order is loaded from disk on
15
+ * construction and saved after every `reorder()` call.
16
+ */
17
+ export class SessionOrder {
18
+ private order: string[] = [];
19
+ private hidden = new Set<string>();
20
+ private readonly persistPath: string | null;
21
+
22
+ constructor(persistPath?: string) {
23
+ this.persistPath = persistPath ?? null;
24
+ if (this.persistPath) {
25
+ try {
26
+ if (existsSync(this.persistPath)) {
27
+ const raw = readFileSync(this.persistPath, "utf-8");
28
+ const parsed = JSON.parse(raw);
29
+ if (Array.isArray(parsed)) {
30
+ this.order = parsed.filter((n): n is string => typeof n === "string");
31
+ } else if (parsed && typeof parsed === "object") {
32
+ const persisted = parsed as PersistedSessionOrder;
33
+ if (Array.isArray(persisted.order)) {
34
+ this.order = persisted.order.filter((n): n is string => typeof n === "string");
35
+ }
36
+ if (Array.isArray(persisted.hidden)) {
37
+ this.hidden = new Set(
38
+ persisted.hidden.filter((n): n is string => typeof n === "string"),
39
+ );
40
+ }
41
+ }
42
+ }
43
+ } catch {
44
+ // Ignore corrupt file — start fresh
45
+ }
46
+ }
47
+ }
48
+
49
+ /** Sync with current session names — adds new ones alphabetically, removes stale ones. */
50
+ sync(names: string[]): void {
51
+ const nameSet = new Set(names);
52
+ // Remove sessions that no longer exist
53
+ this.order = this.order.filter((n) => nameSet.has(n));
54
+ this.hidden = new Set([...this.hidden].filter((n) => nameSet.has(n)));
55
+ // Add new sessions in sorted position
56
+ const newNames = names
57
+ .filter((n) => !this.order.includes(n))
58
+ .sort((a, b) => a.localeCompare(b));
59
+ for (const n of newNames) {
60
+ // Insert alphabetically among existing entries
61
+ const idx = this.order.findIndex((existing) => existing.localeCompare(n) > 0);
62
+ if (idx === -1) {
63
+ this.order.push(n);
64
+ } else {
65
+ this.order.splice(idx, 0, n);
66
+ }
67
+ }
68
+ }
69
+
70
+ /** Move a session by delta (-1 = up, 1 = down). */
71
+ reorder(name: string, delta: -1 | 1): void {
72
+ const idx = this.order.indexOf(name);
73
+ if (idx === -1) return;
74
+ const newIdx = idx + delta;
75
+ if (newIdx < 0 || newIdx >= this.order.length) return;
76
+ // Swap
77
+ [this.order[idx], this.order[newIdx]] = [this.order[newIdx]!, this.order[idx]!];
78
+ this.save();
79
+ }
80
+
81
+ /** Hide a session from the panel without touching the underlying mux session. */
82
+ hide(name: string): void {
83
+ if (!this.order.includes(name) || this.hidden.has(name)) return;
84
+ this.hidden.add(name);
85
+ this.save();
86
+ }
87
+
88
+ /** Make a previously hidden session visible again. */
89
+ show(name: string): void {
90
+ if (!this.hidden.delete(name)) return;
91
+ if (!this.order.includes(name)) {
92
+ this.order.push(name);
93
+ }
94
+ this.save();
95
+ }
96
+
97
+ /** Restore all hidden sessions back into the panel. */
98
+ showAll(): void {
99
+ if (this.hidden.size === 0) return;
100
+ this.hidden.clear();
101
+ this.save();
102
+ }
103
+
104
+ /** Apply the custom order to a list of session names. Returns sorted names. */
105
+ apply(names: string[]): string[] {
106
+ const posMap = new Map(this.order.map((n, i) => [n, i]));
107
+ return names
108
+ .filter((n) => !this.hidden.has(n))
109
+ .sort((a, b) => {
110
+ const pa = posMap.get(a) ?? Infinity;
111
+ const pb = posMap.get(b) ?? Infinity;
112
+ return pa - pb;
113
+ });
114
+ }
115
+
116
+ private save(): void {
117
+ if (!this.persistPath) return;
118
+ try {
119
+ mkdirSync(dirname(this.persistPath), { recursive: true });
120
+ const serialized =
121
+ this.hidden.size === 0 ? this.order : { order: this.order, hidden: [...this.hidden] };
122
+ writeFileSync(this.persistPath, JSON.stringify(serialized) + "\n");
123
+ } catch {
124
+ // Best-effort — don't crash if write fails
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,232 @@
1
+ import { debugLog } from "../debug";
2
+ import { saveConfig } from "../config";
3
+ import { resolveSidebarWidthFromResizeContext, snapshotSidebarWindows } from "./sidebar-width-sync";
4
+ import type { SidebarResizeContext } from "./sidebar-width-sync";
5
+ import type { ServerContext } from "./context";
6
+
7
+ const SIDEBAR_PANE_CACHE_TTL = 300; // ms
8
+
9
+ export function listSidebarPanesByProviderUncached(ctx: ServerContext) {
10
+ return ctx.getProvidersWithSidebar().map((provider) => ({
11
+ provider,
12
+ panes: provider.listSidebarPanes(),
13
+ }));
14
+ }
15
+
16
+ export function listSidebarPanesByProvider(ctx: ServerContext) {
17
+ const now = Date.now();
18
+ if (ctx.sidebarPaneCache && now - ctx.sidebarPaneCacheTs < SIDEBAR_PANE_CACHE_TTL)
19
+ return ctx.sidebarPaneCache;
20
+ ctx.sidebarPaneCache = listSidebarPanesByProviderUncached(ctx);
21
+ ctx.sidebarPaneCacheTs = now;
22
+ return ctx.sidebarPaneCache;
23
+ }
24
+
25
+ export function invalidateSidebarPaneCache(ctx: ServerContext): void {
26
+ ctx.sidebarPaneCache = null;
27
+ ctx.sidebarPaneCacheTs = 0;
28
+ }
29
+
30
+ export function scheduleSidebarResize(ctx: ServerContext, resizeCtx?: SidebarResizeContext): void {
31
+ resizeSidebars(ctx, resizeCtx);
32
+ if (ctx.pendingSidebarResize) clearTimeout(ctx.pendingSidebarResize);
33
+ ctx.pendingSidebarResize = setTimeout(() => {
34
+ ctx.pendingSidebarResize = null;
35
+ resizeSidebars(ctx);
36
+ }, 120);
37
+ }
38
+
39
+ export function toggleSidebar(
40
+ ctx: ServerContext,
41
+ toggleCtx?: { session: string; windowId: string },
42
+ ): void {
43
+ const providers = ctx.getProvidersWithSidebar();
44
+ if (providers.length === 0) {
45
+ debugLog("toggle", "SKIP — no providers with sidebar methods");
46
+ return;
47
+ }
48
+
49
+ invalidateSidebarPaneCache(ctx);
50
+ if (ctx.sidebarVisible) {
51
+ for (const p of providers) {
52
+ const panes = p.listSidebarPanes();
53
+ debugLog("toggle", "OFF — hiding panes", { provider: p.name, count: panes.length });
54
+ for (const pane of panes) {
55
+ p.hideSidebar(pane.paneId);
56
+ }
57
+ }
58
+ ctx.sidebarVisible = false;
59
+ } else {
60
+ ctx.sidebarVisible = true;
61
+ for (const p of providers) {
62
+ const allWindows = p.listActiveWindows();
63
+ debugLog("toggle", "ON — spawning in active windows", {
64
+ provider: p.name,
65
+ count: allWindows.length,
66
+ });
67
+ for (const w of allWindows) {
68
+ ensureSidebarInWindow(ctx, p, { session: w.sessionName, windowId: w.id });
69
+ }
70
+ }
71
+ scheduleSidebarResize(ctx);
72
+ ctx.server.publish("sidebar", JSON.stringify({ type: "re-identify" }));
73
+ }
74
+ debugLog("toggle", "done", { sidebarVisible: ctx.sidebarVisible });
75
+ }
76
+
77
+ export function ensureSidebarInWindow(
78
+ ctx: ServerContext,
79
+ provider?: ReturnType<ServerContext["getProvidersWithSidebar"]>[number],
80
+ windowCtx?: { session: string; windowId: string },
81
+ ): void {
82
+ const p =
83
+ provider ??
84
+ (() => {
85
+ const providers = ctx.getProvidersWithSidebar();
86
+ if (windowCtx?.session) {
87
+ const sessionProvider = ctx.sessionProviders.get(windowCtx.session);
88
+ return providers.find((pp) => pp === sessionProvider) ?? providers[0];
89
+ }
90
+ return providers[0];
91
+ })();
92
+ if (!p || !ctx.sidebarVisible) {
93
+ debugLog("ensure", "SKIP", { hasProvider: !!p, sidebarVisible: ctx.sidebarVisible });
94
+ return;
95
+ }
96
+
97
+ const curSession = windowCtx?.session ?? ctx.getCurrentSession();
98
+ if (!curSession) {
99
+ debugLog("ensure", "SKIP — no current session");
100
+ return;
101
+ }
102
+
103
+ const windowId = windowCtx?.windowId ?? p.getCurrentWindowId();
104
+ if (!windowId) {
105
+ debugLog("ensure", "SKIP — could not get window_id");
106
+ return;
107
+ }
108
+
109
+ const spawnKey = `${p.name}:${windowId}`;
110
+ if (ctx.pendingSidebarSpawns.has(spawnKey)) {
111
+ debugLog("ensure", "SKIP — spawn already in progress", {
112
+ curSession,
113
+ windowId,
114
+ provider: p.name,
115
+ });
116
+ return;
117
+ }
118
+
119
+ const allPanesByProvider = listSidebarPanesByProvider(ctx);
120
+ const providerEntry = allPanesByProvider.find((e) => e.provider === p);
121
+ const existingPanes = providerEntry?.panes ?? [];
122
+ const hasInWindow = existingPanes.some((ep) => ep.windowId === windowId);
123
+ debugLog("ensure", "checking window", {
124
+ curSession,
125
+ windowId,
126
+ existingPanes: existingPanes.length,
127
+ hasInWindow,
128
+ paneIds: existingPanes.map((x) => `${x.paneId}@${x.windowId}`),
129
+ });
130
+
131
+ if (!hasInWindow) {
132
+ invalidateSidebarPaneCache(ctx);
133
+ ctx.pendingSidebarSpawns.add(spawnKey);
134
+ debugLog("ensure", "SPAWNING sidebar", {
135
+ curSession,
136
+ windowId,
137
+ sidebarWidth: ctx.sidebarWidth,
138
+ sidebarPosition: ctx.sidebarPosition,
139
+ });
140
+ try {
141
+ const newPaneId = p.spawnSidebar(curSession, windowId, ctx.sidebarWidth, ctx.sidebarPosition);
142
+ debugLog("ensure", "spawn result", { newPaneId });
143
+ } finally {
144
+ ctx.pendingSidebarSpawns.delete(spawnKey);
145
+ }
146
+ scheduleSidebarResize(ctx);
147
+ }
148
+ }
149
+
150
+ export function debouncedEnsureSidebar(
151
+ ctx: ServerContext,
152
+ windowCtx?: { session: string; windowId: string },
153
+ ): void {
154
+ if (windowCtx) ctx.ensureSidebarPendingCtx = windowCtx;
155
+ if (ctx.ensureSidebarTimer) clearTimeout(ctx.ensureSidebarTimer);
156
+ ctx.ensureSidebarTimer = setTimeout(() => {
157
+ ctx.ensureSidebarTimer = null;
158
+ const nextCtx = ctx.ensureSidebarPendingCtx;
159
+ ctx.ensureSidebarPendingCtx = undefined;
160
+ ensureSidebarInWindow(ctx, undefined, nextCtx);
161
+ }, 150);
162
+ }
163
+
164
+ export function quitAll(ctx: ServerContext): void {
165
+ debugLog("quit", "killing all sidebar panes");
166
+ for (const p of ctx.getProvidersWithSidebar()) {
167
+ const panes = p.listSidebarPanes();
168
+ debugLog("quit", "found panes to kill", { provider: p.name, count: panes.length });
169
+ for (const pane of panes) {
170
+ p.killSidebarPane(pane.paneId);
171
+ }
172
+ }
173
+ for (const p of ctx.getProvidersWithSidebar()) {
174
+ p.cleanupSidebar();
175
+ }
176
+ ctx.server.publish("sidebar", JSON.stringify({ type: "quit" }));
177
+ ctx.sidebarVisible = false;
178
+ ctx.cleanup();
179
+ process.exit(0);
180
+ }
181
+
182
+ export function resizeSidebars(ctx: ServerContext, resizeCtx?: SidebarResizeContext): void {
183
+ const panesByProvider = listSidebarPanesByProvider(ctx);
184
+ const allPanes = panesByProvider.flatMap(({ panes }) => panes);
185
+
186
+ if (allPanes.length === 0) {
187
+ ctx.sidebarSnapshots = new Map();
188
+ return;
189
+ }
190
+
191
+ const nextSidebarWidth = resolveSidebarWidthFromResizeContext({
192
+ ctx: resizeCtx,
193
+ panes: allPanes,
194
+ previousByWindow: ctx.sidebarSnapshots,
195
+ suppressedByPane: ctx.suppressedSidebarResizeAcks,
196
+ });
197
+
198
+ if (nextSidebarWidth != null && nextSidebarWidth !== ctx.sidebarWidth) {
199
+ ctx.sidebarWidth = nextSidebarWidth;
200
+ saveConfig({ sidebarWidth: ctx.sidebarWidth });
201
+ debugLog("resize", "adopted sidebar width from pane resize", {
202
+ paneId: resizeCtx?.paneId ?? null,
203
+ sessionName: resizeCtx?.sessionName ?? null,
204
+ windowId: resizeCtx?.windowId ?? null,
205
+ sidebarWidth: ctx.sidebarWidth,
206
+ });
207
+ ctx.broadcastState();
208
+ }
209
+
210
+ const now = Date.now();
211
+ for (const { provider, panes } of panesByProvider) {
212
+ debugLog("resize", "enforcing width on all panes", {
213
+ provider: provider.name,
214
+ sidebarWidth: ctx.sidebarWidth,
215
+ count: panes.length,
216
+ triggerPaneId: resizeCtx?.paneId ?? null,
217
+ });
218
+ for (const pane of panes) {
219
+ if (pane.width === ctx.sidebarWidth) continue;
220
+ ctx.suppressedSidebarResizeAcks.set(pane.paneId, {
221
+ width: ctx.sidebarWidth,
222
+ expiresAt: now + 1_000,
223
+ });
224
+ provider.resizeSidebarPane(pane.paneId, ctx.sidebarWidth);
225
+ }
226
+ }
227
+
228
+ if (panesByProvider.some(({ panes }) => panes.some((pane) => pane.width !== ctx.sidebarWidth))) {
229
+ invalidateSidebarPaneCache(ctx);
230
+ }
231
+ ctx.sidebarSnapshots = snapshotSidebarWindows(allPanes);
232
+ }
@@ -0,0 +1,66 @@
1
+ import type { SidebarPane } from "../contracts/mux";
2
+
3
+ export interface SidebarResizeContext {
4
+ paneId?: string;
5
+ sessionName?: string;
6
+ windowId?: string;
7
+ width?: number;
8
+ windowWidth?: number;
9
+ }
10
+
11
+ export interface SidebarWindowSnapshot {
12
+ width?: number;
13
+ windowWidth?: number;
14
+ }
15
+
16
+ export interface SidebarResizeSuppression {
17
+ width: number;
18
+ expiresAt: number;
19
+ }
20
+
21
+ export function snapshotSidebarWindows(panes: SidebarPane[]): Map<string, SidebarWindowSnapshot> {
22
+ const snapshots = new Map<string, SidebarWindowSnapshot>();
23
+ for (const pane of panes) {
24
+ snapshots.set(pane.windowId, {
25
+ width: pane.width,
26
+ windowWidth: pane.windowWidth,
27
+ });
28
+ }
29
+ return snapshots;
30
+ }
31
+
32
+ export function resolveSidebarWidthFromResizeContext(params: {
33
+ ctx?: SidebarResizeContext;
34
+ panes: SidebarPane[];
35
+ previousByWindow: Map<string, SidebarWindowSnapshot>;
36
+ suppressedByPane: Map<string, SidebarResizeSuppression>;
37
+ now?: number;
38
+ }): number | null {
39
+ const { ctx, panes, previousByWindow, suppressedByPane, now = Date.now() } = params;
40
+ if (!ctx?.paneId) return null;
41
+
42
+ const pane = panes.find((candidate) => candidate.paneId === ctx.paneId);
43
+ if (!pane) return null;
44
+
45
+ const width = ctx.width ?? pane.width;
46
+ const windowWidth = ctx.windowWidth ?? pane.windowWidth;
47
+ if (width == null || windowWidth == null) return null;
48
+
49
+ const suppressed = suppressedByPane.get(pane.paneId);
50
+ if (suppressed) {
51
+ if (suppressed.width === width && suppressed.expiresAt >= now) {
52
+ suppressedByPane.delete(pane.paneId);
53
+ return null;
54
+ }
55
+ if (suppressed.expiresAt < now || suppressed.width !== width) {
56
+ suppressedByPane.delete(pane.paneId);
57
+ }
58
+ }
59
+
60
+ const previous = previousByWindow.get(pane.windowId);
61
+ if (!previous || previous.width == null || previous.windowWidth == null) return null;
62
+ if (previous.windowWidth !== windowWidth) return null;
63
+ if (previous.width === width) return null;
64
+
65
+ return width;
66
+ }
@@ -0,0 +1,179 @@
1
+ import type { AgentStatus, AgentEvent } from "./contracts/agent";
2
+
3
+ export const SERVER_PORT = 4201;
4
+ export const SERVER_HOST = "127.0.0.1";
5
+ export const PID_FILE = "/tmp/agentboard.pid";
6
+ export const SERVER_IDLE_TIMEOUT_MS = 30_000;
7
+ export const STUCK_RUNNING_TIMEOUT_MS = 3 * 60 * 1000;
8
+
9
+ export interface SessionData {
10
+ name: string;
11
+ createdAt: number;
12
+ dir: string;
13
+ branch: string;
14
+ dirty: boolean;
15
+ isWorktree: boolean;
16
+ filesChanged: number;
17
+ linesAdded: number;
18
+ linesRemoved: number;
19
+ commitsDelta: number;
20
+ unseen: boolean;
21
+ panes: number;
22
+ ports: number[];
23
+ windows: number;
24
+ uptime: string;
25
+ agentState: AgentEvent | null;
26
+ agents: AgentEvent[];
27
+ eventTimestamps: number[];
28
+ metadata?: SessionMetadata | null;
29
+ }
30
+
31
+ export interface ServerState {
32
+ type: "state";
33
+ sessions: SessionData[];
34
+ focusedSession: string | null;
35
+ currentSession: string | null;
36
+ theme: string | undefined;
37
+ sidebarWidth: number;
38
+ ts: number;
39
+ }
40
+
41
+ export interface FocusUpdate {
42
+ type: "focus";
43
+ focusedSession: string | null;
44
+ currentSession: string | null;
45
+ }
46
+
47
+ export interface ResizeNotify {
48
+ type: "resize";
49
+ width: number;
50
+ }
51
+
52
+ export interface QuitNotify {
53
+ type: "quit";
54
+ }
55
+
56
+ export interface YourSession {
57
+ type: "your-session";
58
+ name: string;
59
+ clientTty: string | null;
60
+ }
61
+
62
+ export interface ReIdentify {
63
+ type: "re-identify";
64
+ }
65
+
66
+ export type ServerMessage =
67
+ | ServerState
68
+ | FocusUpdate
69
+ | ResizeNotify
70
+ | QuitNotify
71
+ | YourSession
72
+ | ReIdentify;
73
+
74
+ // --- Programmatic metadata (agent/script-pushed) ---
75
+
76
+ export type MetadataTone = "neutral" | "info" | "success" | "warn" | "error";
77
+
78
+ export interface MetadataStatus {
79
+ text: string;
80
+ tone?: MetadataTone;
81
+ ts: number;
82
+ }
83
+
84
+ export interface MetadataProgress {
85
+ current?: number;
86
+ total?: number;
87
+ percent?: number;
88
+ label?: string;
89
+ ts: number;
90
+ }
91
+
92
+ export interface MetadataLogEntry {
93
+ message: string;
94
+ tone?: MetadataTone;
95
+ source?: string;
96
+ ts: number;
97
+ }
98
+
99
+ export interface SessionMetadata {
100
+ status: MetadataStatus | null;
101
+ progress: MetadataProgress | null;
102
+ logs: MetadataLogEntry[];
103
+ }
104
+
105
+ export type ClientCommand =
106
+ | { type: "switch-session"; name: string; clientTty?: string }
107
+ | { type: "switch-index"; index: number }
108
+ | { type: "new-session" }
109
+ | { type: "hide-session"; name: string }
110
+ | { type: "show-all-sessions" }
111
+ | { type: "kill-session"; name: string }
112
+ | { type: "reorder-session"; name: string; delta: -1 | 1 }
113
+ | { type: "refresh" }
114
+ | { type: "move-focus"; delta: -1 | 1 }
115
+ | { type: "focus-session"; name: string }
116
+ | { type: "mark-seen"; name: string }
117
+ | { type: "dismiss-agent"; session: string; agent: string; threadId?: string }
118
+ | { type: "set-theme"; theme: string }
119
+ | { type: "identify"; clientTty: string }
120
+ | { type: "report-width"; width: number }
121
+ | { type: "quit" }
122
+ | { type: "identify-pane"; paneId: string; sessionName: string }
123
+ | {
124
+ type: "focus-agent-pane";
125
+ session: string;
126
+ agent: string;
127
+ threadId?: string;
128
+ threadName?: string;
129
+ }
130
+ | {
131
+ type: "kill-agent-pane";
132
+ session: string;
133
+ agent: string;
134
+ threadId?: string;
135
+ threadName?: string;
136
+ };
137
+
138
+ // Catppuccin Mocha palette
139
+ export const C = {
140
+ blue: "#89b4fa",
141
+ lavender: "#b4befe",
142
+ pink: "#cba6f7",
143
+ mauve: "#cba6f7",
144
+ yellow: "#f9e2af",
145
+ green: "#a6e3a1",
146
+ red: "#f38ba8",
147
+ peach: "#fab387",
148
+ teal: "#94e2d5",
149
+ sky: "#89dceb",
150
+ text: "#cdd6f4",
151
+ subtext0: "#a6adc8",
152
+ subtext1: "#bac2de",
153
+ overlay0: "#6c7086",
154
+ overlay1: "#7f849c",
155
+ surface0: "#313244",
156
+ surface1: "#45475a",
157
+ surface2: "#585b70",
158
+ base: "#1e1e2e",
159
+ mantle: "#181825",
160
+ crust: "#11111b",
161
+ } as const;
162
+
163
+ export const STATUS_COLORS: Record<AgentStatus, string> = {
164
+ idle: C.surface2,
165
+ running: C.yellow,
166
+ done: C.green,
167
+ error: C.red,
168
+ waiting: C.blue,
169
+ interrupted: C.peach,
170
+ };
171
+
172
+ export const STATUS_ICONS: Record<AgentStatus, string> = {
173
+ idle: "○",
174
+ running: "●",
175
+ done: "✓",
176
+ error: "✗",
177
+ waiting: "◉",
178
+ interrupted: "⚠",
179
+ };