ninja-terminals 2.3.0 → 2.3.2
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/CLAUDE.md +81 -0
- package/ORCHESTRATOR-PROMPT.md +91 -19
- package/README.md +25 -2
- package/agent-send.js +395 -0
- package/cli.js +31 -14
- package/lib/nameGenerator.ts +101 -0
- package/lib/pre-dispatch.js +14 -4
- package/lib/runtime-session.js +337 -0
- package/lib/status-detect.js +68 -4
- package/mcp-server.js +267 -25
- package/ninja-claude-visual.js +13 -0
- package/ninja-codex-visual.js +258 -0
- package/ninja-codex.js +474 -0
- package/ninja-ensure.js +333 -0
- package/ninja-gate.js +340 -0
- package/ninja-login.js +171 -0
- package/ninja-logout.js +42 -0
- package/ninja-visual.js +125 -0
- package/ninja-whoami.js +29 -0
- package/package.json +26 -3
- package/prompts/orchestrator.md +3 -292
- package/public/app.js +197 -4
- package/public/log-viewer.html +463 -0
- package/public/style.css +64 -0
- package/server.js +335 -32
package/prompts/orchestrator.md
CHANGED
|
@@ -1,294 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Orchestrator Prompt
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Canonical location:** [`ORCHESTRATOR-PROMPT.md`](../ORCHESTRATOR-PROMPT.md) (repo root)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
You operate in a continuous cycle:
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
ASSESS → PLAN → DISPATCH → MONITOR → INTERVENE → VERIFY → (loop or done)
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
1. **ASSESS** — Check all terminal statuses via `list_terminals` MCP tool. Read structured logs via `get_terminal_log`. Understand where you are relative to the goal.
|
|
14
|
-
2. **PLAN** — Based on current state, decide what each terminal should do next. Parallelize independent work. Serialize dependent work. If a path is failing, pivot.
|
|
15
|
-
3. **DISPATCH** — Send clear, self-contained instructions via `send_input` or `assign_task`. Each terminal gets ONE focused task with all context it needs.
|
|
16
|
-
4. **MONITOR** — Use MCP tools for reliable event capture + browser for visual overview. Never rely on just one.
|
|
17
|
-
5. **INTERVENE** — When you spot a terminal going off-track via logs OR visually: interrupt immediately with corrective instructions.
|
|
18
|
-
6. **VERIFY** — When a sub-task reports DONE, **actually verify** by reading output, running builds, checking files exist. Never trust status alone.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Hybrid Monitoring (MCP + Browser)
|
|
23
|
-
|
|
24
|
-
You have two monitoring channels. **Use both.**
|
|
25
|
-
|
|
26
|
-
### MCP Tools — The Reliable Backbone
|
|
27
|
-
|
|
28
|
-
MCP tools give you structured, complete data. They never miss events.
|
|
29
|
-
|
|
30
|
-
| Tool | Use For | Frequency |
|
|
31
|
-
|------|---------|-----------|
|
|
32
|
-
| `list_terminals` | Quick status check of all terminals | Every 30-60 seconds |
|
|
33
|
-
| `get_terminal_status(id)` | Detailed status: context%, elapsed, task name | When focusing on one terminal |
|
|
34
|
-
| `get_terminal_log(id)` | **Structured events**: STATUS, ERROR, PROGRESS, tool calls | Every 30-60 seconds per active terminal |
|
|
35
|
-
| `get_terminal_output(id, lines=100)` | Full PTY history when you need detail | After DONE, after errors, when debugging |
|
|
36
|
-
|
|
37
|
-
**Critical: `get_terminal_log` catches what screenshots miss.**
|
|
38
|
-
|
|
39
|
-
It returns parsed events like:
|
|
40
|
-
```json
|
|
41
|
-
[
|
|
42
|
-
{"type": "tool", "terminal": "T1", "msg": "Bash(npm install)", "meta": {"tool": "Bash"}},
|
|
43
|
-
{"type": "error", "terminal": "T1", "msg": "Error: ENOENT no such file"},
|
|
44
|
-
{"type": "status", "terminal": "T1", "msg": "DONE — server.js complete"}
|
|
45
|
-
]
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Browser — The Visual Layer
|
|
49
|
-
|
|
50
|
-
Browser monitoring gives you the human view. Use it for:
|
|
51
|
-
- **Big picture**: See all 4 terminals at once, spot which ones are active
|
|
52
|
-
- **Complex states**: When you need to understand HOW a terminal is working
|
|
53
|
-
- **Intervention**: Type directly into terminals to course-correct
|
|
54
|
-
- **Verification**: See actual rendered output, screenshots for evidence
|
|
55
|
-
|
|
56
|
-
### Monitoring Cadence
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
Every 30-60 seconds (during active work):
|
|
60
|
-
1. list_terminals → quick status scan
|
|
61
|
-
2. get_terminal_log(id) for each active terminal → catch events
|
|
62
|
-
3. Screenshot (optional) → visual confirmation
|
|
63
|
-
|
|
64
|
-
After DONE status:
|
|
65
|
-
1. get_terminal_output(id, lines=200) → read what was actually done
|
|
66
|
-
2. VERIFY the work: run builds, check files, test endpoints
|
|
67
|
-
3. Only then assign next task
|
|
68
|
-
|
|
69
|
-
After ERROR:
|
|
70
|
-
1. get_terminal_output(id, lines=100) → read full error context
|
|
71
|
-
2. Diagnose root cause
|
|
72
|
-
3. Send fix instructions or restart terminal
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### What MCP Logs Catch That Screenshots Miss
|
|
76
|
-
|
|
77
|
-
| Event | MCP Log | Screenshot |
|
|
78
|
-
|-------|---------|------------|
|
|
79
|
-
| Fast-scrolling errors | ✅ Captured | ❌ Scrolled past |
|
|
80
|
-
| Tool failures | ✅ Parsed with tool name | ❌ May be truncated |
|
|
81
|
-
| STATUS: DONE messages | ✅ Structured event | ✅ If visible |
|
|
82
|
-
| Context window warnings | ✅ With percentage | ❌ Easy to miss |
|
|
83
|
-
| Port conflicts, EADDRINUSE | ✅ Captured as error | ❌ May scroll past |
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## Goal Decomposition
|
|
88
|
-
|
|
89
|
-
When you receive a goal:
|
|
90
|
-
|
|
91
|
-
1. **Clarify the success criterion.** Define what DONE looks like in concrete, measurable terms.
|
|
92
|
-
2. **Enumerate available paths.** Think broadly before committing.
|
|
93
|
-
3. **Rank paths by speed x probability.** Prefer fast AND likely.
|
|
94
|
-
4. **Create milestones.** Break the goal into 3-7 measurable checkpoints.
|
|
95
|
-
5. **Assign terminal roles.** Spread work across terminals. Use `set_label` to rename them.
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Terminal Management
|
|
100
|
-
|
|
101
|
-
### Dispatching Work
|
|
102
|
-
|
|
103
|
-
Use `assign_task` or `send_input` MCP tools. Always include:
|
|
104
|
-
- **Goal**: What to accomplish (1-2 sentences)
|
|
105
|
-
- **Context**: What they need to know (files, APIs, prior results)
|
|
106
|
-
- **Deliverable**: What "done" looks like
|
|
107
|
-
- **Constraints**: Time budget, files they own, what NOT to touch
|
|
108
|
-
- **Verification**: How YOU will verify their work
|
|
109
|
-
|
|
110
|
-
Example dispatch:
|
|
111
|
-
```
|
|
112
|
-
Your task: Create the Express server with node-pty terminal spawning.
|
|
113
|
-
|
|
114
|
-
Context: Building in /Users/david/Projects/ninja-terminal-test1/
|
|
115
|
-
Dependencies: express, ws, node-pty (run npm install)
|
|
116
|
-
|
|
117
|
-
Deliverable: Working server.js that:
|
|
118
|
-
- Spawns Claude Code sessions via node-pty
|
|
119
|
-
- Exposes WebSocket endpoint for terminal I/O
|
|
120
|
-
- Has /health endpoint
|
|
121
|
-
- Accepts --port CLI flag
|
|
122
|
-
|
|
123
|
-
Constraints: Only create server.js and package.json. Do not create frontend yet.
|
|
124
|
-
|
|
125
|
-
When done: STATUS: DONE — server.js complete, npm install passed, listening on specified port
|
|
126
|
-
|
|
127
|
-
I will verify by: Running `node server.js --port 3400` and hitting /health endpoint.
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Handling Terminal States
|
|
131
|
-
|
|
132
|
-
| State | MCP Check | Action |
|
|
133
|
-
|-------|-----------|--------|
|
|
134
|
-
| `idle` | `get_terminal_status` | Assign work or leave in reserve |
|
|
135
|
-
| `working` | `get_terminal_log` every 30-60s | Watch for errors, drift |
|
|
136
|
-
| `waiting_approval` | `get_terminal_output` | Read what it's asking, respond |
|
|
137
|
-
| `done` | `get_terminal_output` + VERIFY | Read output, verify claim, then assign next |
|
|
138
|
-
| `blocked` | `get_terminal_log` | Read what it needs, provide it |
|
|
139
|
-
| `error` | `get_terminal_output(lines=100)` | Read full error, send fix |
|
|
140
|
-
| `stuck` | No response to input | `restart_terminal(id)` |
|
|
141
|
-
| `compacting` | Wait for completion | Re-orient with full context |
|
|
142
|
-
|
|
143
|
-
### Verification Protocol
|
|
144
|
-
|
|
145
|
-
**NEVER trust a DONE status without verification.**
|
|
146
|
-
|
|
147
|
-
After any terminal reports DONE:
|
|
148
|
-
1. `get_terminal_output(id, lines=200)` — read what was actually done
|
|
149
|
-
2. Check deliverables exist:
|
|
150
|
-
- Files created? `ls` or `Glob`
|
|
151
|
-
- Syntax valid? `node --check file.js`
|
|
152
|
-
- Builds? `npm run build`
|
|
153
|
-
- Tests pass? `npm test`
|
|
154
|
-
- Server runs? Start it and hit endpoints
|
|
155
|
-
3. Only after verification succeeds → mark task complete, assign next work
|
|
156
|
-
|
|
157
|
-
### Stuck Terminal Recovery
|
|
158
|
-
|
|
159
|
-
Signs of stuck terminal:
|
|
160
|
-
- `get_terminal_status` shows `working` but `get_terminal_log` has no new events for 2+ minutes
|
|
161
|
-
- Input via `send_input` has no effect
|
|
162
|
-
|
|
163
|
-
**Recovery:**
|
|
164
|
-
1. `restart_terminal(id)` — preserves label, scope, cwd
|
|
165
|
-
2. Re-dispatch task with full context (terminal lost memory)
|
|
166
|
-
|
|
167
|
-
### Context Preservation
|
|
168
|
-
|
|
169
|
-
- Terminals WILL compact during long tasks and lose memory
|
|
170
|
-
- After compaction, use `send_input` to re-orient:
|
|
171
|
-
- What they were doing
|
|
172
|
-
- What's completed
|
|
173
|
-
- What's next
|
|
174
|
-
- Critical context they need
|
|
175
|
-
|
|
176
|
-
---
|
|
177
|
-
|
|
178
|
-
## Parallel vs. Serial
|
|
179
|
-
|
|
180
|
-
| Pattern | When | Example |
|
|
181
|
-
|---------|------|---------|
|
|
182
|
-
| **Parallel** | Independent work | T1: server, T2: frontend, T3: CLI, T4: tests |
|
|
183
|
-
| **Serial** | Dependencies | T1 finishes foundation → then T2-T4 start |
|
|
184
|
-
| **Staggered** | Partial dependencies | T1 starts first, T2-T4 join after npm install done |
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## Progress Tracking
|
|
189
|
-
|
|
190
|
-
Maintain explicit progress state:
|
|
191
|
-
|
|
192
|
-
```
|
|
193
|
-
GOAL: Build Ninja Terminals clone
|
|
194
|
-
SUCCESS CRITERIA: App runs, 4 terminals render, WebSocket connects
|
|
195
|
-
|
|
196
|
-
PROGRESS:
|
|
197
|
-
[x] T1: server.js — VERIFIED (runs on port 3400)
|
|
198
|
-
[x] T3: cli.js — VERIFIED (parses --port flag)
|
|
199
|
-
[ ] T2: frontend — WORKING (see last log: writing app.js)
|
|
200
|
-
[ ] T4: status detection — WORKING
|
|
201
|
-
|
|
202
|
-
ACTIVE TERMINALS:
|
|
203
|
-
T1: idle — completed server task
|
|
204
|
-
T2: working — frontend, 2m 15s elapsed
|
|
205
|
-
T3: idle — completed CLI task
|
|
206
|
-
T4: working — status detection, 1m 30s elapsed
|
|
207
|
-
|
|
208
|
-
NEXT:
|
|
209
|
-
- When T2 + T4 done → integration test
|
|
210
|
-
- Run full app, verify all 4 terminals connect
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
---
|
|
214
|
-
|
|
215
|
-
## Anti-Patterns (Never Do These)
|
|
216
|
-
|
|
217
|
-
1. **Screenshot-only monitoring** — MCP tools catch what screenshots miss
|
|
218
|
-
2. **Trusting DONE without verification** — Always verify deliverables
|
|
219
|
-
3. **Blind dispatching** — Watch terminals work, intervene when drifting
|
|
220
|
-
4. **Status-only monitoring** — Read `get_terminal_log`, not just status
|
|
221
|
-
5. **Single-threaded thinking** — Use multiple terminals in parallel
|
|
222
|
-
6. **Vague dispatches** — Give specific instructions with context
|
|
223
|
-
7. **Ignoring errors** — Every error in `get_terminal_log` needs attention
|
|
224
|
-
8. **Re-dispatching without context** — After compaction, re-orient fully
|
|
225
|
-
|
|
226
|
-
---
|
|
227
|
-
|
|
228
|
-
## MCP Tool Reference
|
|
229
|
-
|
|
230
|
-
### Monitoring Tools
|
|
231
|
-
```
|
|
232
|
-
list_terminals()
|
|
233
|
-
→ [{id, label, status, elapsed, contextPct, taskName}, ...]
|
|
234
|
-
|
|
235
|
-
get_terminal_status(id)
|
|
236
|
-
→ {id, label, status, elapsed, contextPct, taskName, progress, scope, cwd}
|
|
237
|
-
|
|
238
|
-
get_terminal_log(id)
|
|
239
|
-
→ [{ts, type, terminal, msg, meta}, ...]
|
|
240
|
-
→ types: status, progress, tool, error, need, build, insight
|
|
241
|
-
|
|
242
|
-
get_terminal_output(id, lines=50, offset=0)
|
|
243
|
-
→ {lines: [...], offset, count}
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Action Tools
|
|
247
|
-
```
|
|
248
|
-
send_input(id, text)
|
|
249
|
-
→ Sends text to terminal (auto-injects learned guidance)
|
|
250
|
-
|
|
251
|
-
assign_task(id, name, description, scope)
|
|
252
|
-
→ Assigns named task, updates tracking, sends description as input
|
|
253
|
-
|
|
254
|
-
spawn_terminal(label, scope, cwd, tier)
|
|
255
|
-
→ Creates new terminal
|
|
256
|
-
|
|
257
|
-
restart_terminal(id)
|
|
258
|
-
→ Restarts terminal with same config
|
|
259
|
-
|
|
260
|
-
kill_terminal(id)
|
|
261
|
-
→ Graceful shutdown (SIGINT → SIGTERM → SIGKILL)
|
|
262
|
-
|
|
263
|
-
set_label(id, label)
|
|
264
|
-
→ Rename terminal
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### Session Tools
|
|
268
|
-
```
|
|
269
|
-
get_session_info()
|
|
270
|
-
→ {tier, terminalsMax, features, terminals, createdAt}
|
|
271
|
-
|
|
272
|
-
finalize_session()
|
|
273
|
-
→ Triggers post-session: tool rating, hypothesis validation, playbook evolution
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## Startup Sequence
|
|
279
|
-
|
|
280
|
-
1. `list_terminals` — check all terminals alive
|
|
281
|
-
2. If any down → `restart_terminal(id)`
|
|
282
|
-
3. Decompose goal → criteria, paths, milestones, assignments
|
|
283
|
-
4. Present plan (3-5 bullets), get approval
|
|
284
|
-
5. Begin dispatching via `assign_task` or `send_input`
|
|
285
|
-
6. Start monitoring loop: MCP tools every 30-60s + occasional screenshots
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
## Safety
|
|
290
|
-
|
|
291
|
-
- Do NOT send money, make purchases, or create financial obligations without approval
|
|
292
|
-
- Do NOT send messages to people without approval
|
|
293
|
-
- Do NOT post public content without approval
|
|
294
|
-
- When in doubt, ask. The cost of asking is low.
|
|
5
|
+
This file exists for backwards compatibility. Read the canonical file instead.
|
package/public/app.js
CHANGED
|
@@ -47,6 +47,31 @@ const auth = {
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
|
|
50
|
+
async tryBootstrap() {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${API_BASE}/api/auth/bootstrap`);
|
|
53
|
+
if (!res.ok) return false;
|
|
54
|
+
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
if (!data.token) return false;
|
|
57
|
+
|
|
58
|
+
// Validate token format
|
|
59
|
+
const parts = data.token.split('.');
|
|
60
|
+
if (parts.length !== 3) return false;
|
|
61
|
+
|
|
62
|
+
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
63
|
+
if (payload.exp && payload.exp * 1000 < Date.now()) return false;
|
|
64
|
+
|
|
65
|
+
// Save to localStorage and use
|
|
66
|
+
localStorage.setItem(TOKEN_KEY, data.token);
|
|
67
|
+
this.token = data.token;
|
|
68
|
+
this.user = payload.sub || payload.email || payload.username || null;
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
50
75
|
async login(usernameOrEmail, password) {
|
|
51
76
|
const res = await fetch(`${AUTH_API}/auth/login`, {
|
|
52
77
|
method: 'POST',
|
|
@@ -317,6 +342,7 @@ const STATE_ICONS = {
|
|
|
317
342
|
waiting_approval: '\u26A0', // warning
|
|
318
343
|
starting: '\u25CB',
|
|
319
344
|
exited: '\u2717',
|
|
345
|
+
stuck: '\u26A0',
|
|
320
346
|
};
|
|
321
347
|
|
|
322
348
|
const STATE_LABELS = {
|
|
@@ -329,6 +355,26 @@ const STATE_LABELS = {
|
|
|
329
355
|
waiting_approval: 'APPROVAL',
|
|
330
356
|
starting: 'STARTING',
|
|
331
357
|
exited: 'EXITED',
|
|
358
|
+
stuck: 'STUCK',
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Task status (semantic, separate from process status)
|
|
362
|
+
const TASK_STATUS_ICONS = {
|
|
363
|
+
pending: '\u25CB', // ○
|
|
364
|
+
running: '\u25B7', // ▷
|
|
365
|
+
done: '\u2713', // ✓
|
|
366
|
+
blocked: '\u26A0', // ⚠
|
|
367
|
+
error: '\u2717', // ✗
|
|
368
|
+
unknown: '?',
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const TASK_STATUS_LABELS = {
|
|
372
|
+
pending: 'PENDING',
|
|
373
|
+
running: 'RUNNING',
|
|
374
|
+
done: 'DONE',
|
|
375
|
+
blocked: 'BLOCKED',
|
|
376
|
+
error: 'ERROR',
|
|
377
|
+
unknown: 'UNKNOWN',
|
|
332
378
|
};
|
|
333
379
|
|
|
334
380
|
// ── Utilities ────────────────────────────────────────────────
|
|
@@ -399,7 +445,7 @@ function createTerminalUI(termData) {
|
|
|
399
445
|
startLabelEdit(id, labelEl);
|
|
400
446
|
});
|
|
401
447
|
|
|
402
|
-
// State indicator
|
|
448
|
+
// State indicator (process status)
|
|
403
449
|
const stateEl = document.createElement('span');
|
|
404
450
|
stateEl.className = 'pane-state';
|
|
405
451
|
const stateIcon = document.createElement('span');
|
|
@@ -412,6 +458,23 @@ function createTerminalUI(termData) {
|
|
|
412
458
|
stateEl.appendChild(stateIcon);
|
|
413
459
|
stateEl.appendChild(stateText);
|
|
414
460
|
|
|
461
|
+
// Task status indicator (semantic status)
|
|
462
|
+
const taskStatusEl = document.createElement('span');
|
|
463
|
+
taskStatusEl.className = 'pane-task-status';
|
|
464
|
+
taskStatusEl.id = `task-status-${id}`;
|
|
465
|
+
const taskState = termData.taskStatus || 'pending';
|
|
466
|
+
const taskIcon = document.createElement('span');
|
|
467
|
+
taskIcon.className = `task-icon ${taskState}`;
|
|
468
|
+
taskIcon.id = `task-icon-${id}`;
|
|
469
|
+
taskIcon.textContent = TASK_STATUS_ICONS[taskState] || '?';
|
|
470
|
+
const taskText = document.createElement('span');
|
|
471
|
+
taskText.className = `task-text ${taskState}`;
|
|
472
|
+
taskText.id = `task-text-${id}`;
|
|
473
|
+
taskText.textContent = TASK_STATUS_LABELS[taskState] || 'PENDING';
|
|
474
|
+
taskStatusEl.appendChild(taskIcon);
|
|
475
|
+
taskStatusEl.appendChild(taskText);
|
|
476
|
+
taskStatusEl.title = termData.taskStatusMessage || '';
|
|
477
|
+
|
|
415
478
|
// Elapsed
|
|
416
479
|
const elapsedEl = document.createElement('span');
|
|
417
480
|
elapsedEl.className = 'pane-elapsed';
|
|
@@ -449,6 +512,7 @@ function createTerminalUI(termData) {
|
|
|
449
512
|
|
|
450
513
|
header.appendChild(labelEl);
|
|
451
514
|
header.appendChild(stateEl);
|
|
515
|
+
header.appendChild(taskStatusEl);
|
|
452
516
|
header.appendChild(elapsedEl);
|
|
453
517
|
header.appendChild(spacer);
|
|
454
518
|
header.appendChild(actionsEl);
|
|
@@ -525,6 +589,68 @@ function createTerminalUI(termData) {
|
|
|
525
589
|
if (ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
526
590
|
});
|
|
527
591
|
|
|
592
|
+
// Auto-press Enter after paste (for orchestrator browser automation)
|
|
593
|
+
// Only triggers on actual paste events, not fast typing
|
|
594
|
+
container.addEventListener('paste', () => {
|
|
595
|
+
setTimeout(() => {
|
|
596
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
597
|
+
ws.send('\r');
|
|
598
|
+
}
|
|
599
|
+
}, 150);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// ── Image Drag & Drop ─────────────────────────────────────────
|
|
603
|
+
// Prevent browser default (opening file in new tab)
|
|
604
|
+
pane.addEventListener('dragover', (e) => {
|
|
605
|
+
e.preventDefault();
|
|
606
|
+
e.stopPropagation();
|
|
607
|
+
pane.classList.add('drag-over');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
pane.addEventListener('dragleave', (e) => {
|
|
611
|
+
e.preventDefault();
|
|
612
|
+
e.stopPropagation();
|
|
613
|
+
pane.classList.remove('drag-over');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
pane.addEventListener('drop', async (e) => {
|
|
617
|
+
e.preventDefault();
|
|
618
|
+
e.stopPropagation();
|
|
619
|
+
pane.classList.remove('drag-over');
|
|
620
|
+
|
|
621
|
+
const files = e.dataTransfer?.files;
|
|
622
|
+
if (!files || files.length === 0) return;
|
|
623
|
+
|
|
624
|
+
const file = files[0];
|
|
625
|
+
term.write(`\r\n\x1b[36m[Uploading ${file.name}...]\x1b[0m`);
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
const formData = new FormData();
|
|
629
|
+
formData.append('file', file);
|
|
630
|
+
|
|
631
|
+
const res = await fetch('/api/upload', {
|
|
632
|
+
method: 'POST',
|
|
633
|
+
headers: auth.token ? { 'Authorization': `Bearer ${auth.token}` } : {},
|
|
634
|
+
body: formData,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
if (!res.ok) {
|
|
638
|
+
const err = await res.json().catch(() => ({}));
|
|
639
|
+
throw new Error(err.error || `Upload failed: ${res.status}`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const data = await res.json();
|
|
643
|
+
term.write(`\r\n\x1b[32m[File saved: ${data.path}]\x1b[0m\r\n`);
|
|
644
|
+
|
|
645
|
+
// Inject the path reference into the terminal for Claude to use
|
|
646
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
647
|
+
ws.send(`[File uploaded: ${data.path}]\r`);
|
|
648
|
+
}
|
|
649
|
+
} catch (err) {
|
|
650
|
+
term.write(`\r\n\x1b[31m[Upload failed: ${err.message}]\x1b[0m\r\n`);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
528
654
|
term.onResize(({ cols, rows }) => {
|
|
529
655
|
if (ws.readyState === WebSocket.OPEN) {
|
|
530
656
|
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
@@ -790,6 +916,40 @@ function fitAll() {
|
|
|
790
916
|
|
|
791
917
|
// ── Status Updates ───────────────────────────────────────────
|
|
792
918
|
|
|
919
|
+
function updateTaskStatus(id, taskState, message) {
|
|
920
|
+
const t = state.terminals.get(id);
|
|
921
|
+
if (!t) return;
|
|
922
|
+
|
|
923
|
+
t.taskStatus = taskState;
|
|
924
|
+
t.taskStatusMessage = message || '';
|
|
925
|
+
|
|
926
|
+
// Update task icon
|
|
927
|
+
const taskIcon = document.getElementById(`task-icon-${id}`);
|
|
928
|
+
if (taskIcon) {
|
|
929
|
+
taskIcon.className = `task-icon ${taskState}`;
|
|
930
|
+
taskIcon.textContent = TASK_STATUS_ICONS[taskState] || '?';
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Update task text
|
|
934
|
+
const taskText = document.getElementById(`task-text-${id}`);
|
|
935
|
+
if (taskText) {
|
|
936
|
+
taskText.className = `task-text ${taskState}`;
|
|
937
|
+
taskText.textContent = TASK_STATUS_LABELS[taskState] || taskState.toUpperCase();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Update tooltip
|
|
941
|
+
const taskStatusEl = document.getElementById(`task-status-${id}`);
|
|
942
|
+
if (taskStatusEl) {
|
|
943
|
+
taskStatusEl.title = message || '';
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Update pane border based on task status (prioritize task status for orchestration)
|
|
947
|
+
if (t.paneEl) {
|
|
948
|
+
t.paneEl.classList.remove('task-done', 'task-blocked', 'task-error', 'task-running');
|
|
949
|
+
t.paneEl.classList.add(`task-${taskState}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
793
953
|
function updateTerminalState(id, newStatus, extra) {
|
|
794
954
|
const t = state.terminals.get(id);
|
|
795
955
|
if (!t) return;
|
|
@@ -934,6 +1094,21 @@ function connectSSE() {
|
|
|
934
1094
|
} catch {}
|
|
935
1095
|
});
|
|
936
1096
|
|
|
1097
|
+
evtSource.addEventListener('task_status_change', (e) => {
|
|
1098
|
+
try {
|
|
1099
|
+
const data = JSON.parse(e.data);
|
|
1100
|
+
const { id, taskStatus } = data;
|
|
1101
|
+
if (taskStatus && taskStatus.state) {
|
|
1102
|
+
updateTaskStatus(id, taskStatus.state, taskStatus.message);
|
|
1103
|
+
|
|
1104
|
+
const t = state.terminals.get(id);
|
|
1105
|
+
const name = t ? t.label : `T${id}`;
|
|
1106
|
+
const stateLabel = TASK_STATUS_LABELS[taskStatus.state] || taskStatus.state;
|
|
1107
|
+
addFeedEntry(`${name} TASK: ${stateLabel}${taskStatus.message ? ` — ${taskStatus.message.slice(0, 50)}` : ''}`, id);
|
|
1108
|
+
}
|
|
1109
|
+
} catch {}
|
|
1110
|
+
});
|
|
1111
|
+
|
|
937
1112
|
evtSource.addEventListener('progress', (e) => {
|
|
938
1113
|
try {
|
|
939
1114
|
const data = JSON.parse(e.data);
|
|
@@ -1026,6 +1201,11 @@ async function pollStatus() {
|
|
|
1026
1201
|
taskName: item.taskName,
|
|
1027
1202
|
});
|
|
1028
1203
|
}
|
|
1204
|
+
|
|
1205
|
+
// Update task status if changed (SSE is primary, this is fallback)
|
|
1206
|
+
if (item.taskStatus && item.taskStatus !== t.taskStatus) {
|
|
1207
|
+
updateTaskStatus(item.id, item.taskStatus, item.taskStatusMessage);
|
|
1208
|
+
}
|
|
1029
1209
|
}
|
|
1030
1210
|
|
|
1031
1211
|
// Check for removed terminals
|
|
@@ -1247,9 +1427,22 @@ async function init() {
|
|
|
1247
1427
|
sessionReadyResolve();
|
|
1248
1428
|
});
|
|
1249
1429
|
} else {
|
|
1250
|
-
// No valid local token —
|
|
1251
|
-
|
|
1252
|
-
|
|
1430
|
+
// No valid local token — try bootstrap from saved server token
|
|
1431
|
+
const bootstrapped = await auth.tryBootstrap();
|
|
1432
|
+
if (bootstrapped) {
|
|
1433
|
+
hideAuthOverlay();
|
|
1434
|
+
startApp();
|
|
1435
|
+
auth.validateTier()
|
|
1436
|
+
.then(result => {
|
|
1437
|
+
if (result?.needsLogin) showAuthOverlay();
|
|
1438
|
+
})
|
|
1439
|
+
.catch(err => console.warn('Tier validation failed:', err))
|
|
1440
|
+
.finally(() => sessionReadyResolve());
|
|
1441
|
+
} else {
|
|
1442
|
+
// No saved token — show login
|
|
1443
|
+
showAuthOverlay();
|
|
1444
|
+
sessionReadyResolve();
|
|
1445
|
+
}
|
|
1253
1446
|
}
|
|
1254
1447
|
}
|
|
1255
1448
|
|