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