@mlpal/yodex-ui 0.2.0 → 0.2.2

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,
@@ -388,15 +408,42 @@ function formatTokens(u) {
388
408
  }
389
409
  // src/client/useYodexSession.ts
390
410
  import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
411
+
412
+ // src/client/rules.ts
413
+ function ruleFor(p) {
414
+ return `${p.toolName}\x00${typeof p.input.command === "string" ? p.input.command : ""}`;
415
+ }
416
+ function ruleAnswers(rules, ask) {
417
+ if (ask.safetyReason)
418
+ return false;
419
+ return rules.has(ruleFor(ask));
420
+ }
421
+ function revokeRules(rules, toolName) {
422
+ for (const rule of rules)
423
+ if (rule.startsWith(`${toolName}\x00`))
424
+ rules.delete(rule);
425
+ }
426
+ function switchesSession(nextProp, lastProp, owned) {
427
+ if (nextProp === lastProp)
428
+ return false;
429
+ return nextProp !== owned;
430
+ }
431
+
432
+ // src/client/useYodexSession.ts
391
433
  function useYodexSession(options) {
392
434
  const [state, dispatch] = useReducer(reduce, undefined, initialState);
393
435
  const sessionIdRef = useRef(options.sessionId);
436
+ const allowed = useRef(new Set);
394
437
  const client = useMemo(() => new YodexClient({
395
438
  url: options.url,
396
439
  token: options.token,
397
440
  socketFactory: options.socketFactory,
398
441
  onState: (s) => dispatch({ type: "connection", state: s }),
399
442
  onMessage: (m) => {
443
+ if (m.type === "permission_request" && ruleAnswers(allowed.current, m)) {
444
+ client.respondPermission(m.id, "allow");
445
+ return;
446
+ }
400
447
  dispatch({ type: "server", message: m });
401
448
  if (m.type === "ready" && sessionIdRef.current) {
402
449
  client.loadSession(sessionIdRef.current).catch((e) => dispatch({ type: "error", message: e.message }));
@@ -412,6 +459,17 @@ function useYodexSession(options) {
412
459
  useEffect(() => {
413
460
  sessionIdRef.current = state.sessionId ?? options.sessionId;
414
461
  }, [state.sessionId, options.sessionId]);
462
+ const lastRequested = useRef(options.sessionId);
463
+ useEffect(() => {
464
+ const switching = switchesSession(options.sessionId, lastRequested.current, sessionIdRef.current);
465
+ lastRequested.current = options.sessionId;
466
+ if (!switching)
467
+ return;
468
+ sessionIdRef.current = options.sessionId;
469
+ dispatch({ type: "clear" });
470
+ if (options.sessionId && client.connected)
471
+ client.loadSession(options.sessionId).catch((e) => dispatch({ type: "error", message: e.message }));
472
+ }, [options.sessionId, client]);
415
473
  const guard = useCallback((fn) => {
416
474
  try {
417
475
  fn();
@@ -452,6 +510,25 @@ function useYodexSession(options) {
452
510
  dispatch({ type: "run_started", sessionId, at: Date.now() });
453
511
  client.respondApproval(sessionId, decision, reason);
454
512
  }), [client, guard]);
513
+ const allowForSession = useCallback((id) => {
514
+ const p = state.pendingPermissions.find((x) => x.id === id);
515
+ if (p)
516
+ allowed.current.add(ruleFor(p));
517
+ guard(() => {
518
+ client.respondPermission(id, "allow");
519
+ dispatch({ type: "permission_answered", id });
520
+ });
521
+ }, [client, guard, state.pendingPermissions]);
522
+ const revokeSessionRule = useCallback((toolName) => revokeRules(allowed.current, toolName), []);
523
+ const clearSessionRules = useCallback(() => allowed.current.clear(), []);
524
+ const steer = useCallback((text) => guard(() => {
525
+ const sessionId = sessionIdRef.current;
526
+ if (!sessionId)
527
+ throw new Error("no session to steer");
528
+ client.sendPeer(sessionId, text);
529
+ dispatch({ type: "local_user", text, ts: new Date().toISOString() });
530
+ }), [client, guard]);
531
+ const waitingOn = state.pendingQuestion ? "question" : state.pendingPermissions.length ? "permission" : undefined;
455
532
  const loadSession = useCallback(async (sessionId) => {
456
533
  sessionIdRef.current = sessionId;
457
534
  dispatch({ type: "clear" });
@@ -479,7 +556,7 @@ function useYodexSession(options) {
479
556
  sessionIdRef.current = undefined;
480
557
  dispatch({ type: "clear" });
481
558
  }, []);
482
- return { state, client, run, stop, allow, deny, answer, approve, loadSession, refreshSessions, refreshModels, newSession };
559
+ return { state, client, run, stop, allow, deny, answer, approve, allowForSession, revokeSessionRule, clearSessionRules, steer, waitingOn, loadSession, refreshSessions, refreshModels, newSession };
483
560
  }
484
561
  // src/components/Transcript.tsx
485
562
  import { useEffect as useEffect2, useRef as useRef2 } from "react";
@@ -496,73 +573,88 @@ function cn(...inputs) {
496
573
  }
497
574
 
498
575
  // src/components/AssistantMessage.tsx
499
- import { jsx } from "react/jsx-runtime";
576
+ import { jsx, jsxs } from "react/jsx-runtime";
500
577
  var plugins = [remarkGfm];
501
578
  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),
579
+ return /* @__PURE__ */ jsxs("div", {
580
+ className: cn("yx-assistant max-w-none break-words text-[15px] leading-6 text-foreground", className),
504
581
  "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", {
582
+ children: [
583
+ /* @__PURE__ */ jsx(Markdown, {
584
+ remarkPlugins: plugins,
585
+ skipHtml: true,
586
+ components: {
587
+ a: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("a", {
588
+ ...props,
589
+ target: "_blank",
590
+ rel: "noopener noreferrer",
591
+ className: "text-info underline underline-offset-2"
592
+ }),
593
+ pre: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("pre", {
594
+ ...props,
595
+ className: "overflow-x-auto rounded-md border border-border bg-card p-3 font-mono text-[12.5px] leading-5"
596
+ }),
597
+ code: ({ node: _n, className: cls, ...props }) => cls ? /* @__PURE__ */ jsx("code", {
598
+ ...props,
599
+ className: cls
600
+ }) : /* @__PURE__ */ jsx("code", {
601
+ ...props,
602
+ className: "rounded bg-muted px-1 py-0.5 font-mono text-[0.9em]"
603
+ }),
604
+ table: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("div", {
605
+ className: "overflow-x-auto",
606
+ children: /* @__PURE__ */ jsx("table", {
607
+ ...props,
608
+ className: "my-2 w-full border-collapse text-sm"
609
+ })
610
+ }),
611
+ th: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("th", {
612
+ ...props,
613
+ className: "border border-border bg-muted px-2 py-1 text-left font-medium text-foreground"
614
+ }),
615
+ strong: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("strong", {
616
+ ...props,
617
+ className: "font-semibold text-foreground"
618
+ }),
619
+ blockquote: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("blockquote", {
620
+ ...props,
621
+ className: "border-l-2 border-border pl-3 text-muted-foreground"
622
+ }),
623
+ td: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("td", {
529
624
  ...props,
530
- className: "my-2 w-full border-collapse text-sm"
625
+ className: "border border-border px-2 py-1 align-top"
531
626
  })
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
- })
627
+ },
628
+ children: text
629
+ }),
630
+ live && /* @__PURE__ */ jsx("span", {
631
+ className: "yx-caret",
632
+ "aria-hidden": true,
633
+ children: "▍"
634
+ })
635
+ ]
544
636
  });
545
637
  }
546
638
 
547
639
  // src/components/PlanChecklist.tsx
548
640
  import { Check, Circle, Loader2 } from "lucide-react";
549
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
641
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
550
642
  function PlanChecklist({ items, className }) {
551
643
  if (!items.length)
552
644
  return null;
553
645
  const done = items.filter((t) => t.status === "completed").length;
554
- return /* @__PURE__ */ jsxs("div", {
646
+ return /* @__PURE__ */ jsxs2("div", {
555
647
  className: cn("yx-plan rounded-md border border-border bg-card p-3 text-sm", className),
556
648
  "aria-label": "plan",
557
649
  children: [
558
- /* @__PURE__ */ jsxs("div", {
650
+ /* @__PURE__ */ jsxs2("div", {
559
651
  className: "mb-2 flex items-center justify-between",
560
652
  children: [
561
653
  /* @__PURE__ */ jsx2("span", {
562
654
  className: "font-medium",
563
655
  children: "Plan"
564
656
  }),
565
- /* @__PURE__ */ jsxs("span", {
657
+ /* @__PURE__ */ jsxs2("span", {
566
658
  className: "font-mono text-[12px] tabular-nums text-muted-foreground",
567
659
  children: [
568
660
  done,
@@ -574,7 +666,7 @@ function PlanChecklist({ items, className }) {
574
666
  }),
575
667
  /* @__PURE__ */ jsx2("ol", {
576
668
  className: "space-y-1",
577
- children: items.map((t) => /* @__PURE__ */ jsxs("li", {
669
+ children: items.map((t) => /* @__PURE__ */ jsxs2("li", {
578
670
  className: cn("flex items-start gap-2", t.status === "completed" && "text-muted-foreground line-through"),
579
671
  "data-status": t.status,
580
672
  children: [
@@ -635,7 +727,7 @@ function DiffView({ diff, className }) {
635
727
  }
636
728
 
637
729
  // src/components/ToolCall.tsx
638
- import { jsx as jsx4, jsxs as jsxs2, Fragment } from "react/jsx-runtime";
730
+ import { jsx as jsx4, jsxs as jsxs3, Fragment } from "react/jsx-runtime";
639
731
  function toolSummary(name, input) {
640
732
  const pick = (...keys) => {
641
733
  for (const k of keys) {
@@ -681,12 +773,12 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
681
773
  const counts = r?.diff ? diffCounts(r.diff) : null;
682
774
  const path = typeof item.input.file_path === "string" ? item.input.file_path : typeof item.input.path === "string" ? item.input.path : undefined;
683
775
  const Icon = r?.diff ? FileDiff : item.name === "Bash" ? Terminal : Wrench;
684
- return /* @__PURE__ */ jsxs2("div", {
776
+ return /* @__PURE__ */ jsxs3("div", {
685
777
  className: cn("yx-tool rounded-md border border-border bg-card text-sm", status === "error" && "border-destructive/40"),
686
778
  "data-tool": item.name,
687
779
  "data-status": status,
688
780
  children: [
689
- /* @__PURE__ */ jsxs2("button", {
781
+ /* @__PURE__ */ jsxs3("button", {
690
782
  type: "button",
691
783
  onClick: () => setOpen((o) => !o),
692
784
  "aria-expanded": open,
@@ -708,10 +800,10 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
708
800
  className: "min-w-0 flex-1 truncate font-mono text-[12.5px] text-muted-foreground",
709
801
  children: clip(summary)
710
802
  }),
711
- counts && /* @__PURE__ */ jsxs2("span", {
803
+ counts && /* @__PURE__ */ jsxs3("span", {
712
804
  className: "shrink-0 font-mono text-[12px] tabular-nums",
713
805
  children: [
714
- /* @__PURE__ */ jsxs2("span", {
806
+ /* @__PURE__ */ jsxs3("span", {
715
807
  className: "text-success",
716
808
  children: [
717
809
  "+",
@@ -719,7 +811,7 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
719
811
  ]
720
812
  }),
721
813
  " ",
722
- /* @__PURE__ */ jsxs2("span", {
814
+ /* @__PURE__ */ jsxs3("span", {
723
815
  className: "text-destructive",
724
816
  children: [
725
817
  "−",
@@ -742,7 +834,7 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
742
834
  })
743
835
  ]
744
836
  }),
745
- open && /* @__PURE__ */ jsxs2("div", {
837
+ open && /* @__PURE__ */ jsxs3("div", {
746
838
  className: "border-t border-border px-3 py-2",
747
839
  children: [
748
840
  /* @__PURE__ */ jsx4("div", {
@@ -753,9 +845,9 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
753
845
  className: "overflow-x-auto whitespace-pre-wrap break-words font-mono text-[12px] leading-5",
754
846
  children: summary && Object.keys(item.input).length === 1 ? summary : JSON.stringify(item.input, null, 2)
755
847
  }),
756
- r && /* @__PURE__ */ jsxs2(Fragment, {
848
+ r && /* @__PURE__ */ jsxs3(Fragment, {
757
849
  children: [
758
- /* @__PURE__ */ jsxs2("div", {
850
+ /* @__PURE__ */ jsxs3("div", {
759
851
  className: "mb-1 mt-2 flex items-center gap-2 text-[11px] uppercase tracking-wide text-muted-foreground",
760
852
  children: [
761
853
  /* @__PURE__ */ jsx4("span", {
@@ -785,7 +877,85 @@ function ToolCall({ item, defaultOpen = false, onOpenFile }) {
785
877
  }
786
878
 
787
879
  // src/components/Transcript.tsx
788
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
880
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
881
+ function TranscriptRow({ item, onOpenFile }) {
882
+ const it = item;
883
+ switch (it.kind) {
884
+ case "user":
885
+ return /* @__PURE__ */ jsx5("div", {
886
+ className: cn("flex", it.author.type === "human" ? "justify-end" : "justify-start"),
887
+ "data-author": it.author.type,
888
+ children: /* @__PURE__ */ jsxs4("div", {
889
+ 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"),
890
+ children: [
891
+ it.author.type !== "human" && /* @__PURE__ */ jsx5("div", {
892
+ className: "mb-1 text-[11px] text-muted-foreground",
893
+ children: it.author.displayName ?? it.author.type
894
+ }),
895
+ it.blocks?.some((b) => b.type === "image") && /* @__PURE__ */ jsx5("div", {
896
+ className: "mb-1 text-[11px] opacity-80",
897
+ children: "[image]"
898
+ }),
899
+ it.text
900
+ ]
901
+ })
902
+ });
903
+ case "assistant":
904
+ return /* @__PURE__ */ jsx5(AssistantMessage, {
905
+ text: it.text,
906
+ live: it.live
907
+ });
908
+ case "tool":
909
+ return /* @__PURE__ */ jsx5(ToolCall, {
910
+ item: it,
911
+ onOpenFile
912
+ });
913
+ case "plan":
914
+ return /* @__PURE__ */ jsx5(PlanChecklist, {
915
+ items: it.items
916
+ });
917
+ case "result": {
918
+ const { label, tone } = resultLabel(it.result);
919
+ return /* @__PURE__ */ jsxs4("div", {
920
+ className: cn("flex items-center gap-2 text-[12px] text-muted-foreground", tone === "bad" && "text-destructive"),
921
+ "data-subtype": it.result.subtype,
922
+ children: [
923
+ /* @__PURE__ */ jsx5("span", {
924
+ children: label
925
+ }),
926
+ /* @__PURE__ */ jsx5("span", {
927
+ children: "·"
928
+ }),
929
+ /* @__PURE__ */ jsxs4("span", {
930
+ className: "font-mono tabular-nums",
931
+ children: [
932
+ it.result.numTurns,
933
+ " step",
934
+ it.result.numTurns === 1 ? "" : "s",
935
+ " · ",
936
+ formatTokens(it.result.usage)
937
+ ]
938
+ })
939
+ ]
940
+ });
941
+ }
942
+ case "error":
943
+ return /* @__PURE__ */ jsx5("div", {
944
+ role: "alert",
945
+ className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-[13px] text-destructive",
946
+ children: it.message
947
+ });
948
+ case "compact":
949
+ return /* @__PURE__ */ jsxs4("div", {
950
+ className: "text-center text-[11px] text-muted-foreground",
951
+ children: [
952
+ "— context compacted (",
953
+ it.replacedMessages,
954
+ " earlier messages summarised) —"
955
+ ]
956
+ });
957
+ }
958
+ }
789
959
  function Transcript({ items, autoScroll = true, onOpenFile, footer, emptyState, className }) {
790
960
  const ref = useRef2(null);
791
961
  const pinned = useRef2(true);
@@ -808,97 +978,23 @@ function Transcript({ items, autoScroll = true, onOpenFile, footer, emptyState,
808
978
  ref,
809
979
  className: cn("yx-transcript flex-1 overflow-y-auto px-4 py-3", className),
810
980
  role: "log",
811
- "aria-live": "polite",
812
- children: /* @__PURE__ */ jsxs3("div", {
981
+ children: /* @__PURE__ */ jsxs4("div", {
813
982
  className: "mx-auto flex w-full max-w-3xl flex-col gap-3",
814
983
  children: [
815
984
  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
- }),
985
+ items.map((it) => /* @__PURE__ */ jsx5(TranscriptRow, {
986
+ item: it,
987
+ onOpenFile
988
+ }, it.id)),
893
989
  footer
894
990
  ]
895
991
  })
896
992
  });
897
993
  }
898
994
  // src/components/PermissionPrompt.tsx
899
- import { useEffect as useEffect3 } from "react";
995
+ import { useEffect as useEffect3, useState as useState2 } from "react";
900
996
  import { ShieldAlert } from "lucide-react";
901
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
997
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
902
998
  function permissionSummary(p) {
903
999
  const v = p.input.command ?? p.input.file_path ?? p.input.path ?? p.input.url;
904
1000
  if (typeof v === "string")
@@ -906,8 +1002,12 @@ function permissionSummary(p) {
906
1002
  const s = JSON.stringify(p.input);
907
1003
  return s.length > 240 ? `${s.slice(0, 239)}…` : s;
908
1004
  }
909
- function PermissionPrompt({ request, onAllow, onDeny, className }) {
1005
+ function PermissionPrompt({ request, onAllow, onDeny, onAllowSession, hotkeys = false, className }) {
1006
+ const [reasonOpen, setReasonOpen] = useState2(false);
1007
+ const [reason, setReason] = useState2("");
910
1008
  useEffect3(() => {
1009
+ if (!hotkeys)
1010
+ return;
911
1011
  const key = (e) => {
912
1012
  const t = e.target;
913
1013
  if (t && (t.tagName === "TEXTAREA" || t.tagName === "INPUT" || t.isContentEditable))
@@ -919,15 +1019,19 @@ function PermissionPrompt({ request, onAllow, onDeny, className }) {
919
1019
  };
920
1020
  document.addEventListener("keydown", key);
921
1021
  return () => document.removeEventListener("keydown", key);
922
- }, [request.id, onAllow, onDeny]);
923
- return /* @__PURE__ */ jsxs4("div", {
1022
+ }, [hotkeys, request.id, onAllow, onDeny]);
1023
+ const kbd = (k) => hotkeys ? /* @__PURE__ */ jsx6("kbd", {
1024
+ className: "ml-1 rounded bg-black/10 px-1 text-[11px]",
1025
+ children: k
1026
+ }) : null;
1027
+ return /* @__PURE__ */ jsxs5("div", {
924
1028
  role: "alertdialog",
925
1029
  "aria-labelledby": `perm-${request.id}`,
926
1030
  className: cn("yx-permission rounded-md border border-warning/50 bg-card p-3 text-sm", className),
927
1031
  children: [
928
- /* @__PURE__ */ jsxs4("div", {
1032
+ /* @__PURE__ */ jsxs5("div", {
929
1033
  id: `perm-${request.id}`,
930
- className: "mb-2 flex items-center gap-2 font-medium",
1034
+ className: "mb-1 flex items-center gap-2 font-medium",
931
1035
  children: [
932
1036
  /* @__PURE__ */ jsx6(ShieldAlert, {
933
1037
  className: "h-4 w-4 text-warning",
@@ -941,46 +1045,116 @@ function PermissionPrompt({ request, onAllow, onDeny, className }) {
941
1045
  "?"
942
1046
  ]
943
1047
  }),
1048
+ (request.safetyReason || request.reason) && /* @__PURE__ */ jsx6("div", {
1049
+ className: "mb-2 text-[12px] text-muted-foreground",
1050
+ children: request.safetyReason ?? request.reason
1051
+ }),
944
1052
  /* @__PURE__ */ jsx6("pre", {
945
1053
  className: "mb-3 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2 font-mono text-[12.5px] leading-5",
946
1054
  children: permissionSummary(request)
947
1055
  }),
948
- /* @__PURE__ */ jsxs4("div", {
949
- className: "flex gap-2",
1056
+ /* @__PURE__ */ jsxs5("div", {
1057
+ className: "flex flex-wrap gap-2",
950
1058
  children: [
951
- /* @__PURE__ */ jsxs4("button", {
1059
+ /* @__PURE__ */ jsxs5("button", {
952
1060
  type: "button",
953
1061
  onClick: () => onAllow(request.id),
954
1062
  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
1063
  children: [
956
1064
  "Allow ",
957
- /* @__PURE__ */ jsx6("kbd", {
958
- className: "ml-1 rounded bg-primary-foreground/20 px-1 text-[11px]",
959
- children: "a"
960
- })
1065
+ kbd("a")
961
1066
  ]
962
1067
  }),
963
- /* @__PURE__ */ jsxs4("button", {
1068
+ onAllowSession && /* @__PURE__ */ jsx6("button", {
1069
+ type: "button",
1070
+ onClick: () => onAllowSession(request.id),
1071
+ className: "min-h-11 flex-1 rounded-md border border-border bg-background px-4 font-medium hover:bg-muted",
1072
+ children: "Allow for this session"
1073
+ }),
1074
+ /* @__PURE__ */ jsxs5("button", {
964
1075
  type: "button",
965
- onClick: () => onDeny(request.id),
1076
+ onClick: () => reasonOpen ? onDeny(request.id, reason.trim() || undefined) : onDeny(request.id),
966
1077
  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
1078
  children: [
968
1079
  "Deny ",
969
- /* @__PURE__ */ jsx6("kbd", {
970
- className: "ml-1 rounded bg-muted px-1 text-[11px]",
971
- children: "d"
972
- })
1080
+ kbd("d")
973
1081
  ]
974
1082
  })
975
1083
  ]
1084
+ }),
1085
+ reasonOpen ? /* @__PURE__ */ jsxs5("label", {
1086
+ className: "mt-2 block",
1087
+ children: [
1088
+ /* @__PURE__ */ jsx6("span", {
1089
+ className: "mb-1 block text-[12px] text-muted-foreground",
1090
+ children: "Tell the agent why (sent with the denial)"
1091
+ }),
1092
+ /* @__PURE__ */ jsx6("input", {
1093
+ value: reason,
1094
+ onChange: (e) => setReason(e.target.value),
1095
+ className: "min-h-11 w-full rounded-md border border-border bg-input-background px-3 text-[16px]"
1096
+ })
1097
+ ]
1098
+ }) : /* @__PURE__ */ jsx6("button", {
1099
+ type: "button",
1100
+ onClick: () => setReasonOpen(true),
1101
+ className: "mt-2 min-h-9 text-[12px] text-info underline-offset-2 hover:underline",
1102
+ children: "Deny with a reason…"
1103
+ })
1104
+ ]
1105
+ });
1106
+ }
1107
+ // src/components/ApprovalPrompt.tsx
1108
+ import { ShieldAlert as ShieldAlert2 } from "lucide-react";
1109
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1110
+ function ApprovalPrompt({ approval, onDecide, className }) {
1111
+ return /* @__PURE__ */ jsxs6("div", {
1112
+ role: "alertdialog",
1113
+ "aria-label": "Approval needed",
1114
+ className: cn("yx-approval rounded-md border border-warning/50 bg-card p-3 text-sm", className),
1115
+ children: [
1116
+ /* @__PURE__ */ jsxs6("div", {
1117
+ className: "mb-1 flex items-center gap-2 font-medium",
1118
+ children: [
1119
+ /* @__PURE__ */ jsx7(ShieldAlert2, {
1120
+ className: "h-4 w-4 text-warning",
1121
+ "aria-hidden": true
1122
+ }),
1123
+ "The agent stopped before this and needs your approval"
1124
+ ]
1125
+ }),
1126
+ /* @__PURE__ */ jsx7("div", {
1127
+ className: "mb-2 text-[12px] text-muted-foreground",
1128
+ children: approval.reason
1129
+ }),
1130
+ /* @__PURE__ */ jsx7("pre", {
1131
+ className: "mb-3 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2 font-mono text-[12.5px] leading-5",
1132
+ children: approval.command
1133
+ }),
1134
+ /* @__PURE__ */ jsxs6("div", {
1135
+ className: "flex gap-2",
1136
+ children: [
1137
+ /* @__PURE__ */ jsx7("button", {
1138
+ type: "button",
1139
+ onClick: () => onDecide("allow"),
1140
+ className: "min-h-11 flex-1 rounded-md bg-primary px-4 font-medium text-primary-foreground hover:opacity-90",
1141
+ children: "Approve and continue"
1142
+ }),
1143
+ /* @__PURE__ */ jsx7("button", {
1144
+ type: "button",
1145
+ onClick: () => onDecide("deny"),
1146
+ className: "min-h-11 flex-1 rounded-md border border-border px-4 font-medium hover:bg-muted",
1147
+ children: "Don't run it"
1148
+ })
1149
+ ]
976
1150
  })
977
1151
  ]
978
1152
  });
979
1153
  }
980
1154
  // src/components/QuestionForm.tsx
981
- import { useState as useState2 } from "react";
1155
+ import { useState as useState3 } from "react";
982
1156
  import { HelpCircle, X } from "lucide-react";
983
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1157
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
984
1158
  function emptyDraft(questions) {
985
1159
  return { selected: questions.map(() => []), notes: questions.map(() => "") };
986
1160
  }
@@ -1003,8 +1177,8 @@ function chip(q, i) {
1003
1177
  }
1004
1178
  function QuestionForm({ request, onSubmit, onDecline, className }) {
1005
1179
  const qs = request.questions;
1006
- const [tab, setTab] = useState2(0);
1007
- const [draft, setDraft] = useState2(() => emptyDraft(qs));
1180
+ const [tab, setTab] = useState3(0);
1181
+ const [draft, setDraft] = useState3(() => emptyDraft(qs));
1008
1182
  const q = qs[tab];
1009
1183
  if (!q)
1010
1184
  return null;
@@ -1024,38 +1198,38 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1024
1198
  });
1025
1199
  const answered = qs.map((_, i) => (draft.selected[i]?.length ?? 0) > 0 || (draft.notes[i] ?? "").trim().length > 0);
1026
1200
  const previewOf = (q.options ?? []).find((o) => picked[0] === o.label)?.preview;
1027
- return /* @__PURE__ */ jsxs5("div", {
1201
+ return /* @__PURE__ */ jsxs7("div", {
1028
1202
  role: "dialog",
1029
1203
  "aria-label": "The agent has a question",
1030
1204
  className: cn("yx-question rounded-md border border-info/50 bg-card text-sm", className),
1031
1205
  children: [
1032
- /* @__PURE__ */ jsxs5("div", {
1206
+ /* @__PURE__ */ jsxs7("div", {
1033
1207
  className: "flex items-center gap-2 border-b border-border px-3 py-2",
1034
1208
  children: [
1035
- /* @__PURE__ */ jsx7(HelpCircle, {
1209
+ /* @__PURE__ */ jsx8(HelpCircle, {
1036
1210
  className: "h-4 w-4 text-info",
1037
1211
  "aria-hidden": true
1038
1212
  }),
1039
- /* @__PURE__ */ jsx7("span", {
1213
+ /* @__PURE__ */ jsx8("span", {
1040
1214
  className: "font-medium",
1041
1215
  children: "The agent needs your input"
1042
1216
  }),
1043
- /* @__PURE__ */ jsx7("button", {
1217
+ /* @__PURE__ */ jsx8("button", {
1044
1218
  type: "button",
1045
1219
  onClick: () => onDecline(request.id),
1046
1220
  "aria-label": "Close and keep talking",
1047
1221
  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, {
1222
+ children: /* @__PURE__ */ jsx8(X, {
1049
1223
  className: "h-4 w-4",
1050
1224
  "aria-hidden": true
1051
1225
  })
1052
1226
  })
1053
1227
  ]
1054
1228
  }),
1055
- qs.length > 1 && /* @__PURE__ */ jsx7("div", {
1229
+ qs.length > 1 && /* @__PURE__ */ jsx8("div", {
1056
1230
  role: "tablist",
1057
1231
  className: "flex gap-1 overflow-x-auto border-b border-border px-2 py-1",
1058
- children: qs.map((qq, i) => /* @__PURE__ */ jsx7("button", {
1232
+ children: qs.map((qq, i) => /* @__PURE__ */ jsx8("button", {
1059
1233
  role: "tab",
1060
1234
  type: "button",
1061
1235
  "aria-selected": i === tab,
@@ -1064,37 +1238,37 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1064
1238
  children: chip(qq, i)
1065
1239
  }, i))
1066
1240
  }),
1067
- /* @__PURE__ */ jsxs5("div", {
1241
+ /* @__PURE__ */ jsxs7("div", {
1068
1242
  className: "space-y-3 p-3",
1069
1243
  children: [
1070
- /* @__PURE__ */ jsx7("p", {
1244
+ /* @__PURE__ */ jsx8("p", {
1071
1245
  className: "font-medium",
1072
1246
  children: q.question
1073
1247
  }),
1074
- (q.options ?? []).length > 0 && /* @__PURE__ */ jsx7("div", {
1248
+ (q.options ?? []).length > 0 && /* @__PURE__ */ jsx8("div", {
1075
1249
  role: q.multiSelect ? "group" : "radiogroup",
1076
1250
  className: "space-y-2",
1077
1251
  children: (q.options ?? []).map((o) => {
1078
1252
  const on = picked.includes(o.label);
1079
- return /* @__PURE__ */ jsxs5("button", {
1253
+ return /* @__PURE__ */ jsxs7("button", {
1080
1254
  type: "button",
1081
1255
  role: q.multiSelect ? "checkbox" : "radio",
1082
1256
  "aria-checked": on,
1083
1257
  onClick: () => toggle(o.label),
1084
1258
  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
1259
  children: [
1086
- /* @__PURE__ */ jsx7("span", {
1260
+ /* @__PURE__ */ jsx8("span", {
1087
1261
  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
1262
  "aria-hidden": true
1089
1263
  }),
1090
- /* @__PURE__ */ jsxs5("span", {
1264
+ /* @__PURE__ */ jsxs7("span", {
1091
1265
  className: "min-w-0",
1092
1266
  children: [
1093
- /* @__PURE__ */ jsx7("span", {
1267
+ /* @__PURE__ */ jsx8("span", {
1094
1268
  className: "block font-medium",
1095
1269
  children: o.label
1096
1270
  }),
1097
- o.description && /* @__PURE__ */ jsx7("span", {
1271
+ o.description && /* @__PURE__ */ jsx8("span", {
1098
1272
  className: "block text-muted-foreground",
1099
1273
  children: o.description
1100
1274
  })
@@ -1104,18 +1278,18 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1104
1278
  }, o.label);
1105
1279
  })
1106
1280
  }),
1107
- previewOf && /* @__PURE__ */ jsx7("pre", {
1281
+ previewOf && /* @__PURE__ */ jsx8("pre", {
1108
1282
  className: "overflow-x-auto rounded-md border border-border bg-muted p-2 font-mono text-[12px] leading-5",
1109
1283
  children: previewOf
1110
1284
  }),
1111
- /* @__PURE__ */ jsxs5("label", {
1285
+ /* @__PURE__ */ jsxs7("label", {
1112
1286
  className: "block",
1113
1287
  children: [
1114
- /* @__PURE__ */ jsx7("span", {
1288
+ /* @__PURE__ */ jsx8("span", {
1115
1289
  className: "mb-1 block text-[12px] text-muted-foreground",
1116
1290
  children: (q.options ?? []).length ? "Add a note (optional)" : "Your answer"
1117
1291
  }),
1118
- /* @__PURE__ */ jsx7("textarea", {
1292
+ /* @__PURE__ */ jsx8("textarea", {
1119
1293
  value: draft.notes[tab] ?? "",
1120
1294
  onChange: (e) => setNote(e.target.value),
1121
1295
  rows: 2,
@@ -1123,16 +1297,16 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1123
1297
  })
1124
1298
  ]
1125
1299
  }),
1126
- /* @__PURE__ */ jsxs5("div", {
1300
+ /* @__PURE__ */ jsxs7("div", {
1127
1301
  className: "flex gap-2",
1128
1302
  children: [
1129
- tab < qs.length - 1 ? /* @__PURE__ */ jsx7("button", {
1303
+ tab < qs.length - 1 ? /* @__PURE__ */ jsx8("button", {
1130
1304
  type: "button",
1131
1305
  onClick: () => setTab(tab + 1),
1132
1306
  className: "min-h-11 flex-1 rounded-md border border-border px-4 font-medium hover:bg-muted",
1133
1307
  children: "Next"
1134
1308
  }) : null,
1135
- /* @__PURE__ */ jsx7("button", {
1309
+ /* @__PURE__ */ jsx8("button", {
1136
1310
  type: "button",
1137
1311
  onClick: () => onSubmit(request.id, buildOutcome(qs, draft)),
1138
1312
  disabled: !answered.some(Boolean),
@@ -1147,11 +1321,11 @@ function QuestionForm({ request, onSubmit, onDecline, className }) {
1147
1321
  });
1148
1322
  }
1149
1323
  // src/components/RunStatus.tsx
1150
- import { useEffect as useEffect4, useState as useState3 } from "react";
1324
+ import { useEffect as useEffect4, useState as useState4 } from "react";
1151
1325
  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);
1326
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1327
+ function RunStatus({ running, startedAt, activity, waitingOn, lastResult, usageTotal, connection, onStop, onApprove, className, now = Date.now }) {
1328
+ const [, tick] = useState4(0);
1155
1329
  useEffect4(() => {
1156
1330
  if (!running)
1157
1331
  return;
@@ -1159,11 +1333,11 @@ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, conne
1159
1333
  return () => clearInterval(h);
1160
1334
  }, [running]);
1161
1335
  if (connection && connection !== "connected") {
1162
- return /* @__PURE__ */ jsxs6("div", {
1336
+ return /* @__PURE__ */ jsxs8("div", {
1163
1337
  className: cn("yx-status flex min-h-9 items-center gap-2 text-[12.5px] text-muted-foreground", className),
1164
1338
  role: "status",
1165
1339
  children: [
1166
- /* @__PURE__ */ jsx8(Loader23, {
1340
+ /* @__PURE__ */ jsx9(Loader23, {
1167
1341
  className: "h-3.5 w-3.5 animate-spin",
1168
1342
  "aria-hidden": true
1169
1343
  }),
@@ -1172,32 +1346,34 @@ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, conne
1172
1346
  });
1173
1347
  }
1174
1348
  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",
1349
+ const label2 = waitingOn === "permission" ? "Waiting for your approval" : waitingOn === "question" ? "Waiting for your answer" : activity ?? "Working";
1350
+ return /* @__PURE__ */ jsxs8("div", {
1351
+ className: cn("yx-status flex min-h-11 items-center gap-2 text-[12.5px] text-muted-foreground", className),
1179
1352
  children: [
1180
- /* @__PURE__ */ jsx8(Loader23, {
1181
- className: "h-3.5 w-3.5 animate-spin text-accent",
1353
+ /* @__PURE__ */ jsx9(Loader23, {
1354
+ className: cn("h-3.5 w-3.5 animate-spin", waitingOn ? "text-warning" : "text-accent"),
1182
1355
  "aria-hidden": true
1183
1356
  }),
1184
- /* @__PURE__ */ jsx8("span", {
1185
- children: activity ?? "Working"
1357
+ /* @__PURE__ */ jsx9("span", {
1358
+ role: "status",
1359
+ "aria-live": "polite",
1360
+ children: label2
1186
1361
  }),
1187
- startedAt !== undefined && /* @__PURE__ */ jsxs6("span", {
1362
+ startedAt !== undefined && /* @__PURE__ */ jsxs8("span", {
1188
1363
  className: "font-mono tabular-nums",
1364
+ "aria-hidden": true,
1189
1365
  children: [
1190
1366
  "· ",
1191
1367
  elapsedLabel(now() - startedAt)
1192
1368
  ]
1193
1369
  }),
1194
- onStop && /* @__PURE__ */ jsxs6("button", {
1370
+ onStop && /* @__PURE__ */ jsxs8("button", {
1195
1371
  type: "button",
1196
1372
  onClick: onStop,
1197
- className: "ml-auto flex min-h-9 items-center gap-1 rounded-md px-2 hover:bg-muted",
1373
+ className: "ml-auto flex min-h-11 items-center gap-1 rounded-md px-3 hover:bg-muted",
1198
1374
  "aria-label": "Stop",
1199
1375
  children: [
1200
- /* @__PURE__ */ jsx8(Square, {
1376
+ /* @__PURE__ */ jsx9(Square, {
1201
1377
  className: "h-3.5 w-3.5",
1202
1378
  "aria-hidden": true
1203
1379
  }),
@@ -1211,30 +1387,30 @@ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, conne
1211
1387
  return null;
1212
1388
  const { label, tone } = resultLabel(lastResult);
1213
1389
  const needsApproval = lastResult.subtype === "needs_approval";
1214
- return /* @__PURE__ */ jsxs6("div", {
1390
+ return /* @__PURE__ */ jsxs8("div", {
1215
1391
  className: cn("yx-status flex min-h-9 flex-wrap items-center gap-2 text-[12.5px]", className),
1216
1392
  role: "status",
1217
1393
  "data-subtype": lastResult.subtype,
1218
1394
  children: [
1219
- tone === "ok" ? /* @__PURE__ */ jsx8(Check3, {
1395
+ tone === "ok" ? /* @__PURE__ */ jsx9(Check3, {
1220
1396
  className: "h-3.5 w-3.5 text-success",
1221
1397
  "aria-hidden": true
1222
- }) : tone === "bad" ? /* @__PURE__ */ jsx8(AlertTriangle, {
1398
+ }) : tone === "bad" ? /* @__PURE__ */ jsx9(AlertTriangle, {
1223
1399
  className: "h-3.5 w-3.5 text-destructive",
1224
1400
  "aria-hidden": true
1225
- }) : /* @__PURE__ */ jsx8(AlertTriangle, {
1401
+ }) : /* @__PURE__ */ jsx9(AlertTriangle, {
1226
1402
  className: cn("h-3.5 w-3.5", tone === "warn" ? "text-warning" : "text-muted-foreground"),
1227
1403
  "aria-hidden": true
1228
1404
  }),
1229
- /* @__PURE__ */ jsx8("span", {
1405
+ /* @__PURE__ */ jsx9("span", {
1230
1406
  className: cn(tone === "ok" && "text-success", tone === "bad" && "text-destructive", tone === "warn" && "text-warning", tone === "muted" && "text-muted-foreground"),
1231
1407
  children: label
1232
1408
  }),
1233
- /* @__PURE__ */ jsx8("span", {
1409
+ /* @__PURE__ */ jsx9("span", {
1234
1410
  className: "text-muted-foreground",
1235
1411
  children: "·"
1236
1412
  }),
1237
- /* @__PURE__ */ jsxs6("span", {
1413
+ /* @__PURE__ */ jsxs8("span", {
1238
1414
  className: "font-mono tabular-nums text-muted-foreground",
1239
1415
  children: [
1240
1416
  lastResult.numTurns,
@@ -1244,42 +1420,19 @@ function RunStatus({ running, startedAt, activity, lastResult, usageTotal, conne
1244
1420
  formatTokens(usageTotal ?? lastResult.usage)
1245
1421
  ]
1246
1422
  }),
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
- ]
1423
+ needsApproval && lastResult.pendingApproval && onApprove && /* @__PURE__ */ jsx9("div", {
1424
+ className: "basis-full",
1425
+ children: /* @__PURE__ */ jsx9(ApprovalPrompt, {
1426
+ approval: lastResult.pendingApproval,
1427
+ onDecide: onApprove
1428
+ })
1276
1429
  })
1277
1430
  ]
1278
1431
  });
1279
1432
  }
1280
1433
  // src/components/SessionList.tsx
1281
1434
  import { Plus } from "lucide-react";
1282
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1435
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1283
1436
  function groupSessions(rows) {
1284
1437
  const groups = new Map;
1285
1438
  for (const r of rows) {
@@ -1299,75 +1452,75 @@ function when(iso) {
1299
1452
  }
1300
1453
  function SessionList({ sessions, selectedId, onSelect, onNew, className }) {
1301
1454
  const groups = groupSessions(sessions);
1302
- return /* @__PURE__ */ jsxs7("nav", {
1455
+ return /* @__PURE__ */ jsxs9("nav", {
1303
1456
  className: cn("yx-sessions text-sm", className),
1304
1457
  "aria-label": "Sessions",
1305
1458
  children: [
1306
- onNew && /* @__PURE__ */ jsxs7("button", {
1459
+ onNew && /* @__PURE__ */ jsxs9("button", {
1307
1460
  type: "button",
1308
1461
  onClick: onNew,
1309
1462
  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
1463
  children: [
1311
- /* @__PURE__ */ jsx9(Plus, {
1464
+ /* @__PURE__ */ jsx10(Plus, {
1312
1465
  className: "h-4 w-4",
1313
1466
  "aria-hidden": true
1314
1467
  }),
1315
1468
  " New session"
1316
1469
  ]
1317
1470
  }),
1318
- sessions.length === 0 && /* @__PURE__ */ jsx9("p", {
1471
+ sessions.length === 0 && /* @__PURE__ */ jsx10("p", {
1319
1472
  className: "px-3 py-2 text-muted-foreground",
1320
1473
  children: "No sessions yet."
1321
1474
  }),
1322
- groups.map((g) => /* @__PURE__ */ jsxs7("div", {
1475
+ groups.map((g) => /* @__PURE__ */ jsxs9("div", {
1323
1476
  className: "mb-2",
1324
1477
  children: [
1325
- g.project && /* @__PURE__ */ jsx9("div", {
1478
+ g.project && /* @__PURE__ */ jsx10("div", {
1326
1479
  className: "px-3 py-1 text-[11px] uppercase tracking-wide text-muted-foreground",
1327
1480
  children: g.project
1328
1481
  }),
1329
- /* @__PURE__ */ jsx9("ul", {
1482
+ /* @__PURE__ */ jsx10("ul", {
1330
1483
  children: g.rows.map((s) => {
1331
1484
  const on = s.sessionId === selectedId;
1332
1485
  const title = s.name || s.title || s.lastMessagePreview || s.sessionId.slice(0, 8);
1333
- return /* @__PURE__ */ jsx9("li", {
1334
- children: /* @__PURE__ */ jsxs7("button", {
1486
+ return /* @__PURE__ */ jsx10("li", {
1487
+ children: /* @__PURE__ */ jsxs9("button", {
1335
1488
  type: "button",
1336
1489
  onClick: () => onSelect(s.sessionId),
1337
1490
  "aria-current": on ? "true" : undefined,
1338
1491
  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
1492
  "data-status": s.status,
1340
1493
  children: [
1341
- /* @__PURE__ */ jsxs7("span", {
1494
+ /* @__PURE__ */ jsxs9("span", {
1342
1495
  className: "flex items-center gap-2",
1343
1496
  children: [
1344
- /* @__PURE__ */ jsx9("span", {
1497
+ /* @__PURE__ */ jsx10("span", {
1345
1498
  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
1499
  "aria-label": s.live ? "working" : s.status,
1347
1500
  "data-live": s.live ? "true" : undefined
1348
1501
  }),
1349
- /* @__PURE__ */ jsx9("span", {
1502
+ /* @__PURE__ */ jsx10("span", {
1350
1503
  className: "min-w-0 flex-1 truncate font-medium",
1351
1504
  children: title
1352
1505
  }),
1353
- s.pendingQuestion && /* @__PURE__ */ jsx9("span", {
1506
+ s.pendingQuestion && /* @__PURE__ */ jsx10("span", {
1354
1507
  className: "shrink-0 rounded-full bg-warning px-1.5 text-[10px] font-medium text-warning-foreground",
1355
1508
  children: "question"
1356
1509
  })
1357
1510
  ]
1358
1511
  }),
1359
- /* @__PURE__ */ jsxs7("span", {
1512
+ /* @__PURE__ */ jsxs9("span", {
1360
1513
  className: "flex gap-2 truncate text-[11.5px] text-muted-foreground",
1361
1514
  children: [
1362
- s.hop && /* @__PURE__ */ jsx9("span", {
1515
+ s.hop && /* @__PURE__ */ jsx10("span", {
1363
1516
  className: "truncate",
1364
1517
  children: s.hop
1365
1518
  }),
1366
- s.relCwd && /* @__PURE__ */ jsx9("span", {
1519
+ s.relCwd && /* @__PURE__ */ jsx10("span", {
1367
1520
  className: "truncate",
1368
1521
  children: s.relCwd
1369
1522
  }),
1370
- /* @__PURE__ */ jsx9("span", {
1523
+ /* @__PURE__ */ jsx10("span", {
1371
1524
  className: "ml-auto shrink-0 tabular-nums",
1372
1525
  children: when(s.updatedAt)
1373
1526
  })
@@ -1384,9 +1537,9 @@ function SessionList({ sessions, selectedId, onSelect, onNew, className }) {
1384
1537
  });
1385
1538
  }
1386
1539
  // src/components/Composer.tsx
1387
- import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef3, useState as useState4 } from "react";
1540
+ import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
1388
1541
  import { ArrowUp, ImagePlus, Square as Square2, X as X2 } from "lucide-react";
1389
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1542
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1390
1543
  var ACCEPT = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1391
1544
  async function fileToAttachment(f) {
1392
1545
  const buf = new Uint8Array(await f.arrayBuffer());
@@ -1395,17 +1548,34 @@ async function fileToAttachment(f) {
1395
1548
  bin += String.fromCharCode(...buf.subarray(i, i + 32768));
1396
1549
  return { name: f.name || "image", mediaType: f.type, data: btoa(bin), previewUrl: URL.createObjectURL(f) };
1397
1550
  }
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);
1551
+ function coarsePointer() {
1552
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
1553
+ }
1554
+ 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 }) {
1555
+ const [own, setOwn] = useState5(initialValue ?? "");
1556
+ const text = value ?? own;
1557
+ const setText = (t) => {
1558
+ if (value === undefined)
1559
+ setOwn(t);
1560
+ onChange?.(t);
1561
+ };
1562
+ const [attachments, setAttachments] = useState5([]);
1563
+ const [note, setNote] = useState5(null);
1402
1564
  const ref = useRef3(null);
1403
1565
  useEffect5(() => {
1404
1566
  const el = ref.current;
1405
1567
  if (!el)
1406
1568
  return;
1407
- el.style.height = "0px";
1408
- el.style.height = `${Math.min(el.scrollHeight, 240)}px`;
1569
+ const fit = () => {
1570
+ el.style.height = "0px";
1571
+ el.style.height = `${Math.min(el.scrollHeight, 240)}px`;
1572
+ };
1573
+ fit();
1574
+ if (typeof ResizeObserver === "undefined")
1575
+ return;
1576
+ const ro = new ResizeObserver(fit);
1577
+ ro.observe(el);
1578
+ return () => ro.disconnect();
1409
1579
  }, [text]);
1410
1580
  const addFiles = useCallback2(async (files) => {
1411
1581
  const list = [...files].filter((f) => ACCEPT.includes(f.type));
@@ -1426,10 +1596,16 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1426
1596
  URL.revokeObjectURL(gone.previewUrl);
1427
1597
  return a.filter((_, j) => j !== i);
1428
1598
  });
1429
- const canSend = !disabled && !running && (text.trim().length > 0 || attachments.length > 0);
1599
+ const steering = Boolean(running && onSteer);
1600
+ const canSend = !disabled && (steering ? text.trim().length > 0 : !running && (text.trim().length > 0 || attachments.length > 0));
1430
1601
  const submit = () => {
1431
1602
  if (!canSend)
1432
1603
  return;
1604
+ if (steering) {
1605
+ onSteer(text.trim());
1606
+ setText("");
1607
+ return;
1608
+ }
1433
1609
  onSubmit(text.trim(), attachments);
1434
1610
  setText("");
1435
1611
  for (const a of attachments)
@@ -1438,10 +1614,13 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1438
1614
  setAttachments([]);
1439
1615
  };
1440
1616
  const onKey = (e) => {
1441
- if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
1442
- e.preventDefault();
1443
- submit();
1444
- }
1617
+ if (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing)
1618
+ return;
1619
+ const enterSends = submitOnEnter ?? !coarsePointer();
1620
+ if (!enterSends)
1621
+ return;
1622
+ e.preventDefault();
1623
+ submit();
1445
1624
  };
1446
1625
  const onPaste = (e) => {
1447
1626
  const files = [...e.clipboardData.files];
@@ -1456,39 +1635,42 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1456
1635
  addFiles(e.dataTransfer.files);
1457
1636
  }
1458
1637
  };
1459
- return /* @__PURE__ */ jsxs8("div", {
1638
+ return /* @__PURE__ */ jsxs10("div", {
1460
1639
  className: cn("yx-composer rounded-lg border border-border bg-card shadow-sm", disabled && "opacity-70", className),
1461
1640
  onDrop,
1462
1641
  onDragOver: (e) => e.preventDefault(),
1463
1642
  "data-disabled": disabled ? "true" : undefined,
1464
1643
  children: [
1465
- attachments.length > 0 && /* @__PURE__ */ jsx10("ul", {
1644
+ attachments.length > 0 && /* @__PURE__ */ jsx11("ul", {
1466
1645
  className: "flex flex-wrap gap-2 px-3 pt-3",
1467
1646
  "aria-label": "attachments",
1468
- children: attachments.map((a, i) => /* @__PURE__ */ jsxs8("li", {
1647
+ children: attachments.map((a, i) => /* @__PURE__ */ jsxs10("li", {
1469
1648
  className: "relative",
1470
1649
  children: [
1471
- a.previewUrl ? /* @__PURE__ */ jsx10("img", {
1650
+ a.previewUrl ? /* @__PURE__ */ jsx11("img", {
1472
1651
  src: a.previewUrl,
1473
1652
  alt: a.name,
1474
1653
  className: "h-14 w-14 rounded-md border border-border object-cover"
1475
- }) : /* @__PURE__ */ jsx10("span", {
1654
+ }) : /* @__PURE__ */ jsx11("span", {
1476
1655
  className: "block h-14 w-14 rounded-md border border-border bg-muted"
1477
1656
  }),
1478
- /* @__PURE__ */ jsx10("button", {
1657
+ /* @__PURE__ */ jsx11("button", {
1479
1658
  type: "button",
1480
1659
  onClick: () => remove(i),
1481
1660
  "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
1661
+ className: "absolute -right-4 -top-4 flex h-11 w-11 items-center justify-center rounded-full",
1662
+ children: /* @__PURE__ */ jsx11("span", {
1663
+ className: "flex h-6 w-6 items-center justify-center rounded-full border border-border bg-background",
1664
+ children: /* @__PURE__ */ jsx11(X2, {
1665
+ className: "h-3 w-3",
1666
+ "aria-hidden": true
1667
+ })
1486
1668
  })
1487
1669
  })
1488
1670
  ]
1489
1671
  }, `${a.name}-${i}`))
1490
1672
  }),
1491
- /* @__PURE__ */ jsx10("textarea", {
1673
+ /* @__PURE__ */ jsx11("textarea", {
1492
1674
  ref,
1493
1675
  value: text,
1494
1676
  onChange: (e) => setText(e.target.value),
@@ -1497,35 +1679,35 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1497
1679
  disabled,
1498
1680
  autoFocus,
1499
1681
  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"
1682
+ placeholder: disabled && disabledReason ? disabledReason : steering ? "Steer the agent (it reads this at its next step)…" : placeholder,
1683
+ "aria-label": steering ? "Steer" : "Message",
1684
+ 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
1685
  }),
1504
- /* @__PURE__ */ jsxs8("div", {
1686
+ /* @__PURE__ */ jsxs10("div", {
1505
1687
  className: "flex items-center gap-1 px-2 pb-2",
1506
1688
  children: [
1507
1689
  leading,
1508
- hopOptions && hopOptions.length > 0 && /* @__PURE__ */ jsx10("select", {
1690
+ hopOptions && hopOptions.length > 0 && /* @__PURE__ */ jsx11("select", {
1509
1691
  value: hop ?? "",
1510
1692
  onChange: (e) => onHopChange?.(e.target.value),
1511
1693
  disabled,
1512
1694
  "aria-label": "HOP",
1513
1695
  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", {
1696
+ children: hopOptions.map((o) => /* @__PURE__ */ jsx11("option", {
1515
1697
  value: o.value,
1516
1698
  title: o.description,
1517
1699
  children: o.label
1518
1700
  }, o.value))
1519
1701
  }),
1520
- /* @__PURE__ */ jsxs8("label", {
1702
+ !hideAttach && /* @__PURE__ */ jsxs10("label", {
1521
1703
  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
1704
  "aria-label": "Attach image",
1523
1705
  children: [
1524
- /* @__PURE__ */ jsx10(ImagePlus, {
1706
+ /* @__PURE__ */ jsx11(ImagePlus, {
1525
1707
  className: "h-4 w-4",
1526
1708
  "aria-hidden": true
1527
1709
  }),
1528
- /* @__PURE__ */ jsx10("input", {
1710
+ /* @__PURE__ */ jsx11("input", {
1529
1711
  type: "file",
1530
1712
  accept: ACCEPT.join(","),
1531
1713
  multiple: true,
@@ -1535,29 +1717,29 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1535
1717
  })
1536
1718
  ]
1537
1719
  }),
1538
- note && /* @__PURE__ */ jsx10("span", {
1720
+ note && /* @__PURE__ */ jsx11("span", {
1539
1721
  className: "truncate text-[12px] text-destructive",
1540
1722
  children: note
1541
1723
  }),
1542
- /* @__PURE__ */ jsx10("span", {
1724
+ /* @__PURE__ */ jsx11("span", {
1543
1725
  className: "flex-1"
1544
1726
  }),
1545
- running ? /* @__PURE__ */ jsx10("button", {
1727
+ running && onStop && !(steering && text.trim()) ? /* @__PURE__ */ jsx11("button", {
1546
1728
  type: "button",
1547
1729
  onClick: onStop,
1548
1730
  "aria-label": "Stop",
1549
1731
  className: "flex h-11 w-11 items-center justify-center rounded-md bg-destructive text-destructive-foreground",
1550
- children: /* @__PURE__ */ jsx10(Square2, {
1732
+ children: /* @__PURE__ */ jsx11(Square2, {
1551
1733
  className: "h-4 w-4",
1552
1734
  "aria-hidden": true
1553
1735
  })
1554
- }) : /* @__PURE__ */ jsx10("button", {
1736
+ }) : /* @__PURE__ */ jsx11("button", {
1555
1737
  type: "button",
1556
1738
  onClick: submit,
1557
1739
  disabled: !canSend,
1558
1740
  "aria-label": "Send",
1559
1741
  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, {
1742
+ children: /* @__PURE__ */ jsx11(ArrowUp, {
1561
1743
  className: "h-4 w-4",
1562
1744
  "aria-hidden": true
1563
1745
  })
@@ -1570,6 +1752,10 @@ function Composer({ onSubmit, running, onStop, disabled, disabledReason, placeho
1570
1752
  export {
1571
1753
  useYodexSession,
1572
1754
  toolSummary,
1755
+ switchesSession,
1756
+ ruleFor,
1757
+ ruleAnswers,
1758
+ revokeRules,
1573
1759
  resultLabel,
1574
1760
  reduce,
1575
1761
  permissionSummary,
@@ -1582,6 +1768,7 @@ export {
1582
1768
  buildOutcome,
1583
1769
  applyEvent,
1584
1770
  YodexClient,
1771
+ TranscriptRow,
1585
1772
  Transcript,
1586
1773
  ToolCall,
1587
1774
  SessionList,
@@ -1591,8 +1778,9 @@ export {
1591
1778
  PermissionPrompt,
1592
1779
  DiffView,
1593
1780
  Composer,
1594
- AssistantMessage
1781
+ AssistantMessage,
1782
+ ApprovalPrompt
1595
1783
  };
1596
1784
 
1597
- //# debugId=20955FCF1BAF8E9564756E2164756E21
1785
+ //# debugId=2F1B2145F90EDF3464756E2164756E21
1598
1786
  //# sourceMappingURL=index.js.map