claude-simple 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/client.js +25 -1
  2. package/dist/app.0.2.0.min.js +868 -0
  3. package/dist/highlight.min.js +448 -430
  4. package/dist/index.html +2 -2
  5. package/dist/style.0.2.0.min.css +1182 -0
  6. package/package.json +1 -1
  7. package/public/agent-panel.js +44 -0
  8. package/public/api.js +61 -10
  9. package/public/app.js +4 -0
  10. package/public/chat.js +338 -31
  11. package/public/files.js +22 -30
  12. package/public/git.js +21 -17
  13. package/public/icons/icon-180.png +0 -0
  14. package/public/icons/icon-192.png +0 -0
  15. package/public/icons/icon-512.png +0 -0
  16. package/public/icons/icon-maskable-512.png +0 -0
  17. package/public/index.html +16 -3
  18. package/public/lib.js +7 -2
  19. package/public/manifest.webmanifest +36 -0
  20. package/public/offline.html +33 -0
  21. package/public/pwa.js +78 -0
  22. package/public/shell/boot.js +3 -0
  23. package/public/shell/hub.js +4 -2
  24. package/public/shell/notes.js +93 -31
  25. package/public/shell/session-list.js +29 -10
  26. package/public/shell/session-nav.js +7 -2
  27. package/public/shell/templates.js +11 -3
  28. package/public/shell.js +101 -8
  29. package/public/state.js +13 -2
  30. package/public/style.css +135 -3
  31. package/public/sw.js +104 -0
  32. package/public/terminal.js +22 -0
  33. package/public/wake-lock.js +45 -0
  34. package/src/agents/claude.js +108 -10
  35. package/src/agents/codex.js +1 -1
  36. package/src/agents/grok.js +40 -32
  37. package/src/agents/meta-agent.js +40 -1
  38. package/src/agents/stream-batch.js +62 -0
  39. package/src/app.js +304 -41
  40. package/src/runtime/approvals.js +67 -0
  41. package/src/runtime/outbox.js +143 -0
  42. package/src/runtime/protocol.js +224 -0
  43. package/dist/app.0.1.0.min.js +0 -825
  44. package/dist/style.0.1.0.min.css +0 -1050
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Low-pressure WebSocket outbox.
3
+ * - Coalesces assistant/thought deltas
4
+ * - Optionally packs several small events into one `batch` frame
5
+ * - Flushes on size, time, or non-mergeable event
6
+ */
7
+
8
+ import { UI_EVENT } from './protocol.js';
9
+
10
+ const DELTA_TYPES = new Set([
11
+ UI_EVENT.ASSISTANT_DELTA,
12
+ UI_EVENT.THOUGHT_DELTA,
13
+ 'shell-output',
14
+ ]);
15
+
16
+ /**
17
+ * @param {(data: object) => void} rawSend — must JSON-send one frame
18
+ * @param {object} [opts]
19
+ */
20
+ export function createOutbox(rawSend, opts = {}) {
21
+ const maxWait = opts.maxWait ?? 45;
22
+ const maxDeltaChars = opts.maxDeltaChars ?? 640;
23
+ const maxBatchItems = opts.maxBatchItems ?? 12;
24
+ const maxBatchBytes = opts.maxBatchBytes ?? 8_000;
25
+ const enableBatch = opts.enableBatch !== false;
26
+
27
+ /** @type {object | null} */
28
+ let pendingDelta = null;
29
+ /** @type {ReturnType<typeof setTimeout> | null} */
30
+ let deltaTimer = null;
31
+ /** @type {object[]} */
32
+ let batch = [];
33
+ let batchBytes = 0;
34
+ /** @type {ReturnType<typeof setTimeout> | null} */
35
+ let batchTimer = null;
36
+
37
+ function emitFrame(frame) {
38
+ if (!frame) return;
39
+ try {
40
+ rawSend(frame);
41
+ } catch { /* ignore closed socket */ }
42
+ }
43
+
44
+ function flushBatch() {
45
+ if (batchTimer) {
46
+ clearTimeout(batchTimer);
47
+ batchTimer = null;
48
+ }
49
+ if (!batch.length) return;
50
+ const items = batch;
51
+ batch = [];
52
+ batchBytes = 0;
53
+ if (items.length === 1) {
54
+ emitFrame(items[0]);
55
+ } else {
56
+ emitFrame({ type: UI_EVENT.BATCH, items });
57
+ }
58
+ }
59
+
60
+ function enqueue(frame) {
61
+ if (!enableBatch) {
62
+ emitFrame(frame);
63
+ return;
64
+ }
65
+ // Control / terminal events go out immediately (after draining batch)
66
+ const urgent = frame.type === 'error'
67
+ || frame.type === 'complete'
68
+ || frame.type === 'result'
69
+ || frame.type === 'permission_request'
70
+ || frame.type === 'session-created'
71
+ || frame.type === 'run-status'
72
+ || frame.type === 'abort-result'
73
+ || frame.type === 'shell-exit';
74
+ if (urgent) {
75
+ flushBatch();
76
+ emitFrame(frame);
77
+ return;
78
+ }
79
+
80
+ let size;
81
+ try {
82
+ size = JSON.stringify(frame).length;
83
+ } catch {
84
+ size = 256;
85
+ }
86
+ batch.push(frame);
87
+ batchBytes += size;
88
+ if (batch.length >= maxBatchItems || batchBytes >= maxBatchBytes) {
89
+ flushBatch();
90
+ return;
91
+ }
92
+ if (!batchTimer) batchTimer = setTimeout(flushBatch, maxWait);
93
+ }
94
+
95
+ function flushDelta() {
96
+ if (deltaTimer) {
97
+ clearTimeout(deltaTimer);
98
+ deltaTimer = null;
99
+ }
100
+ if (!pendingDelta) return;
101
+ const d = pendingDelta;
102
+ pendingDelta = null;
103
+ enqueue(d);
104
+ }
105
+
106
+ /**
107
+ * Accept a UI event (or null to drop).
108
+ * @param {object | null | undefined} event
109
+ */
110
+ function send(event) {
111
+ if (!event) return;
112
+
113
+ if (DELTA_TYPES.has(event.type)) {
114
+ // Merge consecutive deltas of same type (+ same agent/stream key)
115
+ const key = `${event.type}|${event.agent || ''}|${event.stream || ''}`;
116
+ if (pendingDelta) {
117
+ const pKey = `${pendingDelta.type}|${pendingDelta.agent || ''}|${pendingDelta.stream || ''}`;
118
+ if (pKey === key) {
119
+ pendingDelta.data = (pendingDelta.data || '') + (event.data || '');
120
+ if ((pendingDelta.data || '').length >= maxDeltaChars) flushDelta();
121
+ else if (!deltaTimer) deltaTimer = setTimeout(flushDelta, maxWait);
122
+ return;
123
+ }
124
+ flushDelta();
125
+ }
126
+ pendingDelta = { ...event, data: event.data || '' };
127
+ if ((pendingDelta.data || '').length >= maxDeltaChars) flushDelta();
128
+ else if (!deltaTimer) deltaTimer = setTimeout(flushDelta, maxWait);
129
+ return;
130
+ }
131
+
132
+ // Non-delta: flush coalesced text first so order stays correct
133
+ flushDelta();
134
+ enqueue(event);
135
+ }
136
+
137
+ function flush() {
138
+ flushDelta();
139
+ flushBatch();
140
+ }
141
+
142
+ return { send, flush };
143
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Typed control-plane commands + canonical runtime events.
3
+ * Inspired by T3's orchestration/adapters split, kept intentionally thin.
4
+ *
5
+ * Client → server: Command
6
+ * Server → client: RuntimeEvent (optionally wrapped in `batch`)
7
+ */
8
+
9
+ /** @typedef {'turn.start'|'turn.interrupt'|'approval.respond'|'shell.exec'|'session.subscribe'} CommandType */
10
+
11
+ /**
12
+ * Normalize any inbound WS message into a command, or null if not a control command.
13
+ * Keeps legacy aliases working (agent-command, abort-session, …).
14
+ */
15
+ export function normalizeCommand(msg) {
16
+ if (!msg || typeof msg !== 'object') return null;
17
+
18
+ // New shape: { type: 'command', cmd: 'turn.start', ... }
19
+ if (msg.type === 'command' && msg.cmd) {
20
+ return {
21
+ cmd: msg.cmd,
22
+ id: msg.id || null,
23
+ agent: msg.agent || msg.options?.agent || 'claude',
24
+ command: msg.command ?? msg.prompt ?? '',
25
+ options: msg.options || {},
26
+ sessionId: msg.sessionId || msg.options?.sessionId || null,
27
+ requestId: msg.requestId || null,
28
+ allow: msg.allow,
29
+ always: msg.always,
30
+ cwd: msg.cwd || msg.options?.cwd || null,
31
+ };
32
+ }
33
+
34
+ if (msg.type === 'claude-command' || msg.type === 'agent-command') {
35
+ return {
36
+ cmd: 'turn.start',
37
+ id: msg.id || null,
38
+ agent: msg.agent || msg.options?.agent || 'claude',
39
+ command: msg.command || '',
40
+ options: msg.options || {},
41
+ sessionId: msg.options?.sessionId || null,
42
+ requestId: null,
43
+ allow: undefined,
44
+ always: false,
45
+ cwd: msg.options?.cwd || null,
46
+ };
47
+ }
48
+
49
+ if (msg.type === 'abort-session') {
50
+ return {
51
+ cmd: 'turn.interrupt',
52
+ id: msg.id || null,
53
+ agent: null,
54
+ command: '',
55
+ options: {},
56
+ sessionId: msg.sessionId || null,
57
+ requestId: null,
58
+ allow: undefined,
59
+ always: false,
60
+ cwd: null,
61
+ };
62
+ }
63
+
64
+ if (msg.type === 'claude-permission-response' || msg.type === 'approval.respond') {
65
+ return {
66
+ cmd: 'approval.respond',
67
+ id: msg.id || null,
68
+ agent: null,
69
+ command: '',
70
+ options: {},
71
+ sessionId: msg.sessionId || null,
72
+ requestId: msg.requestId || msg.request_id,
73
+ allow: msg.allow !== false && msg.allow !== 0 && msg.allow !== '0',
74
+ always: !!msg.always,
75
+ cwd: null,
76
+ };
77
+ }
78
+
79
+ if (msg.type === 'shell-command') {
80
+ return {
81
+ cmd: 'shell.exec',
82
+ id: msg.id || null,
83
+ agent: null,
84
+ command: msg.command || '',
85
+ options: {},
86
+ sessionId: null,
87
+ requestId: null,
88
+ allow: undefined,
89
+ always: false,
90
+ cwd: msg.cwd || null,
91
+ };
92
+ }
93
+
94
+ if (msg.type === 'subscribe') {
95
+ return {
96
+ cmd: 'session.subscribe',
97
+ id: msg.id || null,
98
+ agent: null,
99
+ command: '',
100
+ options: {},
101
+ sessionId: msg.sessionId || null,
102
+ requestId: null,
103
+ allow: undefined,
104
+ always: false,
105
+ cwd: null,
106
+ };
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ /** Event types the UI understands today (kept stable for back-compat). */
113
+ export const UI_EVENT = {
114
+ SESSION_CREATED: 'session-created',
115
+ ASSISTANT: 'assistant',
116
+ ASSISTANT_STREAM_START: 'assistant_stream_start',
117
+ ASSISTANT_DELTA: 'assistant_delta',
118
+ ASSISTANT_STREAM_END: 'assistant_stream_end',
119
+ THOUGHT_STREAM_START: 'thought_stream_start',
120
+ THOUGHT_DELTA: 'thought_delta',
121
+ THOUGHT_STREAM_END: 'thought_stream_end',
122
+ RESULT: 'result',
123
+ COMPLETE: 'complete',
124
+ ERROR: 'error',
125
+ STATUS: 'status',
126
+ PERMISSION_REQUEST: 'permission_request',
127
+ PERMISSION_RESOLVED: 'permission_resolved',
128
+ SHELL_START: 'shell-start',
129
+ SHELL_OUTPUT: 'shell-output',
130
+ SHELL_EXIT: 'shell-exit',
131
+ RUN_STATUS: 'run-status',
132
+ ABORT_RESULT: 'abort-result',
133
+ BATCH: 'batch',
134
+ PING: 'ping',
135
+ PONG: 'pong',
136
+ };
137
+
138
+ /**
139
+ * Compact tool_use inputs before they hit the wire (mobile tunnel cost).
140
+ * @param {unknown} input
141
+ * @param {number} [max=1200]
142
+ */
143
+ export function compactToolInput(input, max = 1200) {
144
+ if (input == null) return input;
145
+ let raw;
146
+ try {
147
+ raw = typeof input === 'string' ? input : JSON.stringify(input);
148
+ } catch {
149
+ raw = String(input);
150
+ }
151
+ if (raw.length <= max) return input;
152
+ return { _truncated: true, preview: raw.slice(0, max), bytes: raw.length };
153
+ }
154
+
155
+ /**
156
+ * Map a loose agent emit into a UI event, dropping unknown SDK noise.
157
+ * Returns null to drop the frame entirely.
158
+ */
159
+ export function mapAgentEmit(data, { agent, compactTools = true } = {}) {
160
+ if (!data || typeof data !== 'object') return null;
161
+ const t = data.type;
162
+
163
+ // Already a UI event we know
164
+ const known = new Set([
165
+ 'session-created', 'assistant', 'assistant_stream_start', 'assistant_delta',
166
+ 'assistant_stream_end', 'thought_stream_start', 'thought_delta', 'thought_stream_end',
167
+ 'result', 'complete', 'error', 'status', 'permission_request', 'permission_resolved',
168
+ 'shell-start', 'shell-output', 'shell-exit', 'run-status', 'abort-result',
169
+ ]);
170
+ if (known.has(t)) {
171
+ const out = { ...data };
172
+ if (agent && !out.agent) out.agent = agent;
173
+ if (compactTools && out.type === 'assistant' && Array.isArray(out.message?.content)) {
174
+ out.message = {
175
+ ...out.message,
176
+ content: out.message.content.map((b) => {
177
+ if (b?.type === 'tool_use' && b.input != null) {
178
+ return { ...b, input: compactToolInput(b.input) };
179
+ }
180
+ return b;
181
+ }),
182
+ };
183
+ }
184
+ return out;
185
+ }
186
+
187
+ // Claude Agent SDK message shapes → UI
188
+ if (t === 'system' && data.session_id) {
189
+ return { type: 'session-created', sessionId: data.session_id, agent: agent || 'claude' };
190
+ }
191
+
192
+ if (t === 'assistant' && data.message) {
193
+ // SDK assistant message — keep but compact tools
194
+ return mapAgentEmit({ type: 'assistant', message: data.message, ts: data.ts }, { agent, compactTools });
195
+ }
196
+
197
+ if (t === 'result') {
198
+ return {
199
+ type: 'result',
200
+ is_error: !!data.is_error,
201
+ usage: data.usage || null,
202
+ sessionId: data.session_id || data.sessionId || null,
203
+ agent: agent || data.agent || 'claude',
204
+ result: typeof data.result === 'string' && data.result.length > 4000
205
+ ? data.result.slice(0, 4000) + '…'
206
+ : data.result,
207
+ };
208
+ }
209
+
210
+ // Drop high-chatter / unused SDK frames (stream_event, user, tool_progress, auth, …)
211
+ if (
212
+ t === 'stream_event'
213
+ || t === 'user'
214
+ || t === 'tool_progress'
215
+ || t === 'auth_status'
216
+ || t === 'rate_limit_event'
217
+ || t === 'system' // non-session system notices
218
+ ) {
219
+ return null;
220
+ }
221
+
222
+ // Unknown — drop to cut noise (was previously forwarded wholesale from Claude)
223
+ return null;
224
+ }