vibe-coding-master 0.0.12 → 0.0.13
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/README.md +24 -9
- package/dist/backend/api/round-routes.js +33 -0
- package/dist/backend/api/session-routes.js +2 -0
- package/dist/backend/api/task-routes.js +1 -0
- package/dist/backend/server.js +19 -3
- package/dist/backend/services/app-settings-service.js +2 -1
- package/dist/backend/services/message-service.js +158 -55
- package/dist/backend/services/project-service.js +1 -1
- package/dist/backend/services/round-service.js +227 -0
- package/dist/backend/services/task-service.js +14 -6
- package/dist/backend/templates/harness/architect-agent.js +1 -0
- package/dist/backend/templates/harness/claude-root.js +3 -0
- package/dist/backend/templates/harness/coder-agent.js +1 -0
- package/dist/backend/templates/harness/gitignore.js +2 -1
- package/dist/backend/templates/harness/project-manager-agent.js +3 -0
- package/dist/backend/templates/harness/reviewer-agent.js +1 -0
- package/dist/backend/ws/terminal-ws.js +15 -1
- package/dist/shared/types/round.js +1 -0
- package/dist-frontend/assets/index-CyJrJge9.js +88 -0
- package/dist-frontend/assets/index-N5DA0uE9.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +7 -4
- package/docs/product-design.md +59 -25
- package/docs/v1-architecture-design.md +86 -27
- package/docs/v1-implementation-plan.md +74 -12
- package/package.json +1 -1
- package/dist-frontend/assets/index-Bi4X3GSR.css +0 -32
- package/dist-frontend/assets/index-DaHXq14j.js +0 -88
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { ROLE_NAMES } from "../../shared/constants.js";
|
|
2
|
+
export function createRoundService(deps) {
|
|
3
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
4
|
+
const debounceMs = deps.debounceMs ?? 1500;
|
|
5
|
+
const trackers = new Map();
|
|
6
|
+
function ensureTracker(session) {
|
|
7
|
+
if (session.status !== "running") {
|
|
8
|
+
stopSession(session.id);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (trackers.has(session.id)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const tracker = {
|
|
15
|
+
role: session.role,
|
|
16
|
+
sessionId: session.id,
|
|
17
|
+
taskSlug: session.taskSlug,
|
|
18
|
+
status: "unknown",
|
|
19
|
+
pendingToolUseIds: new Set(),
|
|
20
|
+
unsubscribe: () => { }
|
|
21
|
+
};
|
|
22
|
+
tracker.unsubscribe = deps.transcripts.subscribeToRoleSession(session, (event) => {
|
|
23
|
+
handleTranscriptEvent(tracker, event);
|
|
24
|
+
}, {
|
|
25
|
+
onError(error) {
|
|
26
|
+
tracker.status = "unknown";
|
|
27
|
+
tracker.reason = error.message;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
trackers.set(session.id, tracker);
|
|
31
|
+
}
|
|
32
|
+
function handleTranscriptEvent(tracker, event) {
|
|
33
|
+
tracker.lastActivityAt = event.timestamp;
|
|
34
|
+
clearIdleTimer(tracker);
|
|
35
|
+
if (event.kind === "tool_use") {
|
|
36
|
+
tracker.pendingToolUseIds.add(event.id);
|
|
37
|
+
tracker.status = "using_tools";
|
|
38
|
+
tracker.reason = undefined;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (event.kind === "tool_result") {
|
|
42
|
+
tracker.pendingToolUseIds.delete(event.toolResult.tool_use_id);
|
|
43
|
+
tracker.status = tracker.pendingToolUseIds.size > 0 ? "using_tools" : "answering";
|
|
44
|
+
tracker.reason = undefined;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (event.kind === "question") {
|
|
48
|
+
tracker.status = "waiting_user";
|
|
49
|
+
tracker.reason = "Claude Code is asking for user input.";
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (event.kind === "agent" || event.kind === "todo") {
|
|
53
|
+
tracker.status = "answering";
|
|
54
|
+
tracker.reason = undefined;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (event.kind === "thinking") {
|
|
58
|
+
tracker.status = "answering";
|
|
59
|
+
tracker.reason = undefined;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (event.kind === "text") {
|
|
63
|
+
if (event.stopReason === "max_tokens" || event.stopReason === "stop_sequence" || event.stopReason === "refusal") {
|
|
64
|
+
tracker.status = "abnormal";
|
|
65
|
+
tracker.reason = `Claude Code stopped with ${event.stopReason}.`;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
tracker.status = "answering";
|
|
69
|
+
tracker.reason = undefined;
|
|
70
|
+
if (event.stopReason === "end_turn" && tracker.pendingToolUseIds.size === 0 && event.text.trim()) {
|
|
71
|
+
scheduleIdle(tracker, event.timestamp);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function scheduleIdle(tracker, endedAt) {
|
|
76
|
+
tracker.pendingAnswerEndedAt = endedAt;
|
|
77
|
+
tracker.idleTimer = setTimeout(() => {
|
|
78
|
+
if (tracker.pendingToolUseIds.size > 0 || tracker.pendingAnswerEndedAt !== endedAt) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
tracker.status = "idle";
|
|
82
|
+
tracker.lastAnswerEndedAt = endedAt;
|
|
83
|
+
tracker.reason = undefined;
|
|
84
|
+
tracker.idleTimer = undefined;
|
|
85
|
+
}, debounceMs);
|
|
86
|
+
}
|
|
87
|
+
function clearIdleTimer(tracker) {
|
|
88
|
+
tracker.pendingAnswerEndedAt = undefined;
|
|
89
|
+
if (tracker.idleTimer) {
|
|
90
|
+
clearTimeout(tracker.idleTimer);
|
|
91
|
+
tracker.idleTimer = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function stopSession(sessionId) {
|
|
95
|
+
const tracker = trackers.get(sessionId);
|
|
96
|
+
if (!tracker) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
clearIdleTimer(tracker);
|
|
100
|
+
tracker.unsubscribe();
|
|
101
|
+
trackers.delete(sessionId);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
getTaskRoundState(input) {
|
|
105
|
+
const liveSessionIds = new Set(input.sessions.filter((session) => session.status === "running").map((session) => session.id));
|
|
106
|
+
for (const session of input.sessions) {
|
|
107
|
+
ensureTracker(session);
|
|
108
|
+
}
|
|
109
|
+
for (const [sessionId, tracker] of trackers) {
|
|
110
|
+
if (tracker.taskSlug === input.taskSlug && !liveSessionIds.has(sessionId)) {
|
|
111
|
+
stopSession(sessionId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const roleStates = ROLE_NAMES.map((role) => toRoleTurnState(role, input.sessions, trackers));
|
|
115
|
+
const latestDelivered = getLatestDeliveredMessage(input.messages);
|
|
116
|
+
const queuedMessageCount = input.messages.filter((message) => message.status === "queued").length;
|
|
117
|
+
const pendingMessageCount = input.messages.filter((message) => message.status === "pending_approval").length;
|
|
118
|
+
const state = evaluateTaskRoundState({
|
|
119
|
+
taskSlug: input.taskSlug,
|
|
120
|
+
messages: input.messages,
|
|
121
|
+
roleStates,
|
|
122
|
+
updatedAt: now()
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
...state,
|
|
126
|
+
latestMessageId: latestDelivered?.id,
|
|
127
|
+
latestMessageDeliveredAt: getMessageDeliveredAt(latestDelivered),
|
|
128
|
+
queuedMessageCount,
|
|
129
|
+
pendingMessageCount
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
stopSession,
|
|
133
|
+
stopTask(taskSlug) {
|
|
134
|
+
for (const [sessionId, tracker] of trackers) {
|
|
135
|
+
if (tracker.taskSlug === taskSlug) {
|
|
136
|
+
stopSession(sessionId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function evaluateTaskRoundState(input) {
|
|
143
|
+
const latestDelivered = getLatestDeliveredMessage(input.messages);
|
|
144
|
+
const hasQueuedOrPending = input.messages.some((message) => message.status === "queued" || message.status === "pending_approval");
|
|
145
|
+
if (latestDelivered) {
|
|
146
|
+
const deliveredAt = getMessageDeliveredAt(latestDelivered);
|
|
147
|
+
const targetState = input.roleStates.find((roleState) => roleState.role === latestDelivered.toRole);
|
|
148
|
+
const completedAt = targetState && deliveredAt
|
|
149
|
+
? getCompletedAtForDelivery(targetState, deliveredAt)
|
|
150
|
+
: undefined;
|
|
151
|
+
if (completedAt && !hasQueuedOrPending) {
|
|
152
|
+
return {
|
|
153
|
+
taskSlug: input.taskSlug,
|
|
154
|
+
status: "completed",
|
|
155
|
+
activeRole: latestDelivered.toRole,
|
|
156
|
+
completionId: `${latestDelivered.id}:${completedAt}`,
|
|
157
|
+
completedAt,
|
|
158
|
+
roles: input.roleStates,
|
|
159
|
+
updatedAt: input.updatedAt
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
taskSlug: input.taskSlug,
|
|
164
|
+
status: targetState?.status === "waiting_user" ? "waiting_user" : "active",
|
|
165
|
+
activeRole: latestDelivered.toRole,
|
|
166
|
+
roles: input.roleStates,
|
|
167
|
+
updatedAt: input.updatedAt
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const latestIdleRole = !hasQueuedOrPending ? getLatestIdleRole(input.roleStates) : undefined;
|
|
171
|
+
if (latestIdleRole?.lastAnswerEndedAt) {
|
|
172
|
+
return {
|
|
173
|
+
taskSlug: input.taskSlug,
|
|
174
|
+
status: "completed",
|
|
175
|
+
activeRole: latestIdleRole.role,
|
|
176
|
+
completionId: `direct:${latestIdleRole.role}:${latestIdleRole.lastAnswerEndedAt}`,
|
|
177
|
+
completedAt: latestIdleRole.lastAnswerEndedAt,
|
|
178
|
+
roles: input.roleStates,
|
|
179
|
+
updatedAt: input.updatedAt
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const activeRole = input.roleStates.find((roleState) => roleState.status === "answering" ||
|
|
183
|
+
roleState.status === "using_tools" ||
|
|
184
|
+
roleState.status === "waiting_user" ||
|
|
185
|
+
roleState.status === "abnormal");
|
|
186
|
+
return {
|
|
187
|
+
taskSlug: input.taskSlug,
|
|
188
|
+
status: activeRole?.status === "waiting_user" ? "waiting_user" : activeRole ? "active" : "idle",
|
|
189
|
+
activeRole: activeRole?.role,
|
|
190
|
+
roles: input.roleStates,
|
|
191
|
+
updatedAt: input.updatedAt
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function toRoleTurnState(role, sessions, trackers) {
|
|
195
|
+
const session = sessions.find((candidate) => candidate.role === role && candidate.status === "running");
|
|
196
|
+
const tracker = session ? trackers.get(session.id) : undefined;
|
|
197
|
+
return {
|
|
198
|
+
role,
|
|
199
|
+
sessionId: session?.id,
|
|
200
|
+
status: tracker?.status ?? "unknown",
|
|
201
|
+
pendingToolUseCount: tracker?.pendingToolUseIds.size ?? 0,
|
|
202
|
+
lastActivityAt: tracker?.lastActivityAt,
|
|
203
|
+
lastAnswerEndedAt: tracker?.lastAnswerEndedAt,
|
|
204
|
+
reason: tracker?.reason
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function getLatestDeliveredMessage(messages) {
|
|
208
|
+
return messages
|
|
209
|
+
.filter((message) => message.status === "delivered" || message.status === "staged")
|
|
210
|
+
.sort((left, right) => getMessageDeliveredAt(left).localeCompare(getMessageDeliveredAt(right)))
|
|
211
|
+
.at(-1);
|
|
212
|
+
}
|
|
213
|
+
function getMessageDeliveredAt(message) {
|
|
214
|
+
return message?.deliveredAt ?? message?.stagedAt ?? message?.createdAt ?? "";
|
|
215
|
+
}
|
|
216
|
+
function getCompletedAtForDelivery(roleState, deliveredAt) {
|
|
217
|
+
if (roleState.status !== "idle" || !roleState.lastAnswerEndedAt) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return roleState.lastAnswerEndedAt >= deliveredAt ? roleState.lastAnswerEndedAt : undefined;
|
|
221
|
+
}
|
|
222
|
+
function getLatestIdleRole(roleStates) {
|
|
223
|
+
return roleStates
|
|
224
|
+
.filter((roleState) => roleState.status === "idle" && roleState.lastAnswerEndedAt)
|
|
225
|
+
.sort((left, right) => (left.lastAnswerEndedAt ?? "").localeCompare(right.lastAnswerEndedAt ?? ""))
|
|
226
|
+
.at(-1);
|
|
227
|
+
}
|
|
@@ -13,7 +13,7 @@ export function createTaskService(deps) {
|
|
|
13
13
|
? `feature/${input.taskSlug}`
|
|
14
14
|
: await deps.git.getCurrentBranch(repoRoot);
|
|
15
15
|
const worktreePath = shouldCreateWorktree
|
|
16
|
-
? getTaskWorktreePath(repoRoot,
|
|
16
|
+
? getTaskWorktreePath(repoRoot, input.taskSlug)
|
|
17
17
|
: undefined;
|
|
18
18
|
if (await deps.fs.pathExists(taskPath)) {
|
|
19
19
|
throw new VcmError({
|
|
@@ -30,6 +30,14 @@ export function createTaskService(deps) {
|
|
|
30
30
|
hint: "Apply VCM Harness first so .gitignore contains the VCM managed block."
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
if (shouldCreateWorktree && !(await deps.git.isIgnored(repoRoot, ".claude/worktrees/.probe"))) {
|
|
34
|
+
throw new VcmError({
|
|
35
|
+
code: "VCM_WORKTREES_NOT_IGNORED",
|
|
36
|
+
message: ".claude/worktrees/ is not ignored by Git.",
|
|
37
|
+
statusCode: 409,
|
|
38
|
+
hint: "Apply VCM Harness first so .gitignore ignores Claude-compatible task worktrees."
|
|
39
|
+
});
|
|
40
|
+
}
|
|
33
41
|
if (!shouldCreateWorktree) {
|
|
34
42
|
const activeInlineTask = await findActiveInlineTask(deps.fs, repoRoot, config.stateRoot);
|
|
35
43
|
if (activeInlineTask) {
|
|
@@ -161,7 +169,7 @@ export function createTaskService(deps) {
|
|
|
161
169
|
const removedStatePaths = [];
|
|
162
170
|
const cleanedAt = now();
|
|
163
171
|
if (task.worktreePath) {
|
|
164
|
-
assertTaskWorktreePath(repoRoot,
|
|
172
|
+
assertTaskWorktreePath(repoRoot, task.worktreePath);
|
|
165
173
|
await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
|
|
166
174
|
}
|
|
167
175
|
let deletedBranch;
|
|
@@ -189,8 +197,8 @@ export function getTaskRuntimeRepoRoot(task) {
|
|
|
189
197
|
function getTaskPath(repoRoot, stateRoot, taskSlug) {
|
|
190
198
|
return path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`);
|
|
191
199
|
}
|
|
192
|
-
function getTaskWorktreePath(repoRoot,
|
|
193
|
-
return path.join(repoRoot,
|
|
200
|
+
function getTaskWorktreePath(repoRoot, taskSlug) {
|
|
201
|
+
return path.join(repoRoot, ".claude", "worktrees", taskSlug);
|
|
194
202
|
}
|
|
195
203
|
async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
|
|
196
204
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "sessions"));
|
|
@@ -222,8 +230,8 @@ function getTaskStatePaths(baseRepoRoot, taskRepoRoot, stateRoot, handoffRoot, t
|
|
|
222
230
|
path.join(taskRepoRoot, handoffRoot)
|
|
223
231
|
];
|
|
224
232
|
}
|
|
225
|
-
function assertTaskWorktreePath(repoRoot,
|
|
226
|
-
const worktreeRoot = path.resolve(repoRoot,
|
|
233
|
+
function assertTaskWorktreePath(repoRoot, worktreePath) {
|
|
234
|
+
const worktreeRoot = path.resolve(repoRoot, ".claude", "worktrees");
|
|
227
235
|
const resolvedWorktreePath = path.resolve(worktreePath);
|
|
228
236
|
const relative = path.relative(worktreeRoot, resolvedWorktreePath);
|
|
229
237
|
if (relative.startsWith("..") || path.isAbsolute(relative) || relative === "") {
|
|
@@ -8,5 +8,6 @@ export function renderArchitectHarnessRules() {
|
|
|
8
8
|
- Update stale architecture/module/testing/security/dependency docs when the final code made them stale.
|
|
9
9
|
- Write docs-sync-report.md with docs changed, docs intentionally left unchanged, remaining documentation risks, and decision.
|
|
10
10
|
- Stop and reply to project-manager if implementation drift changes architecture, public contracts, dependency direction, schema, auth, permission, payment, or design assumptions.
|
|
11
|
+
- Reply to project-manager once per received VCM message when complete, blocked, or unclear; do not send fragmented progress updates unless project-manager explicitly requested them.
|
|
11
12
|
`;
|
|
12
13
|
}
|
|
@@ -8,6 +8,9 @@ export function renderRootClaudeHarnessRules() {
|
|
|
8
8
|
- Do not create or write task handoffs outside .ai/vcm/handoffs/ for the current task.
|
|
9
9
|
- Use vcmctl for role-to-role messaging instead of asking the user to copy prompts.
|
|
10
10
|
- Non-PM roles only reply to project-manager; they do not message other roles directly.
|
|
11
|
+
- Role messaging is turn-based: do not send more than one active message to the same target role.
|
|
12
|
+
- After sending a message to a role, wait for that role to reply with vcmctl reply or vcmctl result before sending another message to the same role.
|
|
13
|
+
- If new information arrives while a role is still processing, update the relevant handoff artifact or wait; do not spam the target role's terminal.
|
|
11
14
|
- High-risk decisions involving schema, auth, permissions, payment, billing, security, data deletion, or unclear user intent must stop for project-manager/user approval.
|
|
12
15
|
- Required workflow gates: architect plan -> coder implementation/validation -> reviewer review -> architect docs sync -> project-manager final acceptance/commit/PR.
|
|
13
16
|
`;
|
|
@@ -7,5 +7,6 @@ export function renderCoderHarnessRules() {
|
|
|
7
7
|
- Maintain implementation-log.md and validation-log.md under the current task handoff directory.
|
|
8
8
|
- Do not change module boundaries, public contracts, dependency direction, or test strategy without project-manager/architect replan.
|
|
9
9
|
- Stop and reply to project-manager when blocked, unclear, or when the plan no longer matches reality.
|
|
10
|
+
- Reply to project-manager once per received VCM message when complete, blocked, or unclear; do not send fragmented progress updates unless project-manager explicitly requested them.
|
|
10
11
|
`;
|
|
11
12
|
}
|
|
@@ -5,6 +5,9 @@ export function renderProjectManagerHarnessRules() {
|
|
|
5
5
|
- Clarify the user's request, classify task risk, and choose the role route.
|
|
6
6
|
- Use vcmctl send to assign work to architect, coder, or reviewer.
|
|
7
7
|
- Send role work as durable instructions with artifact refs when possible.
|
|
8
|
+
- Enforce per-role turn-taking: keep at most one in-flight message per target role.
|
|
9
|
+
- Before sending another task, question, revise, or review-request to the same role, wait for that role's vcmctl reply or vcmctl result.
|
|
10
|
+
- Use cancel only for urgent supersession; include what is superseded.
|
|
8
11
|
- Track the workflow gates: architecture plan, implementation/validation, review, docs sync, final acceptance.
|
|
9
12
|
- Request architect post-review docs sync after reviewer completes.
|
|
10
13
|
- Prepare final acceptance, commit, and PR only after reviewer and docs-sync gates pass or an explicit exception is approved.
|
|
@@ -9,5 +9,6 @@ export function renderReviewerHarnessRules() {
|
|
|
9
9
|
- Escalate larger implementation issues to project-manager for coder follow-up.
|
|
10
10
|
- Escalate architecture, public contract, design, or documentation drift issues to project-manager for architect follow-up.
|
|
11
11
|
- Do not take over broad implementation and do not weaken tests to pass validation.
|
|
12
|
+
- Reply to project-manager once per received VCM message when complete, blocked, or unclear; do not send fragmented progress updates unless project-manager explicitly requested them.
|
|
12
13
|
`;
|
|
13
14
|
}
|
|
@@ -41,7 +41,9 @@ function bindTerminalSocket(ws, sessionId, runtime) {
|
|
|
41
41
|
runtime.write(sessionId, message.data);
|
|
42
42
|
}
|
|
43
43
|
else if (message.type === "resize") {
|
|
44
|
-
|
|
44
|
+
if (isSafeTerminalResize(message.cols, message.rows)) {
|
|
45
|
+
runtime.resize(sessionId, message.cols, message.rows);
|
|
46
|
+
}
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
catch (error) {
|
|
@@ -53,6 +55,18 @@ function bindTerminalSocket(ws, sessionId, runtime) {
|
|
|
53
55
|
unsubscribe();
|
|
54
56
|
});
|
|
55
57
|
}
|
|
58
|
+
export function isSafeTerminalResize(cols, rows) {
|
|
59
|
+
return (Number.isInteger(cols) &&
|
|
60
|
+
Number.isInteger(rows) &&
|
|
61
|
+
cols >= MIN_TERMINAL_COLS &&
|
|
62
|
+
rows >= MIN_TERMINAL_ROWS &&
|
|
63
|
+
cols <= MAX_TERMINAL_COLS &&
|
|
64
|
+
rows <= MAX_TERMINAL_ROWS);
|
|
65
|
+
}
|
|
66
|
+
const MIN_TERMINAL_COLS = 20;
|
|
67
|
+
const MIN_TERMINAL_ROWS = 5;
|
|
68
|
+
const MAX_TERMINAL_COLS = 1000;
|
|
69
|
+
const MAX_TERMINAL_ROWS = 200;
|
|
56
70
|
function send(ws, message) {
|
|
57
71
|
if (ws.readyState === ws.OPEN) {
|
|
58
72
|
ws.send(JSON.stringify(message));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|