@mlpal/yodex-ui 0.0.0-stage → 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.
package/dist/index.js ADDED
@@ -0,0 +1,1598 @@
1
+ // src/client/client.ts
2
+ class YodexClient {
3
+ socket = null;
4
+ closedByUser = false;
5
+ attempt = 0;
6
+ reconnectHandle = null;
7
+ pending = new Map;
8
+ opts;
9
+ seq = 0;
10
+ state = "closed";
11
+ constructor(options) {
12
+ this.opts = {
13
+ backoffMs: { min: 500, max: 1e4 },
14
+ requestTimeoutMs: 15000,
15
+ socketFactory: (url) => new WebSocket(url),
16
+ setTimer: (fn, ms) => setTimeout(fn, ms),
17
+ clearTimer: (h) => clearTimeout(h),
18
+ random: Math.random,
19
+ ...options
20
+ };
21
+ }
22
+ get url() {
23
+ const u = this.opts.url;
24
+ if (!this.opts.token)
25
+ return u;
26
+ return `${u}${u.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.opts.token)}`;
27
+ }
28
+ connect() {
29
+ this.closedByUser = false;
30
+ this.open();
31
+ }
32
+ close() {
33
+ this.closedByUser = true;
34
+ if (this.reconnectHandle)
35
+ this.opts.clearTimer(this.reconnectHandle);
36
+ this.reconnectHandle = null;
37
+ this.socket?.close();
38
+ this.socket = null;
39
+ this.failPending(new Error("client closed"));
40
+ this.setState("closed");
41
+ }
42
+ setState(s) {
43
+ if (this.state === s)
44
+ return;
45
+ this.state = s;
46
+ this.opts.onState?.(s);
47
+ }
48
+ open() {
49
+ this.setState(this.attempt === 0 ? "connecting" : "reconnecting");
50
+ const ws = this.opts.socketFactory(this.url);
51
+ this.socket = ws;
52
+ ws.onopen = () => {
53
+ this.attempt = 0;
54
+ };
55
+ ws.onmessage = (ev) => {
56
+ let m;
57
+ try {
58
+ m = JSON.parse(String(ev.data));
59
+ } catch {
60
+ return;
61
+ }
62
+ if (m.type === "ready")
63
+ this.setState("connected");
64
+ const rid = m.requestId;
65
+ if (rid && this.pending.has(rid) && m.type !== "done") {
66
+ const p = this.pending.get(rid);
67
+ this.pending.delete(rid);
68
+ this.opts.clearTimer(p.timer);
69
+ p.resolve(m);
70
+ }
71
+ this.opts.onMessage(m);
72
+ };
73
+ ws.onerror = () => {};
74
+ ws.onclose = () => {
75
+ if (this.socket !== ws)
76
+ return;
77
+ this.socket = null;
78
+ this.failPending(new Error("connection lost"));
79
+ if (this.closedByUser)
80
+ return;
81
+ this.scheduleReconnect();
82
+ };
83
+ }
84
+ scheduleReconnect() {
85
+ const { min, max } = this.opts.backoffMs;
86
+ const base = Math.min(max, min * 2 ** this.attempt);
87
+ const delay = Math.round(base * (0.5 + this.opts.random() * 0.5));
88
+ this.attempt += 1;
89
+ this.setState("reconnecting");
90
+ this.reconnectHandle = this.opts.setTimer(() => {
91
+ this.reconnectHandle = null;
92
+ if (!this.closedByUser)
93
+ this.open();
94
+ }, delay);
95
+ }
96
+ failPending(e) {
97
+ for (const p of this.pending.values()) {
98
+ this.opts.clearTimer(p.timer);
99
+ p.reject(e);
100
+ }
101
+ this.pending.clear();
102
+ }
103
+ get connected() {
104
+ return this.state === "connected" && this.socket !== null;
105
+ }
106
+ send(m) {
107
+ if (!this.socket || this.state !== "connected")
108
+ throw new Error("not connected");
109
+ this.socket.send(JSON.stringify(m));
110
+ }
111
+ requestId() {
112
+ return `${Date.now().toString(36)}-${(++this.seq).toString(36)}`;
113
+ }
114
+ request(m, _expect) {
115
+ return new Promise((resolve, reject) => {
116
+ const timer = this.opts.setTimer(() => {
117
+ this.pending.delete(m.requestId);
118
+ reject(new Error(`${m.type}: no reply within ${this.opts.requestTimeoutMs} ms`));
119
+ }, this.opts.requestTimeoutMs);
120
+ this.pending.set(m.requestId, { resolve: (r) => resolve(r), reject, timer });
121
+ try {
122
+ this.send(m);
123
+ } catch (e) {
124
+ this.pending.delete(m.requestId);
125
+ this.opts.clearTimer(timer);
126
+ reject(e);
127
+ }
128
+ });
129
+ }
130
+ run(prompt, options = {}) {
131
+ const requestId = this.requestId();
132
+ this.send({ type: "run", requestId, prompt, ...options });
133
+ return requestId;
134
+ }
135
+ interrupt() {
136
+ this.send({ type: "interrupt" });
137
+ }
138
+ sendPeer(sessionId, text) {
139
+ this.send({ type: "send", sessionId, text });
140
+ }
141
+ respondPermission(id, decision, reason) {
142
+ this.send({ type: "permission_response", id, decision, ...reason ? { reason } : {} });
143
+ }
144
+ respondQuestion(id, outcome) {
145
+ this.send({ type: "question_response", id, outcome });
146
+ }
147
+ respondApproval(sessionId, decision, reason) {
148
+ this.send({ type: "approval_response", sessionId, decision, ...reason ? { reason } : {} });
149
+ }
150
+ listSessions() {
151
+ return this.request({ type: "list_sessions", requestId: this.requestId() }, "sessions");
152
+ }
153
+ listModels() {
154
+ return this.request({ type: "list_models", requestId: this.requestId() }, "models");
155
+ }
156
+ listSkills() {
157
+ return this.request({ type: "list_skills", requestId: this.requestId() }, "skills");
158
+ }
159
+ loadSession(sessionId) {
160
+ return this.request({ type: "load_session", requestId: this.requestId(), sessionId }, "session_history");
161
+ }
162
+ }
163
+ // src/client/reducer.ts
164
+ import { addUsage, emptyUsage, HUMAN } from "@mlpal/harness-protocol";
165
+ function initialState() {
166
+ return {
167
+ connection: "connecting",
168
+ capabilities: [],
169
+ items: [],
170
+ running: false,
171
+ usageTotal: emptyUsage(),
172
+ pendingPermissions: [],
173
+ sessions: [],
174
+ models: [],
175
+ skills: []
176
+ };
177
+ }
178
+ var TASK_TOOLS = new Set(["TaskCreate", "TaskUpdate", "TaskList"]);
179
+ var seq = 0;
180
+ var nextId = (prefix) => `${prefix}-${++seq}`;
181
+ function textOf(content) {
182
+ if (typeof content === "string")
183
+ return content;
184
+ if (!Array.isArray(content))
185
+ return "";
186
+ return content.map((b) => b && typeof b === "object" && b.type === "text" ? String(b.text ?? "") : "").filter(Boolean).join(`
187
+ `);
188
+ }
189
+ function replaceOrAppendPlan(items, plan) {
190
+ const idx = items.findIndex((i) => i.kind === "plan");
191
+ const row = { kind: "plan", id: "plan", items: plan };
192
+ if (idx === -1)
193
+ return [...items, row];
194
+ const out = items.slice();
195
+ out[idx] = row;
196
+ return out;
197
+ }
198
+ function currentPlan(items) {
199
+ const row = items.find((i) => i.kind === "plan");
200
+ return row && row.kind === "plan" ? row.items : [];
201
+ }
202
+ function dropDraft(items) {
203
+ const last = items[items.length - 1];
204
+ return last && last.kind === "assistant" && last.live ? items.slice(0, -1) : items;
205
+ }
206
+ function applyEvent(state, ev, live) {
207
+ switch (ev.type) {
208
+ case "system":
209
+ return { ...state, sessionId: ev.sessionId, model: state.model ?? ev.model, cwd: ev.cwd };
210
+ case "user": {
211
+ if (live && ev.author.type === "human")
212
+ return state;
213
+ const text = textOf(ev.message.content);
214
+ const blocks = Array.isArray(ev.message.content) ? ev.message.content.filter((b) => b.type !== "text") : undefined;
215
+ return {
216
+ ...state,
217
+ items: [...dropDraft(state.items), { kind: "user", id: nextId("u"), text, author: ev.author, ts: ev.ts, ...blocks?.length ? { blocks } : {} }]
218
+ };
219
+ }
220
+ case "text_delta": {
221
+ const items = state.items.slice();
222
+ const last = items[items.length - 1];
223
+ if (last && last.kind === "assistant" && last.live)
224
+ items[items.length - 1] = { ...last, text: last.text + ev.text };
225
+ else
226
+ items.push({ kind: "assistant", id: nextId("a"), text: ev.text, ts: new Date().toISOString(), live: true });
227
+ return { ...state, items, activity: "Responding" };
228
+ }
229
+ case "thinking_delta":
230
+ return state.activity === "Thinking" ? state : { ...state, activity: "Thinking" };
231
+ case "tool_use_start":
232
+ return { ...state, activity: ev.name };
233
+ case "reset":
234
+ return { ...state, items: dropDraft(state.items) };
235
+ case "assistant": {
236
+ let items = dropDraft(state.items);
237
+ const text = textOf(ev.message.content);
238
+ if (text.trim())
239
+ items = [...items, { kind: "assistant", id: nextId("a"), text, ts: ev.ts, live: false }];
240
+ let plan = currentPlan(items);
241
+ let planChanged = false;
242
+ if (Array.isArray(ev.message.content)) {
243
+ for (const b of ev.message.content) {
244
+ if (b.type !== "tool_use")
245
+ continue;
246
+ const input = b.input ?? {};
247
+ if (TASK_TOOLS.has(b.name)) {
248
+ if (b.name === "TaskCreate") {
249
+ plan = [...plan, { id: b.id, subject: String(input.subject ?? ""), ...input.description ? { description: String(input.description) } : {}, status: input.status ?? "pending" }];
250
+ planChanged = true;
251
+ } else if (b.name === "TaskUpdate") {
252
+ const id = String(input.id ?? "");
253
+ plan = plan.map((p) => p.id === id ? { ...p, ...input.status ? { status: input.status } : {}, ...input.subject ? { subject: String(input.subject) } : {}, ...input.description ? { description: String(input.description) } : {} } : p);
254
+ planChanged = true;
255
+ }
256
+ continue;
257
+ }
258
+ items = [...items, { kind: "tool", id: b.id, name: b.name, input, ts: ev.ts }];
259
+ }
260
+ }
261
+ if (planChanged)
262
+ items = replaceOrAppendPlan(items, plan);
263
+ return { ...state, items, usageTotal: addUsage(state.usageTotal, ev.usage), activity: undefined };
264
+ }
265
+ case "tool_result": {
266
+ const idx = state.items.findIndex((i) => i.kind === "tool" && i.id === ev.toolUseId);
267
+ if (idx !== -1) {
268
+ const items = state.items.slice();
269
+ const t = items[idx];
270
+ items[idx] = { ...t, result: { content: ev.content, isError: ev.isError, ...ev.diff ? { diff: ev.diff } : {}, ...ev.blocks ? { blocks: ev.blocks } : {} } };
271
+ return { ...state, items };
272
+ }
273
+ const m = /^#(\d+) created:/.exec(ev.content);
274
+ if (m && ev.toolName === "TaskCreate") {
275
+ const plan = currentPlan(state.items).map((p) => p.id === ev.toolUseId ? { ...p, id: m[1] } : p);
276
+ return { ...state, items: replaceOrAppendPlan(state.items, plan) };
277
+ }
278
+ return state;
279
+ }
280
+ case "result":
281
+ return {
282
+ ...state,
283
+ running: false,
284
+ activity: undefined,
285
+ lastResult: ev,
286
+ items: [...dropDraft(state.items), { kind: "result", id: nextId("r"), result: ev }]
287
+ };
288
+ case "error":
289
+ return { ...state, items: [...dropDraft(state.items), { kind: "error", id: nextId("e"), message: ev.message, ts: ev.ts }] };
290
+ case "compact":
291
+ return { ...state, items: [...state.items, { kind: "compact", id: nextId("c"), replacedMessages: ev.replacedMessages, ts: ev.ts }] };
292
+ case "rewind":
293
+ return state;
294
+ }
295
+ }
296
+ function reduce(state, action) {
297
+ switch (action.type) {
298
+ case "connection":
299
+ return { ...state, connection: action.state };
300
+ case "clear":
301
+ return { ...initialState(), connection: state.connection, protocol: state.protocol, capabilities: state.capabilities, hop: state.hop, rootCwd: state.rootCwd, sessions: state.sessions, models: state.models, skills: state.skills };
302
+ case "error":
303
+ return { ...state, error: action.message };
304
+ case "local_user":
305
+ return {
306
+ ...state,
307
+ items: [...state.items, { kind: "user", id: nextId("u"), text: action.text, author: HUMAN, ts: action.ts, ...action.blocks?.length ? { blocks: action.blocks } : {} }]
308
+ };
309
+ case "run_started":
310
+ return { ...state, running: true, runStartedAt: action.at, activity: "Thinking", error: undefined, lastResult: undefined, pendingApproval: undefined, ...action.sessionId ? { sessionId: action.sessionId } : {} };
311
+ case "permission_answered":
312
+ return { ...state, pendingPermissions: state.pendingPermissions.filter((p) => p.id !== action.id) };
313
+ case "question_answered":
314
+ return state.pendingQuestion?.id === action.id ? { ...state, pendingQuestion: undefined } : state;
315
+ case "history": {
316
+ let s = { ...state, items: [], sessionId: action.sessionId, usageTotal: emptyUsage(), lastResult: undefined };
317
+ for (const m of action.messages)
318
+ s = applyEvent(s, m, false);
319
+ return { ...s, running: state.running };
320
+ }
321
+ case "server": {
322
+ const m = action.message;
323
+ switch (m.type) {
324
+ case "ready":
325
+ return { ...state, connection: "connected", protocol: m.protocol, capabilities: m.capabilities ?? [], hop: m.hop, rootCwd: m.cwd };
326
+ case "event":
327
+ return applyEvent(state.sessionId ? state : { ...state, sessionId: m.sessionId }, m.event, true);
328
+ case "permission_request":
329
+ return { ...state, pendingPermissions: [...state.pendingPermissions, { id: m.id, sessionId: m.sessionId, toolName: m.toolName, input: m.input }] };
330
+ case "question_request":
331
+ return { ...state, pendingQuestion: { id: m.id, sessionId: m.sessionId, questions: m.questions } };
332
+ case "sessions":
333
+ return { ...state, sessions: m.sessions };
334
+ case "models":
335
+ return { ...state, models: m.models };
336
+ case "skills":
337
+ return { ...state, skills: m.skills };
338
+ case "session_history": {
339
+ const s = reduce(state, { type: "history", sessionId: m.sessionId, messages: m.messages });
340
+ return {
341
+ ...s,
342
+ pendingQuestion: m.pendingQuestion ? { id: m.pendingQuestion.id, sessionId: m.sessionId, questions: m.pendingQuestion.questions } : undefined,
343
+ pendingApproval: m.pendingApproval,
344
+ liveElsewhere: m.live ?? false
345
+ };
346
+ }
347
+ case "done":
348
+ return {
349
+ ...state,
350
+ running: false,
351
+ activity: undefined,
352
+ pendingPermissions: [],
353
+ sessionId: m.sessionId,
354
+ ...m.pendingApproval ? { pendingApproval: m.pendingApproval } : {},
355
+ ...m.usage && !state.lastResult ? { usageTotal: addUsage(state.usageTotal, m.usage) } : {}
356
+ };
357
+ case "error":
358
+ return { ...state, error: m.message, items: [...dropDraft(state.items), { kind: "error", id: nextId("e"), message: m.message, ts: new Date().toISOString() }] };
359
+ default:
360
+ return state;
361
+ }
362
+ }
363
+ }
364
+ }
365
+ function elapsedLabel(ms) {
366
+ const s = Math.max(0, Math.floor(ms / 1000));
367
+ return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
368
+ }
369
+ function resultLabel(r) {
370
+ switch (r.subtype) {
371
+ case "success":
372
+ return { label: "Done", tone: "ok" };
373
+ case "cancelled":
374
+ return { label: "Interrupted", tone: "muted" };
375
+ case "needs_approval":
376
+ return { label: "Needs your approval", tone: "warn" };
377
+ case "max_turns":
378
+ return { label: "Stopped at the turn limit", tone: "warn" };
379
+ case "error":
380
+ return { label: r.reason ? `Failed: ${r.reason}` : "Failed", tone: "bad" };
381
+ default:
382
+ return { label: String(r.subtype).replace(/_/g, " "), tone: "warn" };
383
+ }
384
+ }
385
+ function formatTokens(u) {
386
+ const k = (n) => n >= 1e4 ? `${(n / 1000).toFixed(1)}k` : String(n);
387
+ return `${k(u.input_tokens)}↑ ${k(u.output_tokens)}↓`;
388
+ }
389
+ // src/client/useYodexSession.ts
390
+ import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
391
+ function useYodexSession(options) {
392
+ const [state, dispatch] = useReducer(reduce, undefined, initialState);
393
+ const sessionIdRef = useRef(options.sessionId);
394
+ const client = useMemo(() => new YodexClient({
395
+ url: options.url,
396
+ token: options.token,
397
+ socketFactory: options.socketFactory,
398
+ onState: (s) => dispatch({ type: "connection", state: s }),
399
+ onMessage: (m) => {
400
+ dispatch({ type: "server", message: m });
401
+ if (m.type === "ready" && sessionIdRef.current) {
402
+ client.loadSession(sessionIdRef.current).catch((e) => dispatch({ type: "error", message: e.message }));
403
+ }
404
+ }
405
+ }), [options.url, options.token, options.socketFactory]);
406
+ useEffect(() => {
407
+ if (options.autoConnect === false)
408
+ return;
409
+ client.connect();
410
+ return () => client.close();
411
+ }, [client, options.autoConnect]);
412
+ useEffect(() => {
413
+ sessionIdRef.current = state.sessionId ?? options.sessionId;
414
+ }, [state.sessionId, options.sessionId]);
415
+ const guard = useCallback((fn) => {
416
+ try {
417
+ fn();
418
+ } catch (e) {
419
+ dispatch({ type: "error", message: e.message });
420
+ }
421
+ }, []);
422
+ const run = useCallback((text, attachments = [], runOptions = {}) => {
423
+ const blocks = attachments.map((a) => ({ type: "image", source: { type: "base64", media_type: a.mediaType, data: a.data } }));
424
+ if (blocks.length && !state.capabilities.includes("attachments")) {
425
+ dispatch({ type: "error", message: "This server does not accept image attachments; the message was not sent." });
426
+ return;
427
+ }
428
+ guard(() => {
429
+ const sessionId = sessionIdRef.current;
430
+ dispatch({ type: "local_user", text, ts: new Date().toISOString(), blocks });
431
+ dispatch({ type: "run_started", sessionId, at: Date.now() });
432
+ client.run(text, { ...sessionId ? { sessionId } : {}, ...options.run, ...runOptions, ...blocks.length ? { blocks } : {} });
433
+ });
434
+ }, [client, guard, options.run, state.capabilities]);
435
+ const stop = useCallback(() => guard(() => client.interrupt()), [client, guard]);
436
+ const allow = useCallback((id) => guard(() => {
437
+ client.respondPermission(id, "allow");
438
+ dispatch({ type: "permission_answered", id });
439
+ }), [client, guard]);
440
+ const deny = useCallback((id, reason) => guard(() => {
441
+ client.respondPermission(id, "deny", reason);
442
+ dispatch({ type: "permission_answered", id });
443
+ }), [client, guard]);
444
+ const answer = useCallback((id, outcome) => guard(() => {
445
+ client.respondQuestion(id, outcome);
446
+ dispatch({ type: "question_answered", id });
447
+ }), [client, guard]);
448
+ const approve = useCallback((decision, reason) => guard(() => {
449
+ const sessionId = sessionIdRef.current;
450
+ if (!sessionId)
451
+ throw new Error("no session to approve");
452
+ dispatch({ type: "run_started", sessionId, at: Date.now() });
453
+ client.respondApproval(sessionId, decision, reason);
454
+ }), [client, guard]);
455
+ const loadSession = useCallback(async (sessionId) => {
456
+ sessionIdRef.current = sessionId;
457
+ dispatch({ type: "clear" });
458
+ try {
459
+ await client.loadSession(sessionId);
460
+ } catch (e) {
461
+ dispatch({ type: "error", message: e.message });
462
+ }
463
+ }, [client]);
464
+ const refreshSessions = useCallback(async () => {
465
+ try {
466
+ await client.listSessions();
467
+ } catch (e) {
468
+ dispatch({ type: "error", message: e.message });
469
+ }
470
+ }, [client]);
471
+ const refreshModels = useCallback(async () => {
472
+ try {
473
+ await client.listModels();
474
+ } catch (e) {
475
+ dispatch({ type: "error", message: e.message });
476
+ }
477
+ }, [client]);
478
+ const newSession = useCallback(() => {
479
+ sessionIdRef.current = undefined;
480
+ dispatch({ type: "clear" });
481
+ }, []);
482
+ return { state, client, run, stop, allow, deny, answer, approve, loadSession, refreshSessions, refreshModels, newSession };
483
+ }
484
+ // src/components/Transcript.tsx
485
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
486
+
487
+ // src/components/AssistantMessage.tsx
488
+ import Markdown from "react-markdown";
489
+ import remarkGfm from "remark-gfm";
490
+
491
+ // src/components/cn.ts
492
+ import { clsx } from "clsx";
493
+ import { twMerge } from "tailwind-merge";
494
+ function cn(...inputs) {
495
+ return twMerge(clsx(inputs));
496
+ }
497
+
498
+ // src/components/AssistantMessage.tsx
499
+ import { jsx } from "react/jsx-runtime";
500
+ var plugins = [remarkGfm];
501
+ function AssistantMessage({ text, live, className }) {
502
+ return /* @__PURE__ */ jsx("div", {
503
+ className: cn("yx-assistant prose prose-sm max-w-none break-words text-foreground", live && "yx-live", className),
504
+ "data-live": live ? "true" : undefined,
505
+ children: /* @__PURE__ */ jsx(Markdown, {
506
+ remarkPlugins: plugins,
507
+ skipHtml: true,
508
+ components: {
509
+ a: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("a", {
510
+ ...props,
511
+ target: "_blank",
512
+ rel: "noopener noreferrer",
513
+ className: "text-info underline underline-offset-2"
514
+ }),
515
+ pre: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("pre", {
516
+ ...props,
517
+ className: "overflow-x-auto rounded-md border border-border bg-card p-3 font-mono text-[12.5px] leading-5"
518
+ }),
519
+ code: ({ node: _n, className: cls, ...props }) => cls ? /* @__PURE__ */ jsx("code", {
520
+ ...props,
521
+ className: cls
522
+ }) : /* @__PURE__ */ jsx("code", {
523
+ ...props,
524
+ className: "rounded bg-muted px-1 py-0.5 font-mono text-[0.9em]"
525
+ }),
526
+ table: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("div", {
527
+ className: "overflow-x-auto",
528
+ children: /* @__PURE__ */ jsx("table", {
529
+ ...props,
530
+ className: "my-2 w-full border-collapse text-sm"
531
+ })
532
+ }),
533
+ th: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("th", {
534
+ ...props,
535
+ className: "border border-border bg-muted px-2 py-1 text-left font-medium"
536
+ }),
537
+ td: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("td", {
538
+ ...props,
539
+ className: "border border-border px-2 py-1 align-top"
540
+ })
541
+ },
542
+ children: text
543
+ })
544
+ });
545
+ }
546
+
547
+ // src/components/PlanChecklist.tsx
548
+ import { Check, Circle, Loader2 } from "lucide-react";
549
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
550
+ function PlanChecklist({ items, className }) {
551
+ if (!items.length)
552
+ return null;
553
+ const done = items.filter((t) => t.status === "completed").length;
554
+ return /* @__PURE__ */ jsxs("div", {
555
+ className: cn("yx-plan rounded-md border border-border bg-card p-3 text-sm", className),
556
+ "aria-label": "plan",
557
+ children: [
558
+ /* @__PURE__ */ jsxs("div", {
559
+ className: "mb-2 flex items-center justify-between",
560
+ children: [
561
+ /* @__PURE__ */ jsx2("span", {
562
+ className: "font-medium",
563
+ children: "Plan"
564
+ }),
565
+ /* @__PURE__ */ jsxs("span", {
566
+ className: "font-mono text-[12px] tabular-nums text-muted-foreground",
567
+ children: [
568
+ done,
569
+ " / ",
570
+ items.length
571
+ ]
572
+ })
573
+ ]
574
+ }),
575
+ /* @__PURE__ */ jsx2("ol", {
576
+ className: "space-y-1",
577
+ children: items.map((t) => /* @__PURE__ */ jsxs("li", {
578
+ className: cn("flex items-start gap-2", t.status === "completed" && "text-muted-foreground line-through"),
579
+ "data-status": t.status,
580
+ children: [
581
+ t.status === "completed" ? /* @__PURE__ */ jsx2(Check, {
582
+ className: "mt-0.5 h-4 w-4 shrink-0 text-success",
583
+ "aria-label": "completed"
584
+ }) : t.status === "in_progress" ? /* @__PURE__ */ jsx2(Loader2, {
585
+ className: "mt-0.5 h-4 w-4 shrink-0 animate-spin text-info",
586
+ "aria-label": "in progress"
587
+ }) : /* @__PURE__ */ jsx2(Circle, {
588
+ className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground",
589
+ "aria-label": "pending"
590
+ }),
591
+ /* @__PURE__ */ jsx2("span", {
592
+ children: t.subject
593
+ })
594
+ ]
595
+ }, t.id))
596
+ })
597
+ ]
598
+ });
599
+ }
600
+
601
+ // src/components/ToolCall.tsx
602
+ import { useState } from "react";
603
+ import { AlertCircle, Check as Check2, ChevronRight, FileDiff, Loader2 as Loader22, Terminal, Wrench } from "lucide-react";
604
+
605
+ // src/components/DiffView.tsx
606
+ import { jsx as jsx3 } from "react/jsx-runtime";
607
+ function diffCounts(diff) {
608
+ let added = 0;
609
+ let removed = 0;
610
+ for (const line of diff.split(`
611
+ `)) {
612
+ if (line.startsWith("+++") || line.startsWith("---"))
613
+ continue;
614
+ if (line.startsWith("+"))
615
+ added += 1;
616
+ else if (line.startsWith("-"))
617
+ removed += 1;
618
+ }
619
+ return { added, removed };
620
+ }
621
+ function DiffView({ diff, className }) {
622
+ const lines = diff.split(`
623
+ `);
624
+ return /* @__PURE__ */ jsx3("pre", {
625
+ className: cn("yx-diff overflow-x-auto rounded-md border border-border bg-card font-mono text-[12px] leading-5", className),
626
+ "aria-label": "diff",
627
+ children: lines.map((line, i) => {
628
+ const kind = line.startsWith("+++") || line.startsWith("---") ? "file" : line.startsWith("@@") ? "hunk" : line.startsWith("+") ? "add" : line.startsWith("-") ? "del" : "ctx";
629
+ return /* @__PURE__ */ jsx3("div", {
630
+ className: cn("whitespace-pre px-3", kind === "add" && "bg-success/10 text-success", kind === "del" && "bg-destructive/10 text-destructive", kind === "hunk" && "text-info", kind === "file" && "text-muted-foreground"),
631
+ children: line || " "
632
+ }, i);
633
+ })
634
+ });
635
+ }
636
+
637
+ // src/components/ToolCall.tsx
638
+ import { jsx as jsx4, jsxs as jsxs2, Fragment } from "react/jsx-runtime";
639
+ function toolSummary(name, input) {
640
+ const pick = (...keys) => {
641
+ for (const k of keys) {
642
+ const v = input[k];
643
+ if (typeof v === "string" && v.trim())
644
+ return v.trim();
645
+ }
646
+ return "";
647
+ };
648
+ switch (name) {
649
+ case "Bash":
650
+ return pick("command");
651
+ case "Read":
652
+ case "Write":
653
+ case "Edit":
654
+ case "List":
655
+ return pick("file_path", "path");
656
+ case "Grep":
657
+ case "Glob":
658
+ return pick("pattern");
659
+ case "Task":
660
+ case "Agent":
661
+ return pick("description", "prompt");
662
+ case "WebFetch":
663
+ return pick("url");
664
+ case "WebSearch":
665
+ return pick("query");
666
+ case "Skill":
667
+ return pick("name", "skill");
668
+ default:
669
+ return pick("command", "path", "file_path", "pattern", "query", "description", "name") || "";
670
+ }
671
+ }
672
+ function clip(s, n = 160) {
673
+ return s.length > n ? `${s.slice(0, n - 1)}…` : s;
674
+ }
675
+ var MAX_OUTPUT = 4000;
676
+ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
677
+ const [open, setOpen] = useState(defaultOpen);
678
+ const summary = toolSummary(item.name, item.input);
679
+ const r = item.result;
680
+ const status = !r ? "running" : r.isError ? "error" : "ok";
681
+ const counts = r?.diff ? diffCounts(r.diff) : null;
682
+ const path = typeof item.input.file_path === "string" ? item.input.file_path : typeof item.input.path === "string" ? item.input.path : undefined;
683
+ const Icon = r?.diff ? FileDiff : item.name === "Bash" ? Terminal : Wrench;
684
+ return /* @__PURE__ */ jsxs2("div", {
685
+ className: cn("yx-tool rounded-md border border-border bg-card text-sm", status === "error" && "border-destructive/40"),
686
+ "data-tool": item.name,
687
+ "data-status": status,
688
+ children: [
689
+ /* @__PURE__ */ jsxs2("button", {
690
+ type: "button",
691
+ onClick: () => setOpen((o) => !o),
692
+ "aria-expanded": open,
693
+ className: "flex min-h-11 w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
694
+ children: [
695
+ /* @__PURE__ */ jsx4(ChevronRight, {
696
+ className: cn("h-4 w-4 shrink-0 text-muted-foreground transition-transform", open && "rotate-90"),
697
+ "aria-hidden": true
698
+ }),
699
+ /* @__PURE__ */ jsx4(Icon, {
700
+ className: "h-4 w-4 shrink-0 text-muted-foreground",
701
+ "aria-hidden": true
702
+ }),
703
+ /* @__PURE__ */ jsx4("span", {
704
+ className: "font-medium",
705
+ children: item.name
706
+ }),
707
+ summary && /* @__PURE__ */ jsx4("span", {
708
+ className: "min-w-0 flex-1 truncate font-mono text-[12.5px] text-muted-foreground",
709
+ children: clip(summary)
710
+ }),
711
+ counts && /* @__PURE__ */ jsxs2("span", {
712
+ className: "shrink-0 font-mono text-[12px] tabular-nums",
713
+ children: [
714
+ /* @__PURE__ */ jsxs2("span", {
715
+ className: "text-success",
716
+ children: [
717
+ "+",
718
+ counts.added
719
+ ]
720
+ }),
721
+ " ",
722
+ /* @__PURE__ */ jsxs2("span", {
723
+ className: "text-destructive",
724
+ children: [
725
+ "−",
726
+ counts.removed
727
+ ]
728
+ })
729
+ ]
730
+ }),
731
+ status === "running" && /* @__PURE__ */ jsx4(Loader22, {
732
+ className: "h-4 w-4 shrink-0 animate-spin text-muted-foreground",
733
+ "aria-label": "running"
734
+ }),
735
+ status === "ok" && /* @__PURE__ */ jsx4(Check2, {
736
+ className: "h-4 w-4 shrink-0 text-success",
737
+ "aria-label": "ok"
738
+ }),
739
+ status === "error" && /* @__PURE__ */ jsx4(AlertCircle, {
740
+ className: "h-4 w-4 shrink-0 text-destructive",
741
+ "aria-label": "error"
742
+ })
743
+ ]
744
+ }),
745
+ open && /* @__PURE__ */ jsxs2("div", {
746
+ className: "border-t border-border px-3 py-2",
747
+ children: [
748
+ /* @__PURE__ */ jsx4("div", {
749
+ className: "mb-1 text-[11px] uppercase tracking-wide text-muted-foreground",
750
+ children: "input"
751
+ }),
752
+ /* @__PURE__ */ jsx4("pre", {
753
+ className: "overflow-x-auto whitespace-pre-wrap break-words font-mono text-[12px] leading-5",
754
+ children: summary && Object.keys(item.input).length === 1 ? summary : JSON.stringify(item.input, null, 2)
755
+ }),
756
+ r && /* @__PURE__ */ jsxs2(Fragment, {
757
+ children: [
758
+ /* @__PURE__ */ jsxs2("div", {
759
+ className: "mb-1 mt-2 flex items-center gap-2 text-[11px] uppercase tracking-wide text-muted-foreground",
760
+ children: [
761
+ /* @__PURE__ */ jsx4("span", {
762
+ children: r.isError ? "error" : "result"
763
+ }),
764
+ path && onOpenFile && /* @__PURE__ */ jsx4("button", {
765
+ type: "button",
766
+ onClick: () => onOpenFile(path),
767
+ className: "ml-auto min-h-8 rounded px-2 text-info hover:bg-muted",
768
+ children: "open file"
769
+ })
770
+ ]
771
+ }),
772
+ r.diff ? /* @__PURE__ */ jsx4(DiffView, {
773
+ diff: r.diff
774
+ }) : /* @__PURE__ */ jsx4("pre", {
775
+ className: cn("max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-[12px] leading-5", r.isError && "text-destructive"),
776
+ children: r.content.length > MAX_OUTPUT ? `${r.content.slice(0, MAX_OUTPUT)}
777
+ … (${r.content.length - MAX_OUTPUT} more characters)` : r.content || "(no output)"
778
+ })
779
+ ]
780
+ })
781
+ ]
782
+ })
783
+ ]
784
+ });
785
+ }
786
+
787
+ // src/components/Transcript.tsx
788
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
789
+ function Transcript({ items, autoScroll = true, onOpenFile, footer, emptyState, className }) {
790
+ const ref = useRef2(null);
791
+ const pinned = useRef2(true);
792
+ useEffect2(() => {
793
+ const el = ref.current;
794
+ if (!el)
795
+ return;
796
+ const onScroll = () => {
797
+ pinned.current = el.scrollHeight - el.scrollTop - el.clientHeight < 48;
798
+ };
799
+ el.addEventListener("scroll", onScroll, { passive: true });
800
+ return () => el.removeEventListener("scroll", onScroll);
801
+ }, []);
802
+ useEffect2(() => {
803
+ const el = ref.current;
804
+ if (el && autoScroll && pinned.current)
805
+ el.scrollTop = el.scrollHeight;
806
+ });
807
+ return /* @__PURE__ */ jsx5("div", {
808
+ ref,
809
+ className: cn("yx-transcript flex-1 overflow-y-auto px-4 py-3", className),
810
+ role: "log",
811
+ "aria-live": "polite",
812
+ children: /* @__PURE__ */ jsxs3("div", {
813
+ className: "mx-auto flex w-full max-w-3xl flex-col gap-3",
814
+ children: [
815
+ items.length === 0 && emptyState,
816
+ items.map((it) => {
817
+ switch (it.kind) {
818
+ case "user":
819
+ return /* @__PURE__ */ jsx5("div", {
820
+ className: cn("flex", it.author.type === "human" ? "justify-end" : "justify-start"),
821
+ "data-author": it.author.type,
822
+ children: /* @__PURE__ */ jsxs3("div", {
823
+ className: cn("max-w-[85%] whitespace-pre-wrap break-words rounded-2xl px-4 py-2 text-[15px] leading-6", it.author.type === "human" ? "rounded-br-md bg-primary text-primary-foreground" : "rounded-bl-md border border-border bg-card"),
824
+ children: [
825
+ it.author.type !== "human" && /* @__PURE__ */ jsx5("div", {
826
+ className: "mb-1 text-[11px] text-muted-foreground",
827
+ children: it.author.displayName ?? it.author.type
828
+ }),
829
+ it.blocks?.some((b) => b.type === "image") && /* @__PURE__ */ jsx5("div", {
830
+ className: "mb-1 text-[11px] opacity-80",
831
+ children: "[image]"
832
+ }),
833
+ it.text
834
+ ]
835
+ })
836
+ }, it.id);
837
+ case "assistant":
838
+ return /* @__PURE__ */ jsx5(AssistantMessage, {
839
+ text: it.text,
840
+ live: it.live
841
+ }, it.id);
842
+ case "tool":
843
+ return /* @__PURE__ */ jsx5(ToolCall, {
844
+ item: it,
845
+ onOpenFile
846
+ }, it.id);
847
+ case "plan":
848
+ return /* @__PURE__ */ jsx5(PlanChecklist, {
849
+ items: it.items
850
+ }, "plan");
851
+ case "result": {
852
+ const { label, tone } = resultLabel(it.result);
853
+ return /* @__PURE__ */ jsxs3("div", {
854
+ className: cn("flex items-center gap-2 text-[12px] text-muted-foreground", tone === "bad" && "text-destructive"),
855
+ "data-subtype": it.result.subtype,
856
+ children: [
857
+ /* @__PURE__ */ jsx5("span", {
858
+ children: label
859
+ }),
860
+ /* @__PURE__ */ jsx5("span", {
861
+ children: "·"
862
+ }),
863
+ /* @__PURE__ */ jsxs3("span", {
864
+ className: "font-mono tabular-nums",
865
+ children: [
866
+ it.result.numTurns,
867
+ " step",
868
+ it.result.numTurns === 1 ? "" : "s",
869
+ " · ",
870
+ formatTokens(it.result.usage)
871
+ ]
872
+ })
873
+ ]
874
+ }, it.id);
875
+ }
876
+ case "error":
877
+ return /* @__PURE__ */ jsx5("div", {
878
+ role: "alert",
879
+ className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-[13px] text-destructive",
880
+ children: it.message
881
+ }, it.id);
882
+ case "compact":
883
+ return /* @__PURE__ */ jsxs3("div", {
884
+ className: "text-center text-[11px] text-muted-foreground",
885
+ children: [
886
+ "— context compacted (",
887
+ it.replacedMessages,
888
+ " earlier messages summarised) —"
889
+ ]
890
+ }, it.id);
891
+ }
892
+ }),
893
+ footer
894
+ ]
895
+ })
896
+ });
897
+ }
898
+ // src/components/PermissionPrompt.tsx
899
+ import { useEffect as useEffect3 } from "react";
900
+ import { ShieldAlert } from "lucide-react";
901
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
902
+ function permissionSummary(p) {
903
+ const v = p.input.command ?? p.input.file_path ?? p.input.path ?? p.input.url;
904
+ if (typeof v === "string")
905
+ return v;
906
+ const s = JSON.stringify(p.input);
907
+ return s.length > 240 ? `${s.slice(0, 239)}…` : s;
908
+ }
909
+ function PermissionPrompt({ request, onAllow, onDeny, className }) {
910
+ useEffect3(() => {
911
+ const key = (e) => {
912
+ const t = e.target;
913
+ if (t && (t.tagName === "TEXTAREA" || t.tagName === "INPUT" || t.isContentEditable))
914
+ return;
915
+ if (e.key === "a")
916
+ onAllow(request.id);
917
+ else if (e.key === "d")
918
+ onDeny(request.id);
919
+ };
920
+ document.addEventListener("keydown", key);
921
+ return () => document.removeEventListener("keydown", key);
922
+ }, [request.id, onAllow, onDeny]);
923
+ return /* @__PURE__ */ jsxs4("div", {
924
+ role: "alertdialog",
925
+ "aria-labelledby": `perm-${request.id}`,
926
+ className: cn("yx-permission rounded-md border border-warning/50 bg-card p-3 text-sm", className),
927
+ children: [
928
+ /* @__PURE__ */ jsxs4("div", {
929
+ id: `perm-${request.id}`,
930
+ className: "mb-2 flex items-center gap-2 font-medium",
931
+ children: [
932
+ /* @__PURE__ */ jsx6(ShieldAlert, {
933
+ className: "h-4 w-4 text-warning",
934
+ "aria-hidden": true
935
+ }),
936
+ "Run ",
937
+ /* @__PURE__ */ jsx6("span", {
938
+ className: "font-mono",
939
+ children: request.toolName
940
+ }),
941
+ "?"
942
+ ]
943
+ }),
944
+ /* @__PURE__ */ jsx6("pre", {
945
+ className: "mb-3 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2 font-mono text-[12.5px] leading-5",
946
+ children: permissionSummary(request)
947
+ }),
948
+ /* @__PURE__ */ jsxs4("div", {
949
+ className: "flex gap-2",
950
+ children: [
951
+ /* @__PURE__ */ jsxs4("button", {
952
+ type: "button",
953
+ onClick: () => onAllow(request.id),
954
+ className: "min-h-11 flex-1 rounded-md bg-primary px-4 font-medium text-primary-foreground hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
955
+ children: [
956
+ "Allow ",
957
+ /* @__PURE__ */ jsx6("kbd", {
958
+ className: "ml-1 rounded bg-primary-foreground/20 px-1 text-[11px]",
959
+ children: "a"
960
+ })
961
+ ]
962
+ }),
963
+ /* @__PURE__ */ jsxs4("button", {
964
+ type: "button",
965
+ onClick: () => onDeny(request.id),
966
+ className: "min-h-11 flex-1 rounded-md border border-border bg-background px-4 font-medium hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
967
+ children: [
968
+ "Deny ",
969
+ /* @__PURE__ */ jsx6("kbd", {
970
+ className: "ml-1 rounded bg-muted px-1 text-[11px]",
971
+ children: "d"
972
+ })
973
+ ]
974
+ })
975
+ ]
976
+ })
977
+ ]
978
+ });
979
+ }
980
+ // src/components/QuestionForm.tsx
981
+ import { useState as useState2 } from "react";
982
+ import { HelpCircle, X } from "lucide-react";
983
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
984
+ function emptyDraft(questions) {
985
+ return { selected: questions.map(() => []), notes: questions.map(() => "") };
986
+ }
987
+ function buildOutcome(questions, draft, declined = false) {
988
+ if (declined)
989
+ return { declined: true, answers: [] };
990
+ return {
991
+ declined: false,
992
+ answers: questions.map((q, i) => {
993
+ const picked = draft.selected[i] ?? [];
994
+ const note = (draft.notes[i] ?? "").trim();
995
+ const answers = picked.length ? picked : note ? [note] : [];
996
+ return { question: q.question, answers, ...picked.length && note ? { note } : {} };
997
+ })
998
+ };
999
+ }
1000
+ function chip(q, i) {
1001
+ const h = (q.header ?? "").trim();
1002
+ return h || `Q${i + 1}`;
1003
+ }
1004
+ function QuestionForm({ request, onSubmit, onDecline, className }) {
1005
+ const qs = request.questions;
1006
+ const [tab, setTab] = useState2(0);
1007
+ const [draft, setDraft] = useState2(() => emptyDraft(qs));
1008
+ const q = qs[tab];
1009
+ if (!q)
1010
+ return null;
1011
+ const picked = draft.selected[tab] ?? [];
1012
+ const toggle = (label) => {
1013
+ setDraft((d) => {
1014
+ const selected = d.selected.slice();
1015
+ const cur = selected[tab] ?? [];
1016
+ selected[tab] = q.multiSelect ? cur.includes(label) ? cur.filter((l) => l !== label) : [...cur, label] : cur[0] === label ? [] : [label];
1017
+ return { ...d, selected };
1018
+ });
1019
+ };
1020
+ const setNote = (v) => setDraft((d) => {
1021
+ const notes = d.notes.slice();
1022
+ notes[tab] = v;
1023
+ return { ...d, notes };
1024
+ });
1025
+ const answered = qs.map((_, i) => (draft.selected[i]?.length ?? 0) > 0 || (draft.notes[i] ?? "").trim().length > 0);
1026
+ const previewOf = (q.options ?? []).find((o) => picked[0] === o.label)?.preview;
1027
+ return /* @__PURE__ */ jsxs5("div", {
1028
+ role: "dialog",
1029
+ "aria-label": "The agent has a question",
1030
+ className: cn("yx-question rounded-md border border-info/50 bg-card text-sm", className),
1031
+ children: [
1032
+ /* @__PURE__ */ jsxs5("div", {
1033
+ className: "flex items-center gap-2 border-b border-border px-3 py-2",
1034
+ children: [
1035
+ /* @__PURE__ */ jsx7(HelpCircle, {
1036
+ className: "h-4 w-4 text-info",
1037
+ "aria-hidden": true
1038
+ }),
1039
+ /* @__PURE__ */ jsx7("span", {
1040
+ className: "font-medium",
1041
+ children: "The agent needs your input"
1042
+ }),
1043
+ /* @__PURE__ */ jsx7("button", {
1044
+ type: "button",
1045
+ onClick: () => onDecline(request.id),
1046
+ "aria-label": "Close and keep talking",
1047
+ className: "ml-auto flex h-11 w-11 items-center justify-center rounded-md text-muted-foreground hover:bg-muted",
1048
+ children: /* @__PURE__ */ jsx7(X, {
1049
+ className: "h-4 w-4",
1050
+ "aria-hidden": true
1051
+ })
1052
+ })
1053
+ ]
1054
+ }),
1055
+ qs.length > 1 && /* @__PURE__ */ jsx7("div", {
1056
+ role: "tablist",
1057
+ className: "flex gap-1 overflow-x-auto border-b border-border px-2 py-1",
1058
+ children: qs.map((qq, i) => /* @__PURE__ */ jsx7("button", {
1059
+ role: "tab",
1060
+ type: "button",
1061
+ "aria-selected": i === tab,
1062
+ onClick: () => setTab(i),
1063
+ className: cn("min-h-9 shrink-0 rounded-md px-3 text-[12px]", i === tab ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-muted/70", answered[i] && i !== tab && "ring-1 ring-success"),
1064
+ children: chip(qq, i)
1065
+ }, i))
1066
+ }),
1067
+ /* @__PURE__ */ jsxs5("div", {
1068
+ className: "space-y-3 p-3",
1069
+ children: [
1070
+ /* @__PURE__ */ jsx7("p", {
1071
+ className: "font-medium",
1072
+ children: q.question
1073
+ }),
1074
+ (q.options ?? []).length > 0 && /* @__PURE__ */ jsx7("div", {
1075
+ role: q.multiSelect ? "group" : "radiogroup",
1076
+ className: "space-y-2",
1077
+ children: (q.options ?? []).map((o) => {
1078
+ const on = picked.includes(o.label);
1079
+ return /* @__PURE__ */ jsxs5("button", {
1080
+ type: "button",
1081
+ role: q.multiSelect ? "checkbox" : "radio",
1082
+ "aria-checked": on,
1083
+ onClick: () => toggle(o.label),
1084
+ className: cn("flex min-h-11 w-full items-start gap-3 rounded-md border px-3 py-2 text-left hover:bg-muted/60", on ? "border-primary bg-muted" : "border-border"),
1085
+ children: [
1086
+ /* @__PURE__ */ jsx7("span", {
1087
+ className: cn("mt-1 h-4 w-4 shrink-0 border", q.multiSelect ? "rounded" : "rounded-full", on ? "border-primary bg-primary" : "border-muted-foreground"),
1088
+ "aria-hidden": true
1089
+ }),
1090
+ /* @__PURE__ */ jsxs5("span", {
1091
+ className: "min-w-0",
1092
+ children: [
1093
+ /* @__PURE__ */ jsx7("span", {
1094
+ className: "block font-medium",
1095
+ children: o.label
1096
+ }),
1097
+ o.description && /* @__PURE__ */ jsx7("span", {
1098
+ className: "block text-muted-foreground",
1099
+ children: o.description
1100
+ })
1101
+ ]
1102
+ })
1103
+ ]
1104
+ }, o.label);
1105
+ })
1106
+ }),
1107
+ previewOf && /* @__PURE__ */ jsx7("pre", {
1108
+ className: "overflow-x-auto rounded-md border border-border bg-muted p-2 font-mono text-[12px] leading-5",
1109
+ children: previewOf
1110
+ }),
1111
+ /* @__PURE__ */ jsxs5("label", {
1112
+ className: "block",
1113
+ children: [
1114
+ /* @__PURE__ */ jsx7("span", {
1115
+ className: "mb-1 block text-[12px] text-muted-foreground",
1116
+ children: (q.options ?? []).length ? "Add a note (optional)" : "Your answer"
1117
+ }),
1118
+ /* @__PURE__ */ jsx7("textarea", {
1119
+ value: draft.notes[tab] ?? "",
1120
+ onChange: (e) => setNote(e.target.value),
1121
+ rows: 2,
1122
+ className: "w-full rounded-md border border-border bg-input-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1123
+ })
1124
+ ]
1125
+ }),
1126
+ /* @__PURE__ */ jsxs5("div", {
1127
+ className: "flex gap-2",
1128
+ children: [
1129
+ tab < qs.length - 1 ? /* @__PURE__ */ jsx7("button", {
1130
+ type: "button",
1131
+ onClick: () => setTab(tab + 1),
1132
+ className: "min-h-11 flex-1 rounded-md border border-border px-4 font-medium hover:bg-muted",
1133
+ children: "Next"
1134
+ }) : null,
1135
+ /* @__PURE__ */ jsx7("button", {
1136
+ type: "button",
1137
+ onClick: () => onSubmit(request.id, buildOutcome(qs, draft)),
1138
+ disabled: !answered.some(Boolean),
1139
+ className: "min-h-11 flex-1 rounded-md bg-primary px-4 font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50",
1140
+ children: "Submit"
1141
+ })
1142
+ ]
1143
+ })
1144
+ ]
1145
+ })
1146
+ ]
1147
+ });
1148
+ }
1149
+ // src/components/RunStatus.tsx
1150
+ import { useEffect as useEffect4, useState as useState3 } from "react";
1151
+ import { AlertTriangle, Check as Check3, Loader2 as Loader23, Square } from "lucide-react";
1152
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1153
+ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, connection, onStop, onApprove, className, now = Date.now }) {
1154
+ const [, tick] = useState3(0);
1155
+ useEffect4(() => {
1156
+ if (!running)
1157
+ return;
1158
+ const h = setInterval(() => tick((n) => n + 1), 1000);
1159
+ return () => clearInterval(h);
1160
+ }, [running]);
1161
+ if (connection && connection !== "connected") {
1162
+ return /* @__PURE__ */ jsxs6("div", {
1163
+ className: cn("yx-status flex min-h-9 items-center gap-2 text-[12.5px] text-muted-foreground", className),
1164
+ role: "status",
1165
+ children: [
1166
+ /* @__PURE__ */ jsx8(Loader23, {
1167
+ className: "h-3.5 w-3.5 animate-spin",
1168
+ "aria-hidden": true
1169
+ }),
1170
+ connection === "closed" ? "Disconnected" : connection === "reconnecting" ? "Reconnecting…" : "Connecting…"
1171
+ ]
1172
+ });
1173
+ }
1174
+ if (running) {
1175
+ return /* @__PURE__ */ jsxs6("div", {
1176
+ className: cn("yx-status flex min-h-9 items-center gap-2 text-[12.5px] text-muted-foreground", className),
1177
+ role: "status",
1178
+ "aria-live": "polite",
1179
+ children: [
1180
+ /* @__PURE__ */ jsx8(Loader23, {
1181
+ className: "h-3.5 w-3.5 animate-spin text-accent",
1182
+ "aria-hidden": true
1183
+ }),
1184
+ /* @__PURE__ */ jsx8("span", {
1185
+ children: activity ?? "Working"
1186
+ }),
1187
+ startedAt !== undefined && /* @__PURE__ */ jsxs6("span", {
1188
+ className: "font-mono tabular-nums",
1189
+ children: [
1190
+ "· ",
1191
+ elapsedLabel(now() - startedAt)
1192
+ ]
1193
+ }),
1194
+ onStop && /* @__PURE__ */ jsxs6("button", {
1195
+ type: "button",
1196
+ onClick: onStop,
1197
+ className: "ml-auto flex min-h-9 items-center gap-1 rounded-md px-2 hover:bg-muted",
1198
+ "aria-label": "Stop",
1199
+ children: [
1200
+ /* @__PURE__ */ jsx8(Square, {
1201
+ className: "h-3.5 w-3.5",
1202
+ "aria-hidden": true
1203
+ }),
1204
+ " Stop"
1205
+ ]
1206
+ })
1207
+ ]
1208
+ });
1209
+ }
1210
+ if (!lastResult)
1211
+ return null;
1212
+ const { label, tone } = resultLabel(lastResult);
1213
+ const needsApproval = lastResult.subtype === "needs_approval";
1214
+ return /* @__PURE__ */ jsxs6("div", {
1215
+ className: cn("yx-status flex min-h-9 flex-wrap items-center gap-2 text-[12.5px]", className),
1216
+ role: "status",
1217
+ "data-subtype": lastResult.subtype,
1218
+ children: [
1219
+ tone === "ok" ? /* @__PURE__ */ jsx8(Check3, {
1220
+ className: "h-3.5 w-3.5 text-success",
1221
+ "aria-hidden": true
1222
+ }) : tone === "bad" ? /* @__PURE__ */ jsx8(AlertTriangle, {
1223
+ className: "h-3.5 w-3.5 text-destructive",
1224
+ "aria-hidden": true
1225
+ }) : /* @__PURE__ */ jsx8(AlertTriangle, {
1226
+ className: cn("h-3.5 w-3.5", tone === "warn" ? "text-warning" : "text-muted-foreground"),
1227
+ "aria-hidden": true
1228
+ }),
1229
+ /* @__PURE__ */ jsx8("span", {
1230
+ className: cn(tone === "ok" && "text-success", tone === "bad" && "text-destructive", tone === "warn" && "text-warning", tone === "muted" && "text-muted-foreground"),
1231
+ children: label
1232
+ }),
1233
+ /* @__PURE__ */ jsx8("span", {
1234
+ className: "text-muted-foreground",
1235
+ children: "·"
1236
+ }),
1237
+ /* @__PURE__ */ jsxs6("span", {
1238
+ className: "font-mono tabular-nums text-muted-foreground",
1239
+ children: [
1240
+ lastResult.numTurns,
1241
+ " step",
1242
+ lastResult.numTurns === 1 ? "" : "s",
1243
+ " · ",
1244
+ formatTokens(usageTotal ?? lastResult.usage)
1245
+ ]
1246
+ }),
1247
+ needsApproval && lastResult.pendingApproval && /* @__PURE__ */ jsxs6("div", {
1248
+ className: "basis-full rounded-md border border-warning/50 bg-card p-2",
1249
+ children: [
1250
+ /* @__PURE__ */ jsx8("div", {
1251
+ className: "mb-1 text-[12px] text-muted-foreground",
1252
+ children: lastResult.pendingApproval.reason
1253
+ }),
1254
+ /* @__PURE__ */ jsx8("pre", {
1255
+ className: "mb-2 overflow-x-auto whitespace-pre-wrap break-words font-mono text-[12px]",
1256
+ children: lastResult.pendingApproval.command
1257
+ }),
1258
+ onApprove && /* @__PURE__ */ jsxs6("div", {
1259
+ className: "flex gap-2",
1260
+ children: [
1261
+ /* @__PURE__ */ jsx8("button", {
1262
+ type: "button",
1263
+ onClick: () => onApprove("allow"),
1264
+ className: "min-h-11 flex-1 rounded-md bg-primary px-3 font-medium text-primary-foreground",
1265
+ children: "Approve and continue"
1266
+ }),
1267
+ /* @__PURE__ */ jsx8("button", {
1268
+ type: "button",
1269
+ onClick: () => onApprove("deny"),
1270
+ className: "min-h-11 flex-1 rounded-md border border-border px-3 font-medium hover:bg-muted",
1271
+ children: "Don't run it"
1272
+ })
1273
+ ]
1274
+ })
1275
+ ]
1276
+ })
1277
+ ]
1278
+ });
1279
+ }
1280
+ // src/components/SessionList.tsx
1281
+ import { Plus } from "lucide-react";
1282
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1283
+ function groupSessions(rows) {
1284
+ const groups = new Map;
1285
+ for (const r of rows) {
1286
+ const key = r.projectName ?? "";
1287
+ if (!groups.has(key))
1288
+ groups.set(key, []);
1289
+ groups.get(key).push(r);
1290
+ }
1291
+ return [...groups.entries()].map(([project, rows2]) => ({ project, rows: rows2 }));
1292
+ }
1293
+ function when(iso) {
1294
+ const t = Date.parse(iso);
1295
+ if (Number.isNaN(t))
1296
+ return "";
1297
+ const d = new Date(t);
1298
+ return `${d.toLocaleDateString(undefined, { month: "short", day: "numeric" })} ${d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`;
1299
+ }
1300
+ function SessionList({ sessions, selectedId, onSelect, onNew, className }) {
1301
+ const groups = groupSessions(sessions);
1302
+ return /* @__PURE__ */ jsxs7("nav", {
1303
+ className: cn("yx-sessions text-sm", className),
1304
+ "aria-label": "Sessions",
1305
+ children: [
1306
+ onNew && /* @__PURE__ */ jsxs7("button", {
1307
+ type: "button",
1308
+ onClick: onNew,
1309
+ className: "mb-2 flex min-h-11 w-full items-center gap-2 rounded-md border border-dashed border-border px-3 text-left hover:bg-muted",
1310
+ children: [
1311
+ /* @__PURE__ */ jsx9(Plus, {
1312
+ className: "h-4 w-4",
1313
+ "aria-hidden": true
1314
+ }),
1315
+ " New session"
1316
+ ]
1317
+ }),
1318
+ sessions.length === 0 && /* @__PURE__ */ jsx9("p", {
1319
+ className: "px-3 py-2 text-muted-foreground",
1320
+ children: "No sessions yet."
1321
+ }),
1322
+ groups.map((g) => /* @__PURE__ */ jsxs7("div", {
1323
+ className: "mb-2",
1324
+ children: [
1325
+ g.project && /* @__PURE__ */ jsx9("div", {
1326
+ className: "px-3 py-1 text-[11px] uppercase tracking-wide text-muted-foreground",
1327
+ children: g.project
1328
+ }),
1329
+ /* @__PURE__ */ jsx9("ul", {
1330
+ children: g.rows.map((s) => {
1331
+ const on = s.sessionId === selectedId;
1332
+ const title = s.name || s.title || s.lastMessagePreview || s.sessionId.slice(0, 8);
1333
+ return /* @__PURE__ */ jsx9("li", {
1334
+ children: /* @__PURE__ */ jsxs7("button", {
1335
+ type: "button",
1336
+ onClick: () => onSelect(s.sessionId),
1337
+ "aria-current": on ? "true" : undefined,
1338
+ className: cn("flex min-h-11 w-full flex-col gap-0.5 rounded-md px-3 py-2 text-left hover:bg-muted", on && "bg-muted"),
1339
+ "data-status": s.status,
1340
+ children: [
1341
+ /* @__PURE__ */ jsxs7("span", {
1342
+ className: "flex items-center gap-2",
1343
+ children: [
1344
+ /* @__PURE__ */ jsx9("span", {
1345
+ className: cn("h-2 w-2 shrink-0 rounded-full", s.live ? "animate-pulse bg-accent" : s.status === "active" ? "bg-success" : s.status === "listening" ? "bg-info" : s.status === "error" ? "bg-destructive" : "bg-muted-foreground/40"),
1346
+ "aria-label": s.live ? "working" : s.status,
1347
+ "data-live": s.live ? "true" : undefined
1348
+ }),
1349
+ /* @__PURE__ */ jsx9("span", {
1350
+ className: "min-w-0 flex-1 truncate font-medium",
1351
+ children: title
1352
+ }),
1353
+ s.pendingQuestion && /* @__PURE__ */ jsx9("span", {
1354
+ className: "shrink-0 rounded-full bg-warning px-1.5 text-[10px] font-medium text-warning-foreground",
1355
+ children: "question"
1356
+ })
1357
+ ]
1358
+ }),
1359
+ /* @__PURE__ */ jsxs7("span", {
1360
+ className: "flex gap-2 truncate text-[11.5px] text-muted-foreground",
1361
+ children: [
1362
+ s.hop && /* @__PURE__ */ jsx9("span", {
1363
+ className: "truncate",
1364
+ children: s.hop
1365
+ }),
1366
+ s.relCwd && /* @__PURE__ */ jsx9("span", {
1367
+ className: "truncate",
1368
+ children: s.relCwd
1369
+ }),
1370
+ /* @__PURE__ */ jsx9("span", {
1371
+ className: "ml-auto shrink-0 tabular-nums",
1372
+ children: when(s.updatedAt)
1373
+ })
1374
+ ]
1375
+ })
1376
+ ]
1377
+ })
1378
+ }, s.sessionId);
1379
+ })
1380
+ })
1381
+ ]
1382
+ }, g.project))
1383
+ ]
1384
+ });
1385
+ }
1386
+ // src/components/Composer.tsx
1387
+ import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef3, useState as useState4 } from "react";
1388
+ import { ArrowUp, ImagePlus, Square as Square2, X as X2 } from "lucide-react";
1389
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1390
+ var ACCEPT = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1391
+ async function fileToAttachment(f) {
1392
+ const buf = new Uint8Array(await f.arrayBuffer());
1393
+ let bin = "";
1394
+ for (let i = 0;i < buf.length; i += 32768)
1395
+ bin += String.fromCharCode(...buf.subarray(i, i + 32768));
1396
+ return { name: f.name || "image", mediaType: f.type, data: btoa(bin), previewUrl: URL.createObjectURL(f) };
1397
+ }
1398
+ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeholder = "Ask yodex to do something…", hopOptions, hop, onHopChange, leading, maxAttachmentBytes = 8 * 1024 * 1024, className, autoFocus }) {
1399
+ const [text, setText] = useState4("");
1400
+ const [attachments, setAttachments] = useState4([]);
1401
+ const [note, setNote] = useState4(null);
1402
+ const ref = useRef3(null);
1403
+ useEffect5(() => {
1404
+ const el = ref.current;
1405
+ if (!el)
1406
+ return;
1407
+ el.style.height = "0px";
1408
+ el.style.height = `${Math.min(el.scrollHeight, 240)}px`;
1409
+ }, [text]);
1410
+ const addFiles = useCallback2(async (files) => {
1411
+ const list = [...files].filter((f) => ACCEPT.includes(f.type));
1412
+ if (!list.length)
1413
+ return;
1414
+ const big = list.find((f) => f.size > maxAttachmentBytes);
1415
+ if (big) {
1416
+ setNote(`${big.name} is larger than ${Math.round(maxAttachmentBytes / 1024 / 1024)} MB`);
1417
+ return;
1418
+ }
1419
+ const next = await Promise.all(list.map(fileToAttachment));
1420
+ setAttachments((a) => [...a, ...next]);
1421
+ setNote(null);
1422
+ }, [maxAttachmentBytes]);
1423
+ const remove = (i) => setAttachments((a) => {
1424
+ const gone = a[i];
1425
+ if (gone?.previewUrl)
1426
+ URL.revokeObjectURL(gone.previewUrl);
1427
+ return a.filter((_, j) => j !== i);
1428
+ });
1429
+ const canSend = !disabled && !running && (text.trim().length > 0 || attachments.length > 0);
1430
+ const submit = () => {
1431
+ if (!canSend)
1432
+ return;
1433
+ onSubmit(text.trim(), attachments);
1434
+ setText("");
1435
+ for (const a of attachments)
1436
+ if (a.previewUrl)
1437
+ URL.revokeObjectURL(a.previewUrl);
1438
+ setAttachments([]);
1439
+ };
1440
+ const onKey = (e) => {
1441
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
1442
+ e.preventDefault();
1443
+ submit();
1444
+ }
1445
+ };
1446
+ const onPaste = (e) => {
1447
+ const files = [...e.clipboardData.files];
1448
+ if (files.length) {
1449
+ e.preventDefault();
1450
+ addFiles(files);
1451
+ }
1452
+ };
1453
+ const onDrop = (e) => {
1454
+ if (e.dataTransfer.files.length) {
1455
+ e.preventDefault();
1456
+ addFiles(e.dataTransfer.files);
1457
+ }
1458
+ };
1459
+ return /* @__PURE__ */ jsxs8("div", {
1460
+ className: cn("yx-composer rounded-lg border border-border bg-card shadow-sm", disabled && "opacity-70", className),
1461
+ onDrop,
1462
+ onDragOver: (e) => e.preventDefault(),
1463
+ "data-disabled": disabled ? "true" : undefined,
1464
+ children: [
1465
+ attachments.length > 0 && /* @__PURE__ */ jsx10("ul", {
1466
+ className: "flex flex-wrap gap-2 px-3 pt-3",
1467
+ "aria-label": "attachments",
1468
+ children: attachments.map((a, i) => /* @__PURE__ */ jsxs8("li", {
1469
+ className: "relative",
1470
+ children: [
1471
+ a.previewUrl ? /* @__PURE__ */ jsx10("img", {
1472
+ src: a.previewUrl,
1473
+ alt: a.name,
1474
+ className: "h-14 w-14 rounded-md border border-border object-cover"
1475
+ }) : /* @__PURE__ */ jsx10("span", {
1476
+ className: "block h-14 w-14 rounded-md border border-border bg-muted"
1477
+ }),
1478
+ /* @__PURE__ */ jsx10("button", {
1479
+ type: "button",
1480
+ onClick: () => remove(i),
1481
+ "aria-label": `Remove ${a.name}`,
1482
+ className: "absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-background",
1483
+ children: /* @__PURE__ */ jsx10(X2, {
1484
+ className: "h-3 w-3",
1485
+ "aria-hidden": true
1486
+ })
1487
+ })
1488
+ ]
1489
+ }, `${a.name}-${i}`))
1490
+ }),
1491
+ /* @__PURE__ */ jsx10("textarea", {
1492
+ ref,
1493
+ value: text,
1494
+ onChange: (e) => setText(e.target.value),
1495
+ onKeyDown: onKey,
1496
+ onPaste,
1497
+ disabled,
1498
+ autoFocus,
1499
+ rows: 1,
1500
+ placeholder: disabled && disabledReason ? disabledReason : placeholder,
1501
+ "aria-label": "Message",
1502
+ className: "block w-full resize-none bg-transparent px-3 py-3 text-[15px] leading-6 placeholder:text-muted-foreground focus-visible:outline-none"
1503
+ }),
1504
+ /* @__PURE__ */ jsxs8("div", {
1505
+ className: "flex items-center gap-1 px-2 pb-2",
1506
+ children: [
1507
+ leading,
1508
+ hopOptions && hopOptions.length > 0 && /* @__PURE__ */ jsx10("select", {
1509
+ value: hop ?? "",
1510
+ onChange: (e) => onHopChange?.(e.target.value),
1511
+ disabled,
1512
+ "aria-label": "HOP",
1513
+ className: "min-h-9 max-w-[45%] rounded-md border border-border bg-background px-2 text-[12.5px]",
1514
+ children: hopOptions.map((o) => /* @__PURE__ */ jsx10("option", {
1515
+ value: o.value,
1516
+ title: o.description,
1517
+ children: o.label
1518
+ }, o.value))
1519
+ }),
1520
+ /* @__PURE__ */ jsxs8("label", {
1521
+ className: cn("flex h-11 w-11 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-muted", disabled && "pointer-events-none"),
1522
+ "aria-label": "Attach image",
1523
+ children: [
1524
+ /* @__PURE__ */ jsx10(ImagePlus, {
1525
+ className: "h-4 w-4",
1526
+ "aria-hidden": true
1527
+ }),
1528
+ /* @__PURE__ */ jsx10("input", {
1529
+ type: "file",
1530
+ accept: ACCEPT.join(","),
1531
+ multiple: true,
1532
+ className: "sr-only",
1533
+ disabled,
1534
+ onChange: (e) => e.target.files && void addFiles(e.target.files)
1535
+ })
1536
+ ]
1537
+ }),
1538
+ note && /* @__PURE__ */ jsx10("span", {
1539
+ className: "truncate text-[12px] text-destructive",
1540
+ children: note
1541
+ }),
1542
+ /* @__PURE__ */ jsx10("span", {
1543
+ className: "flex-1"
1544
+ }),
1545
+ running ? /* @__PURE__ */ jsx10("button", {
1546
+ type: "button",
1547
+ onClick: onStop,
1548
+ "aria-label": "Stop",
1549
+ className: "flex h-11 w-11 items-center justify-center rounded-md bg-destructive text-destructive-foreground",
1550
+ children: /* @__PURE__ */ jsx10(Square2, {
1551
+ className: "h-4 w-4",
1552
+ "aria-hidden": true
1553
+ })
1554
+ }) : /* @__PURE__ */ jsx10("button", {
1555
+ type: "button",
1556
+ onClick: submit,
1557
+ disabled: !canSend,
1558
+ "aria-label": "Send",
1559
+ className: "flex h-11 w-11 items-center justify-center rounded-md bg-primary text-primary-foreground disabled:opacity-40",
1560
+ children: /* @__PURE__ */ jsx10(ArrowUp, {
1561
+ className: "h-4 w-4",
1562
+ "aria-hidden": true
1563
+ })
1564
+ })
1565
+ ]
1566
+ })
1567
+ ]
1568
+ });
1569
+ }
1570
+ export {
1571
+ useYodexSession,
1572
+ toolSummary,
1573
+ resultLabel,
1574
+ reduce,
1575
+ permissionSummary,
1576
+ initialState,
1577
+ groupSessions,
1578
+ formatTokens,
1579
+ elapsedLabel,
1580
+ diffCounts,
1581
+ cn,
1582
+ buildOutcome,
1583
+ applyEvent,
1584
+ YodexClient,
1585
+ Transcript,
1586
+ ToolCall,
1587
+ SessionList,
1588
+ RunStatus,
1589
+ QuestionForm,
1590
+ PlanChecklist,
1591
+ PermissionPrompt,
1592
+ DiffView,
1593
+ Composer,
1594
+ AssistantMessage
1595
+ };
1596
+
1597
+ //# debugId=20955FCF1BAF8E9564756E2164756E21
1598
+ //# sourceMappingURL=index.js.map