@mlpal/yodex-ui 0.2.0 → 0.2.1

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 CHANGED
@@ -9,6 +9,7 @@ class YodexClient {
9
9
  seq = 0;
10
10
  state = "closed";
11
11
  constructor(options) {
12
+ const given = Object.fromEntries(Object.entries(options).filter(([, v]) => v !== undefined));
12
13
  this.opts = {
13
14
  backoffMs: { min: 500, max: 1e4 },
14
15
  requestTimeoutMs: 15000,
@@ -16,7 +17,7 @@ class YodexClient {
16
17
  setTimer: (fn, ms) => setTimeout(fn, ms),
17
18
  clearTimer: (h) => clearTimeout(h),
18
19
  random: Math.random,
19
- ...options
20
+ ...given
20
21
  };
21
22
  }
22
23
  get url() {
@@ -295,8 +296,19 @@ function applyEvent(state, ev, live) {
295
296
  }
296
297
  function reduce(state, action) {
297
298
  switch (action.type) {
298
- case "connection":
299
- return { ...state, connection: action.state };
299
+ case "connection": {
300
+ if (action.state === "connected" || !state.running)
301
+ return { ...state, connection: action.state };
302
+ return {
303
+ ...state,
304
+ connection: action.state,
305
+ running: false,
306
+ activity: undefined,
307
+ pendingPermissions: [],
308
+ pendingQuestion: undefined,
309
+ items: [...dropDraft(state.items), { kind: "error", id: nextId("e"), message: "Connection lost while the agent was working; what streamed so far was kept.", ts: new Date().toISOString() }]
310
+ };
311
+ }
300
312
  case "clear":
301
313
  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
314
  case "error":
@@ -324,10 +336,16 @@ function reduce(state, action) {
324
336
  case "ready":
325
337
  return { ...state, connection: "connected", protocol: m.protocol, capabilities: m.capabilities ?? [], hop: m.hop, rootCwd: m.cwd };
326
338
  case "event":
339
+ if (state.sessionId && m.sessionId !== state.sessionId)
340
+ return state;
327
341
  return applyEvent(state.sessionId ? state : { ...state, sessionId: m.sessionId }, m.event, true);
328
342
  case "permission_request":
329
- return { ...state, pendingPermissions: [...state.pendingPermissions, { id: m.id, sessionId: m.sessionId, toolName: m.toolName, input: m.input }] };
343
+ if (state.sessionId && m.sessionId !== state.sessionId)
344
+ return state;
345
+ return { ...state, pendingPermissions: [...state.pendingPermissions, { id: m.id, sessionId: m.sessionId, toolName: m.toolName, input: m.input, ...m.reason ? { reason: m.reason } : {}, ...m.safetyReason ? { safetyReason: m.safetyReason } : {} }] };
330
346
  case "question_request":
347
+ if (state.sessionId && m.sessionId !== state.sessionId)
348
+ return state;
331
349
  return { ...state, pendingQuestion: { id: m.id, sessionId: m.sessionId, questions: m.questions } };
332
350
  case "sessions":
333
351
  return { ...state, sessions: m.sessions };
@@ -345,6 +363,8 @@ function reduce(state, action) {
345
363
  };
346
364
  }
347
365
  case "done":
366
+ if (state.sessionId && m.sessionId !== state.sessionId)
367
+ return state;
348
368
  return {
349
369
  ...state,
350
370
  running: false,
@@ -391,12 +411,18 @@ import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
391
411
  function useYodexSession(options) {
392
412
  const [state, dispatch] = useReducer(reduce, undefined, initialState);
393
413
  const sessionIdRef = useRef(options.sessionId);
414
+ const allowed = useRef(new Set);
415
+ const ruleFor = (p) => `${p.toolName}\x00${typeof p.input.command === "string" ? p.input.command : ""}`;
394
416
  const client = useMemo(() => new YodexClient({
395
417
  url: options.url,
396
418
  token: options.token,
397
419
  socketFactory: options.socketFactory,
398
420
  onState: (s) => dispatch({ type: "connection", state: s }),
399
421
  onMessage: (m) => {
422
+ if (m.type === "permission_request" && allowed.current.has(ruleFor(m))) {
423
+ client.respondPermission(m.id, "allow");
424
+ return;
425
+ }
400
426
  dispatch({ type: "server", message: m });
401
427
  if (m.type === "ready" && sessionIdRef.current) {
402
428
  client.loadSession(sessionIdRef.current).catch((e) => dispatch({ type: "error", message: e.message }));
@@ -412,6 +438,16 @@ function useYodexSession(options) {
412
438
  useEffect(() => {
413
439
  sessionIdRef.current = state.sessionId ?? options.sessionId;
414
440
  }, [state.sessionId, options.sessionId]);
441
+ const lastRequested = useRef(options.sessionId);
442
+ useEffect(() => {
443
+ if (options.sessionId === lastRequested.current)
444
+ return;
445
+ lastRequested.current = options.sessionId;
446
+ sessionIdRef.current = options.sessionId;
447
+ dispatch({ type: "clear" });
448
+ if (options.sessionId && client.connected)
449
+ client.loadSession(options.sessionId).catch((e) => dispatch({ type: "error", message: e.message }));
450
+ }, [options.sessionId, client]);
415
451
  const guard = useCallback((fn) => {
416
452
  try {
417
453
  fn();
@@ -452,6 +488,23 @@ function useYodexSession(options) {
452
488
  dispatch({ type: "run_started", sessionId, at: Date.now() });
453
489
  client.respondApproval(sessionId, decision, reason);
454
490
  }), [client, guard]);
491
+ const allowForSession = useCallback((id) => {
492
+ const p = state.pendingPermissions.find((x) => x.id === id);
493
+ if (p)
494
+ allowed.current.add(ruleFor(p));
495
+ guard(() => {
496
+ client.respondPermission(id, "allow");
497
+ dispatch({ type: "permission_answered", id });
498
+ });
499
+ }, [client, guard, state.pendingPermissions]);
500
+ const steer = useCallback((text) => guard(() => {
501
+ const sessionId = sessionIdRef.current;
502
+ if (!sessionId)
503
+ throw new Error("no session to steer");
504
+ client.sendPeer(sessionId, text);
505
+ dispatch({ type: "local_user", text, ts: new Date().toISOString() });
506
+ }), [client, guard]);
507
+ const waitingOn = state.pendingQuestion ? "question" : state.pendingPermissions.length ? "permission" : undefined;
455
508
  const loadSession = useCallback(async (sessionId) => {
456
509
  sessionIdRef.current = sessionId;
457
510
  dispatch({ type: "clear" });
@@ -479,7 +532,7 @@ function useYodexSession(options) {
479
532
  sessionIdRef.current = undefined;
480
533
  dispatch({ type: "clear" });
481
534
  }, []);
482
- return { state, client, run, stop, allow, deny, answer, approve, loadSession, refreshSessions, refreshModels, newSession };
535
+ return { state, client, run, stop, allow, deny, answer, approve, allowForSession, steer, waitingOn, loadSession, refreshSessions, refreshModels, newSession };
483
536
  }
484
537
  // src/components/Transcript.tsx
485
538
  import { useEffect as useEffect2, useRef as useRef2 } from "react";
@@ -496,73 +549,88 @@ function cn(...inputs) {
496
549
  }
497
550
 
498
551
  // src/components/AssistantMessage.tsx
499
- import { jsx } from "react/jsx-runtime";
552
+ import { jsx, jsxs } from "react/jsx-runtime";
500
553
  var plugins = [remarkGfm];
501
554
  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),
555
+ return /* @__PURE__ */ jsxs("div", {
556
+ className: cn("yx-assistant max-w-none break-words text-[15px] leading-6 text-foreground", className),
504
557
  "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", {
558
+ children: [
559
+ /* @__PURE__ */ jsx(Markdown, {
560
+ remarkPlugins: plugins,
561
+ skipHtml: true,
562
+ components: {
563
+ a: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("a", {
529
564
  ...props,
530
- className: "my-2 w-full border-collapse text-sm"
565
+ target: "_blank",
566
+ rel: "noopener noreferrer",
567
+ className: "text-info underline underline-offset-2"
568
+ }),
569
+ pre: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("pre", {
570
+ ...props,
571
+ className: "overflow-x-auto rounded-md border border-border bg-card p-3 font-mono text-[12.5px] leading-5"
572
+ }),
573
+ code: ({ node: _n, className: cls, ...props }) => cls ? /* @__PURE__ */ jsx("code", {
574
+ ...props,
575
+ className: cls
576
+ }) : /* @__PURE__ */ jsx("code", {
577
+ ...props,
578
+ className: "rounded bg-muted px-1 py-0.5 font-mono text-[0.9em]"
579
+ }),
580
+ table: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("div", {
581
+ className: "overflow-x-auto",
582
+ children: /* @__PURE__ */ jsx("table", {
583
+ ...props,
584
+ className: "my-2 w-full border-collapse text-sm"
585
+ })
586
+ }),
587
+ th: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("th", {
588
+ ...props,
589
+ className: "border border-border bg-muted px-2 py-1 text-left font-medium text-foreground"
590
+ }),
591
+ strong: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("strong", {
592
+ ...props,
593
+ className: "font-semibold text-foreground"
594
+ }),
595
+ blockquote: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("blockquote", {
596
+ ...props,
597
+ className: "border-l-2 border-border pl-3 text-muted-foreground"
598
+ }),
599
+ td: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("td", {
600
+ ...props,
601
+ className: "border border-border px-2 py-1 align-top"
531
602
  })
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
- })
603
+ },
604
+ children: text
605
+ }),
606
+ live && /* @__PURE__ */ jsx("span", {
607
+ className: "yx-caret",
608
+ "aria-hidden": true,
609
+ children: "▍"
610
+ })
611
+ ]
544
612
  });
545
613
  }
546
614
 
547
615
  // src/components/PlanChecklist.tsx
548
616
  import { Check, Circle, Loader2 } from "lucide-react";
549
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
617
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
550
618
  function PlanChecklist({ items, className }) {
551
619
  if (!items.length)
552
620
  return null;
553
621
  const done = items.filter((t) => t.status === "completed").length;
554
- return /* @__PURE__ */ jsxs("div", {
622
+ return /* @__PURE__ */ jsxs2("div", {
555
623
  className: cn("yx-plan rounded-md border border-border bg-card p-3 text-sm", className),
556
624
  "aria-label": "plan",
557
625
  children: [
558
- /* @__PURE__ */ jsxs("div", {
626
+ /* @__PURE__ */ jsxs2("div", {
559
627
  className: "mb-2 flex items-center justify-between",
560
628
  children: [
561
629
  /* @__PURE__ */ jsx2("span", {
562
630
  className: "font-medium",
563
631
  children: "Plan"
564
632
  }),
565
- /* @__PURE__ */ jsxs("span", {
633
+ /* @__PURE__ */ jsxs2("span", {
566
634
  className: "font-mono text-[12px] tabular-nums text-muted-foreground",
567
635
  children: [
568
636
  done,
@@ -574,7 +642,7 @@ function PlanChecklist({ items, className }) {
574
642
  }),
575
643
  /* @__PURE__ */ jsx2("ol", {
576
644
  className: "space-y-1",
577
- children: items.map((t) => /* @__PURE__ */ jsxs("li", {
645
+ children: items.map((t) => /* @__PURE__ */ jsxs2("li", {
578
646
  className: cn("flex items-start gap-2", t.status === "completed" && "text-muted-foreground line-through"),
579
647
  "data-status": t.status,
580
648
  children: [
@@ -635,7 +703,7 @@ function DiffView({ diff, className }) {
635
703
  }
636
704
 
637
705
  // src/components/ToolCall.tsx
638
- import { jsx as jsx4, jsxs as jsxs2, Fragment } from "react/jsx-runtime";
706
+ import { jsx as jsx4, jsxs as jsxs3, Fragment } from "react/jsx-runtime";
639
707
  function toolSummary(name, input) {
640
708
  const pick = (...keys) => {
641
709
  for (const k of keys) {
@@ -681,12 +749,12 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
681
749
  const counts = r?.diff ? diffCounts(r.diff) : null;
682
750
  const path = typeof item.input.file_path === "string" ? item.input.file_path : typeof item.input.path === "string" ? item.input.path : undefined;
683
751
  const Icon = r?.diff ? FileDiff : item.name === "Bash" ? Terminal : Wrench;
684
- return /* @__PURE__ */ jsxs2("div", {
752
+ return /* @__PURE__ */ jsxs3("div", {
685
753
  className: cn("yx-tool rounded-md border border-border bg-card text-sm", status === "error" && "border-destructive/40"),
686
754
  "data-tool": item.name,
687
755
  "data-status": status,
688
756
  children: [
689
- /* @__PURE__ */ jsxs2("button", {
757
+ /* @__PURE__ */ jsxs3("button", {
690
758
  type: "button",
691
759
  onClick: () => setOpen((o) => !o),
692
760
  "aria-expanded": open,
@@ -708,10 +776,10 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
708
776
  className: "min-w-0 flex-1 truncate font-mono text-[12.5px] text-muted-foreground",
709
777
  children: clip(summary)
710
778
  }),
711
- counts && /* @__PURE__ */ jsxs2("span", {
779
+ counts && /* @__PURE__ */ jsxs3("span", {
712
780
  className: "shrink-0 font-mono text-[12px] tabular-nums",
713
781
  children: [
714
- /* @__PURE__ */ jsxs2("span", {
782
+ /* @__PURE__ */ jsxs3("span", {
715
783
  className: "text-success",
716
784
  children: [
717
785
  "+",
@@ -719,7 +787,7 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
719
787
  ]
720
788
  }),
721
789
  " ",
722
- /* @__PURE__ */ jsxs2("span", {
790
+ /* @__PURE__ */ jsxs3("span", {
723
791
  className: "text-destructive",
724
792
  children: [
725
793
  "−",
@@ -742,7 +810,7 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
742
810
  })
743
811
  ]
744
812
  }),
745
- open && /* @__PURE__ */ jsxs2("div", {
813
+ open && /* @__PURE__ */ jsxs3("div", {
746
814
  className: "border-t border-border px-3 py-2",
747
815
  children: [
748
816
  /* @__PURE__ */ jsx4("div", {
@@ -753,9 +821,9 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
753
821
  className: "overflow-x-auto whitespace-pre-wrap break-words font-mono text-[12px] leading-5",
754
822
  children: summary && Object.keys(item.input).length === 1 ? summary : JSON.stringify(item.input, null, 2)
755
823
  }),
756
- r && /* @__PURE__ */ jsxs2(Fragment, {
824
+ r && /* @__PURE__ */ jsxs3(Fragment, {
757
825
  children: [
758
- /* @__PURE__ */ jsxs2("div", {
826
+ /* @__PURE__ */ jsxs3("div", {
759
827
  className: "mb-1 mt-2 flex items-center gap-2 text-[11px] uppercase tracking-wide text-muted-foreground",
760
828
  children: [
761
829
  /* @__PURE__ */ jsx4("span", {
@@ -785,7 +853,85 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
785
853
  }
786
854
 
787
855
  // src/components/Transcript.tsx
788
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
856
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
857
+ function TranscriptRow({ item, onOpenFile }) {
858
+ const it = item;
859
+ switch (it.kind) {
860
+ case "user":
861
+ return /* @__PURE__ */ jsx5("div", {
862
+ className: cn("flex", it.author.type === "human" ? "justify-end" : "justify-start"),
863
+ "data-author": it.author.type,
864
+ children: /* @__PURE__ */ jsxs4("div", {
865
+ 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"),
866
+ children: [
867
+ it.author.type !== "human" && /* @__PURE__ */ jsx5("div", {
868
+ className: "mb-1 text-[11px] text-muted-foreground",
869
+ children: it.author.displayName ?? it.author.type
870
+ }),
871
+ it.blocks?.some((b) => b.type === "image") && /* @__PURE__ */ jsx5("div", {
872
+ className: "mb-1 text-[11px] opacity-80",
873
+ children: "[image]"
874
+ }),
875
+ it.text
876
+ ]
877
+ })
878
+ });
879
+ case "assistant":
880
+ return /* @__PURE__ */ jsx5(AssistantMessage, {
881
+ text: it.text,
882
+ live: it.live
883
+ });
884
+ case "tool":
885
+ return /* @__PURE__ */ jsx5(ToolCall, {
886
+ item: it,
887
+ onOpenFile
888
+ });
889
+ case "plan":
890
+ return /* @__PURE__ */ jsx5(PlanChecklist, {
891
+ items: it.items
892
+ });
893
+ case "result": {
894
+ const { label, tone } = resultLabel(it.result);
895
+ return /* @__PURE__ */ jsxs4("div", {
896
+ className: cn("flex items-center gap-2 text-[12px] text-muted-foreground", tone === "bad" && "text-destructive"),
897
+ "data-subtype": it.result.subtype,
898
+ children: [
899
+ /* @__PURE__ */ jsx5("span", {
900
+ children: label
901
+ }),
902
+ /* @__PURE__ */ jsx5("span", {
903
+ children: "·"
904
+ }),
905
+ /* @__PURE__ */ jsxs4("span", {
906
+ className: "font-mono tabular-nums",
907
+ children: [
908
+ it.result.numTurns,
909
+ " step",
910
+ it.result.numTurns === 1 ? "" : "s",
911
+ " · ",
912
+ formatTokens(it.result.usage)
913
+ ]
914
+ })
915
+ ]
916
+ });
917
+ }
918
+ case "error":
919
+ return /* @__PURE__ */ jsx5("div", {
920
+ role: "alert",
921
+ className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-[13px] text-destructive",
922
+ children: it.message
923
+ });
924
+ case "compact":
925
+ return /* @__PURE__ */ jsxs4("div", {
926
+ className: "text-center text-[11px] text-muted-foreground",
927
+ children: [
928
+ "— context compacted (",
929
+ it.replacedMessages,
930
+ " earlier messages summarised) —"
931
+ ]
932
+ });
933
+ }
934
+ }
789
935
  function Transcript({ items, autoScroll = true, onOpenFile, footer, emptyState, className }) {
790
936
  const ref = useRef2(null);
791
937
  const pinned = useRef2(true);
@@ -808,97 +954,23 @@ function Transcript({ items, autoScroll = true, onOpenFile, footer, emptyState,
808
954
  ref,
809
955
  className: cn("yx-transcript flex-1 overflow-y-auto px-4 py-3", className),
810
956
  role: "log",
811
- "aria-live": "polite",
812
- children: /* @__PURE__ */ jsxs3("div", {
957
+ children: /* @__PURE__ */ jsxs4("div", {
813
958
  className: "mx-auto flex w-full max-w-3xl flex-col gap-3",
814
959
  children: [
815
960
  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
- }),
961
+ items.map((it) => /* @__PURE__ */ jsx5(TranscriptRow, {
962
+ item: it,
963
+ onOpenFile
964
+ }, it.id)),
893
965
  footer
894
966
  ]
895
967
  })
896
968
  });
897
969
  }
898
970
  // src/components/PermissionPrompt.tsx
899
- import { useEffect as useEffect3 } from "react";
971
+ import { useEffect as useEffect3, useState as useState2 } from "react";
900
972
  import { ShieldAlert } from "lucide-react";
901
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
973
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
902
974
  function permissionSummary(p) {
903
975
  const v = p.input.command ?? p.input.file_path ?? p.input.path ?? p.input.url;
904
976
  if (typeof v === "string")
@@ -906,8 +978,12 @@ function permissionSummary(p) {
906
978
  const s = JSON.stringify(p.input);
907
979
  return s.length > 240 ? `${s.slice(0, 239)}…` : s;
908
980
  }
909
- function PermissionPrompt({ request, onAllow, onDeny, className }) {
981
+ function PermissionPrompt({ request, onAllow, onDeny, onAllowSession, hotkeys = false, className }) {
982
+ const [reasonOpen, setReasonOpen] = useState2(false);
983
+ const [reason, setReason] = useState2("");
910
984
  useEffect3(() => {
985
+ if (!hotkeys)
986
+ return;
911
987
  const key = (e) => {
912
988
  const t = e.target;
913
989
  if (t && (t.tagName === "TEXTAREA" || t.tagName === "INPUT" || t.isContentEditable))
@@ -919,15 +995,19 @@ function PermissionPrompt({ request, onAllow, onDeny, className }) {
919
995
  };
920
996
  document.addEventListener("keydown", key);
921
997
  return () => document.removeEventListener("keydown", key);
922
- }, [request.id, onAllow, onDeny]);
923
- return /* @__PURE__ */ jsxs4("div", {
998
+ }, [hotkeys, request.id, onAllow, onDeny]);
999
+ const kbd = (k) => hotkeys ? /* @__PURE__ */ jsx6("kbd", {
1000
+ className: "ml-1 rounded bg-black/10 px-1 text-[11px]",
1001
+ children: k
1002
+ }) : null;
1003
+ return /* @__PURE__ */ jsxs5("div", {
924
1004
  role: "alertdialog",
925
1005
  "aria-labelledby": `perm-${request.id}`,
926
1006
  className: cn("yx-permission rounded-md border border-warning/50 bg-card p-3 text-sm", className),
927
1007
  children: [
928
- /* @__PURE__ */ jsxs4("div", {
1008
+ /* @__PURE__ */ jsxs5("div", {
929
1009
  id: `perm-${request.id}`,
930
- className: "mb-2 flex items-center gap-2 font-medium",
1010
+ className: "mb-1 flex items-center gap-2 font-medium",
931
1011
  children: [
932
1012
  /* @__PURE__ */ jsx6(ShieldAlert, {
933
1013
  className: "h-4 w-4 text-warning",
@@ -941,46 +1021,116 @@ function PermissionPrompt({ request, onAllow, onDeny, className }) {
941
1021
  "?"
942
1022
  ]
943
1023
  }),
1024
+ (request.safetyReason || request.reason) && /* @__PURE__ */ jsx6("div", {
1025
+ className: "mb-2 text-[12px] text-muted-foreground",
1026
+ children: request.safetyReason ?? request.reason
1027
+ }),
944
1028
  /* @__PURE__ */ jsx6("pre", {
945
1029
  className: "mb-3 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2 font-mono text-[12.5px] leading-5",
946
1030
  children: permissionSummary(request)
947
1031
  }),
948
- /* @__PURE__ */ jsxs4("div", {
949
- className: "flex gap-2",
1032
+ /* @__PURE__ */ jsxs5("div", {
1033
+ className: "flex flex-wrap gap-2",
950
1034
  children: [
951
- /* @__PURE__ */ jsxs4("button", {
1035
+ /* @__PURE__ */ jsxs5("button", {
952
1036
  type: "button",
953
1037
  onClick: () => onAllow(request.id),
954
1038
  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
1039
  children: [
956
1040
  "Allow ",
957
- /* @__PURE__ */ jsx6("kbd", {
958
- className: "ml-1 rounded bg-primary-foreground/20 px-1 text-[11px]",
959
- children: "a"
960
- })
1041
+ kbd("a")
961
1042
  ]
962
1043
  }),
963
- /* @__PURE__ */ jsxs4("button", {
1044
+ onAllowSession && /* @__PURE__ */ jsx6("button", {
1045
+ type: "button",
1046
+ onClick: () => onAllowSession(request.id),
1047
+ className: "min-h-11 flex-1 rounded-md border border-border bg-background px-4 font-medium hover:bg-muted",
1048
+ children: "Allow for this session"
1049
+ }),
1050
+ /* @__PURE__ */ jsxs5("button", {
964
1051
  type: "button",
965
- onClick: () => onDeny(request.id),
1052
+ onClick: () => reasonOpen ? onDeny(request.id, reason.trim() || undefined) : onDeny(request.id),
966
1053
  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
1054
  children: [
968
1055
  "Deny ",
969
- /* @__PURE__ */ jsx6("kbd", {
970
- className: "ml-1 rounded bg-muted px-1 text-[11px]",
971
- children: "d"
972
- })
1056
+ kbd("d")
973
1057
  ]
974
1058
  })
975
1059
  ]
1060
+ }),
1061
+ reasonOpen ? /* @__PURE__ */ jsxs5("label", {
1062
+ className: "mt-2 block",
1063
+ children: [
1064
+ /* @__PURE__ */ jsx6("span", {
1065
+ className: "mb-1 block text-[12px] text-muted-foreground",
1066
+ children: "Tell the agent why (sent with the denial)"
1067
+ }),
1068
+ /* @__PURE__ */ jsx6("input", {
1069
+ value: reason,
1070
+ onChange: (e) => setReason(e.target.value),
1071
+ className: "min-h-11 w-full rounded-md border border-border bg-input-background px-3 text-[16px]"
1072
+ })
1073
+ ]
1074
+ }) : /* @__PURE__ */ jsx6("button", {
1075
+ type: "button",
1076
+ onClick: () => setReasonOpen(true),
1077
+ className: "mt-2 min-h-9 text-[12px] text-info underline-offset-2 hover:underline",
1078
+ children: "Deny with a reason…"
1079
+ })
1080
+ ]
1081
+ });
1082
+ }
1083
+ // src/components/ApprovalPrompt.tsx
1084
+ import { ShieldAlert as ShieldAlert2 } from "lucide-react";
1085
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1086
+ function ApprovalPrompt({ approval, onDecide, className }) {
1087
+ return /* @__PURE__ */ jsxs6("div", {
1088
+ role: "alertdialog",
1089
+ "aria-label": "Approval needed",
1090
+ className: cn("yx-approval rounded-md border border-warning/50 bg-card p-3 text-sm", className),
1091
+ children: [
1092
+ /* @__PURE__ */ jsxs6("div", {
1093
+ className: "mb-1 flex items-center gap-2 font-medium",
1094
+ children: [
1095
+ /* @__PURE__ */ jsx7(ShieldAlert2, {
1096
+ className: "h-4 w-4 text-warning",
1097
+ "aria-hidden": true
1098
+ }),
1099
+ "The agent stopped before this and needs your approval"
1100
+ ]
1101
+ }),
1102
+ /* @__PURE__ */ jsx7("div", {
1103
+ className: "mb-2 text-[12px] text-muted-foreground",
1104
+ children: approval.reason
1105
+ }),
1106
+ /* @__PURE__ */ jsx7("pre", {
1107
+ className: "mb-3 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2 font-mono text-[12.5px] leading-5",
1108
+ children: approval.command
1109
+ }),
1110
+ /* @__PURE__ */ jsxs6("div", {
1111
+ className: "flex gap-2",
1112
+ children: [
1113
+ /* @__PURE__ */ jsx7("button", {
1114
+ type: "button",
1115
+ onClick: () => onDecide("allow"),
1116
+ className: "min-h-11 flex-1 rounded-md bg-primary px-4 font-medium text-primary-foreground hover:opacity-90",
1117
+ children: "Approve and continue"
1118
+ }),
1119
+ /* @__PURE__ */ jsx7("button", {
1120
+ type: "button",
1121
+ onClick: () => onDecide("deny"),
1122
+ className: "min-h-11 flex-1 rounded-md border border-border px-4 font-medium hover:bg-muted",
1123
+ children: "Don't run it"
1124
+ })
1125
+ ]
976
1126
  })
977
1127
  ]
978
1128
  });
979
1129
  }
980
1130
  // src/components/QuestionForm.tsx
981
- import { useState as useState2 } from "react";
1131
+ import { useState as useState3 } from "react";
982
1132
  import { HelpCircle, X } from "lucide-react";
983
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1133
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
984
1134
  function emptyDraft(questions) {
985
1135
  return { selected: questions.map(() => []), notes: questions.map(() => "") };
986
1136
  }
@@ -1003,8 +1153,8 @@ function chip(q, i) {
1003
1153
  }
1004
1154
  function QuestionForm({ request, onSubmit, onDecline, className }) {
1005
1155
  const qs = request.questions;
1006
- const [tab, setTab] = useState2(0);
1007
- const [draft, setDraft] = useState2(() => emptyDraft(qs));
1156
+ const [tab, setTab] = useState3(0);
1157
+ const [draft, setDraft] = useState3(() => emptyDraft(qs));
1008
1158
  const q = qs[tab];
1009
1159
  if (!q)
1010
1160
  return null;
@@ -1024,38 +1174,38 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1024
1174
  });
1025
1175
  const answered = qs.map((_, i) => (draft.selected[i]?.length ?? 0) > 0 || (draft.notes[i] ?? "").trim().length > 0);
1026
1176
  const previewOf = (q.options ?? []).find((o) => picked[0] === o.label)?.preview;
1027
- return /* @__PURE__ */ jsxs5("div", {
1177
+ return /* @__PURE__ */ jsxs7("div", {
1028
1178
  role: "dialog",
1029
1179
  "aria-label": "The agent has a question",
1030
1180
  className: cn("yx-question rounded-md border border-info/50 bg-card text-sm", className),
1031
1181
  children: [
1032
- /* @__PURE__ */ jsxs5("div", {
1182
+ /* @__PURE__ */ jsxs7("div", {
1033
1183
  className: "flex items-center gap-2 border-b border-border px-3 py-2",
1034
1184
  children: [
1035
- /* @__PURE__ */ jsx7(HelpCircle, {
1185
+ /* @__PURE__ */ jsx8(HelpCircle, {
1036
1186
  className: "h-4 w-4 text-info",
1037
1187
  "aria-hidden": true
1038
1188
  }),
1039
- /* @__PURE__ */ jsx7("span", {
1189
+ /* @__PURE__ */ jsx8("span", {
1040
1190
  className: "font-medium",
1041
1191
  children: "The agent needs your input"
1042
1192
  }),
1043
- /* @__PURE__ */ jsx7("button", {
1193
+ /* @__PURE__ */ jsx8("button", {
1044
1194
  type: "button",
1045
1195
  onClick: () => onDecline(request.id),
1046
1196
  "aria-label": "Close and keep talking",
1047
1197
  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, {
1198
+ children: /* @__PURE__ */ jsx8(X, {
1049
1199
  className: "h-4 w-4",
1050
1200
  "aria-hidden": true
1051
1201
  })
1052
1202
  })
1053
1203
  ]
1054
1204
  }),
1055
- qs.length > 1 && /* @__PURE__ */ jsx7("div", {
1205
+ qs.length > 1 && /* @__PURE__ */ jsx8("div", {
1056
1206
  role: "tablist",
1057
1207
  className: "flex gap-1 overflow-x-auto border-b border-border px-2 py-1",
1058
- children: qs.map((qq, i) => /* @__PURE__ */ jsx7("button", {
1208
+ children: qs.map((qq, i) => /* @__PURE__ */ jsx8("button", {
1059
1209
  role: "tab",
1060
1210
  type: "button",
1061
1211
  "aria-selected": i === tab,
@@ -1064,37 +1214,37 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1064
1214
  children: chip(qq, i)
1065
1215
  }, i))
1066
1216
  }),
1067
- /* @__PURE__ */ jsxs5("div", {
1217
+ /* @__PURE__ */ jsxs7("div", {
1068
1218
  className: "space-y-3 p-3",
1069
1219
  children: [
1070
- /* @__PURE__ */ jsx7("p", {
1220
+ /* @__PURE__ */ jsx8("p", {
1071
1221
  className: "font-medium",
1072
1222
  children: q.question
1073
1223
  }),
1074
- (q.options ?? []).length > 0 && /* @__PURE__ */ jsx7("div", {
1224
+ (q.options ?? []).length > 0 && /* @__PURE__ */ jsx8("div", {
1075
1225
  role: q.multiSelect ? "group" : "radiogroup",
1076
1226
  className: "space-y-2",
1077
1227
  children: (q.options ?? []).map((o) => {
1078
1228
  const on = picked.includes(o.label);
1079
- return /* @__PURE__ */ jsxs5("button", {
1229
+ return /* @__PURE__ */ jsxs7("button", {
1080
1230
  type: "button",
1081
1231
  role: q.multiSelect ? "checkbox" : "radio",
1082
1232
  "aria-checked": on,
1083
1233
  onClick: () => toggle(o.label),
1084
1234
  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
1235
  children: [
1086
- /* @__PURE__ */ jsx7("span", {
1236
+ /* @__PURE__ */ jsx8("span", {
1087
1237
  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
1238
  "aria-hidden": true
1089
1239
  }),
1090
- /* @__PURE__ */ jsxs5("span", {
1240
+ /* @__PURE__ */ jsxs7("span", {
1091
1241
  className: "min-w-0",
1092
1242
  children: [
1093
- /* @__PURE__ */ jsx7("span", {
1243
+ /* @__PURE__ */ jsx8("span", {
1094
1244
  className: "block font-medium",
1095
1245
  children: o.label
1096
1246
  }),
1097
- o.description && /* @__PURE__ */ jsx7("span", {
1247
+ o.description && /* @__PURE__ */ jsx8("span", {
1098
1248
  className: "block text-muted-foreground",
1099
1249
  children: o.description
1100
1250
  })
@@ -1104,18 +1254,18 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1104
1254
  }, o.label);
1105
1255
  })
1106
1256
  }),
1107
- previewOf && /* @__PURE__ */ jsx7("pre", {
1257
+ previewOf && /* @__PURE__ */ jsx8("pre", {
1108
1258
  className: "overflow-x-auto rounded-md border border-border bg-muted p-2 font-mono text-[12px] leading-5",
1109
1259
  children: previewOf
1110
1260
  }),
1111
- /* @__PURE__ */ jsxs5("label", {
1261
+ /* @__PURE__ */ jsxs7("label", {
1112
1262
  className: "block",
1113
1263
  children: [
1114
- /* @__PURE__ */ jsx7("span", {
1264
+ /* @__PURE__ */ jsx8("span", {
1115
1265
  className: "mb-1 block text-[12px] text-muted-foreground",
1116
1266
  children: (q.options ?? []).length ? "Add a note (optional)" : "Your answer"
1117
1267
  }),
1118
- /* @__PURE__ */ jsx7("textarea", {
1268
+ /* @__PURE__ */ jsx8("textarea", {
1119
1269
  value: draft.notes[tab] ?? "",
1120
1270
  onChange: (e) => setNote(e.target.value),
1121
1271
  rows: 2,
@@ -1123,16 +1273,16 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1123
1273
  })
1124
1274
  ]
1125
1275
  }),
1126
- /* @__PURE__ */ jsxs5("div", {
1276
+ /* @__PURE__ */ jsxs7("div", {
1127
1277
  className: "flex gap-2",
1128
1278
  children: [
1129
- tab < qs.length - 1 ? /* @__PURE__ */ jsx7("button", {
1279
+ tab < qs.length - 1 ? /* @__PURE__ */ jsx8("button", {
1130
1280
  type: "button",
1131
1281
  onClick: () => setTab(tab + 1),
1132
1282
  className: "min-h-11 flex-1 rounded-md border border-border px-4 font-medium hover:bg-muted",
1133
1283
  children: "Next"
1134
1284
  }) : null,
1135
- /* @__PURE__ */ jsx7("button", {
1285
+ /* @__PURE__ */ jsx8("button", {
1136
1286
  type: "button",
1137
1287
  onClick: () => onSubmit(request.id, buildOutcome(qs, draft)),
1138
1288
  disabled: !answered.some(Boolean),
@@ -1147,11 +1297,11 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1147
1297
  });
1148
1298
  }
1149
1299
  // src/components/RunStatus.tsx
1150
- import { useEffect as useEffect4, useState as useState3 } from "react";
1300
+ import { useEffect as useEffect4, useState as useState4 } from "react";
1151
1301
  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);
1302
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1303
+ function RunStatus({ running, startedAt, activity, waitingOn, lastResult, usageTotal, connection, onStop, onApprove, className, now = Date.now }) {
1304
+ const [, tick] = useState4(0);
1155
1305
  useEffect4(() => {
1156
1306
  if (!running)
1157
1307
  return;
@@ -1159,11 +1309,11 @@ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, conne
1159
1309
  return () => clearInterval(h);
1160
1310
  }, [running]);
1161
1311
  if (connection && connection !== "connected") {
1162
- return /* @__PURE__ */ jsxs6("div", {
1312
+ return /* @__PURE__ */ jsxs8("div", {
1163
1313
  className: cn("yx-status flex min-h-9 items-center gap-2 text-[12.5px] text-muted-foreground", className),
1164
1314
  role: "status",
1165
1315
  children: [
1166
- /* @__PURE__ */ jsx8(Loader23, {
1316
+ /* @__PURE__ */ jsx9(Loader23, {
1167
1317
  className: "h-3.5 w-3.5 animate-spin",
1168
1318
  "aria-hidden": true
1169
1319
  }),
@@ -1172,32 +1322,34 @@ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, conne
1172
1322
  });
1173
1323
  }
1174
1324
  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",
1325
+ const label2 = waitingOn === "permission" ? "Waiting for your approval" : waitingOn === "question" ? "Waiting for your answer" : activity ?? "Working";
1326
+ return /* @__PURE__ */ jsxs8("div", {
1327
+ className: cn("yx-status flex min-h-11 items-center gap-2 text-[12.5px] text-muted-foreground", className),
1179
1328
  children: [
1180
- /* @__PURE__ */ jsx8(Loader23, {
1181
- className: "h-3.5 w-3.5 animate-spin text-accent",
1329
+ /* @__PURE__ */ jsx9(Loader23, {
1330
+ className: cn("h-3.5 w-3.5 animate-spin", waitingOn ? "text-warning" : "text-accent"),
1182
1331
  "aria-hidden": true
1183
1332
  }),
1184
- /* @__PURE__ */ jsx8("span", {
1185
- children: activity ?? "Working"
1333
+ /* @__PURE__ */ jsx9("span", {
1334
+ role: "status",
1335
+ "aria-live": "polite",
1336
+ children: label2
1186
1337
  }),
1187
- startedAt !== undefined && /* @__PURE__ */ jsxs6("span", {
1338
+ startedAt !== undefined && /* @__PURE__ */ jsxs8("span", {
1188
1339
  className: "font-mono tabular-nums",
1340
+ "aria-hidden": true,
1189
1341
  children: [
1190
1342
  "· ",
1191
1343
  elapsedLabel(now() - startedAt)
1192
1344
  ]
1193
1345
  }),
1194
- onStop && /* @__PURE__ */ jsxs6("button", {
1346
+ onStop && /* @__PURE__ */ jsxs8("button", {
1195
1347
  type: "button",
1196
1348
  onClick: onStop,
1197
- className: "ml-auto flex min-h-9 items-center gap-1 rounded-md px-2 hover:bg-muted",
1349
+ className: "ml-auto flex min-h-11 items-center gap-1 rounded-md px-3 hover:bg-muted",
1198
1350
  "aria-label": "Stop",
1199
1351
  children: [
1200
- /* @__PURE__ */ jsx8(Square, {
1352
+ /* @__PURE__ */ jsx9(Square, {
1201
1353
  className: "h-3.5 w-3.5",
1202
1354
  "aria-hidden": true
1203
1355
  }),
@@ -1211,30 +1363,30 @@ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, conne
1211
1363
  return null;
1212
1364
  const { label, tone } = resultLabel(lastResult);
1213
1365
  const needsApproval = lastResult.subtype === "needs_approval";
1214
- return /* @__PURE__ */ jsxs6("div", {
1366
+ return /* @__PURE__ */ jsxs8("div", {
1215
1367
  className: cn("yx-status flex min-h-9 flex-wrap items-center gap-2 text-[12.5px]", className),
1216
1368
  role: "status",
1217
1369
  "data-subtype": lastResult.subtype,
1218
1370
  children: [
1219
- tone === "ok" ? /* @__PURE__ */ jsx8(Check3, {
1371
+ tone === "ok" ? /* @__PURE__ */ jsx9(Check3, {
1220
1372
  className: "h-3.5 w-3.5 text-success",
1221
1373
  "aria-hidden": true
1222
- }) : tone === "bad" ? /* @__PURE__ */ jsx8(AlertTriangle, {
1374
+ }) : tone === "bad" ? /* @__PURE__ */ jsx9(AlertTriangle, {
1223
1375
  className: "h-3.5 w-3.5 text-destructive",
1224
1376
  "aria-hidden": true
1225
- }) : /* @__PURE__ */ jsx8(AlertTriangle, {
1377
+ }) : /* @__PURE__ */ jsx9(AlertTriangle, {
1226
1378
  className: cn("h-3.5 w-3.5", tone === "warn" ? "text-warning" : "text-muted-foreground"),
1227
1379
  "aria-hidden": true
1228
1380
  }),
1229
- /* @__PURE__ */ jsx8("span", {
1381
+ /* @__PURE__ */ jsx9("span", {
1230
1382
  className: cn(tone === "ok" && "text-success", tone === "bad" && "text-destructive", tone === "warn" && "text-warning", tone === "muted" && "text-muted-foreground"),
1231
1383
  children: label
1232
1384
  }),
1233
- /* @__PURE__ */ jsx8("span", {
1385
+ /* @__PURE__ */ jsx9("span", {
1234
1386
  className: "text-muted-foreground",
1235
1387
  children: "·"
1236
1388
  }),
1237
- /* @__PURE__ */ jsxs6("span", {
1389
+ /* @__PURE__ */ jsxs8("span", {
1238
1390
  className: "font-mono tabular-nums text-muted-foreground",
1239
1391
  children: [
1240
1392
  lastResult.numTurns,
@@ -1244,42 +1396,19 @@ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, conne
1244
1396
  formatTokens(usageTotal ?? lastResult.usage)
1245
1397
  ]
1246
1398
  }),
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
- ]
1399
+ needsApproval && lastResult.pendingApproval && onApprove && /* @__PURE__ */ jsx9("div", {
1400
+ className: "basis-full",
1401
+ children: /* @__PURE__ */ jsx9(ApprovalPrompt, {
1402
+ approval: lastResult.pendingApproval,
1403
+ onDecide: onApprove
1404
+ })
1276
1405
  })
1277
1406
  ]
1278
1407
  });
1279
1408
  }
1280
1409
  // src/components/SessionList.tsx
1281
1410
  import { Plus } from "lucide-react";
1282
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1411
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1283
1412
  function groupSessions(rows) {
1284
1413
  const groups = new Map;
1285
1414
  for (const r of rows) {
@@ -1299,75 +1428,75 @@ function when(iso) {
1299
1428
  }
1300
1429
  function SessionList({ sessions, selectedId, onSelect, onNew, className }) {
1301
1430
  const groups = groupSessions(sessions);
1302
- return /* @__PURE__ */ jsxs7("nav", {
1431
+ return /* @__PURE__ */ jsxs9("nav", {
1303
1432
  className: cn("yx-sessions text-sm", className),
1304
1433
  "aria-label": "Sessions",
1305
1434
  children: [
1306
- onNew && /* @__PURE__ */ jsxs7("button", {
1435
+ onNew && /* @__PURE__ */ jsxs9("button", {
1307
1436
  type: "button",
1308
1437
  onClick: onNew,
1309
1438
  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
1439
  children: [
1311
- /* @__PURE__ */ jsx9(Plus, {
1440
+ /* @__PURE__ */ jsx10(Plus, {
1312
1441
  className: "h-4 w-4",
1313
1442
  "aria-hidden": true
1314
1443
  }),
1315
1444
  " New session"
1316
1445
  ]
1317
1446
  }),
1318
- sessions.length === 0 && /* @__PURE__ */ jsx9("p", {
1447
+ sessions.length === 0 && /* @__PURE__ */ jsx10("p", {
1319
1448
  className: "px-3 py-2 text-muted-foreground",
1320
1449
  children: "No sessions yet."
1321
1450
  }),
1322
- groups.map((g) => /* @__PURE__ */ jsxs7("div", {
1451
+ groups.map((g) => /* @__PURE__ */ jsxs9("div", {
1323
1452
  className: "mb-2",
1324
1453
  children: [
1325
- g.project && /* @__PURE__ */ jsx9("div", {
1454
+ g.project && /* @__PURE__ */ jsx10("div", {
1326
1455
  className: "px-3 py-1 text-[11px] uppercase tracking-wide text-muted-foreground",
1327
1456
  children: g.project
1328
1457
  }),
1329
- /* @__PURE__ */ jsx9("ul", {
1458
+ /* @__PURE__ */ jsx10("ul", {
1330
1459
  children: g.rows.map((s) => {
1331
1460
  const on = s.sessionId === selectedId;
1332
1461
  const title = s.name || s.title || s.lastMessagePreview || s.sessionId.slice(0, 8);
1333
- return /* @__PURE__ */ jsx9("li", {
1334
- children: /* @__PURE__ */ jsxs7("button", {
1462
+ return /* @__PURE__ */ jsx10("li", {
1463
+ children: /* @__PURE__ */ jsxs9("button", {
1335
1464
  type: "button",
1336
1465
  onClick: () => onSelect(s.sessionId),
1337
1466
  "aria-current": on ? "true" : undefined,
1338
1467
  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
1468
  "data-status": s.status,
1340
1469
  children: [
1341
- /* @__PURE__ */ jsxs7("span", {
1470
+ /* @__PURE__ */ jsxs9("span", {
1342
1471
  className: "flex items-center gap-2",
1343
1472
  children: [
1344
- /* @__PURE__ */ jsx9("span", {
1473
+ /* @__PURE__ */ jsx10("span", {
1345
1474
  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
1475
  "aria-label": s.live ? "working" : s.status,
1347
1476
  "data-live": s.live ? "true" : undefined
1348
1477
  }),
1349
- /* @__PURE__ */ jsx9("span", {
1478
+ /* @__PURE__ */ jsx10("span", {
1350
1479
  className: "min-w-0 flex-1 truncate font-medium",
1351
1480
  children: title
1352
1481
  }),
1353
- s.pendingQuestion && /* @__PURE__ */ jsx9("span", {
1482
+ s.pendingQuestion && /* @__PURE__ */ jsx10("span", {
1354
1483
  className: "shrink-0 rounded-full bg-warning px-1.5 text-[10px] font-medium text-warning-foreground",
1355
1484
  children: "question"
1356
1485
  })
1357
1486
  ]
1358
1487
  }),
1359
- /* @__PURE__ */ jsxs7("span", {
1488
+ /* @__PURE__ */ jsxs9("span", {
1360
1489
  className: "flex gap-2 truncate text-[11.5px] text-muted-foreground",
1361
1490
  children: [
1362
- s.hop && /* @__PURE__ */ jsx9("span", {
1491
+ s.hop && /* @__PURE__ */ jsx10("span", {
1363
1492
  className: "truncate",
1364
1493
  children: s.hop
1365
1494
  }),
1366
- s.relCwd && /* @__PURE__ */ jsx9("span", {
1495
+ s.relCwd && /* @__PURE__ */ jsx10("span", {
1367
1496
  className: "truncate",
1368
1497
  children: s.relCwd
1369
1498
  }),
1370
- /* @__PURE__ */ jsx9("span", {
1499
+ /* @__PURE__ */ jsx10("span", {
1371
1500
  className: "ml-auto shrink-0 tabular-nums",
1372
1501
  children: when(s.updatedAt)
1373
1502
  })
@@ -1384,9 +1513,9 @@ function SessionList({ sessions, selectedId, onSelect, onNew, className }) {
1384
1513
  });
1385
1514
  }
1386
1515
  // src/components/Composer.tsx
1387
- import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef3, useState as useState4 } from "react";
1516
+ import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
1388
1517
  import { ArrowUp, ImagePlus, Square as Square2, X as X2 } from "lucide-react";
1389
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1518
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1390
1519
  var ACCEPT = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1391
1520
  async function fileToAttachment(f) {
1392
1521
  const buf = new Uint8Array(await f.arrayBuffer());
@@ -1395,17 +1524,34 @@ async function fileToAttachment(f) {
1395
1524
  bin += String.fromCharCode(...buf.subarray(i, i + 32768));
1396
1525
  return { name: f.name || "image", mediaType: f.type, data: btoa(bin), previewUrl: URL.createObjectURL(f) };
1397
1526
  }
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);
1527
+ function coarsePointer() {
1528
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
1529
+ }
1530
+ function Composer({ onSubmit, running, onStop, onSteer, value, onChange, initialValue, submitOnEnter, hideAttach, disabled, disabledReason, placeholder = "Ask yodex to do something…", hopOptions, hop, onHopChange, leading, maxAttachmentBytes = 8 * 1024 * 1024, className, autoFocus }) {
1531
+ const [own, setOwn] = useState5(initialValue ?? "");
1532
+ const text = value ?? own;
1533
+ const setText = (t) => {
1534
+ if (value === undefined)
1535
+ setOwn(t);
1536
+ onChange?.(t);
1537
+ };
1538
+ const [attachments, setAttachments] = useState5([]);
1539
+ const [note, setNote] = useState5(null);
1402
1540
  const ref = useRef3(null);
1403
1541
  useEffect5(() => {
1404
1542
  const el = ref.current;
1405
1543
  if (!el)
1406
1544
  return;
1407
- el.style.height = "0px";
1408
- el.style.height = `${Math.min(el.scrollHeight, 240)}px`;
1545
+ const fit = () => {
1546
+ el.style.height = "0px";
1547
+ el.style.height = `${Math.min(el.scrollHeight, 240)}px`;
1548
+ };
1549
+ fit();
1550
+ if (typeof ResizeObserver === "undefined")
1551
+ return;
1552
+ const ro = new ResizeObserver(fit);
1553
+ ro.observe(el);
1554
+ return () => ro.disconnect();
1409
1555
  }, [text]);
1410
1556
  const addFiles = useCallback2(async (files) => {
1411
1557
  const list = [...files].filter((f) => ACCEPT.includes(f.type));
@@ -1426,10 +1572,16 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1426
1572
  URL.revokeObjectURL(gone.previewUrl);
1427
1573
  return a.filter((_, j) => j !== i);
1428
1574
  });
1429
- const canSend = !disabled && !running && (text.trim().length > 0 || attachments.length > 0);
1575
+ const steering = Boolean(running && onSteer);
1576
+ const canSend = !disabled && (steering ? text.trim().length > 0 : !running && (text.trim().length > 0 || attachments.length > 0));
1430
1577
  const submit = () => {
1431
1578
  if (!canSend)
1432
1579
  return;
1580
+ if (steering) {
1581
+ onSteer(text.trim());
1582
+ setText("");
1583
+ return;
1584
+ }
1433
1585
  onSubmit(text.trim(), attachments);
1434
1586
  setText("");
1435
1587
  for (const a of attachments)
@@ -1438,10 +1590,13 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1438
1590
  setAttachments([]);
1439
1591
  };
1440
1592
  const onKey = (e) => {
1441
- if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
1442
- e.preventDefault();
1443
- submit();
1444
- }
1593
+ if (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing)
1594
+ return;
1595
+ const enterSends = submitOnEnter ?? !coarsePointer();
1596
+ if (!enterSends)
1597
+ return;
1598
+ e.preventDefault();
1599
+ submit();
1445
1600
  };
1446
1601
  const onPaste = (e) => {
1447
1602
  const files = [...e.clipboardData.files];
@@ -1456,39 +1611,42 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1456
1611
  addFiles(e.dataTransfer.files);
1457
1612
  }
1458
1613
  };
1459
- return /* @__PURE__ */ jsxs8("div", {
1614
+ return /* @__PURE__ */ jsxs10("div", {
1460
1615
  className: cn("yx-composer rounded-lg border border-border bg-card shadow-sm", disabled && "opacity-70", className),
1461
1616
  onDrop,
1462
1617
  onDragOver: (e) => e.preventDefault(),
1463
1618
  "data-disabled": disabled ? "true" : undefined,
1464
1619
  children: [
1465
- attachments.length > 0 && /* @__PURE__ */ jsx10("ul", {
1620
+ attachments.length > 0 && /* @__PURE__ */ jsx11("ul", {
1466
1621
  className: "flex flex-wrap gap-2 px-3 pt-3",
1467
1622
  "aria-label": "attachments",
1468
- children: attachments.map((a, i) => /* @__PURE__ */ jsxs8("li", {
1623
+ children: attachments.map((a, i) => /* @__PURE__ */ jsxs10("li", {
1469
1624
  className: "relative",
1470
1625
  children: [
1471
- a.previewUrl ? /* @__PURE__ */ jsx10("img", {
1626
+ a.previewUrl ? /* @__PURE__ */ jsx11("img", {
1472
1627
  src: a.previewUrl,
1473
1628
  alt: a.name,
1474
1629
  className: "h-14 w-14 rounded-md border border-border object-cover"
1475
- }) : /* @__PURE__ */ jsx10("span", {
1630
+ }) : /* @__PURE__ */ jsx11("span", {
1476
1631
  className: "block h-14 w-14 rounded-md border border-border bg-muted"
1477
1632
  }),
1478
- /* @__PURE__ */ jsx10("button", {
1633
+ /* @__PURE__ */ jsx11("button", {
1479
1634
  type: "button",
1480
1635
  onClick: () => remove(i),
1481
1636
  "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
1637
+ className: "absolute -right-4 -top-4 flex h-11 w-11 items-center justify-center rounded-full",
1638
+ children: /* @__PURE__ */ jsx11("span", {
1639
+ className: "flex h-6 w-6 items-center justify-center rounded-full border border-border bg-background",
1640
+ children: /* @__PURE__ */ jsx11(X2, {
1641
+ className: "h-3 w-3",
1642
+ "aria-hidden": true
1643
+ })
1486
1644
  })
1487
1645
  })
1488
1646
  ]
1489
1647
  }, `${a.name}-${i}`))
1490
1648
  }),
1491
- /* @__PURE__ */ jsx10("textarea", {
1649
+ /* @__PURE__ */ jsx11("textarea", {
1492
1650
  ref,
1493
1651
  value: text,
1494
1652
  onChange: (e) => setText(e.target.value),
@@ -1497,35 +1655,35 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1497
1655
  disabled,
1498
1656
  autoFocus,
1499
1657
  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"
1658
+ placeholder: disabled && disabledReason ? disabledReason : steering ? "Steer the agent (it reads this at its next step)…" : placeholder,
1659
+ "aria-label": steering ? "Steer" : "Message",
1660
+ className: "block w-full resize-none bg-transparent px-3 py-3 text-[16px] leading-6 placeholder:text-muted-foreground focus-visible:outline-none"
1503
1661
  }),
1504
- /* @__PURE__ */ jsxs8("div", {
1662
+ /* @__PURE__ */ jsxs10("div", {
1505
1663
  className: "flex items-center gap-1 px-2 pb-2",
1506
1664
  children: [
1507
1665
  leading,
1508
- hopOptions && hopOptions.length > 0 && /* @__PURE__ */ jsx10("select", {
1666
+ hopOptions && hopOptions.length > 0 && /* @__PURE__ */ jsx11("select", {
1509
1667
  value: hop ?? "",
1510
1668
  onChange: (e) => onHopChange?.(e.target.value),
1511
1669
  disabled,
1512
1670
  "aria-label": "HOP",
1513
1671
  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", {
1672
+ children: hopOptions.map((o) => /* @__PURE__ */ jsx11("option", {
1515
1673
  value: o.value,
1516
1674
  title: o.description,
1517
1675
  children: o.label
1518
1676
  }, o.value))
1519
1677
  }),
1520
- /* @__PURE__ */ jsxs8("label", {
1678
+ !hideAttach && /* @__PURE__ */ jsxs10("label", {
1521
1679
  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
1680
  "aria-label": "Attach image",
1523
1681
  children: [
1524
- /* @__PURE__ */ jsx10(ImagePlus, {
1682
+ /* @__PURE__ */ jsx11(ImagePlus, {
1525
1683
  className: "h-4 w-4",
1526
1684
  "aria-hidden": true
1527
1685
  }),
1528
- /* @__PURE__ */ jsx10("input", {
1686
+ /* @__PURE__ */ jsx11("input", {
1529
1687
  type: "file",
1530
1688
  accept: ACCEPT.join(","),
1531
1689
  multiple: true,
@@ -1535,29 +1693,29 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1535
1693
  })
1536
1694
  ]
1537
1695
  }),
1538
- note && /* @__PURE__ */ jsx10("span", {
1696
+ note && /* @__PURE__ */ jsx11("span", {
1539
1697
  className: "truncate text-[12px] text-destructive",
1540
1698
  children: note
1541
1699
  }),
1542
- /* @__PURE__ */ jsx10("span", {
1700
+ /* @__PURE__ */ jsx11("span", {
1543
1701
  className: "flex-1"
1544
1702
  }),
1545
- running ? /* @__PURE__ */ jsx10("button", {
1703
+ running && onStop && !(steering && text.trim()) ? /* @__PURE__ */ jsx11("button", {
1546
1704
  type: "button",
1547
1705
  onClick: onStop,
1548
1706
  "aria-label": "Stop",
1549
1707
  className: "flex h-11 w-11 items-center justify-center rounded-md bg-destructive text-destructive-foreground",
1550
- children: /* @__PURE__ */ jsx10(Square2, {
1708
+ children: /* @__PURE__ */ jsx11(Square2, {
1551
1709
  className: "h-4 w-4",
1552
1710
  "aria-hidden": true
1553
1711
  })
1554
- }) : /* @__PURE__ */ jsx10("button", {
1712
+ }) : /* @__PURE__ */ jsx11("button", {
1555
1713
  type: "button",
1556
1714
  onClick: submit,
1557
1715
  disabled: !canSend,
1558
1716
  "aria-label": "Send",
1559
1717
  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, {
1718
+ children: /* @__PURE__ */ jsx11(ArrowUp, {
1561
1719
  className: "h-4 w-4",
1562
1720
  "aria-hidden": true
1563
1721
  })
@@ -1582,6 +1740,7 @@ export {
1582
1740
  buildOutcome,
1583
1741
  applyEvent,
1584
1742
  YodexClient,
1743
+ TranscriptRow,
1585
1744
  Transcript,
1586
1745
  ToolCall,
1587
1746
  SessionList,
@@ -1591,8 +1750,9 @@ export {
1591
1750
  PermissionPrompt,
1592
1751
  DiffView,
1593
1752
  Composer,
1594
- AssistantMessage
1753
+ AssistantMessage,
1754
+ ApprovalPrompt
1595
1755
  };
1596
1756
 
1597
- //# debugId=20955FCF1BAF8E9564756E2164756E21
1757
+ //# debugId=C0D62ACAF9016C3D64756E2164756E21
1598
1758
  //# sourceMappingURL=index.js.map