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