@hellix-ai/cubes 0.1.4 → 0.1.5
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 +9 -1
- package/dist/attach.js +53 -5
- package/dist/cli.js +7 -2
- package/dist/termstatus.js +275 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,14 @@ Every key goes to the session, exactly as if Claude Code were running locally, e
|
|
|
28
28
|
takes it back, the terminal says so and goes read-only; detach and attach again to reclaim it.
|
|
29
29
|
`cubes attach --view` watches without taking it.
|
|
30
30
|
|
|
31
|
+
**Your tab shows the session.** The terminal tab is titled after the session, with its state in front,
|
|
32
|
+
the way Claude Code does it locally: a spinner while it works, `✳` when idle, `✋` when it is waiting
|
|
33
|
+
for you. The moment it starts waiting, the terminal also gets a bell (most terminals mark the tab or
|
|
34
|
+
bounce the dock) and a desktop notification with Claude's question (iTerm2, Ghostty, WezTerm, kitty and
|
|
35
|
+
others). In Warp, the same events Warp's own Claude Code plugin sends drive its tab status and
|
|
36
|
+
notifications instead. Muted sessions stay quiet. Your own title comes back when you detach.
|
|
37
|
+
`--no-status` turns all of it off.
|
|
38
|
+
|
|
31
39
|
A stopped or hibernated session is resumed before attaching (`--no-resume` to refuse), with the
|
|
32
40
|
spinning Cubes mark on screen until it is up. A dropped connection reconnects on its own and replays
|
|
33
41
|
the screen.
|
|
@@ -43,7 +51,7 @@ cubes logout
|
|
|
43
51
|
cubes whoami
|
|
44
52
|
cubes org [ALIAS]
|
|
45
53
|
cubes ls [--mine] [--live] [--all] [--json]
|
|
46
|
-
cubes attach [SESSION] [--view] [--no-resume]
|
|
54
|
+
cubes attach [SESSION] [--view] [--no-resume] [--no-status]
|
|
47
55
|
cubes upgrade [--check]
|
|
48
56
|
```
|
|
49
57
|
|
package/dist/attach.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// on a read-only tile. The writer also decides the pane's size, so the session re-fits to THIS window.
|
|
15
15
|
import { ApiError } from "./api.js";
|
|
16
16
|
import { InputFilter, PASTE_IMAGE_BYTE } from "./keys.js";
|
|
17
|
+
import { StreamFilter, TermStatus, TITLE_POP, TITLE_PUSH } from "./termstatus.js";
|
|
17
18
|
export const TTY_SUBPROTOCOL = "cube.tty.v1";
|
|
18
19
|
const OPEN = 1;
|
|
19
20
|
/** Close codes (server/src/engine/tty.ts, web/src/cubes/Terminal.tsx). */
|
|
@@ -59,6 +60,45 @@ export function attach(deps, session, opts = {}) {
|
|
|
59
60
|
// an image paste is uploading (so what is typed after Ctrl-V lands after the path, not before).
|
|
60
61
|
let pending = [];
|
|
61
62
|
let pasting = false;
|
|
63
|
+
// Our own sequences (title, bell, notifications) wait here until the session's stream is between
|
|
64
|
+
// escape sequences - see termstatus.ts. A fresh filter per connection: a replay starts clean.
|
|
65
|
+
let stream = new StreamFilter();
|
|
66
|
+
let injections = [];
|
|
67
|
+
let status = null;
|
|
68
|
+
let pollTimer = null;
|
|
69
|
+
let spinTimer = null;
|
|
70
|
+
const inject = (seq) => {
|
|
71
|
+
if (stream.safe)
|
|
72
|
+
stdout.write(seq);
|
|
73
|
+
else
|
|
74
|
+
injections.push(seq);
|
|
75
|
+
};
|
|
76
|
+
const flushInjections = () => {
|
|
77
|
+
if (!injections.length || !stream.safe)
|
|
78
|
+
return;
|
|
79
|
+
stdout.write(injections.join(""));
|
|
80
|
+
injections = [];
|
|
81
|
+
};
|
|
82
|
+
const startStatus = () => {
|
|
83
|
+
if (opts.status === false || status)
|
|
84
|
+
return;
|
|
85
|
+
const cfg = opts.status ?? {};
|
|
86
|
+
stdout.write(TITLE_PUSH);
|
|
87
|
+
status = new TermStatus({ status: "running", ...session }, inject, cfg.env);
|
|
88
|
+
status.update({ status: "running", ...session });
|
|
89
|
+
let inFlight = false;
|
|
90
|
+
pollTimer = setInterval(() => {
|
|
91
|
+
if (inFlight || done)
|
|
92
|
+
return;
|
|
93
|
+
inFlight = true;
|
|
94
|
+
api.get(`/sessions/${encodeURIComponent(session.id)}`)
|
|
95
|
+
.then((row) => { if (!done)
|
|
96
|
+
status?.update(row); })
|
|
97
|
+
.catch(() => { })
|
|
98
|
+
.finally(() => { inFlight = false; });
|
|
99
|
+
}, cfg.pollMs ?? 3000);
|
|
100
|
+
spinTimer = setInterval(() => { status?.tick(); flushInjections(); }, cfg.spinMs ?? 250);
|
|
101
|
+
};
|
|
62
102
|
const note = (msg) => {
|
|
63
103
|
stderr.write(entered ? `\r\n\x1b[2m[cubes] ${msg}\x1b[0m\r\n` : `[cubes] ${msg}\n`);
|
|
64
104
|
};
|
|
@@ -69,6 +109,7 @@ export function attach(deps, session, opts = {}) {
|
|
|
69
109
|
if (stdin.isTTY)
|
|
70
110
|
stdin.setRawMode?.(true);
|
|
71
111
|
stdout.write(ENTER_ALT);
|
|
112
|
+
startStatus();
|
|
72
113
|
};
|
|
73
114
|
const finish = (code, message) => {
|
|
74
115
|
if (done)
|
|
@@ -76,6 +117,10 @@ export function attach(deps, session, opts = {}) {
|
|
|
76
117
|
done = true;
|
|
77
118
|
if (timer)
|
|
78
119
|
clearTimeout(timer);
|
|
120
|
+
if (pollTimer)
|
|
121
|
+
clearInterval(pollTimer);
|
|
122
|
+
if (spinTimer)
|
|
123
|
+
clearInterval(spinTimer);
|
|
79
124
|
stdin.off("data", onData);
|
|
80
125
|
stdout.off("resize", onResize);
|
|
81
126
|
const sock = ws;
|
|
@@ -89,7 +134,7 @@ export function attach(deps, session, opts = {}) {
|
|
|
89
134
|
catch { /* already gone */ }
|
|
90
135
|
}
|
|
91
136
|
if (entered) {
|
|
92
|
-
stdout.write(RESTORE_TERMINAL + LEAVE_ALT);
|
|
137
|
+
stdout.write(RESTORE_TERMINAL + (status ? TITLE_POP : "") + LEAVE_ALT);
|
|
93
138
|
if (stdin.isTTY)
|
|
94
139
|
stdin.setRawMode?.(false);
|
|
95
140
|
}
|
|
@@ -247,6 +292,7 @@ export function attach(deps, session, opts = {}) {
|
|
|
247
292
|
const sock = deps.openWs(wsUrlFor(deps.host, session.id), [TTY_SUBPROTOCOL, ticket]);
|
|
248
293
|
sock.binaryType = "arraybuffer";
|
|
249
294
|
ws = sock;
|
|
295
|
+
stream = new StreamFilter();
|
|
250
296
|
askedTakeover = false;
|
|
251
297
|
takeoverPending = false;
|
|
252
298
|
sock.onopen = () => {
|
|
@@ -296,10 +342,12 @@ export function attach(deps, session, opts = {}) {
|
|
|
296
342
|
}
|
|
297
343
|
return;
|
|
298
344
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
345
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data)
|
|
346
|
+
: ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : null;
|
|
347
|
+
if (!bytes)
|
|
348
|
+
return;
|
|
349
|
+
stdout.write(stream.feed(bytes));
|
|
350
|
+
flushInjections();
|
|
303
351
|
};
|
|
304
352
|
sock.onclose = (ev) => { if (ws === sock)
|
|
305
353
|
onClose(ev.code, ev.reason); };
|
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,8 @@ Usage:
|
|
|
27
27
|
cubes org [ALIAS] list your orgs, or switch the default
|
|
28
28
|
cubes ls [--mine] [--live] [--all] [--json] list sessions by group (--mine: you started them,
|
|
29
29
|
--live: running now, --all: include spawned ones)
|
|
30
|
-
cubes attach [SESSION] [--view] [--no-resume]
|
|
30
|
+
cubes attach [SESSION] [--view] [--no-resume] [--no-status]
|
|
31
|
+
attach to a session
|
|
31
32
|
cubes upgrade [--check] install the newest cubes (--check only reports)
|
|
32
33
|
|
|
33
34
|
SESSION is an id, the start of one (4+ characters), or part of its title. Left out, you pick from
|
|
@@ -40,6 +41,10 @@ While attached, every key goes to the session except:
|
|
|
40
41
|
Attaching takes the keyboard from any browser tab on the same session (it becomes read-only) and
|
|
41
42
|
fits the session to this window. --view watches without taking it.
|
|
42
43
|
|
|
44
|
+
The tab is titled after the session, with its state: a spinner while it works, ✳ when idle, ✋ when it
|
|
45
|
+
is waiting for you - and a bell plus a desktop notification the moment it starts waiting (in Warp, its
|
|
46
|
+
own agent status instead). --no-status turns all of that off.
|
|
47
|
+
|
|
43
48
|
Global: --org ALIAS overrides the default org for one command.
|
|
44
49
|
Env: CUBES_HOST + CUBES_TOKEN (+ CUBES_ORG) use a token instead of the stored login.
|
|
45
50
|
Config: ${configPath()}
|
|
@@ -262,7 +267,7 @@ async function cmdAttach(a) {
|
|
|
262
267
|
stdout: process.stdout,
|
|
263
268
|
stderr: process.stderr,
|
|
264
269
|
readClipboardImage: () => readClipboardImage(),
|
|
265
|
-
}, row, { view, signal: ac.signal });
|
|
270
|
+
}, row, { view, signal: ac.signal, status: a.flags.has("no-status") ? false : undefined });
|
|
266
271
|
process.stderr.write(`[cubes] ${result.message}\n`);
|
|
267
272
|
return result.code;
|
|
268
273
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Telling YOUR terminal what the attached session is doing: the tab title, a "needs you" signal, and
|
|
2
|
+
// Warp's per-tab agent status.
|
|
3
|
+
//
|
|
4
|
+
// Running Claude Code locally, a terminal tab shows its title and whether it is working or waiting.
|
|
5
|
+
// Attached through `cubes`, the program on the other end is tmux in a pod, which says nothing about
|
|
6
|
+
// either. So the CLI says it, from the server's own activity signal (the hooks every session reports:
|
|
7
|
+
// working / ready / waiting-with-a-reason), in the three dialects terminals understand:
|
|
8
|
+
//
|
|
9
|
+
// • OSC 0 TITLE - every terminal. `<glyph> <session title>`, the convention Claude Code itself uses:
|
|
10
|
+
// a spinner while working, ✳ when idle, ✋ when it is waiting for you.
|
|
11
|
+
// • BEL + OSC 9 - the generic "this tab wants you": a bell marks the tab / bounces the dock almost
|
|
12
|
+
// everywhere, and OSC 9 raises a desktop notification in iTerm2, Ghostty, WezTerm, kitty, Warp.
|
|
13
|
+
// • OSC 777 `warp://cli-agent` - Warp's structured agent protocol, the same events and payload as
|
|
14
|
+
// warpdotdev/claude-code-warp sends from Claude Code's hooks (the remote program IS Claude Code).
|
|
15
|
+
// Gated exactly as that plugin gates it, and used INSTEAD of BEL+OSC 9 there, not as well.
|
|
16
|
+
//
|
|
17
|
+
// ⚠ NOTHING HERE MAY BE WRITTEN MID-SEQUENCE. The session's output arrives in arbitrary chunks, so a
|
|
18
|
+
// chunk can end halfway through an escape sequence or a UTF-8 character; a title written into that gap
|
|
19
|
+
// would corrupt both. `StreamFilter` tracks where the stream is, and the caller only writes ours when
|
|
20
|
+
// the stream is between sequences.
|
|
21
|
+
export const WARP_PLUGIN_COMPAT = "2.3.0"; // the claude-code-warp release whose event set this mirrors
|
|
22
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
23
|
+
const IDLE = "✳";
|
|
24
|
+
const WAITING = "✋";
|
|
25
|
+
/** Title push / pop (xterm window ops): our title is set on top of yours and yours comes back after. */
|
|
26
|
+
export const TITLE_PUSH = "\x1b[22;0t";
|
|
27
|
+
export const TITLE_POP = "\x1b]0;\x07\x1b[23;0t"; // clear first, for terminals with no title stack
|
|
28
|
+
/** Nothing the server hands us may reach the terminal as a control character. */
|
|
29
|
+
export function clean(s, max = 120) {
|
|
30
|
+
const t = s.replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim();
|
|
31
|
+
return t.length > max ? t.slice(0, max - 1) + "…" : t;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Passes the session's output through, minus the two things that would fight us for the title -
|
|
35
|
+
* OSC 0/1/2 (set title) and CSI 22/23 t (title push/pop, which the session's tmux sends on attach and
|
|
36
|
+
* exit and which would unbalance our own push/pop) - and knows at every chunk boundary whether it is
|
|
37
|
+
* safe to write a sequence of our own.
|
|
38
|
+
*/
|
|
39
|
+
export class StreamFilter {
|
|
40
|
+
st = "ground";
|
|
41
|
+
seq = []; // the CSI or OSC-prefix being decided
|
|
42
|
+
utf8Left = 0;
|
|
43
|
+
oscEsc = false; // saw ESC inside a string, waiting for '\'
|
|
44
|
+
get safe() {
|
|
45
|
+
return this.st === "ground" && this.utf8Left === 0;
|
|
46
|
+
}
|
|
47
|
+
feed(chunk) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const b of chunk) {
|
|
50
|
+
switch (this.st) {
|
|
51
|
+
case "ground":
|
|
52
|
+
if (b === 0x1b) {
|
|
53
|
+
this.st = "esc";
|
|
54
|
+
this.seq = [b];
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (this.utf8Left > 0 && (b & 0xc0) === 0x80)
|
|
58
|
+
this.utf8Left--;
|
|
59
|
+
else if ((b & 0xe0) === 0xc0)
|
|
60
|
+
this.utf8Left = 1;
|
|
61
|
+
else if ((b & 0xf0) === 0xe0)
|
|
62
|
+
this.utf8Left = 2;
|
|
63
|
+
else if ((b & 0xf8) === 0xf0)
|
|
64
|
+
this.utf8Left = 3;
|
|
65
|
+
else
|
|
66
|
+
this.utf8Left = 0;
|
|
67
|
+
out.push(b);
|
|
68
|
+
break;
|
|
69
|
+
case "esc":
|
|
70
|
+
if (b === 0x1b) {
|
|
71
|
+
out.push(0x1b);
|
|
72
|
+
this.seq = [b];
|
|
73
|
+
break;
|
|
74
|
+
} // a bare ESC, then a new sequence
|
|
75
|
+
this.seq.push(b);
|
|
76
|
+
if (b === 0x5b) {
|
|
77
|
+
this.st = "csi";
|
|
78
|
+
break;
|
|
79
|
+
} // [
|
|
80
|
+
if (b === 0x5d) {
|
|
81
|
+
this.st = "osc";
|
|
82
|
+
break;
|
|
83
|
+
} // ]
|
|
84
|
+
if (b === 0x50 || b === 0x58 || b === 0x5e || b === 0x5f) { // P X ^ _ : DCS SOS PM APC
|
|
85
|
+
out.push(...this.seq);
|
|
86
|
+
this.st = "str";
|
|
87
|
+
this.oscEsc = false;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (b >= 0x20 && b <= 0x2f)
|
|
91
|
+
break; // intermediate bytes, the final follows
|
|
92
|
+
out.push(...this.seq);
|
|
93
|
+
this.st = "ground";
|
|
94
|
+
break;
|
|
95
|
+
case "csi":
|
|
96
|
+
this.seq.push(b);
|
|
97
|
+
if (b >= 0x40 && b <= 0x7e) {
|
|
98
|
+
const body = String.fromCharCode(...this.seq.slice(2, -1));
|
|
99
|
+
const drop = b === 0x74 && /^2[23](;|$)/.test(body); // CSI 22/23 ... t
|
|
100
|
+
if (!drop)
|
|
101
|
+
out.push(...this.seq);
|
|
102
|
+
this.st = "ground";
|
|
103
|
+
}
|
|
104
|
+
else if (this.seq.length > 64) { // not a CSI anyone sends; stop holding it
|
|
105
|
+
out.push(...this.seq);
|
|
106
|
+
this.st = "ground";
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case "osc": {
|
|
110
|
+
// Hold only until the number is known: 0/1/2 is a title and is dropped, anything else
|
|
111
|
+
// (hyperlinks, clipboard, notifications) streams through untouched.
|
|
112
|
+
const ends = b === 0x07 || b === 0x1b;
|
|
113
|
+
if (b === 0x3b || ends || this.seq.length > 8) {
|
|
114
|
+
const num = String.fromCharCode(...this.seq.slice(2));
|
|
115
|
+
const title = /^[012]$/.test(num);
|
|
116
|
+
if (!title)
|
|
117
|
+
out.push(...this.seq);
|
|
118
|
+
this.seq = [];
|
|
119
|
+
if (ends) {
|
|
120
|
+
if (b === 0x07) {
|
|
121
|
+
if (!title)
|
|
122
|
+
out.push(b);
|
|
123
|
+
this.st = "ground";
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (!title)
|
|
127
|
+
out.push(b);
|
|
128
|
+
this.st = title ? "oscDrop" : "str";
|
|
129
|
+
this.oscEsc = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
if (!title)
|
|
134
|
+
out.push(b);
|
|
135
|
+
this.st = title ? "oscDrop" : "str";
|
|
136
|
+
this.oscEsc = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this.seq.push(b);
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "oscDrop":
|
|
145
|
+
case "str": {
|
|
146
|
+
const keep = this.st === "str";
|
|
147
|
+
if (keep)
|
|
148
|
+
out.push(b);
|
|
149
|
+
if (this.oscEsc && b === 0x5c) {
|
|
150
|
+
this.st = "ground";
|
|
151
|
+
this.oscEsc = false;
|
|
152
|
+
break;
|
|
153
|
+
} // ST = ESC \
|
|
154
|
+
this.oscEsc = b === 0x1b;
|
|
155
|
+
if (b === 0x07 && !this.oscEsc)
|
|
156
|
+
this.st = "ground"; // BEL ends an OSC (and is harmless in DCS)
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return Uint8Array.from(out);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function stateOf(r) {
|
|
165
|
+
if (r.status !== "running")
|
|
166
|
+
return "idle";
|
|
167
|
+
if (r.activity_status === "working")
|
|
168
|
+
return "working";
|
|
169
|
+
if (r.activity_status === "waiting")
|
|
170
|
+
return "waiting";
|
|
171
|
+
return "idle";
|
|
172
|
+
}
|
|
173
|
+
/** Warp's structured notifications, exactly as warpdotdev/claude-code-warp decides it may use them. */
|
|
174
|
+
export function warpProtocol(env) {
|
|
175
|
+
const v = Number.parseInt(env.WARP_CLI_AGENT_PROTOCOL_VERSION ?? "", 10);
|
|
176
|
+
if (!Number.isFinite(v) || v < 1)
|
|
177
|
+
return null;
|
|
178
|
+
const client = env.WARP_CLIENT_VERSION ?? "";
|
|
179
|
+
if (!client)
|
|
180
|
+
return null;
|
|
181
|
+
// Builds that advertised the protocol before they could render it.
|
|
182
|
+
const broken = client.includes("stable") ? "v0.2026.03.25.08.24.stable_05"
|
|
183
|
+
: client.includes("preview") ? "v0.2026.03.25.08.24.preview_05" : "";
|
|
184
|
+
if (broken && !(client > broken))
|
|
185
|
+
return null;
|
|
186
|
+
return Math.min(1, v);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Turns successive reads of the session row into terminal sequences. `emit` must deliver them only at
|
|
190
|
+
* a safe point in the stream (see StreamFilter); this class only decides WHAT to say and WHEN.
|
|
191
|
+
*/
|
|
192
|
+
export class TermStatus {
|
|
193
|
+
emit;
|
|
194
|
+
state = null;
|
|
195
|
+
frame = 0;
|
|
196
|
+
shown = "";
|
|
197
|
+
row;
|
|
198
|
+
warp;
|
|
199
|
+
constructor(row, emit, env = process.env) {
|
|
200
|
+
this.emit = emit;
|
|
201
|
+
this.row = row;
|
|
202
|
+
this.warp = warpProtocol(env);
|
|
203
|
+
}
|
|
204
|
+
title() {
|
|
205
|
+
const glyph = this.state === "working" ? SPINNER[this.frame % SPINNER.length]
|
|
206
|
+
: this.state === "waiting" ? WAITING : IDLE;
|
|
207
|
+
return `${glyph} ${clean(this.row.title || "Cubes session", 80)}`;
|
|
208
|
+
}
|
|
209
|
+
/** Only when it changed: the activity is re-read every few seconds, and most reads change nothing. */
|
|
210
|
+
setTitle() {
|
|
211
|
+
const t = this.title();
|
|
212
|
+
if (t === this.shown)
|
|
213
|
+
return;
|
|
214
|
+
this.shown = t;
|
|
215
|
+
this.emit(`\x1b]0;${t}\x07`);
|
|
216
|
+
}
|
|
217
|
+
warpEvent(event, extra = {}) {
|
|
218
|
+
if (this.warp === null)
|
|
219
|
+
return;
|
|
220
|
+
const body = JSON.stringify({
|
|
221
|
+
v: this.warp, agent: "claude", event, session_id: this.row.id, cwd: "",
|
|
222
|
+
project: clean(this.row.title || "cubes", 60), ...extra,
|
|
223
|
+
});
|
|
224
|
+
this.emit(`\x1b]777;notify;warp://cli-agent;${body}\x07`);
|
|
225
|
+
}
|
|
226
|
+
/** Feed the latest row. The first call only establishes the state - nobody needs to be told that a
|
|
227
|
+
* session they are attaching to right now is waiting for them. */
|
|
228
|
+
update(row) {
|
|
229
|
+
this.row = row;
|
|
230
|
+
const next = stateOf(row);
|
|
231
|
+
const first = this.state === null;
|
|
232
|
+
const prev = this.state;
|
|
233
|
+
// Only the step INTO waiting. Claude Code reports one wait in several hook events (measured: the
|
|
234
|
+
// question itself, then "Claude needs your permission" for the same question a moment later), and
|
|
235
|
+
// each moves activity_at, so keying on it rang twice for one thing to answer.
|
|
236
|
+
const newWait = next === "waiting" && prev !== "waiting";
|
|
237
|
+
this.state = next;
|
|
238
|
+
this.setTitle();
|
|
239
|
+
if (first) {
|
|
240
|
+
this.warpEvent("session_start", { plugin_version: WARP_PLUGIN_COMPAT });
|
|
241
|
+
if (next === "working")
|
|
242
|
+
this.warpEvent("prompt_submit", { query: "" });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (next === "working" && prev !== "working")
|
|
246
|
+
this.warpEvent("prompt_submit", { query: "" });
|
|
247
|
+
if (next === "idle" && prev === "working") {
|
|
248
|
+
this.warpEvent("stop", { query: "", response: clean(row.activity_message ?? "", 200), transcript_path: "" });
|
|
249
|
+
}
|
|
250
|
+
if (newWait) {
|
|
251
|
+
const summary = clean(row.activity_message || "Waiting for you", 120);
|
|
252
|
+
if (this.warp !== null) {
|
|
253
|
+
if (row.activity_reason === "permission") {
|
|
254
|
+
this.warpEvent("permission_request", { summary, tool_name: "", tool_input: {} });
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.warpEvent("idle_prompt", { summary });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (!row.muted_at) {
|
|
261
|
+
// Bell first (marks the tab nearly everywhere), then the desktop notification where supported.
|
|
262
|
+
// "Cubes" leads the text so it can never be read as ConEmu's numeric OSC 9 sub-commands.
|
|
263
|
+
this.emit(`\x07\x1b]9;Cubes · ${clean(row.title || "session", 60)}: ${summary}\x07`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Advance the working spinner. Returns false when there is nothing to animate. */
|
|
268
|
+
tick() {
|
|
269
|
+
if (this.state !== "working")
|
|
270
|
+
return false;
|
|
271
|
+
this.frame++;
|
|
272
|
+
this.setTitle();
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|