@tabbio-technologies/cli 1.2.8 → 1.3.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.
Files changed (27) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +86 -19
  3. package/dist/chunks/{chunk-7LNC3FIV.js → chunk-GGJEDG3P.js} +95 -14
  4. package/dist/chunks/{chunk-7LNC3FIV.js.map → chunk-GGJEDG3P.js.map} +4 -4
  5. package/dist/chunks/{chunk-Y6GWIFHI.js → chunk-KBIQKZUK.js} +3 -53
  6. package/dist/chunks/chunk-KBIQKZUK.js.map +7 -0
  7. package/dist/chunks/{chunk-QVR5KIEQ.js → chunk-KHLGZ5IV.js} +1796 -244
  8. package/dist/chunks/chunk-KHLGZ5IV.js.map +7 -0
  9. package/dist/chunks/{chunk-NAWA6NCZ.js → chunk-NRFVKK3S.js} +37 -96
  10. package/dist/chunks/chunk-NRFVKK3S.js.map +7 -0
  11. package/dist/chunks/chunk-TP3CRQJD.js +106 -0
  12. package/dist/chunks/chunk-TP3CRQJD.js.map +7 -0
  13. package/dist/chunks/{entry-WENOOT6W.js → entry-ZAXWZSEE.js} +2023 -770
  14. package/dist/chunks/entry-ZAXWZSEE.js.map +7 -0
  15. package/dist/chunks/{mount-NGUARMCL.js → mount-36JUVX3R.js} +3 -2
  16. package/dist/chunks/{mount-NGUARMCL.js.map → mount-36JUVX3R.js.map} +1 -1
  17. package/dist/chunks/{program-O2BA5L6Z.js → program-J6GENE2V.js} +314 -375
  18. package/dist/chunks/program-J6GENE2V.js.map +7 -0
  19. package/dist/chunks/{status-I5UAQPBY.js → status-2C7BECKK.js} +3 -3
  20. package/dist/cli.js +1 -1
  21. package/package.json +4 -8
  22. package/dist/chunks/chunk-NAWA6NCZ.js.map +0 -7
  23. package/dist/chunks/chunk-QVR5KIEQ.js.map +0 -7
  24. package/dist/chunks/chunk-Y6GWIFHI.js.map +0 -7
  25. package/dist/chunks/entry-WENOOT6W.js.map +0 -7
  26. package/dist/chunks/program-O2BA5L6Z.js.map +0 -7
  27. /package/dist/chunks/{status-I5UAQPBY.js.map → status-2C7BECKK.js.map} +0 -0
@@ -4,38 +4,33 @@ import {
4
4
  Page,
5
5
  Spinner,
6
6
  Working,
7
- clipLines,
8
- displayWidth,
9
- estimateHeight,
10
- formatElapsed,
11
7
  fromTerminal,
12
- oneLine,
13
- padEnd,
14
- padStart,
15
8
  pageMargin,
16
- safeJson,
17
- sentenceCase,
18
- shortId,
19
- stripAnsi,
20
- summarizeArgs,
21
- truncate,
22
- truncateStart,
23
9
  useClockTick,
24
- useTerminalSize,
25
- wrapWords
26
- } from "./chunk-NAWA6NCZ.js";
10
+ useTerminalSize
11
+ } from "./chunk-TP3CRQJD.js";
27
12
  import {
28
13
  CAPABILITY_CHOICES,
29
14
  CAPABILITY_CHOICE_HELP,
15
+ CV_EXPORT_FORMATS,
30
16
  DEFAULT_CAPABILITIES,
17
+ artifactsDir,
31
18
  autoFilledProperties,
19
+ autoSaveDisabled,
32
20
  capabilityRequestFields,
21
+ cardFromToolResult,
22
+ cardLines,
33
23
  chatReducer,
34
24
  confirmApproval,
35
25
  describeSchema,
26
+ describeUser,
27
+ displayPath,
36
28
  draftSummary,
37
29
  exampleInvocation,
30
+ findCardJob,
31
+ findCardLink,
38
32
  findListWrapper,
33
+ formatBytes,
39
34
  getArtifact,
40
35
  getPath,
41
36
  getThreadMessages,
@@ -43,6 +38,8 @@ import {
43
38
  hyperlink,
44
39
  initialChatState,
45
40
  isDestructiveTool,
41
+ isTextFile,
42
+ latestCardLinks,
46
43
  listModels,
47
44
  listThreads,
48
45
  loadCapabilityPrefs,
@@ -52,32 +49,67 @@ import {
52
49
  pickColumns,
53
50
  previewUrl,
54
51
  previewVariantFor,
52
+ readSavedPreview,
55
53
  rejectApproval,
56
54
  renderScreen,
55
+ saveAppSession,
56
+ saveArtifactToDisk,
57
57
  saveCapabilityPrefs,
58
+ saveCvExportToDisk,
59
+ saveTargets,
60
+ saveTokenSession,
61
+ savedLines,
58
62
  schemaToFlags,
59
63
  streamChat,
60
64
  supportsHyperlinks,
61
65
  unavailableNotices,
62
66
  waitForArtifact
63
- } from "./chunk-QVR5KIEQ.js";
67
+ } from "./chunk-KHLGZ5IV.js";
68
+ import {
69
+ clipLines,
70
+ displayWidth,
71
+ estimateHeight,
72
+ formatElapsed,
73
+ oneLine,
74
+ padEnd,
75
+ padStart,
76
+ safeJson,
77
+ sentenceCase,
78
+ shortId,
79
+ stripAnsi,
80
+ summarizeArgs,
81
+ truncate,
82
+ truncateStart,
83
+ wrapWords
84
+ } from "./chunk-NRFVKK3S.js";
64
85
  import {
86
+ ApiClient,
65
87
  CLI_VERSION,
88
+ ExitCode,
89
+ PRODUCTION_APP_URL,
90
+ createCommandContext,
91
+ debug,
92
+ deviceLabel,
66
93
  getMcpAccessState,
94
+ hasDisplay,
67
95
  interruptedError,
68
96
  isCliError,
97
+ loadStoredCredentials,
98
+ loginWithBrowser,
99
+ loginWithEmailOtp,
100
+ logoutProfile,
69
101
  readCachedCatalog,
70
102
  relativeTime,
71
103
  toCliError
72
- } from "./chunk-7LNC3FIV.js";
104
+ } from "./chunk-GGJEDG3P.js";
73
105
  import "./chunk-NK5RPNSV.js";
74
106
  import {
75
107
  theme
76
108
  } from "./chunk-VHHZFMIF.js";
77
109
 
78
110
  // src/ui/entry.tsx
79
- import { Box as Box25, Static as Static2, Text as Text25 } from "ink";
80
- import { useEffect as useEffect9 } from "react";
111
+ import { Box as Box27, Static as Static2, Text as Text27 } from "ink";
112
+ import { useEffect as useEffect12, useState as useState11 } from "react";
81
113
 
82
114
  // src/ui/components/ApprovalCard.tsx
83
115
  import { Box as Box3, Text as Text3, useInput } from "ink";
@@ -430,8 +462,8 @@ function formKeyAction(state, input, key) {
430
462
  if (key.ctrl || key.meta) return null;
431
463
  return input ? { type: "type", text: input } : null;
432
464
  }
433
- function shellQuote(text) {
434
- return /^[\w@%+=:,./-]+$/.test(text) ? text : `'${text.replace(/'/g, `'\\''`)}'`;
465
+ function shellQuote(text2) {
466
+ return /^[\w@%+=:,./-]+$/.test(text2) ? text2 : `'${text2.replace(/'/g, `'\\''`)}'`;
435
467
  }
436
468
  function equivalentCommand(tool, input, skip = []) {
437
469
  const parts = ["tabbio", ...tool.commandPath];
@@ -442,8 +474,8 @@ function equivalentCommand(tool, input, skip = []) {
442
474
  parts.push(value ? `--${spec.flag}` : `--${spec.flag}=false`);
443
475
  continue;
444
476
  }
445
- const text = spec.kind === "array" && Array.isArray(value) && value.every((v) => typeof v !== "object") ? value.join(",") : typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
446
- parts.push(`--${spec.flag}`, shellQuote(text));
477
+ const text2 = spec.kind === "array" && Array.isArray(value) && value.every((v) => typeof v !== "object") ? value.join(",") : typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
478
+ parts.push(`--${spec.flag}`, shellQuote(text2));
447
479
  }
448
480
  return parts.join(" ");
449
481
  }
@@ -460,8 +492,8 @@ function valueText(field, raw, focused2) {
460
492
  return `${focused2 ? theme.accent(box) : box} ${label}`;
461
493
  }
462
494
  if (kind === "enum") {
463
- const text = raw || theme.dim("none");
464
- return focused2 ? `${theme.accent("\u2039")} ${text} ${theme.accent("\u203A")}` : text;
495
+ const text2 = raw || theme.dim("none");
496
+ return focused2 ? `${theme.accent("\u2039")} ${text2} ${theme.accent("\u203A")}` : text2;
465
497
  }
466
498
  return raw;
467
499
  }
@@ -545,11 +577,14 @@ function Form(props) {
545
577
  }
546
578
 
547
579
  // src/ui/screens/chat/Chat.tsx
548
- import { Box as Box15, Static, Text as Text15, useInput as useInput5 } from "ink";
549
- import { memo as memo14, useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useReducer as useReducer3, useRef as useRef4, useState as useState4 } from "react";
580
+ import { Box as Box17, Static, Text as Text17, useInput as useInput7 } from "ink";
581
+ import { useCallback as useCallback5, useEffect as useEffect8, useMemo as useMemo5, useReducer as useReducer4, useRef as useRef7, useState as useState5 } from "react";
550
582
 
551
583
  // src/ui/components/editor-model.ts
552
584
  var emptyEditor = { value: "", cursor: 0, history: [], historyIndex: null, draft: "" };
585
+ function editorWith(value) {
586
+ return { ...emptyEditor, value, cursor: [...value].length };
587
+ }
553
588
  function chars(value) {
554
589
  return [...value];
555
590
  }
@@ -560,8 +595,8 @@ function editorReducer(state, action) {
560
595
  const list = chars(state.value);
561
596
  switch (action.type) {
562
597
  case "insert": {
563
- const text = action.text.replace(/\r\n?/g, "\n");
564
- const inserted = chars(text);
598
+ const text2 = action.text.replace(/\r\n?/g, "\n");
599
+ const inserted = chars(text2);
565
600
  return place(state, [...list.slice(0, state.cursor), ...inserted, ...list.slice(state.cursor)], state.cursor + inserted.length);
566
601
  }
567
602
  case "newline":
@@ -753,33 +788,10 @@ function SelectList(props) {
753
788
  ] });
754
789
  }
755
790
 
756
- // src/ui/components/StatusBar.tsx
757
- import { Box as Box6, Text as Text6 } from "ink";
791
+ // src/ui/components/TextInput.tsx
792
+ import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
758
793
  import { memo as memo5 } from "react";
759
794
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
760
- function fitStatusBar(segments, hints, width) {
761
- const sep = ` ${theme.symbols.middot} `;
762
- let segs = [...segments];
763
- let fitted = [...hints];
764
- const measure = () => displayWidth(segs.join(sep)) + (fitted.length ? displayWidth(formatHints(fitted)) + 2 : 0);
765
- while (measure() > width && fitted.length > 1) fitted = fitHints(fitted, displayWidth(formatHints(fitted)) - 1);
766
- while (measure() > width && segs.length > 1) segs = segs.slice(0, -1);
767
- if (measure() > width) fitted = [];
768
- return { left: theme.dim(segs.join(sep)), right: fitted.length ? formatHints(fitted) : "" };
769
- }
770
- var StatusBar = memo5(function StatusBar2({ segments, hints = [], width }) {
771
- const clean = segments.filter((s) => typeof s === "string" && s.length > 0);
772
- const { left, right } = fitStatusBar(clean, hints, width);
773
- return /* @__PURE__ */ jsxs5(Box6, { width, children: [
774
- /* @__PURE__ */ jsx7(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx7(Text6, { wrap: "truncate-end", children: left }) }),
775
- right ? /* @__PURE__ */ jsx7(Text6, { wrap: "truncate-end", children: right }) : null
776
- ] });
777
- });
778
-
779
- // src/ui/components/TextInput.tsx
780
- import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
781
- import { memo as memo6 } from "react";
782
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
783
795
  function cursorCell(char) {
784
796
  if (theme.chalk.level > 0) return theme.inverse(char);
785
797
  return `${theme.symbols.cursor}${char === " " ? "" : char}`;
@@ -814,7 +826,7 @@ function renderComposerLine(state, width, placeholder, showCursor) {
814
826
  if (end === cells.length && !showCursor) out.pop();
815
827
  return `${start > 0 ? theme.dim(theme.symbols.ellipsis) : ""}${out.join("")}`;
816
828
  }
817
- var TextInput = memo6(function TextInput2(props) {
829
+ var TextInput = memo5(function TextInput2(props) {
818
830
  const { editor, dispatch, onSubmit, onKey, isActive, width, placeholder = "", boxed = false, prompt } = props;
819
831
  useInput4(
820
832
  (input, key) => {
@@ -829,23 +841,286 @@ var TextInput = memo6(function TextInput2(props) {
829
841
  const promptText = prompt ?? theme.symbols.pointer;
830
842
  const inner = boxed ? width - 4 : width;
831
843
  const line = renderComposerLine(editor, Math.max(4, inner - displayWidth(promptText) - 1), placeholder, isActive);
832
- const content = /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate-end", children: [
844
+ const content = /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate-end", children: [
833
845
  isActive ? theme.accent(promptText) : theme.dim(promptText),
834
846
  " ",
835
847
  line
836
848
  ] });
837
849
  if (!boxed) return content;
838
- return /* @__PURE__ */ jsx8(Box7, { borderStyle: theme.boxStyle, borderDimColor: true, paddingX: 1, width, children: content });
850
+ return /* @__PURE__ */ jsx7(Box6, { borderStyle: theme.boxStyle, borderDimColor: true, paddingX: 1, width, children: content });
839
851
  });
840
852
 
853
+ // src/ui/screens/login/LoginPanel.tsx
854
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
855
+ import { useCallback, useEffect as useEffect3, useReducer as useReducer2, useRef } from "react";
856
+
857
+ // src/ui/screens/login/login-model.ts
858
+ var LOGIN_CHOICES = [
859
+ { method: "browser", label: "Open the browser (recommended)", hint: "Approve this computer in Tabbio" },
860
+ { method: "email", label: "Email code", hint: "A 6-digit code, for SSH and headless machines" },
861
+ { method: "token", label: "Personal token", hint: "tabbio_mcp_... from Settings; tools only" }
862
+ ];
863
+ var LOGIN_USAGE = "Use /login, /login browser, /login email you@example.com or /login token";
864
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
865
+ function parseLoginArgs(arg) {
866
+ const [first = "", second = "", ...rest] = arg.trim().split(/\s+/).filter(Boolean);
867
+ const word = first.toLowerCase();
868
+ if (!word) return { method: "choose" };
869
+ if (rest.length > 0) return { error: LOGIN_USAGE };
870
+ if ((word === "browser" || word === "web") && !second) return { method: "browser" };
871
+ if (word === "token" && !second) return { method: "token" };
872
+ if (word === "email" || word === "code") return second ? { method: "email", email: second } : { method: "email" };
873
+ if (EMAIL.test(first) && !second) return { method: "email", email: first };
874
+ return { error: LOGIN_USAGE };
875
+ }
876
+ function isEmail(value) {
877
+ return EMAIL.test(value.trim());
878
+ }
879
+ function initialLoginPhase(start, now) {
880
+ return loginReducer({ name: "choose", index: 0 }, { type: "start", start, now });
881
+ }
882
+ function printable(text2) {
883
+ return text2.replace(/[\u0000-\u001f\u007f]/g, "");
884
+ }
885
+ function loginReducer(phase, action) {
886
+ switch (action.type) {
887
+ case "move":
888
+ if (phase.name !== "choose") return phase;
889
+ return { ...phase, index: (phase.index + action.delta + LOGIN_CHOICES.length) % LOGIN_CHOICES.length };
890
+ case "start":
891
+ switch (action.start.method) {
892
+ case "choose":
893
+ return { name: "choose", index: 0 };
894
+ case "browser":
895
+ return { name: "browser", since: action.now };
896
+ case "token":
897
+ return { name: "token-input", value: "" };
898
+ default:
899
+ return action.start.email && isEmail(action.start.email) ? { name: "email-sending", email: action.start.email.trim().toLowerCase(), since: action.now } : { name: "email-address", value: action.start.email ?? "", ...action.start.email ? { error: `Not a valid email address: ${action.start.email}` } : {} };
900
+ }
901
+ case "type": {
902
+ const text2 = printable(action.text);
903
+ if (!text2) return phase;
904
+ if (phase.name === "email-address" || phase.name === "token-input") return { ...phase, value: phase.value + text2, error: void 0 };
905
+ if (phase.name === "email-code") return { ...phase, value: (phase.value + text2).replace(/\s+/g, "").slice(0, 12), error: phase.error };
906
+ return phase;
907
+ }
908
+ case "erase":
909
+ if (phase.name === "email-address" || phase.name === "token-input" || phase.name === "email-code") {
910
+ return { ...phase, value: [...phase.value].slice(0, -1).join("") };
911
+ }
912
+ return phase;
913
+ case "browser-waiting":
914
+ return phase.name === "browser" ? { ...phase, url: action.url, opened: action.opened } : phase;
915
+ case "email-sending":
916
+ return { name: "email-sending", email: action.email, since: action.now };
917
+ case "code-needed":
918
+ if (phase.name === "email-sending" || phase.name === "email-verifying" || phase.name === "email-code") {
919
+ const error = phase.name === "email-code" ? phase.error : void 0;
920
+ return { name: "email-code", email: phase.email, value: "", attempt: action.attempt, ...error ? { error } : {} };
921
+ }
922
+ return phase;
923
+ case "code-rejected":
924
+ if (phase.name !== "email-verifying" && phase.name !== "email-code") return phase;
925
+ return {
926
+ name: "email-code",
927
+ email: phase.email,
928
+ value: "",
929
+ attempt: phase.name === "email-code" ? phase.attempt : 1,
930
+ error: `That code did not match. ${action.attemptsLeft} attempt${action.attemptsLeft === 1 ? "" : "s"} left.`
931
+ };
932
+ case "verifying":
933
+ return phase.name === "email-code" ? { name: "email-verifying", email: phase.email, since: action.now } : phase;
934
+ case "token-checking":
935
+ return phase.name === "token-input" ? { name: "token-checking", since: action.now } : phase;
936
+ case "invalid":
937
+ if (phase.name === "email-address" || phase.name === "token-input" || phase.name === "email-code") return { ...phase, error: action.error };
938
+ return phase;
939
+ default:
940
+ return phase;
941
+ }
942
+ }
943
+ function inputDisplay(phase, mask) {
944
+ if (phase.name === "token-input") return mask.repeat(Math.min(48, [...phase.value].length));
945
+ if (phase.name === "email-address" || phase.name === "email-code") return phase.value;
946
+ return null;
947
+ }
948
+ function waitingLabel(phase) {
949
+ switch (phase.name) {
950
+ case "browser":
951
+ return phase.url ? "Waiting for you to approve in the browser" : "Opening the browser";
952
+ case "email-sending":
953
+ return `Sending a sign-in code to ${phase.email}`;
954
+ case "email-verifying":
955
+ return "Checking the code";
956
+ case "token-checking":
957
+ return "Checking the token";
958
+ default:
959
+ return null;
960
+ }
961
+ }
962
+
963
+ // src/ui/screens/login/LoginPanel.tsx
964
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
965
+ var CHOICE_WIDTH = Math.max(...LOGIN_CHOICES.map((c) => c.label.length)) + 5;
966
+ function LoginPanel(props) {
967
+ const { services, width, onDone, isActive = true } = props;
968
+ const [phase, dispatch] = useReducer2(loginReducer, props.start, (start) => initialLoginPhase(start, Date.now()));
969
+ const abort = useRef(null);
970
+ const pending = useRef(null);
971
+ const finished = useRef(false);
972
+ const flow = useRef(false);
973
+ const release = useCallback(() => {
974
+ abort.current?.abort();
975
+ pending.current?.reject(interruptedError("Login cancelled"));
976
+ pending.current = null;
977
+ }, []);
978
+ const finish = useCallback(
979
+ (outcome) => {
980
+ if (finished.current) return;
981
+ finished.current = true;
982
+ release();
983
+ onDone(outcome);
984
+ },
985
+ [onDone, release]
986
+ );
987
+ useEffect3(
988
+ () => () => {
989
+ finished.current = true;
990
+ release();
991
+ },
992
+ [release]
993
+ );
994
+ const run = useCallback(
995
+ (work) => {
996
+ work.then(
997
+ (change) => finish({ status: "done", change }),
998
+ (error2) => {
999
+ const cli = toCliError(error2);
1000
+ finish(cli.exitCode === 130 ? { status: "cancelled" } : { status: "failed", message: cli.message, hint: cli.hint });
1001
+ }
1002
+ );
1003
+ },
1004
+ [finish]
1005
+ );
1006
+ const session = services.session;
1007
+ useEffect3(() => {
1008
+ if (flow.current || finished.current) return;
1009
+ if (!session && phase.name !== "choose") {
1010
+ finish({ status: "failed", message: "Signing in is not available here", hint: "Run `tabbio login` in a terminal." });
1011
+ return;
1012
+ }
1013
+ if (!session) return;
1014
+ if (phase.name === "browser") {
1015
+ flow.current = true;
1016
+ const controller = new AbortController();
1017
+ abort.current = controller;
1018
+ run(session.loginWithBrowser({ signal: controller.signal, onWaiting: (url, opened) => dispatch({ type: "browser-waiting", url, opened }) }));
1019
+ } else if (phase.name === "email-sending") {
1020
+ flow.current = true;
1021
+ run(
1022
+ session.loginWithEmail({
1023
+ email: phase.email,
1024
+ promptCode: (attempt) => new Promise((resolve, reject) => {
1025
+ if (finished.current) return reject(interruptedError("Login cancelled"));
1026
+ pending.current = { resolve, reject };
1027
+ dispatch({ type: "code-needed", attempt });
1028
+ }),
1029
+ onInvalidCode: (attemptsLeft) => dispatch({ type: "code-rejected", attemptsLeft })
1030
+ })
1031
+ );
1032
+ }
1033
+ }, [phase, session, run, finish]);
1034
+ useInput5(
1035
+ (input, key) => {
1036
+ if (key.escape || key.ctrl && input === "c") {
1037
+ finish({ status: "cancelled" });
1038
+ return;
1039
+ }
1040
+ if (phase.name === "choose") {
1041
+ if (key.upArrow || input === "k") dispatch({ type: "move", delta: -1 });
1042
+ else if (key.downArrow || input === "j") dispatch({ type: "move", delta: 1 });
1043
+ const digit = /^[1-3]$/.test(input) ? Number(input) - 1 : null;
1044
+ const choice = LOGIN_CHOICES[digit ?? (key.return ? phase.index : -1)];
1045
+ if (choice) dispatch({ type: "start", start: choice.method === "email" ? { method: "email" } : { method: choice.method }, now: Date.now() });
1046
+ return;
1047
+ }
1048
+ if (phase.name !== "email-address" && phase.name !== "email-code" && phase.name !== "token-input") return;
1049
+ if (key.return) {
1050
+ const value = phase.value.trim();
1051
+ if (phase.name === "email-address") {
1052
+ if (isEmail(value)) dispatch({ type: "email-sending", email: value.toLowerCase(), now: Date.now() });
1053
+ else dispatch({ type: "invalid", error: value ? `Not a valid email address: ${value}` : "Type your email address" });
1054
+ } else if (phase.name === "email-code") {
1055
+ if (value.length < 4) dispatch({ type: "invalid", error: "Enter the code from the email" });
1056
+ else {
1057
+ pending.current?.resolve(value);
1058
+ pending.current = null;
1059
+ dispatch({ type: "verifying", now: Date.now() });
1060
+ }
1061
+ } else if (!value) dispatch({ type: "invalid", error: "Paste a token first" });
1062
+ else if (session) {
1063
+ dispatch({ type: "token-checking", now: Date.now() });
1064
+ run(session.loginWithToken(value));
1065
+ }
1066
+ return;
1067
+ }
1068
+ if (key.backspace || key.delete) dispatch({ type: "erase" });
1069
+ else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow && input) {
1070
+ dispatch({ type: "type", text: input });
1071
+ }
1072
+ },
1073
+ { isActive }
1074
+ );
1075
+ const inner = Math.max(10, width - 4);
1076
+ const hints = (list) => /* @__PURE__ */ jsx8(Text7, { wrap: "truncate-end", children: formatHints(list) });
1077
+ const waiting = waitingLabel(phase);
1078
+ const display = inputDisplay(phase, theme.symbols.bullet);
1079
+ const error = "error" in phase && phase.error ? /* @__PURE__ */ jsx8(Text7, { wrap: "wrap", children: theme.attention(`${theme.symbols.attention} ${phase.error}`) }) : null;
1080
+ return /* @__PURE__ */ jsxs6(Box7, { borderStyle: theme.boxStyle, borderDimColor: true, paddingX: 1, flexDirection: "column", width, children: [
1081
+ /* @__PURE__ */ jsx8(Text7, { children: theme.bold("Sign in to Tabbio") }),
1082
+ phase.name === "choose" ? /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
1083
+ LOGIN_CHOICES.map((choice, i) => {
1084
+ const label = padEnd(`${i + 1} ${choice.label}`, CHOICE_WIDTH);
1085
+ return /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate-end", children: [
1086
+ i === phase.index ? theme.accent(`${theme.symbols.pointer} ${label}`) : ` ${label}`,
1087
+ theme.dim(choice.hint)
1088
+ ] }, choice.method);
1089
+ }),
1090
+ hints([
1091
+ { key: theme.symbols.upDown, label: "choose" },
1092
+ { key: "enter", label: "select" },
1093
+ { key: "esc", label: "cancel" }
1094
+ ])
1095
+ ] }) : null,
1096
+ waiting ? /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
1097
+ /* @__PURE__ */ jsx8(Working, { label: waiting, since: "since" in phase ? phase.since : Date.now() }),
1098
+ phase.name === "browser" && phase.url ? /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
1099
+ /* @__PURE__ */ jsx8(Text7, { children: theme.dim(phase.opened ? "If your browser did not open, visit:" : "Open this link to continue:") }),
1100
+ /* @__PURE__ */ jsx8(Text7, { wrap: "wrap", children: theme.accent(services.hyperlinks ? hyperlink(phase.url, phase.url) : phase.url) })
1101
+ ] }) : null,
1102
+ hints([{ key: "esc", label: "cancel" }])
1103
+ ] }) : null,
1104
+ display !== null ? /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
1105
+ /* @__PURE__ */ jsx8(Text7, { wrap: "wrap", children: phase.name === "email-address" ? "Your email address" : phase.name === "email-code" ? `Code sent to ${phase.email}. Enter it here:` : "Paste a personal token (tabbio_mcp_...). It stays on this computer." }),
1106
+ /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate-end", children: [
1107
+ theme.accent(theme.symbols.pointer),
1108
+ " ",
1109
+ renderComposerLine(editorWith(display), inner - 2, phase.name === "email-address" ? "you@example.com" : "", true)
1110
+ ] }),
1111
+ error,
1112
+ hints([
1113
+ { key: "enter", label: phase.name === "email-address" ? "send code" : phase.name === "email-code" ? "sign in" : "check" },
1114
+ { key: "esc", label: "cancel" }
1115
+ ])
1116
+ ] }) : null
1117
+ ] });
1118
+ }
1119
+
841
1120
  // src/ui/screens/Palette.tsx
842
1121
  import { Box as Box8, Text as Text8 } from "ink";
843
- import { memo as memo7 } from "react";
1122
+ import { Fragment, memo as memo6 } from "react";
844
1123
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
845
- function paletteMatches(commands, draft) {
846
- if (!draft.startsWith("/") || /\s/.test(draft)) return [];
847
- return fuzzyFilter(commands, draft.slice(1), (c) => [c.name, c.description]);
848
- }
849
1124
  function paletteArgMatches(commands, draft) {
850
1125
  const match = /^\/([a-z][\w-]*) (\S*)$/i.exec(draft);
851
1126
  if (!match) return [];
@@ -859,27 +1134,197 @@ function paletteArgMatches(commands, draft) {
859
1134
  completion: `/${command.name} ${arg.value}`
860
1135
  }));
861
1136
  }
862
- function parseSlash(text) {
863
- const match = /^\/([a-z][\w-]*)\s*([\s\S]*)$/i.exec(text.trim());
1137
+ function parseSlash(text2) {
1138
+ const match = /^\/([a-z][\w-]*)\s*([\s\S]*)$/i.exec(text2.trim());
864
1139
  return match ? { name: match[1].toLowerCase(), arg: (match[2] ?? "").trim() } : null;
865
1140
  }
866
- var Palette = memo7(function Palette2(props) {
867
- const { commands, selected, width, maxRows = 6 } = props;
1141
+ var Palette = memo6(function Palette2(props) {
1142
+ const { commands, selected, width, maxRows = 6, footer } = props;
1143
+ const footerRow = footer ? /* @__PURE__ */ jsx9(Text8, { wrap: "truncate-end", children: theme.dim(` ${footer}`) }) : null;
1144
+ if (commands.length === 0) {
1145
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", width, children: [
1146
+ /* @__PURE__ */ jsx9(Text8, { children: theme.dim(" No commands match") }),
1147
+ footerRow
1148
+ ] });
1149
+ }
868
1150
  const nameWidth = Math.min(22, Math.max(8, ...commands.map((c) => displayWidth(c.usage ?? `/${c.name}`)))) + 2;
869
1151
  const { start, end } = windowAround(commands.length, selected, maxRows);
870
- if (commands.length === 0) return /* @__PURE__ */ jsx9(Text8, { children: theme.dim(" No commands match") });
871
- return /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", width, children: commands.slice(start, end).map((command, i) => {
872
- const active = start + i === selected;
873
- const name = padEnd(truncate(command.usage ?? `/${command.name}`, nameWidth - 1), nameWidth);
874
- return /* @__PURE__ */ jsxs7(Text8, { wrap: "truncate-end", children: [
875
- active ? theme.accent(theme.symbols.pointer) : " ",
876
- " ",
877
- active ? theme.accent(name) : name,
878
- theme.dim(command.description),
879
- command.hint ? ` ${command.hint}` : ""
880
- ] }, command.name);
881
- }) });
1152
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", width, children: [
1153
+ commands.slice(start, end).map((command, i) => {
1154
+ const index = start + i;
1155
+ const active = index === selected;
1156
+ const name = padEnd(truncate(command.usage ?? `/${command.name}`, nameWidth - 1), nameWidth);
1157
+ const title = command.group && (i === 0 || commands[index - 1]?.group !== command.group) ? command.group : null;
1158
+ return /* @__PURE__ */ jsxs7(Fragment, { children: [
1159
+ title ? /* @__PURE__ */ jsx9(Text8, { wrap: "truncate-end", children: theme.dim(` ${title}`) }) : null,
1160
+ /* @__PURE__ */ jsxs7(Text8, { wrap: "truncate-end", children: [
1161
+ active ? theme.accent(theme.symbols.pointer) : " ",
1162
+ " ",
1163
+ active ? theme.accent(name) : name,
1164
+ theme.dim(command.description),
1165
+ command.hint ? ` ${command.hint}` : ""
1166
+ ] })
1167
+ ] }, `${command.group ?? ""}:${command.name}`);
1168
+ }),
1169
+ footerRow
1170
+ ] });
1171
+ });
1172
+
1173
+ // src/ui/screens/palette-model.ts
1174
+ var SKILLS_GROUP = "Skills";
1175
+ function isRecord2(value) {
1176
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1177
+ }
1178
+ function text(value) {
1179
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1180
+ }
1181
+ var SKILL_ID = /^[a-z0-9][\w.-]*$/i;
1182
+ function parseSkillsResult(result) {
1183
+ const list = Array.isArray(result) ? result : isRecord2(result) ? [result.skills, result.items, result.data, result.results].find(Array.isArray) ?? [] : [];
1184
+ const seen = /* @__PURE__ */ new Set();
1185
+ const out = [];
1186
+ for (const entry of list) {
1187
+ if (!isRecord2(entry)) continue;
1188
+ const id = text(entry.skillId) ?? text(entry.slug) ?? text(entry.id);
1189
+ if (!id || !SKILL_ID.test(id)) continue;
1190
+ const key = id.toLowerCase();
1191
+ if (seen.has(key)) continue;
1192
+ seen.add(key);
1193
+ const name = text(entry.name) ?? text(entry.title) ?? id;
1194
+ const description = text(entry.description);
1195
+ out.push({ id, name, ...description ? { description } : {} });
1196
+ }
1197
+ return out;
1198
+ }
1199
+ function skillCommands(skills, local) {
1200
+ const taken = new Set(local.map((c) => c.name.toLowerCase()));
1201
+ return skills.filter((s) => !taken.has(s.id.toLowerCase())).map((s) => ({ name: s.id, description: s.name, group: SKILLS_GROUP, skillId: s.id }));
1202
+ }
1203
+ function isBareSlash(draft) {
1204
+ return /^\/(?:\s[\s\S]*)?$/.test(draft.trimStart());
1205
+ }
1206
+ var BARE_SLASH_HINT = "Type a command, Esc to clear";
1207
+ function paletteView(commands, skills, draft, opts = {}) {
1208
+ if (!draft.startsWith("/")) return { items: [], args: false, open: false };
1209
+ const skillRows = skillCommands(skills.skills, commands);
1210
+ const loading = skills.status === "loading" ? `Loading skills${opts.ellipsis ?? "..."}` : void 0;
1211
+ if (isBareSlash(draft)) {
1212
+ const more = skillRows.length ? ` ${opts.separator ?? "-"} ${skillRows.length} skill${skillRows.length === 1 ? "" : "s"} below, type to filter` : "";
1213
+ return { items: [...commands, ...skillRows], args: false, open: true, footer: loading ?? `${BARE_SLASH_HINT}${more}` };
1214
+ }
1215
+ if (!/\s/.test(draft)) {
1216
+ const query = draft.slice(1);
1217
+ const local = fuzzyFilter(commands, query, (c) => [c.name, c.description]);
1218
+ const remote = fuzzyFilter(skillRows, query, (c) => [c.name, c.description]);
1219
+ return { items: [...local, ...remote], args: false, open: true, ...loading ? { footer: loading } : {} };
1220
+ }
1221
+ const args = paletteArgMatches(commands, draft);
1222
+ return { items: args, args: args.length > 0, open: args.length > 0 };
1223
+ }
1224
+ function skillMessage(id, arg) {
1225
+ return arg ? `/${id} ${arg}` : `/${id}`;
1226
+ }
1227
+ function resolveSubmit(value, ctx) {
1228
+ const trimmed = value.trim();
1229
+ if (!trimmed) return { kind: "empty" };
1230
+ if (!trimmed.startsWith("/")) return { kind: "message", text: trimmed };
1231
+ const pick = (row, arg) => row.skillId ? { kind: "skill", id: row.skillId, arg, message: skillMessage(row.skillId, arg) } : { kind: "command", name: row.name, arg, picked: true };
1232
+ if (isBareSlash(trimmed)) return ctx.highlighted && ctx.navigated ? pick(ctx.highlighted, "") : { kind: "bare" };
1233
+ const slash = parseSlash(trimmed);
1234
+ if (!slash) return { kind: "unknown", name: trimmed.slice(1).split(/\s/, 1)[0] ?? "" };
1235
+ if (ctx.commands.some((c) => c.name === slash.name) || ctx.aliases?.includes(slash.name)) return { kind: "command", name: slash.name, arg: slash.arg, picked: false };
1236
+ const skill = ctx.skills.skills.find((s) => s.id.toLowerCase() === slash.name);
1237
+ if (skill) return { kind: "skill", id: skill.id, arg: slash.arg, message: skillMessage(skill.id, slash.arg) };
1238
+ if (!slash.arg && ctx.highlighted && !ctx.highlighted.completion) return pick(ctx.highlighted, "");
1239
+ if (ctx.skills.status === "loading") return { kind: "loading", name: slash.name };
1240
+ return { kind: "unknown", name: slash.name };
1241
+ }
1242
+
1243
+ // src/ui/screens/useSkills.ts
1244
+ import { useEffect as useEffect4, useRef as useRef2, useState as useState3 } from "react";
1245
+ var IDLE = { status: "idle", skills: [] };
1246
+ function useSkills(services, wanted) {
1247
+ const [entry, setEntry] = useState3({ owner: services, state: IDLE });
1248
+ const mounted = useRef2(true);
1249
+ useEffect4(
1250
+ () => () => {
1251
+ mounted.current = false;
1252
+ },
1253
+ []
1254
+ );
1255
+ const current = entry.owner === services ? entry.state : IDLE;
1256
+ const load = services.account.signedIn ? services.loadSkills : void 0;
1257
+ useEffect4(() => {
1258
+ if (!wanted || !load || current.status !== "idle") return;
1259
+ const settle = (state) => {
1260
+ if (mounted.current) setEntry((e) => e.owner === services ? { owner: services, state } : e);
1261
+ };
1262
+ setEntry({ owner: services, state: { status: "loading", skills: [] } });
1263
+ load().then(
1264
+ (skills) => settle({ status: "ready", skills }),
1265
+ (error) => settle({ status: "failed", skills: [], error: toCliError(error).message })
1266
+ );
1267
+ }, [wanted, load, current.status, services]);
1268
+ return current;
1269
+ }
1270
+
1271
+ // src/ui/screens/chat/ChatStatus.tsx
1272
+ import { useInput as useInput6 } from "ink";
1273
+ import { memo as memo8 } from "react";
1274
+
1275
+ // src/ui/components/StatusBar.tsx
1276
+ import { Box as Box9, Text as Text9 } from "ink";
1277
+ import { memo as memo7 } from "react";
1278
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1279
+ function fitStatusBar(segments, hints, width) {
1280
+ const sep = ` ${theme.symbols.middot} `;
1281
+ let segs = [...segments];
1282
+ let fitted = [...hints];
1283
+ const measure = () => displayWidth(segs.join(sep)) + (fitted.length ? displayWidth(formatHints(fitted)) + 2 : 0);
1284
+ while (measure() > width && fitted.length > 1) fitted = fitHints(fitted, displayWidth(formatHints(fitted)) - 1);
1285
+ while (measure() > width && segs.length > 1) segs = segs.slice(0, -1);
1286
+ if (measure() > width) fitted = [];
1287
+ return { left: theme.dim(segs.join(sep)), right: fitted.length ? formatHints(fitted) : "" };
1288
+ }
1289
+ var StatusBar = memo7(function StatusBar2({ segments, hints = [], width }) {
1290
+ const clean = segments.filter((s) => typeof s === "string" && s.length > 0);
1291
+ const { left, right } = fitStatusBar(clean, hints, width);
1292
+ return /* @__PURE__ */ jsxs8(Box9, { width, children: [
1293
+ /* @__PURE__ */ jsx10(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx10(Text9, { wrap: "truncate-end", children: left }) }),
1294
+ right ? /* @__PURE__ */ jsx10(Text9, { wrap: "truncate-end", children: right }) : null
1295
+ ] });
1296
+ });
1297
+
1298
+ // src/ui/screens/chat/ChatStatus.tsx
1299
+ import { jsx as jsx11 } from "react/jsx-runtime";
1300
+ var ChatStatus = memo8(function ChatStatus2(props) {
1301
+ useClockTick(props.since !== null);
1302
+ const elapsed = props.since !== null ? formatElapsed(Date.now() - props.since) : null;
1303
+ const hints = props.approval ? [
1304
+ { key: "y", label: "approve" },
1305
+ { key: "n", label: "reject" },
1306
+ { key: "l", label: "later" }
1307
+ ] : props.overlay ? [{ key: "esc", label: "close" }] : props.streaming ? [
1308
+ { key: "enter", label: "queue" },
1309
+ { key: "esc", label: "interrupt" },
1310
+ { key: "/", label: "commands" }
1311
+ ] : [
1312
+ { key: "enter", label: "send" },
1313
+ { key: "/", label: "commands" },
1314
+ { key: "ctrl+c", label: "quit" }
1315
+ ];
1316
+ return /* @__PURE__ */ jsx11(StatusBar, { width: props.width, segments: [...props.segments, elapsed], hints });
882
1317
  });
1318
+ function capabilitySegments(mode, capabilities) {
1319
+ if (mode === "employer") return [];
1320
+ return ["research", "image", "result"].filter((key) => capabilities[key] !== DEFAULT_CAPABILITIES[key]).map((key) => `${key} ${capabilities[key]}`);
1321
+ }
1322
+ function SelectEscape({ onClose }) {
1323
+ useInput6((input, key) => {
1324
+ if (key.escape || input === "q") onClose();
1325
+ });
1326
+ return null;
1327
+ }
883
1328
 
884
1329
  // src/ui/screens/chat/commands.ts
885
1330
  function choices2(key) {
@@ -902,7 +1347,14 @@ function chatCommands(opts) {
902
1347
  { name: "research", usage: "/research auto|on|off", description: "Web research for the next messages", args: choices2("research") },
903
1348
  { name: "image", usage: "/image auto|on|off", description: "Image generation", args: choices2("image") },
904
1349
  { name: "result", usage: "/result auto|summary|document|\u2026", description: "What to make: answer, document, slides, page, image", args: choices2("result") },
1350
+ { name: "open", usage: "/open <n|id>", description: "Open link n of the last result, or a file" },
1351
+ { name: "apply", usage: "/apply <n>", description: "Prepare an application for job n" },
1352
+ { name: "save", usage: "/save <n|id> [folder]", description: "Save a file Tabbio made (1 = newest)" },
1353
+ { name: "show", usage: "/show <n|id>", description: "Print a saved file here (1 = newest)" },
1354
+ { name: "download", usage: "/download [cvId] [pdf|docx]", description: "Save a CV (your main CV without an id)" },
905
1355
  { name: "help", description: "Keys and commands" },
1356
+ { name: "login", usage: "/login [email|token]", description: "Sign in or switch account" },
1357
+ { name: "logout", description: "Sign out of this profile" },
906
1358
  ...opts.hostedByHome ? [{ name: "home", description: "Back to the home screen" }] : [],
907
1359
  { name: "exit", description: "Quit tabbio" }
908
1360
  ];
@@ -912,21 +1364,33 @@ function parseMode(value) {
912
1364
  return v === "seeker" || v === "employer" ? v : null;
913
1365
  }
914
1366
  var CHAT_HELP = [
915
- "Commands: /new \xB7 /threads \xB7 /mode seeker|employer \xB7 /model <id|auto> \xB7 /help \xB7 /exit",
916
- " /research auto|on|off \xB7 /image auto|on|off \xB7 /result auto|summary|document|slides|page|image",
917
- " (saved for this profile; sent with every message in seeker mode)",
918
- "Keys: enter send (queues while streaming) \xB7 ctrl+j or \\ then enter newline \xB7 esc interrupt",
919
- " \u2191\u2193 history \xB7 shift+tab switch mode \xB7 ctrl+o expand the last tool \xB7 ctrl+c twice quit"
1367
+ "Chat /new /threads /mode seeker|employer /model <id|auto>",
1368
+ " /help /home /exit",
1369
+ "Modes /research auto|on|off /image auto|on|off",
1370
+ " /result auto|summary|document|slides|page|image",
1371
+ " (saved for this profile; sent with every seeker message)",
1372
+ "Results /open <n> opens link n of the last result",
1373
+ " /apply <n> prepares the application for job n (no chat)",
1374
+ "Files /show 1 prints the newest file of this chat, /show 2 the one",
1375
+ " before; /save <n|id> [folder] /open <n|id> (ids work too)",
1376
+ " /download [cvId] [pdf|docx|txt|md] (main CV without an id)",
1377
+ " generated files are saved to ./tabbio-artifacts",
1378
+ "Account /login /login email you@example.com /login token /logout",
1379
+ "Skills type / to list your skills; enter runs /<skill> <details>",
1380
+ "Keys enter send (queues while streaming) ctrl+j newline",
1381
+ " esc interrupt, or back to home \u2191\u2193 history",
1382
+ " shift+tab switch mode ctrl+o expand the last tool",
1383
+ " ctrl+c twice quit"
920
1384
  ].join("\n");
921
1385
 
922
1386
  // src/ui/screens/chat/Transcript.tsx
923
- import { Box as Box14, Text as Text14 } from "ink";
924
- import { memo as memo13 } from "react";
1387
+ import { Box as Box16, Text as Text16 } from "ink";
1388
+ import { memo as memo15 } from "react";
925
1389
 
926
1390
  // src/ui/components/ArtifactRow.tsx
927
- import { Box as Box9, Text as Text9 } from "ink";
928
- import { memo as memo8 } from "react";
929
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1391
+ import { Box as Box10, Text as Text10 } from "ink";
1392
+ import { memo as memo9 } from "react";
1393
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
930
1394
  function kindGlyph(kind) {
931
1395
  switch (kind) {
932
1396
  case "document":
@@ -951,7 +1415,7 @@ function artifactStatusText(artifact, draft) {
951
1415
  }
952
1416
  return artifact.status;
953
1417
  }
954
- var ArtifactRow = memo8(function ArtifactRow2({ artifact, draft, width, hint = true }) {
1418
+ var ArtifactRow = memo9(function ArtifactRow2({ artifact, draft, width, hint = true }) {
955
1419
  const status = artifactStatusText(artifact, draft);
956
1420
  const tone = artifact.status === "failed" ? theme.error : artifact.status === "ready" ? theme.success : theme.dim;
957
1421
  const meta = [kindLabel(artifact.kind).toLowerCase(), status, shortId(artifact.id, 6, 4)];
@@ -960,28 +1424,46 @@ var ArtifactRow = memo8(function ArtifactRow2({ artifact, draft, width, hint = t
960
1424
  const titleBudget = Math.max(8, Math.min(48, width - 4 - displayWidth(meta.join(" \xB7 ")) - 6));
961
1425
  const title = truncate(artifact.title || "Untitled", titleBudget);
962
1426
  const linked = artifact.status === "ready" && artifact.previewUrl;
963
- return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", width, children: [
964
- /* @__PURE__ */ jsxs8(Box9, { width, children: [
965
- artifact.status === "pending" ? /* @__PURE__ */ jsx10(Spinner, {}) : /* @__PURE__ */ jsx10(Text9, { children: artifact.status === "ready" ? theme.success(theme.symbols.success) : theme.error(theme.symbols.error) }),
966
- /* @__PURE__ */ jsx10(Box9, { flexShrink: 0, marginLeft: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
1427
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", width, children: [
1428
+ /* @__PURE__ */ jsxs9(Box10, { width, children: [
1429
+ artifact.status === "pending" ? /* @__PURE__ */ jsx12(Spinner, {}) : /* @__PURE__ */ jsx12(Text10, { children: artifact.status === "ready" ? theme.success(theme.symbols.success) : theme.error(theme.symbols.error) }),
1430
+ /* @__PURE__ */ jsx12(Box10, { flexShrink: 0, marginLeft: 1, children: /* @__PURE__ */ jsxs9(Text10, { children: [
967
1431
  theme.accent(kindGlyph(artifact.kind)),
968
1432
  " ",
969
1433
  linked ? hyperlink(theme.bold(title), artifact.previewUrl) : theme.bold(title)
970
1434
  ] }) }),
971
- /* @__PURE__ */ jsx10(Box9, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsxs8(Text9, { wrap: "truncate-end", children: [
1435
+ /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsxs9(Text10, { wrap: "truncate-end", children: [
972
1436
  theme.dim(` ${theme.symbols.middot} `),
973
1437
  tone(meta.join(` ${theme.symbols.middot} `)),
974
1438
  openHint ? theme.dim(` ${openHint}`) : ""
975
1439
  ] }) })
976
1440
  ] }),
977
- artifact.failure?.message ? /* @__PURE__ */ jsx10(Text9, { wrap: "truncate-end", children: theme.dim(` ${theme.symbols.arrowRight} ${artifact.failure.message}`) }) : null
1441
+ artifact.failure?.message ? /* @__PURE__ */ jsx12(Text10, { wrap: "truncate-end", children: theme.dim(` ${theme.symbols.arrowRight} ${artifact.failure.message}`) }) : null
978
1442
  ] });
979
1443
  });
980
1444
 
981
- // src/ui/components/Markdown.tsx
1445
+ // src/ui/components/cards/CardView.tsx
982
1446
  import { Box as Box11, Text as Text11 } from "ink";
1447
+ import { memo as memo10 } from "react";
1448
+ import { jsx as jsx13 } from "react/jsx-runtime";
1449
+ function Line({ text: text2, width }) {
1450
+ const cells = displayWidth(text2);
1451
+ if (cells <= width) return /* @__PURE__ */ jsx13(Text11, { children: text2 });
1452
+ return /* @__PURE__ */ jsx13(Box11, { width: cells, flexShrink: 0, children: /* @__PURE__ */ jsx13(Text11, { wrap: "truncate-end", children: text2 }) });
1453
+ }
1454
+ var CardView = memo10(function CardView2({ card, width, links }) {
1455
+ const lines = cardLines(card, { width, links: links ?? supportsHyperlinks() });
1456
+ return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", width, children: lines.map((line, i) => /* @__PURE__ */ jsx13(Line, { text: line, width }, i)) });
1457
+ });
1458
+ var SavedFilesView = memo10(function SavedFilesView2({ saved, width }) {
1459
+ const lines = savedLines(saved, { width });
1460
+ return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", width, children: lines.map((line, i) => /* @__PURE__ */ jsx13(Line, { text: line, width }, i)) });
1461
+ });
1462
+
1463
+ // src/ui/components/Markdown.tsx
1464
+ import { Box as Box13, Text as Text13 } from "ink";
983
1465
  import { marked } from "marked";
984
- import { memo as memo10, useMemo as useMemo3 } from "react";
1466
+ import { memo as memo12, useMemo as useMemo3 } from "react";
985
1467
 
986
1468
  // src/ui/components/markdown-inline.ts
987
1469
  var ENTITIES = {
@@ -993,8 +1475,8 @@ var ENTITIES = {
993
1475
  "&apos;": "'",
994
1476
  "&nbsp;": " "
995
1477
  };
996
- function decodeEntities(text) {
997
- return text.replace(/&(?:amp|lt|gt|quot|apos|nbsp|#39);/g, (m) => ENTITIES[m] ?? m).replace(/&#(\d{1,6});/g, (m, code) => {
1478
+ function decodeEntities(text2) {
1479
+ return text2.replace(/&(?:amp|lt|gt|quot|apos|nbsp|#39);/g, (m) => ENTITIES[m] ?? m).replace(/&#(\d{1,6});/g, (m, code) => {
998
1480
  const n = Number(code);
999
1481
  return n > 0 && n < 1114111 ? String.fromCodePoint(n) : m;
1000
1482
  });
@@ -1004,8 +1486,8 @@ function childTokens(token) {
1004
1486
  return Array.isArray(tokens) ? tokens : void 0;
1005
1487
  }
1006
1488
  function textOf(token) {
1007
- const text = token.text;
1008
- if (typeof text === "string") return text;
1489
+ const text2 = token.text;
1490
+ if (typeof text2 === "string") return text2;
1009
1491
  const raw = token.raw;
1010
1492
  return typeof raw === "string" ? raw : "";
1011
1493
  }
@@ -1041,8 +1523,8 @@ function renderToken(token) {
1041
1523
  return `${theme.dim("[image]")} ${image.text || ""}${image.href ? ` ${theme.dim(`(${image.href})`)}` : ""}`.trim();
1042
1524
  }
1043
1525
  case "html": {
1044
- const text = textOf(token);
1045
- return /^<br\s*\/?>$/i.test(text.trim()) ? "\n" : "";
1526
+ const text2 = textOf(token);
1527
+ return /^<br\s*\/?>$/i.test(text2.trim()) ? "\n" : "";
1046
1528
  }
1047
1529
  case "checkbox":
1048
1530
  return "";
@@ -1064,8 +1546,8 @@ function renderInline(tokens, fallback = "") {
1064
1546
  }
1065
1547
 
1066
1548
  // src/ui/components/Table.tsx
1067
- import { Box as Box10, Text as Text10 } from "ink";
1068
- import { memo as memo9, useMemo as useMemo2 } from "react";
1549
+ import { Box as Box12, Text as Text12 } from "ink";
1550
+ import { memo as memo11, useMemo as useMemo2 } from "react";
1069
1551
 
1070
1552
  // src/ui/components/table-layout.ts
1071
1553
  var TABLE_GAP = 2;
@@ -1103,8 +1585,8 @@ function layoutTable(columns, rows, width, gap = TABLE_GAP) {
1103
1585
  function formatTableRow(layout, cells, gap = TABLE_GAP) {
1104
1586
  return layout.columns.map((column, i) => {
1105
1587
  const w = layout.widths[i];
1106
- const text = truncate((cells[column.key] ?? "").replace(/\s+/g, " "), w);
1107
- return column.align === "right" ? padStart(text, w) : padEnd(text, w);
1588
+ const text2 = truncate((cells[column.key] ?? "").replace(/\s+/g, " "), w);
1589
+ return column.align === "right" ? padStart(text2, w) : padEnd(text2, w);
1108
1590
  }).join(" ".repeat(gap)).trimEnd();
1109
1591
  }
1110
1592
  function formatTableHeader(layout, gap = TABLE_GAP) {
@@ -1114,23 +1596,23 @@ function formatTableHeader(layout, gap = TABLE_GAP) {
1114
1596
  }
1115
1597
 
1116
1598
  // src/ui/components/Table.tsx
1117
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1118
- var Table = memo9(function Table2({ columns, rows, width, offset = 0, limit }) {
1599
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1600
+ var Table = memo11(function Table2({ columns, rows, width, offset = 0, limit }) {
1119
1601
  const layout = useMemo2(() => layoutTable(columns, rows, width), [columns, rows, width]);
1120
1602
  const shown = rows.slice(offset, limit === void 0 ? void 0 : offset + limit);
1121
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", width, children: [
1122
- /* @__PURE__ */ jsx11(Text10, { wrap: "truncate-end", children: theme.dim(formatTableHeader(layout)) }),
1123
- shown.map((row, i) => /* @__PURE__ */ jsx11(Text10, { wrap: "truncate-end", children: formatTableRow(layout, row) }, offset + i))
1603
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", width, children: [
1604
+ /* @__PURE__ */ jsx14(Text12, { wrap: "truncate-end", children: theme.dim(formatTableHeader(layout)) }),
1605
+ shown.map((row, i) => /* @__PURE__ */ jsx14(Text12, { wrap: "truncate-end", children: formatTableRow(layout, row) }, offset + i))
1124
1606
  ] });
1125
1607
  });
1126
1608
 
1127
1609
  // src/ui/components/Markdown.tsx
1128
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1129
- function lexMarkdown(text) {
1610
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1611
+ function lexMarkdown(text2) {
1130
1612
  try {
1131
- return marked.lexer(text.replace(/\r\n?/g, "\n"), { gfm: true });
1613
+ return marked.lexer(text2.replace(/\r\n?/g, "\n"), { gfm: true });
1132
1614
  } catch {
1133
- return [{ type: "paragraph", raw: text, text, tokens: [{ type: "text", raw: text, text }] }];
1615
+ return [{ type: "paragraph", raw: text2, text: text2, tokens: [{ type: "text", raw: text2, text: text2 }] }];
1134
1616
  }
1135
1617
  }
1136
1618
  function tokensOf(token) {
@@ -1138,8 +1620,8 @@ function tokensOf(token) {
1138
1620
  return Array.isArray(tokens) ? tokens : void 0;
1139
1621
  }
1140
1622
  function textOf2(token) {
1141
- const text = token.text;
1142
- return typeof text === "string" ? text : typeof token.raw === "string" ? token.raw : "";
1623
+ const text2 = token.text;
1624
+ return typeof text2 === "string" ? text2 : typeof token.raw === "string" ? token.raw : "";
1143
1625
  }
1144
1626
  function renderList(list, ctx, key, trailing = "") {
1145
1627
  const start = typeof list.start === "number" ? list.start : 1;
@@ -1147,9 +1629,9 @@ function renderList(list, ctx, key, trailing = "") {
1147
1629
  (item, i) => list.ordered ? `${start + i}.` : item.task ? item.checked ? "[x]" : "[ ]" : theme.symbols.bullet
1148
1630
  );
1149
1631
  const markerWidth = Math.max(...markers.map((m) => stripAnsi(m).length)) + 1;
1150
- return /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", children: list.items.map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
1151
- /* @__PURE__ */ jsx12(Box11, { width: markerWidth, flexShrink: 0, children: /* @__PURE__ */ jsx12(Text11, { children: list.ordered ? markers[i] : theme.dim(markers[i]) }) }),
1152
- /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", width: Math.max(8, ctx.width - markerWidth), children: renderBlocks(
1632
+ return /* @__PURE__ */ jsx15(Box13, { flexDirection: "column", children: list.items.map((item, i) => /* @__PURE__ */ jsxs11(Box13, { children: [
1633
+ /* @__PURE__ */ jsx15(Box13, { width: markerWidth, flexShrink: 0, children: /* @__PURE__ */ jsx15(Text13, { children: list.ordered ? markers[i] : theme.dim(markers[i]) }) }),
1634
+ /* @__PURE__ */ jsx15(Box13, { flexDirection: "column", width: Math.max(8, ctx.width - markerWidth), children: renderBlocks(
1153
1635
  item.tokens,
1154
1636
  { width: Math.max(8, ctx.width - markerWidth), trailing: i === list.items.length - 1 && trailing ? trailing : void 0 },
1155
1637
  `${key}-${i}`,
@@ -1170,18 +1652,18 @@ function renderTable(table, ctx, key) {
1170
1652
  });
1171
1653
  return record;
1172
1654
  });
1173
- return /* @__PURE__ */ jsx12(Table, { columns, rows, width: ctx.width }, key);
1655
+ return /* @__PURE__ */ jsx15(Table, { columns, rows, width: ctx.width }, key);
1174
1656
  }
1175
1657
  function renderBlock(token, ctx, key, trailing) {
1176
1658
  switch (token.type) {
1177
1659
  case "heading":
1178
- return /* @__PURE__ */ jsxs10(Text11, { bold: true, children: [
1660
+ return /* @__PURE__ */ jsxs11(Text13, { bold: true, children: [
1179
1661
  renderInline(tokensOf(token), textOf2(token)),
1180
1662
  trailing
1181
1663
  ] }, key);
1182
1664
  case "paragraph":
1183
1665
  case "text":
1184
- return /* @__PURE__ */ jsxs10(Text11, { children: [
1666
+ return /* @__PURE__ */ jsxs11(Text13, { children: [
1185
1667
  renderInline(tokensOf(token), textOf2(token)),
1186
1668
  trailing
1187
1669
  ] }, key);
@@ -1189,8 +1671,8 @@ function renderBlock(token, ctx, key, trailing) {
1189
1671
  return renderList(token, ctx, key, trailing);
1190
1672
  case "code": {
1191
1673
  const lines = textOf2(token).replace(/\n$/, "").split("\n");
1192
- return /* @__PURE__ */ jsx12(
1193
- Box11,
1674
+ return /* @__PURE__ */ jsx15(
1675
+ Box13,
1194
1676
  {
1195
1677
  flexDirection: "column",
1196
1678
  marginLeft: 2,
@@ -1200,24 +1682,24 @@ function renderBlock(token, ctx, key, trailing) {
1200
1682
  borderBottom: false,
1201
1683
  borderLeftDimColor: true,
1202
1684
  paddingLeft: 1,
1203
- children: lines.map((line, i) => /* @__PURE__ */ jsx12(Text11, { children: line || " " }, i))
1685
+ children: lines.map((line, i) => /* @__PURE__ */ jsx15(Text13, { children: line || " " }, i))
1204
1686
  },
1205
1687
  key
1206
1688
  );
1207
1689
  }
1208
1690
  case "blockquote":
1209
- return /* @__PURE__ */ jsx12(Rail, { tone: "muted", children: renderBlocks(tokensOf(token) ?? [], { width: Math.max(8, ctx.width - 2) }, key, false) }, key);
1691
+ return /* @__PURE__ */ jsx15(Rail, { tone: "muted", children: renderBlocks(tokensOf(token) ?? [], { width: Math.max(8, ctx.width - 2) }, key, false) }, key);
1210
1692
  case "hr":
1211
- return /* @__PURE__ */ jsx12(Text11, { children: theme.dim(theme.symbols.line.repeat(Math.max(3, Math.min(ctx.width, 60)))) }, key);
1693
+ return /* @__PURE__ */ jsx15(Text13, { children: theme.dim(theme.symbols.line.repeat(Math.max(3, Math.min(ctx.width, 60)))) }, key);
1212
1694
  case "table":
1213
1695
  return renderTable(token, ctx, key);
1214
1696
  case "html":
1215
- return /* @__PURE__ */ jsx12(Text11, { children: theme.dim(textOf2(token).trim()) }, key);
1697
+ return /* @__PURE__ */ jsx15(Text13, { children: theme.dim(textOf2(token).trim()) }, key);
1216
1698
  case "space":
1217
1699
  case "def":
1218
1700
  return null;
1219
1701
  default:
1220
- return /* @__PURE__ */ jsx12(Text11, { children: textOf2(token) }, key);
1702
+ return /* @__PURE__ */ jsx15(Text13, { children: textOf2(token) }, key);
1221
1703
  }
1222
1704
  }
1223
1705
  var INLINE_TAIL = /* @__PURE__ */ new Set(["paragraph", "text", "heading", "list"]);
@@ -1231,24 +1713,24 @@ function renderBlocks(tokens, ctx, keyPrefix, spaced = true) {
1231
1713
  try {
1232
1714
  node = renderBlock(token, ctx, `${keyPrefix}-${i}`, trailing);
1233
1715
  } catch {
1234
- node = /* @__PURE__ */ jsx12(Text11, { children: token.raw }, `${keyPrefix}-${i}`);
1716
+ node = /* @__PURE__ */ jsx15(Text13, { children: token.raw }, `${keyPrefix}-${i}`);
1235
1717
  }
1236
- if (spaced && i > 0) out.push(/* @__PURE__ */ jsx12(Text11, { children: " " }, `${keyPrefix}-${i}-gap`));
1718
+ if (spaced && i > 0) out.push(/* @__PURE__ */ jsx15(Text13, { children: " " }, `${keyPrefix}-${i}-gap`));
1237
1719
  out.push(node);
1238
- if (isLast && ctx.trailing && !trailing) out.push(/* @__PURE__ */ jsx12(Text11, { children: ctx.trailing }, `${keyPrefix}-tail`));
1720
+ if (isLast && ctx.trailing && !trailing) out.push(/* @__PURE__ */ jsx15(Text13, { children: ctx.trailing }, `${keyPrefix}-tail`));
1239
1721
  });
1240
- if (blocks.length === 0 && ctx.trailing) out.push(/* @__PURE__ */ jsx12(Text11, { children: ctx.trailing }, `${keyPrefix}-tail`));
1722
+ if (blocks.length === 0 && ctx.trailing) out.push(/* @__PURE__ */ jsx15(Text13, { children: ctx.trailing }, `${keyPrefix}-tail`));
1241
1723
  return out;
1242
1724
  }
1243
- var Markdown = memo10(function Markdown2({ text, width, trailing }) {
1244
- const tokens = useMemo3(() => lexMarkdown(text), [text]);
1245
- return /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", width, children: renderBlocks(tokens, { width, trailing }, "md") });
1725
+ var Markdown = memo12(function Markdown2({ text: text2, width, trailing }) {
1726
+ const tokens = useMemo3(() => lexMarkdown(text2), [text2]);
1727
+ return /* @__PURE__ */ jsx15(Box13, { flexDirection: "column", width, children: renderBlocks(tokens, { width, trailing }, "md") });
1246
1728
  });
1247
1729
 
1248
1730
  // src/ui/components/SourcesList.tsx
1249
- import { Box as Box12, Text as Text12 } from "ink";
1250
- import { memo as memo11 } from "react";
1251
- import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1731
+ import { Box as Box14, Text as Text14 } from "ink";
1732
+ import { memo as memo13 } from "react";
1733
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1252
1734
  var MAX_SOURCES = 8;
1253
1735
  function hostOf(source) {
1254
1736
  if (source.domain) return source.domain;
@@ -1258,64 +1740,65 @@ function hostOf(source) {
1258
1740
  return "";
1259
1741
  }
1260
1742
  }
1261
- var SourcesList = memo11(function SourcesList2(props) {
1743
+ var SourcesList = memo13(function SourcesList2(props) {
1262
1744
  const { sources, width } = props;
1263
1745
  const links = props.links ?? supportsHyperlinks();
1264
1746
  const shown = sources.slice(0, MAX_SOURCES);
1265
1747
  const numberWidth = String(shown.length).length;
1266
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", width, children: [
1267
- /* @__PURE__ */ jsx13(Text12, { children: theme.dim("Sources") }),
1748
+ return /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", width, children: [
1749
+ /* @__PURE__ */ jsx16(Text14, { children: theme.dim("Sources") }),
1268
1750
  shown.map((source, i) => {
1269
1751
  const host = hostOf(source);
1270
1752
  const title = truncate(source.title, Math.max(10, Math.min(60, width - numberWidth - displayWidth(host) - 8)));
1271
- return /* @__PURE__ */ jsxs11(Box12, { width, children: [
1272
- /* @__PURE__ */ jsx13(Box12, { flexShrink: 0, children: /* @__PURE__ */ jsxs11(Text12, { children: [
1753
+ return /* @__PURE__ */ jsxs12(Box14, { width, children: [
1754
+ /* @__PURE__ */ jsx16(Box14, { flexShrink: 0, children: /* @__PURE__ */ jsxs12(Text14, { children: [
1273
1755
  theme.dim(String(i + 1).padStart(numberWidth)),
1274
1756
  " ",
1275
1757
  links ? hyperlink(title, source.url) : title
1276
1758
  ] }) }),
1277
- /* @__PURE__ */ jsx13(Box12, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsxs11(Text12, { wrap: "truncate-end", children: [
1759
+ /* @__PURE__ */ jsx16(Box14, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsxs12(Text14, { wrap: "truncate-end", children: [
1278
1760
  host && host !== source.title ? theme.dim(` ${theme.symbols.middot} ${host}`) : "",
1279
1761
  links ? "" : theme.dim(` ${source.url}`)
1280
1762
  ] }) })
1281
1763
  ] }, `${i}-${source.url}`);
1282
1764
  }),
1283
- sources.length > shown.length ? /* @__PURE__ */ jsx13(Text12, { children: theme.dim(`+${sources.length - shown.length} more`) }) : null
1765
+ sources.length > shown.length ? /* @__PURE__ */ jsx16(Text14, { children: theme.dim(`+${sources.length - shown.length} more`) }) : null
1284
1766
  ] });
1285
1767
  });
1286
1768
 
1287
1769
  // src/ui/components/ToolCallRow.tsx
1288
- import { Box as Box13, Text as Text13 } from "ink";
1289
- import { memo as memo12 } from "react";
1290
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1291
- function isRecord2(value) {
1770
+ import { Box as Box15, Text as Text15 } from "ink";
1771
+ import { memo as memo14 } from "react";
1772
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1773
+ function isRecord3(value) {
1292
1774
  return typeof value === "object" && value !== null && !Array.isArray(value);
1293
1775
  }
1776
+ var LIST_RESULT_KEYS = ["jobs", "cvs", "items", "skills", "automations", "results", "applications"];
1294
1777
  function resultSummary(result) {
1295
1778
  if (Array.isArray(result)) return `${result.length} found`;
1296
- if (!isRecord2(result)) return "";
1779
+ if (!isRecord3(result)) return "";
1297
1780
  if (result.ok === false || result.error === true) return "failed";
1298
- for (const key of ["total", "totalCount", "count"]) {
1299
- if (typeof result[key] === "number") return `${result[key]} found`;
1300
- }
1301
- const arrays = Object.values(result).filter(Array.isArray);
1302
- return arrays.length === 1 ? `${arrays[0].length} found` : "";
1781
+ const key = LIST_RESULT_KEYS.find((k) => Array.isArray(result[k]));
1782
+ if (!key) return "";
1783
+ const shown = result[key].length;
1784
+ const total = typeof result.total === "number" && Number.isFinite(result.total) ? result.total : void 0;
1785
+ return `${total !== void 0 && total >= shown ? total : shown} found`;
1303
1786
  }
1304
1787
  function glyph(call) {
1305
- if (call.decision === "approve") return /* @__PURE__ */ jsx14(Text13, { children: theme.success(theme.symbols.success) });
1306
- if (call.decision === "reject") return /* @__PURE__ */ jsx14(Text13, { children: theme.error(theme.symbols.error) });
1307
- if (call.left) return /* @__PURE__ */ jsx14(Text13, { children: theme.attention(theme.symbols.attention) });
1788
+ if (call.decision === "approve") return /* @__PURE__ */ jsx17(Text15, { children: theme.success(theme.symbols.success) });
1789
+ if (call.decision === "reject") return /* @__PURE__ */ jsx17(Text15, { children: theme.error(theme.symbols.error) });
1790
+ if (call.left) return /* @__PURE__ */ jsx17(Text15, { children: theme.attention(theme.symbols.attention) });
1308
1791
  switch (call.stage) {
1309
1792
  case "running":
1310
- return /* @__PURE__ */ jsx14(Spinner, {});
1793
+ return /* @__PURE__ */ jsx17(Spinner, {});
1311
1794
  case "pending_approval":
1312
- return /* @__PURE__ */ jsx14(Text13, { children: theme.attention(theme.symbols.attention) });
1795
+ return /* @__PURE__ */ jsx17(Text15, { children: theme.attention(theme.symbols.attention) });
1313
1796
  case "completed":
1314
- return /* @__PURE__ */ jsx14(Text13, { children: theme.success(theme.symbols.success) });
1797
+ return /* @__PURE__ */ jsx17(Text15, { children: theme.success(theme.symbols.success) });
1315
1798
  case "failed":
1316
- return /* @__PURE__ */ jsx14(Text13, { children: theme.error(theme.symbols.error) });
1799
+ return /* @__PURE__ */ jsx17(Text15, { children: theme.error(theme.symbols.error) });
1317
1800
  default:
1318
- return /* @__PURE__ */ jsx14(Text13, { children: theme.dim(theme.symbols.error) });
1801
+ return /* @__PURE__ */ jsx17(Text15, { children: theme.dim(theme.symbols.error) });
1319
1802
  }
1320
1803
  }
1321
1804
  function titleText(call) {
@@ -1330,17 +1813,17 @@ function formatToolDuration(ms) {
1330
1813
  if (ms === void 0) return null;
1331
1814
  return ms < 100 ? "<0.1s" : formatElapsed(ms);
1332
1815
  }
1333
- var RunningTime = memo12(function RunningTime2({ since }) {
1816
+ var RunningTime = memo14(function RunningTime2({ since }) {
1334
1817
  useClockTick(true);
1335
1818
  const elapsed = Date.now() - since;
1336
- return elapsed >= 1e3 ? /* @__PURE__ */ jsx14(Text13, { children: theme.dim(formatElapsed(elapsed)) }) : null;
1819
+ return elapsed >= 1e3 ? /* @__PURE__ */ jsx17(Text15, { children: theme.dim(formatElapsed(elapsed)) }) : null;
1337
1820
  });
1338
- var ToolCallRow = memo12(function ToolCallRow2({ call, width, expanded = false }) {
1821
+ var ToolCallRow = memo14(function ToolCallRow2({ call, width, expanded = false }) {
1339
1822
  const card = cardOf(call);
1340
1823
  if (card && !(call.stage === "pending_approval" && !call.decision)) {
1341
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", width, children: [
1342
- /* @__PURE__ */ jsx14(ArtifactRow, { artifact: card, draft: call.draft, width }),
1343
- expanded ? /* @__PURE__ */ jsx14(ToolCallDetail, { call, width }) : null
1824
+ return /* @__PURE__ */ jsxs13(Box15, { flexDirection: "column", width, children: [
1825
+ /* @__PURE__ */ jsx17(ArtifactRow, { artifact: card, draft: call.draft, width }),
1826
+ expanded ? /* @__PURE__ */ jsx17(ToolCallDetail, { call, width }) : null
1344
1827
  ] });
1345
1828
  }
1346
1829
  const waiting = call.stage === "pending_approval" && !call.decision && !call.left;
@@ -1350,23 +1833,23 @@ var ToolCallRow = memo12(function ToolCallRow2({ call, width, expanded = false }
1350
1833
  const running = call.stage === "running" && call.endedAt === void 0;
1351
1834
  const rightWidth = displayWidth(summary) + 2 + (duration ? displayWidth(duration) : 6);
1352
1835
  const args = summarizeArgs(call.args, Math.max(8, width - displayWidth(titleText(call)) - rightWidth - 7));
1353
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", width, children: [
1354
- /* @__PURE__ */ jsxs12(Box13, { width, children: [
1836
+ return /* @__PURE__ */ jsxs13(Box15, { flexDirection: "column", width, children: [
1837
+ /* @__PURE__ */ jsxs13(Box15, { width, children: [
1355
1838
  glyph(call),
1356
- /* @__PURE__ */ jsx14(Box13, { flexGrow: 1, flexShrink: 1, marginLeft: 1, children: /* @__PURE__ */ jsxs12(Text13, { wrap: "truncate-end", children: [
1839
+ /* @__PURE__ */ jsx17(Box15, { flexGrow: 1, flexShrink: 1, marginLeft: 1, children: /* @__PURE__ */ jsxs13(Text15, { wrap: "truncate-end", children: [
1357
1840
  titleText(call),
1358
1841
  args ? theme.dim(` ${theme.symbols.middot} ${args}`) : ""
1359
1842
  ] }) }),
1360
- /* @__PURE__ */ jsxs12(Box13, { flexShrink: 0, marginLeft: 1, children: [
1361
- summary ? /* @__PURE__ */ jsxs12(Text13, { children: [
1843
+ /* @__PURE__ */ jsxs13(Box15, { flexShrink: 0, marginLeft: 1, children: [
1844
+ summary ? /* @__PURE__ */ jsxs13(Text15, { children: [
1362
1845
  summary,
1363
1846
  " "
1364
1847
  ] }) : null,
1365
- duration ? /* @__PURE__ */ jsx14(Text13, { children: duration }) : running ? /* @__PURE__ */ jsx14(RunningTime, { since: call.startedAt }) : null
1848
+ duration ? /* @__PURE__ */ jsx17(Text15, { children: duration }) : running ? /* @__PURE__ */ jsx17(RunningTime, { since: call.startedAt }) : null
1366
1849
  ] })
1367
1850
  ] }),
1368
- call.denial ? /* @__PURE__ */ jsx14(Text13, { wrap: "truncate-end", children: theme.dim(` ${theme.symbols.arrowRight} ${call.denial.message}${call.denial.reason ? ` (${call.denial.reason})` : ""}`) }) : null,
1369
- expanded ? /* @__PURE__ */ jsx14(ToolCallDetail, { call, width }) : null
1851
+ call.denial ? /* @__PURE__ */ jsx17(Text15, { wrap: "truncate-end", children: theme.dim(` ${theme.symbols.arrowRight} ${call.denial.message}${call.denial.reason ? ` (${call.denial.reason})` : ""}`) }) : null,
1852
+ expanded ? /* @__PURE__ */ jsx17(ToolCallDetail, { call, width }) : null
1370
1853
  ] });
1371
1854
  });
1372
1855
  function cardOf(call) {
@@ -1381,43 +1864,44 @@ function sourcesSummary(call) {
1381
1864
  return n ? `${n} ${n === 1 ? "source" : "sources"}` : "no sources";
1382
1865
  }
1383
1866
  function researchLines(call) {
1384
- const result = isRecord2(call.result) ? call.result : {};
1867
+ const result = isRecord3(call.result) ? call.result : {};
1385
1868
  const queries = Array.isArray(result.queries) ? result.queries.filter((q) => typeof q === "string") : [];
1386
1869
  const lines = queries.map((q) => `query ${q}`);
1387
1870
  (call.sources ?? []).forEach((source, i) => lines.push(`${i + 1} ${source.title} ${theme.dim(source.url)}`));
1388
1871
  return lines;
1389
1872
  }
1390
- var ToolCallDetail = memo12(function ToolCallDetail2({ call, width }) {
1873
+ var ToolCallDetail = memo14(function ToolCallDetail2({ call, width }) {
1391
1874
  if (call.research && call.result !== void 0 && !call.denial) {
1392
1875
  const { lines, hidden } = clipLines(researchLines(call).join("\n"), 16);
1393
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", marginLeft: 2, width: Math.max(10, width - 2), children: [
1394
- lines.map((line, i) => /* @__PURE__ */ jsxs12(Text13, { wrap: "truncate-end", children: [
1876
+ return /* @__PURE__ */ jsxs13(Box15, { flexDirection: "column", marginLeft: 2, width: Math.max(10, width - 2), children: [
1877
+ lines.map((line, i) => /* @__PURE__ */ jsxs13(Text15, { wrap: "truncate-end", children: [
1395
1878
  theme.dim(theme.symbols.gutter),
1396
1879
  " ",
1397
1880
  line
1398
1881
  ] }, i)),
1399
- hidden > 0 ? /* @__PURE__ */ jsx14(Text13, { children: theme.dim(` +${hidden} lines`) }) : null
1882
+ hidden > 0 ? /* @__PURE__ */ jsx17(Text15, { children: theme.dim(` +${hidden} lines`) }) : null
1400
1883
  ] });
1401
1884
  }
1402
1885
  const blocks = [["args", call.args]];
1403
1886
  if (call.result !== void 0) blocks.push(["result", call.result]);
1404
- return /* @__PURE__ */ jsx14(Box13, { flexDirection: "column", marginLeft: 2, width: Math.max(10, width - 2), children: blocks.map(([label, value]) => {
1887
+ return /* @__PURE__ */ jsx17(Box15, { flexDirection: "column", marginLeft: 2, width: Math.max(10, width - 2), children: blocks.map(([label, value]) => {
1405
1888
  const { lines, hidden } = clipLines(safeJson(value), 16);
1406
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
1407
- /* @__PURE__ */ jsx14(Text13, { children: theme.dim(label) }),
1408
- lines.map((line, i) => /* @__PURE__ */ jsxs12(Text13, { wrap: "truncate-end", children: [
1889
+ return /* @__PURE__ */ jsxs13(Box15, { flexDirection: "column", children: [
1890
+ /* @__PURE__ */ jsx17(Text15, { children: theme.dim(label) }),
1891
+ lines.map((line, i) => /* @__PURE__ */ jsxs13(Text15, { wrap: "truncate-end", children: [
1409
1892
  theme.dim(theme.symbols.gutter),
1410
1893
  " ",
1411
1894
  line
1412
1895
  ] }, i)),
1413
- hidden > 0 ? /* @__PURE__ */ jsx14(Text13, { children: theme.dim(` +${hidden} lines`) }) : null
1896
+ hidden > 0 ? /* @__PURE__ */ jsx17(Text15, { children: theme.dim(` +${hidden} lines`) }) : null
1414
1897
  ] }, label);
1415
1898
  }) });
1416
1899
  });
1417
1900
 
1418
1901
  // src/ui/screens/chat/Transcript.tsx
1419
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1902
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1420
1903
  var ASSISTANT_INDENT = 2;
1904
+ var CARD_INDENT = ASSISTANT_INDENT + 2;
1421
1905
  var USER_MAX_LINES = 6;
1422
1906
  var WAITING_AFTER_MS = 1e4;
1423
1907
  function noticeGlyph(tone) {
@@ -1426,65 +1910,69 @@ function noticeGlyph(tone) {
1426
1910
  if (tone === "attention") return theme.attention(theme.symbols.attention);
1427
1911
  return theme.dim(theme.symbols.dot);
1428
1912
  }
1429
- function UserTurn({ text, width }) {
1430
- const lines = text.split("\n");
1913
+ function UserTurn({ text: text2, width }) {
1914
+ const lines = text2.split("\n");
1431
1915
  const shown = lines.slice(0, USER_MAX_LINES);
1432
- return /* @__PURE__ */ jsx15(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Rail, { tone: "accent", width, children: [
1433
- shown.map((line, i) => /* @__PURE__ */ jsx15(Text14, { wrap: "wrap", children: line || " " }, i)),
1434
- lines.length > USER_MAX_LINES ? /* @__PURE__ */ jsx15(Text14, { children: theme.dim(`+${lines.length - USER_MAX_LINES} lines`) }) : null
1916
+ return /* @__PURE__ */ jsx18(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Rail, { tone: "accent", width, children: [
1917
+ shown.map((line, i) => /* @__PURE__ */ jsx18(Text16, { wrap: "wrap", children: line || " " }, i)),
1918
+ lines.length > USER_MAX_LINES ? /* @__PURE__ */ jsx18(Text16, { children: theme.dim(`+${lines.length - USER_MAX_LINES} lines`) }) : null
1435
1919
  ] }) });
1436
1920
  }
1437
- var EntryView = memo13(function EntryView2({ entry, width }) {
1921
+ var EntryView = memo15(function EntryView2({ entry, width }) {
1438
1922
  const inner = Math.max(10, width - ASSISTANT_INDENT);
1439
1923
  switch (entry.kind) {
1440
1924
  case "header": {
1441
1925
  const fill = Math.max(1, width - displayWidth(entry.text) - displayWidth(entry.right));
1442
- return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", width, children: [
1443
- /* @__PURE__ */ jsxs13(Text14, { wrap: "truncate-end", children: [
1926
+ return /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", width, children: [
1927
+ /* @__PURE__ */ jsxs14(Text16, { wrap: "truncate-end", children: [
1444
1928
  entry.text,
1445
1929
  " ".repeat(fill),
1446
1930
  theme.dim(entry.right)
1447
1931
  ] }),
1448
- /* @__PURE__ */ jsx15(Text14, { children: theme.dim(theme.symbols.line.repeat(width)) })
1932
+ /* @__PURE__ */ jsx18(Text16, { children: theme.dim(theme.symbols.line.repeat(width)) })
1449
1933
  ] });
1450
1934
  }
1451
1935
  case "user":
1452
- return /* @__PURE__ */ jsx15(UserTurn, { text: entry.text, width });
1936
+ return /* @__PURE__ */ jsx18(UserTurn, { text: entry.text, width });
1453
1937
  case "assistant":
1454
- return /* @__PURE__ */ jsx15(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text14, { children: [
1938
+ return /* @__PURE__ */ jsx18(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text16, { children: [
1455
1939
  theme.accent(theme.symbols.dot),
1456
1940
  " ",
1457
1941
  theme.bold("Tabbio")
1458
1942
  ] }) });
1459
1943
  case "markdown":
1460
- return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, marginTop: entry.gap ? 1 : 0, width, children: /* @__PURE__ */ jsx15(Markdown, { text: entry.text, width: inner }) });
1944
+ return /* @__PURE__ */ jsx18(Box16, { paddingLeft: ASSISTANT_INDENT, marginTop: entry.gap ? 1 : 0, width, children: /* @__PURE__ */ jsx18(Markdown, { text: entry.text, width: inner }) });
1461
1945
  case "tool":
1462
- return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, marginTop: entry.gap ? 1 : 0, width, children: /* @__PURE__ */ jsx15(ToolCallRow, { call: entry.call, width: inner }) });
1946
+ return /* @__PURE__ */ jsx18(Box16, { paddingLeft: ASSISTANT_INDENT, marginTop: entry.gap ? 1 : 0, width, children: /* @__PURE__ */ jsx18(ToolCallRow, { call: entry.call, width: inner }) });
1463
1947
  case "tool-detail":
1464
- return /* @__PURE__ */ jsxs13(Box14, { paddingLeft: ASSISTANT_INDENT, flexDirection: "column", width, children: [
1465
- /* @__PURE__ */ jsx15(Text14, { children: theme.dim(`${theme.symbols.expanded} ${entry.call.title}`) }),
1466
- /* @__PURE__ */ jsx15(ToolCallDetail, { call: entry.call, width: inner })
1948
+ return /* @__PURE__ */ jsxs14(Box16, { paddingLeft: ASSISTANT_INDENT, flexDirection: "column", width, children: [
1949
+ /* @__PURE__ */ jsx18(Text16, { children: theme.dim(`${theme.symbols.expanded} ${entry.call.title}`) }),
1950
+ /* @__PURE__ */ jsx18(ToolCallDetail, { call: entry.call, width: inner })
1467
1951
  ] });
1468
1952
  case "artifact":
1469
- return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, width, children: /* @__PURE__ */ jsx15(ArtifactRow, { artifact: entry.artifact, width: inner }) });
1953
+ return /* @__PURE__ */ jsx18(Box16, { paddingLeft: ASSISTANT_INDENT, width, children: /* @__PURE__ */ jsx18(ArtifactRow, { artifact: entry.artifact, width: inner }) });
1954
+ case "card":
1955
+ return /* @__PURE__ */ jsx18(Box16, { paddingLeft: CARD_INDENT, width, children: /* @__PURE__ */ jsx18(CardView, { card: entry.card, width: Math.max(10, width - CARD_INDENT) }) });
1956
+ case "saved":
1957
+ return /* @__PURE__ */ jsx18(Box16, { paddingLeft: CARD_INDENT, width, children: /* @__PURE__ */ jsx18(SavedFilesView, { saved: entry.saved, width: Math.max(10, width - CARD_INDENT) }) });
1470
1958
  case "sources":
1471
- return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, marginTop: 1, width, children: /* @__PURE__ */ jsx15(SourcesList, { sources: entry.sources, width: inner }) });
1959
+ return /* @__PURE__ */ jsx18(Box16, { paddingLeft: ASSISTANT_INDENT, marginTop: 1, width, children: /* @__PURE__ */ jsx18(SourcesList, { sources: entry.sources, width: inner }) });
1472
1960
  case "notice":
1473
- return /* @__PURE__ */ jsxs13(Box14, { paddingLeft: ASSISTANT_INDENT, marginTop: 1, flexDirection: "column", width, children: [
1474
- /* @__PURE__ */ jsxs13(Text14, { wrap: "wrap", children: [
1961
+ return /* @__PURE__ */ jsxs14(Box16, { paddingLeft: ASSISTANT_INDENT, marginTop: 1, flexDirection: "column", width, children: [
1962
+ /* @__PURE__ */ jsxs14(Text16, { wrap: "wrap", children: [
1475
1963
  noticeGlyph(entry.tone),
1476
1964
  " ",
1477
1965
  entry.tone === "error" ? theme.error(entry.text) : entry.tone === "info" ? theme.dim(entry.text) : entry.text
1478
1966
  ] }),
1479
- entry.hint ? /* @__PURE__ */ jsx15(Text14, { wrap: "wrap", children: theme.dim(` ${theme.symbols.arrowRight} ${entry.hint}`) }) : null
1967
+ entry.hint ? /* @__PURE__ */ jsx18(Text16, { wrap: "wrap", children: theme.dim(` ${theme.symbols.arrowRight} ${entry.hint}`) }) : null
1480
1968
  ] });
1481
1969
  default:
1482
1970
  return null;
1483
1971
  }
1484
1972
  });
1485
- function clipLiveText(text, width, maxLines) {
1486
- if (estimateHeight(text, width) <= maxLines) return { text, hidden: 0 };
1487
- const lines = text.split("\n");
1973
+ function clipLiveText(text2, width, maxLines) {
1974
+ if (estimateHeight(text2, width) <= maxLines) return { text: text2, hidden: 0 };
1975
+ const lines = text2.split("\n");
1488
1976
  const kept = [];
1489
1977
  let used = 1;
1490
1978
  for (let i = lines.length - 1; i >= 0; i -= 1) {
@@ -1501,23 +1989,24 @@ function WaitingLine({ turn }) {
1501
1989
  const empty = turn.items.length === 0;
1502
1990
  if (!empty && idleFor < WAITING_AFTER_MS) return null;
1503
1991
  const label = empty && idleFor < WAITING_AFTER_MS ? "Thinking\u2026" : "waiting for Tabbio\u2026";
1504
- return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, children: /* @__PURE__ */ jsx15(Spinner, { label: theme.dim(`${label} ${formatElapsed(Date.now() - (empty ? turn.startedAt : turn.lastEventAt))}`) }) });
1992
+ return /* @__PURE__ */ jsx18(Box16, { paddingLeft: ASSISTANT_INDENT, children: /* @__PURE__ */ jsx18(Spinner, { label: theme.dim(`${label} ${formatElapsed(Date.now() - (empty ? turn.startedAt : turn.lastEventAt))}`) }) });
1505
1993
  }
1506
1994
  function liveGap(prev, item) {
1507
1995
  if (item.kind === "tool") return prev === "markdown" || prev === "text";
1508
1996
  return prev === "tool" || prev === "markdown" || item.committed > 0;
1509
1997
  }
1510
- var LiveItemView = memo13(function LiveItemView2(props) {
1511
- const { item, width, expanded, cursor, maxLines, gap } = props;
1998
+ var LiveItemView = memo15(function LiveItemView2(props) {
1999
+ const { item, width, expanded, cursor, maxLines, gap, echoAt } = props;
1512
2000
  if (item.kind === "tool") {
1513
- return /* @__PURE__ */ jsx15(Box14, { marginTop: gap ? 1 : 0, children: /* @__PURE__ */ jsx15(ToolCallRow, { call: item.call, width, expanded }) });
2001
+ return /* @__PURE__ */ jsx18(Box16, { marginTop: gap ? 1 : 0, children: /* @__PURE__ */ jsx18(ToolCallRow, { call: item.call, width, expanded }) });
1514
2002
  }
1515
- const pending = item.text.slice(item.committed);
2003
+ const end = echoAt !== void 0 && item.start !== void 0 ? Math.max(item.committed, echoAt - item.start) : item.text.length;
2004
+ const pending = item.text.slice(item.committed, end);
1516
2005
  if (!pending && !cursor) return null;
1517
2006
  const clipped = clipLiveText(pending, width, maxLines);
1518
- return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", width, marginTop: gap ? 1 : 0, children: [
1519
- clipped.hidden > 0 ? /* @__PURE__ */ jsx15(Text14, { children: theme.dim(`${theme.symbols.ellipsis} ${clipped.hidden} more lines above (streaming)`) }) : null,
1520
- /* @__PURE__ */ jsx15(Markdown, { text: clipped.text, width, trailing: cursor ? theme.accent(theme.symbols.cursor) : void 0 })
2007
+ return /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", width, marginTop: gap ? 1 : 0, children: [
2008
+ clipped.hidden > 0 ? /* @__PURE__ */ jsx18(Text16, { children: theme.dim(`${theme.symbols.ellipsis} ${clipped.hidden} more lines above (streaming)`) }) : null,
2009
+ /* @__PURE__ */ jsx18(Markdown, { text: clipped.text, width, trailing: cursor ? theme.accent(theme.symbols.cursor) : void 0 })
1521
2010
  ] });
1522
2011
  });
1523
2012
  function LiveTurn(props) {
@@ -1526,65 +2015,393 @@ function LiveTurn(props) {
1526
2015
  const last = turn.items[turn.items.length - 1];
1527
2016
  const tools = turn.items.filter((i) => i.kind === "tool").length;
1528
2017
  const textBudget = Math.max(3, maxLines - tools - 1);
1529
- return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", width, children: [
1530
- /* @__PURE__ */ jsx15(Box14, { flexDirection: "column", paddingLeft: ASSISTANT_INDENT, width, children: turn.items.map((item, i) => {
1531
- const before = turn.items[i - 1];
1532
- const prev = before ? before.kind === "tool" ? "tool" : "text" : turn.lastKind;
1533
- return /* @__PURE__ */ jsx15(
1534
- LiveItemView,
1535
- {
1536
- item,
1537
- width: inner,
1538
- expanded: expandTools,
1539
- cursor: streaming && item === last && item.kind === "text",
1540
- maxLines: textBudget,
1541
- gap: liveGap(prev, item)
1542
- },
1543
- item.id
1544
- );
1545
- }) }),
1546
- streaming ? /* @__PURE__ */ jsx15(WaitingLine, { turn }) : null
2018
+ return /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", width, children: [
2019
+ /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", paddingLeft: ASSISTANT_INDENT, width, children: [
2020
+ expandTools ? (turn.hidden ?? []).map((call) => /* @__PURE__ */ jsx18(ToolCallRow, { call, width: inner, expanded: true }, `hidden-${call.id}`)) : null,
2021
+ turn.items.map((item, i) => {
2022
+ const before = turn.items[i - 1];
2023
+ const prev = before ? before.kind === "tool" ? "tool" : "text" : turn.lastKind;
2024
+ return /* @__PURE__ */ jsx18(
2025
+ LiveItemView,
2026
+ {
2027
+ item,
2028
+ width: inner,
2029
+ expanded: expandTools,
2030
+ cursor: streaming && item === last && item.kind === "text",
2031
+ maxLines: textBudget,
2032
+ gap: liveGap(prev, item),
2033
+ echoAt: turn.echoAt
2034
+ },
2035
+ item.id
2036
+ );
2037
+ })
2038
+ ] }),
2039
+ streaming ? /* @__PURE__ */ jsx18(WaitingLine, { turn }) : null
1547
2040
  ] });
1548
2041
  }
1549
2042
 
1550
2043
  // src/ui/screens/chat/useChatCommands.ts
1551
- import { useCallback, useRef } from "react";
1552
- function useChatCommands(deps) {
1553
- const { session, services, companyId, hostedByHome, onExit, openThreads } = deps;
1554
- const { state } = session;
1555
- const availability = useRef(null);
1556
- const warnIfUnavailable = useCallback(
1557
- (capabilities) => {
1558
- if (!services.loadCapabilities) return;
1559
- availability.current ??= services.loadCapabilities().catch(() => null);
1560
- void availability.current.then((answer) => {
1561
- for (const text of unavailableNotices(capabilities, answer)) session.dispatch({ type: "notice", tone: "info", text });
1562
- });
2044
+ import { useCallback as useCallback3, useRef as useRef4 } from "react";
2045
+
2046
+ // src/ui/screens/chat/useFileCommands.ts
2047
+ import { homedir as homedir2 } from "node:os";
2048
+ import { extname, resolve as resolvePath } from "node:path";
2049
+ import { useCallback as useCallback2, useRef as useRef3 } from "react";
2050
+
2051
+ // src/ui/screens/chat/apply-command.ts
2052
+ var CARD_TYPE = "application-preview";
2053
+ function notFound(result) {
2054
+ return !result.ok && (result.error.exitCode === ExitCode.NotFound || /not found/i.test(result.error.message));
2055
+ }
2056
+ function applyNotes(result, tool, appUrl) {
2057
+ const record = typeof result === "object" && result !== null ? result : {};
2058
+ if (record.redirectRequired !== true) return [];
2059
+ const card = cardFromToolResult({ id: "apply", name: tool, args: {}, result, cardType: CARD_TYPE }, { appUrl });
2060
+ const link = card?.links[0];
2061
+ return link ? [{ tone: "info", text: `/open ${link.n} opens the employer's application page` }] : [];
2062
+ }
2063
+ function prepareApplicationTool(opts) {
2064
+ const { services, job, source } = opts;
2065
+ const tool = source === "jobs" ? "applicationPrepare" : "catalogApplyPrepare";
2066
+ const appUrl = services.appUrl ?? PRODUCTION_APP_URL;
2067
+ return {
2068
+ text: `/apply ${opts.n}`,
2069
+ name: tool,
2070
+ title: "Prepare application",
2071
+ cardType: CARD_TYPE,
2072
+ args: { job: job.title, ...source === "jobs" ? { jobId: job.id } : { catalogJobId: job.id } },
2073
+ async run(signal) {
2074
+ if (source === "jobs") return services.callTool("application.prepare", { jobId: job.id }, { signal });
2075
+ const first = await services.callTool("catalog.applyPrepare", { catalogJobId: job.id }, { signal });
2076
+ if (source === void 0 && notFound(first)) return services.callTool("application.prepare", { jobId: job.id }, { signal });
2077
+ return first;
1563
2078
  },
1564
- [services, session]
1565
- );
1566
- const setCapability = useCallback(
1567
- (key, arg) => {
1568
- if (!arg) {
1569
- session.dispatch({ type: "notice", tone: "info", text: `${key} ${state.capabilities[key]} ${theme.symbols.middot} /${key} <mode> to change it` });
1570
- return;
1571
- }
1572
- let value;
1573
- try {
1574
- value = parseCapabilityMode(key, arg);
1575
- } catch (error) {
1576
- session.dispatch({ type: "notice", tone: "error", text: toCliError(error).message.replace(/^--/, "/") });
1577
- return;
1578
- }
1579
- const next = { ...state.capabilities, [key]: value };
1580
- session.dispatch({ type: "set-capability", key, value });
1581
- let saved = false;
1582
- try {
1583
- services.saveCapabilityPrefs?.(next);
1584
- saved = Boolean(services.saveCapabilityPrefs);
1585
- } catch {
1586
- saved = false;
1587
- }
2079
+ after: (result) => applyNotes(result, tool, appUrl)
2080
+ };
2081
+ }
2082
+
2083
+ // src/ui/screens/chat/card-commands.ts
2084
+ import { homedir } from "node:os";
2085
+ function placedCards(state) {
2086
+ const toolNames = /* @__PURE__ */ new Map();
2087
+ const placed = [];
2088
+ state.entries.forEach((entry, at) => {
2089
+ if (entry.kind === "tool") toolNames.set(entry.call.id, entry.call.name);
2090
+ if (entry.kind === "card") placed.push({ card: entry.card, tool: toolNames.get(entry.toolCallId), at });
2091
+ });
2092
+ (state.turn?.items ?? []).forEach((item, i) => {
2093
+ if (item.kind === "tool" && item.call.card) placed.push({ card: item.call.card, tool: item.call.name, at: state.entries.length + i });
2094
+ });
2095
+ return placed;
2096
+ }
2097
+ function chatCards(state) {
2098
+ return placedCards(state).map((p) => p.card);
2099
+ }
2100
+ function applySource(state) {
2101
+ const withJobs = placedCards(state).filter((p) => p.card.rows.some((row) => row.job));
2102
+ const tool = withJobs[withJobs.length - 1]?.tool;
2103
+ if (!tool) return void 0;
2104
+ if (tool.startsWith("catalog")) return "catalog";
2105
+ if (/^(job|application)[A-Z]/.test(tool)) return "jobs";
2106
+ return void 0;
2107
+ }
2108
+ function recentFiles(state, touched = []) {
2109
+ const seen = [];
2110
+ const add = (id, at) => {
2111
+ if (id) seen.push({ id, at });
2112
+ };
2113
+ state.entries.forEach((entry, at) => {
2114
+ if (entry.kind === "tool") {
2115
+ if (entry.call.artifact?.status === "ready") add(entry.call.artifact.id, at);
2116
+ if (entry.call.saved?.files.length) add(entry.call.saved.artifactId, at);
2117
+ } else if (entry.kind === "artifact" && entry.artifact.status === "ready") add(entry.artifact.id, at);
2118
+ else if (entry.kind === "saved" && entry.saved.files.length) add(entry.saved.artifactId, at);
2119
+ });
2120
+ (state.turn?.items ?? []).forEach((item, i) => {
2121
+ if (item.kind === "tool" && item.call.artifact?.status === "ready") add(item.call.artifact.id, state.entries.length + i);
2122
+ });
2123
+ for (const t of touched) add(t.id, t.at);
2124
+ const newest = /* @__PURE__ */ new Map();
2125
+ for (const { id, at } of seen) newest.set(id, Math.max(at, newest.get(id) ?? -1));
2126
+ return [...newest.entries()].map(([id, at]) => ({ id, at })).sort((a, b) => b.at - a.at);
2127
+ }
2128
+ function resolveFileRef(arg, files) {
2129
+ const value = arg.trim();
2130
+ if (!/^\d+$/.test(value)) return { kind: "id", id: value };
2131
+ const n = Number(value);
2132
+ const file = n >= 1 ? files[n - 1] : void 0;
2133
+ if (file) return { kind: "id", id: file.id };
2134
+ return {
2135
+ kind: "error",
2136
+ message: files.length ? `There is no file ${n}: this chat has ${files.length} file${files.length === 1 ? "" : "s"} (${range(files.length)}, 1 is the newest)` : `There is no file ${n}: no file in this chat yet (use the file id)`
2137
+ };
2138
+ }
2139
+ function range(count) {
2140
+ return count === 1 ? "1" : `1 to ${count}`;
2141
+ }
2142
+ var ARTIFACT_ID = /^[A-Za-z][\w-]{5,}$/;
2143
+ function resolveOpenTarget(arg, placed, files = []) {
2144
+ const linkCards = placed.filter((p) => p.card.links.length > 0);
2145
+ const newestLinks = linkCards[linkCards.length - 1];
2146
+ const plain = placed.map((p) => p.card);
2147
+ const count = latestCardLinks(plain).length;
2148
+ const value = arg.trim();
2149
+ const filesFirst = files.length > 0 && (!newestLinks || files[0].at > newestLinks.at);
2150
+ if (!value) {
2151
+ return {
2152
+ kind: "error",
2153
+ message: filesFirst ? `Use /open <n> (${range(files.length)}, 1 is the newest file) or /open <file id>` : count ? `Use /open <n> (${range(count)}) or /open <file id>` : "Use /open <n> after a result with links, or /open <file id>"
2154
+ };
2155
+ }
2156
+ if (/^\d+$/.test(value)) {
2157
+ if (filesFirst) {
2158
+ const ref = resolveFileRef(value, files);
2159
+ return ref.kind === "id" ? { kind: "artifact", id: ref.id } : ref;
2160
+ }
2161
+ const n = Number(value);
2162
+ const link = findCardLink(plain, n);
2163
+ if (link) return { kind: "link", link };
2164
+ return {
2165
+ kind: "error",
2166
+ message: count ? `There is no link ${n}: the last result has ${count} link${count === 1 ? "" : "s"} (${range(count)})` : `There is no link ${n}: no result with links yet`
2167
+ };
2168
+ }
2169
+ if (ARTIFACT_ID.test(value)) return { kind: "artifact", id: value };
2170
+ return { kind: "error", message: `Not a link number or a file id: ${value}` };
2171
+ }
2172
+ function jobCount(cards) {
2173
+ for (let i = cards.length - 1; i >= 0; i -= 1) {
2174
+ const rows = cards[i].rows.filter((row) => row.job);
2175
+ if (rows.length) return rows.length;
2176
+ }
2177
+ return 0;
2178
+ }
2179
+ function resolveApply(arg, cards) {
2180
+ const count = jobCount(cards);
2181
+ const value = arg.trim();
2182
+ if (!/^\d+$/.test(value)) {
2183
+ return { kind: "error", message: count ? `Use /apply <n> (${range(count)})` : "Use /apply <n> after a job search result" };
2184
+ }
2185
+ const n = Number(value);
2186
+ const job = findCardJob(cards, n);
2187
+ if (!job) {
2188
+ return {
2189
+ kind: "error",
2190
+ message: count ? `There is no job ${n}: the last result lists ${count} job${count === 1 ? "" : "s"} (${range(count)})` : `There is no job ${n}: no job result yet`
2191
+ };
2192
+ }
2193
+ return { kind: "apply", n, job };
2194
+ }
2195
+ function savedLine(files, cwd) {
2196
+ return `Saved ${files.map((f) => `${displayPath(f.path, cwd)} (${formatBytes(f.bytes)})`).join(", ")}`;
2197
+ }
2198
+ function previewBlock(path, preview, gutter, cwd) {
2199
+ const shown = displayPath(path, cwd);
2200
+ const body = preview.lines.map((line) => `${gutter} ${line.replace(/\t/g, " ")}`);
2201
+ const more = preview.total > preview.lines.length ? [`+${preview.total - preview.lines.length} more lines in ${shown}`] : [];
2202
+ return [`${shown} (${preview.total} line${preview.total === 1 ? "" : "s"})`, ...body, ...more].join("\n");
2203
+ }
2204
+ var FORMATS = CV_EXPORT_FORMATS;
2205
+ function parseDownloadArgs(arg) {
2206
+ const words = arg.trim().split(/\s+/).filter(Boolean);
2207
+ if (words.length > 2) return { error: `Use /download [cvId] [${FORMATS.join("|")}]` };
2208
+ const [first, second] = words;
2209
+ const firstIsFormat = first !== void 0 && second === void 0 && FORMATS.includes(first.toLowerCase());
2210
+ const cvId = firstIsFormat ? null : first ?? null;
2211
+ const format = (firstIsFormat ? first : second ?? "pdf").toLowerCase();
2212
+ if (!FORMATS.includes(format)) return { error: `Unknown format ${second ?? first}: use ${FORMATS.join(", ")}` };
2213
+ return { cvId, format };
2214
+ }
2215
+ function parseSaveArgs(arg) {
2216
+ const match = /^(\S+)(?:\s+(.+))?$/.exec(arg.trim());
2217
+ if (!match) return { error: "Use /save <file id> [folder]" };
2218
+ const dir = match[2]?.trim().replace(/^["']|["']$/g, "").replace(/^~(?=$|[\\/])/, homedir());
2219
+ return { id: match[1], ...dir ? { dir } : {} };
2220
+ }
2221
+
2222
+ // src/ui/screens/chat/useFileCommands.ts
2223
+ var SHOW_MAX_LINES = 200;
2224
+ function looksLikePath(value) {
2225
+ return /[\\/]/.test(value) || value.startsWith(".") || value.startsWith("~") || isTextFile(value) || Boolean(extname(value) && /\.\w{2,5}$/.test(value));
2226
+ }
2227
+ function useFileCommands(session, services) {
2228
+ const { state, dispatch } = session;
2229
+ const touched = useRef3([]);
2230
+ const touch = useCallback2((id) => touched.current.push({ id, at: state.entries.length }), [state.entries.length]);
2231
+ const note = useCallback2(
2232
+ (tone, text2, hint) => dispatch({ type: "notice", tone, text: text2, ...hint ? { hint } : {} }),
2233
+ [dispatch]
2234
+ );
2235
+ const fail = useCallback2(
2236
+ (prefix) => (error) => {
2237
+ const cli = toCliError(error);
2238
+ note("error", `${prefix}: ${cli.message}`, cli.hint);
2239
+ },
2240
+ [note]
2241
+ );
2242
+ const openLink = useCallback2(
2243
+ async (url, label) => {
2244
+ const opened = services.openUrl ? await services.openUrl(url).catch(() => false) : false;
2245
+ note(opened ? "success" : "info", opened ? `Opened ${label} in the browser` : `Open ${label}:`, services.hyperlinks ? hyperlink(url, url) : url);
2246
+ },
2247
+ [services, note]
2248
+ );
2249
+ const openArtifact = useCallback2(
2250
+ async (id) => {
2251
+ if (!services.getArtifact || !services.artifactPreviewUrl) {
2252
+ note("error", "Opening files is not available here", `Run \`tabbio artifacts open ${id}\`.`);
2253
+ return;
2254
+ }
2255
+ const artifact = await services.getArtifact(id);
2256
+ if (artifact.status === "failed") {
2257
+ note("error", `${artifact.title} failed: ${artifact.error?.message ?? "unknown error"}`);
2258
+ return;
2259
+ }
2260
+ if (artifact.status !== "ready") {
2261
+ note("info", `${artifact.title} is still being created`, `Try /open ${id} again in a moment.`);
2262
+ return;
2263
+ }
2264
+ await openLink(await services.artifactPreviewUrl(id, artifact.kind), artifact.title);
2265
+ },
2266
+ [services, note, openLink]
2267
+ );
2268
+ const show = useCallback2(
2269
+ async (target) => {
2270
+ let files;
2271
+ if (looksLikePath(target)) files = [{ path: resolvePath(target.replace(/^~(?=$|[\\/])/, homedir2())), bytes: 0, kind: "file" }];
2272
+ else {
2273
+ files = services.savedFiles?.(target);
2274
+ if (!files?.length) {
2275
+ if (!services.saveArtifact) {
2276
+ note("error", "Saving files is not available here", `Run \`tabbio artifacts download ${target}\`.`);
2277
+ return;
2278
+ }
2279
+ files = await services.saveArtifact(target);
2280
+ if (files.length) note("success", savedLine(files));
2281
+ }
2282
+ }
2283
+ const text2 = files.find((f) => isTextFile(f.path));
2284
+ if (!text2) {
2285
+ note(
2286
+ "info",
2287
+ files.length ? `No text to print for ${files.map((f) => displayPath(f.path)).join(", ")}` : `Nothing saved for ${target} yet`,
2288
+ looksLikePath(target) ? void 0 : `/open ${target} opens it in the browser`
2289
+ );
2290
+ return;
2291
+ }
2292
+ if (!services.readSavedPreview) {
2293
+ note("error", "Printing files is not available here");
2294
+ return;
2295
+ }
2296
+ const preview = await services.readSavedPreview(text2.path, SHOW_MAX_LINES);
2297
+ note("info", previewBlock(text2.path, preview, theme.symbols.gutter), looksLikePath(target) ? void 0 : `/open ${target} opens it in the browser`);
2298
+ },
2299
+ [services, note]
2300
+ );
2301
+ return useCallback2(
2302
+ (name, arg) => {
2303
+ const files = recentFiles(state, touched.current);
2304
+ const fileId = (value) => {
2305
+ const ref = resolveFileRef(value, files);
2306
+ if (ref.kind === "error") note("error", ref.message);
2307
+ return ref.kind === "id" ? ref.id : null;
2308
+ };
2309
+ switch (name) {
2310
+ case "open": {
2311
+ const target = resolveOpenTarget(arg, placedCards(state), files);
2312
+ if (target.kind === "error") note("error", target.message);
2313
+ else if (target.kind === "link") void openLink(target.link.url, `${target.link.n}. ${target.link.label}`).catch(fail("Could not open the link"));
2314
+ else {
2315
+ touch(target.id);
2316
+ void openArtifact(target.id).catch(fail(`Could not open ${target.id}`));
2317
+ }
2318
+ return;
2319
+ }
2320
+ case "apply": {
2321
+ const target = resolveApply(arg, chatCards(state));
2322
+ if (target.kind === "error") return note("error", target.message);
2323
+ const started = session.runTool(prepareApplicationTool({ services, n: target.n, job: target.job, source: applySource(state) }));
2324
+ if (!started) note("attention", "Tabbio is still answering", `Wait for the reply (esc stops it), then /apply ${target.n} again.`);
2325
+ return;
2326
+ }
2327
+ case "save": {
2328
+ const parsed = parseSaveArgs(arg);
2329
+ if ("error" in parsed) return note("error", parsed.error);
2330
+ const id = fileId(parsed.id);
2331
+ if (!id) return;
2332
+ if (!services.saveArtifact) return note("error", "Saving files is not available here", `Run \`tabbio artifacts download ${id}\`.`);
2333
+ note("info", `Saving ${id}${theme.symbols.ellipsis}`);
2334
+ services.saveArtifact(id, parsed.dir ? { dir: parsed.dir } : {}).then((saved) => {
2335
+ if (!saved.length) return note("attention", `Nothing was saved for ${id}`, "It may still be rendering: try again in a moment.");
2336
+ touch(id);
2337
+ note("success", savedLine(saved), `/show ${id} prints it ${theme.symbols.middot} /open ${id} opens it in the browser`);
2338
+ }, fail(`Could not save ${id}`));
2339
+ return;
2340
+ }
2341
+ case "show": {
2342
+ const value = arg.trim();
2343
+ if (!value) return note("error", "Use /show <n> (1 = the newest file of this chat), /show <file id>, or a saved file path");
2344
+ const target = looksLikePath(value) ? value : fileId(value);
2345
+ if (!target) return;
2346
+ if (!looksLikePath(target)) touch(target);
2347
+ void show(target).catch(fail(`Could not show ${target}`));
2348
+ return;
2349
+ }
2350
+ case "download": {
2351
+ const parsed = parseDownloadArgs(arg);
2352
+ if ("error" in parsed) return note("error", parsed.error);
2353
+ const label = !parsed.cvId || /^(master|main)$/i.test(parsed.cvId) ? "your main CV" : parsed.cvId;
2354
+ if (!services.saveCvExport) return note("error", "Saving CVs is not available here");
2355
+ note("info", `Exporting ${label} as ${parsed.format.toUpperCase()}${theme.symbols.ellipsis}`);
2356
+ services.saveCvExport(parsed.cvId, { format: parsed.format }).then((file) => note("success", savedLine([file])), fail(`Could not export ${label}`));
2357
+ return;
2358
+ }
2359
+ default:
2360
+ note("error", `Unknown command /${name}`);
2361
+ }
2362
+ },
2363
+ [state, session, services, note, fail, openLink, openArtifact, show, touch]
2364
+ );
2365
+ }
2366
+
2367
+ // src/ui/screens/chat/useChatCommands.ts
2368
+ function useChatCommands(deps) {
2369
+ const { session, services, companyId, hostedByHome, onExit, openThreads, openLogin, onAccountChange } = deps;
2370
+ const { state } = session;
2371
+ const availability = useRef4(null);
2372
+ const runFileCommand = useFileCommands(session, services);
2373
+ const warnIfUnavailable = useCallback3(
2374
+ (capabilities) => {
2375
+ if (!services.loadCapabilities) return;
2376
+ availability.current ??= services.loadCapabilities().catch(() => null);
2377
+ void availability.current.then((answer) => {
2378
+ for (const text2 of unavailableNotices(capabilities, answer)) session.dispatch({ type: "notice", tone: "info", text: text2 });
2379
+ });
2380
+ },
2381
+ [services, session]
2382
+ );
2383
+ const setCapability = useCallback3(
2384
+ (key, arg) => {
2385
+ if (!arg) {
2386
+ session.dispatch({ type: "notice", tone: "info", text: `${key} ${state.capabilities[key]} ${theme.symbols.middot} /${key} <mode> to change it` });
2387
+ return;
2388
+ }
2389
+ let value;
2390
+ try {
2391
+ value = parseCapabilityMode(key, arg);
2392
+ } catch (error) {
2393
+ session.dispatch({ type: "notice", tone: "error", text: toCliError(error).message.replace(/^--/, "/") });
2394
+ return;
2395
+ }
2396
+ const next = { ...state.capabilities, [key]: value };
2397
+ session.dispatch({ type: "set-capability", key, value });
2398
+ let saved = false;
2399
+ try {
2400
+ services.saveCapabilityPrefs?.(next);
2401
+ saved = Boolean(services.saveCapabilityPrefs);
2402
+ } catch {
2403
+ saved = false;
2404
+ }
1588
2405
  const scope = state.mode === "employer" ? " (employer chats ignore it)" : "";
1589
2406
  session.dispatch({
1590
2407
  type: "notice",
@@ -1595,7 +2412,7 @@ function useChatCommands(deps) {
1595
2412
  },
1596
2413
  [session, services, state.capabilities, state.mode, warnIfUnavailable]
1597
2414
  );
1598
- return useCallback(
2415
+ return useCallback3(
1599
2416
  (name, arg) => {
1600
2417
  switch (name) {
1601
2418
  case "new":
@@ -1636,6 +2453,35 @@ function useChatCommands(deps) {
1636
2453
  case "help":
1637
2454
  session.dispatch({ type: "notice", tone: "info", text: CHAT_HELP });
1638
2455
  return;
2456
+ case "open":
2457
+ case "apply":
2458
+ case "save":
2459
+ case "show":
2460
+ case "download":
2461
+ runFileCommand(name, arg);
2462
+ return;
2463
+ case "login": {
2464
+ const start = parseLoginArgs(arg);
2465
+ if ("error" in start) session.dispatch({ type: "notice", tone: "error", text: start.error });
2466
+ else if (!services.session || !onAccountChange) {
2467
+ session.dispatch({ type: "notice", tone: "error", text: "Signing in is not available here", hint: "Run `tabbio login` in a terminal." });
2468
+ } else openLogin(start);
2469
+ return;
2470
+ }
2471
+ case "logout": {
2472
+ const controls = services.session;
2473
+ if (!controls || !onAccountChange) {
2474
+ session.dispatch({ type: "notice", tone: "error", text: "Signing out is not available here", hint: "Run `tabbio logout` in a terminal." });
2475
+ return;
2476
+ }
2477
+ session.interrupt();
2478
+ session.dispatch({ type: "notice", tone: "info", text: `Signing out${theme.symbols.ellipsis}` });
2479
+ controls.logout().then(onAccountChange, (error) => {
2480
+ const cli = toCliError(error);
2481
+ session.dispatch({ type: "notice", tone: "error", text: `Could not sign out: ${cli.message}`, hint: cli.hint });
2482
+ });
2483
+ return;
2484
+ }
1639
2485
  case "home":
1640
2486
  onExit(hostedByHome ? "home" : "exit");
1641
2487
  return;
@@ -1647,24 +2493,83 @@ function useChatCommands(deps) {
1647
2493
  session.dispatch({ type: "notice", tone: "error", text: `Unknown command /${name}`, hint: "Type / to see the commands." });
1648
2494
  }
1649
2495
  },
1650
- [session, state.mode, state.modelId, openThreads, onExit, hostedByHome, services, companyId, setCapability]
2496
+ [session, state.mode, state.modelId, openThreads, onExit, hostedByHome, services, companyId, setCapability, runFileCommand, openLogin, onAccountChange]
1651
2497
  );
1652
2498
  }
1653
2499
 
1654
2500
  // src/ui/screens/chat/useChatSession.ts
1655
- import { useCallback as useCallback2, useEffect as useEffect4, useReducer as useReducer2, useRef as useRef3, useState as useState3 } from "react";
2501
+ import { useCallback as useCallback4, useEffect as useEffect7, useReducer as useReducer3, useRef as useRef6, useState as useState4 } from "react";
2502
+
2503
+ // src/ui/screens/chat/useArtifactFollowUp.ts
2504
+ import { useEffect as useEffect6, useRef as useRef5 } from "react";
2505
+
2506
+ // src/ui/screens/chat/useAutoSave.ts
2507
+ import { useEffect as useEffect5, useMemo as useMemo4 } from "react";
2508
+ var PREVIEW_LINES = 12;
2509
+ async function previewOf(services, files) {
2510
+ const text2 = files.find((file) => isTextFile(file.path));
2511
+ if (!text2) return void 0;
2512
+ try {
2513
+ const read = services.readSavedPreview ?? readSavedPreview;
2514
+ const { lines, total } = await read(text2.path, PREVIEW_LINES);
2515
+ return { path: text2.path, lines, total };
2516
+ } catch {
2517
+ return void 0;
2518
+ }
2519
+ }
2520
+ async function save(services, target, signal) {
2521
+ if (target.type === "artifact") {
2522
+ const base2 = { artifactId: target.artifactId, title: target.title, kind: target.kind };
2523
+ try {
2524
+ if (!services.saveArtifact) return { ...base2, files: [] };
2525
+ const files = await services.saveArtifact(target.artifactId, { versionId: target.versionId, signal });
2526
+ if (signal.aborted) return { ...base2, files: [] };
2527
+ const preview = await previewOf(services, files);
2528
+ return { ...base2, files, ...preview ? { preview } : {} };
2529
+ } catch (error) {
2530
+ return { ...base2, files: [], error: toCliError(error).message };
2531
+ }
2532
+ }
2533
+ const base = { cvId: target.cvId, title: target.title, kind: "cv" };
2534
+ try {
2535
+ if (!services.saveCvExport) return { ...base, files: [] };
2536
+ const file = await services.saveCvExport(target.cvId, { format: target.format, signal });
2537
+ if (signal.aborted) return { ...base, files: [] };
2538
+ return { ...base, files: [file] };
2539
+ } catch (error) {
2540
+ return { ...base, files: [], error: toCliError(error).message };
2541
+ }
2542
+ }
2543
+ function useAutoSave(state, dispatch, services, enabled) {
2544
+ const started = useMemo4(() => /* @__PURE__ */ new Set(), [services]);
2545
+ const controller = useMemo4(() => new AbortController(), [services]);
2546
+ useEffect5(() => () => controller.abort(), [controller]);
2547
+ const targets = enabled && (services.saveArtifact || services.saveCvExport) ? saveTargets(state) : [];
2548
+ const fresh = targets.filter((target) => !started.has(target.key));
2549
+ const key = fresh.map((target) => target.key).join("|");
2550
+ useEffect5(() => {
2551
+ for (const target of fresh) {
2552
+ if (started.has(target.key)) continue;
2553
+ started.add(target.key);
2554
+ void save(services, target, controller.signal).then((saved) => {
2555
+ if (controller.signal.aborted || !saved.files.length && !saved.error) return;
2556
+ dispatch({ type: "files-saved", ...target.toolCallId ? { toolCallId: target.toolCallId } : {}, saved });
2557
+ });
2558
+ }
2559
+ }, [key, dispatch, services, started, controller]);
2560
+ }
1656
2561
 
1657
2562
  // src/ui/screens/chat/useArtifactFollowUp.ts
1658
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
1659
2563
  var FOLLOW_TIMEOUT_MS = 5 * 6e4;
1660
2564
  function liveArtifacts(state) {
1661
2565
  const items = state.turn?.items ?? [];
1662
2566
  return items.flatMap((item) => item.kind === "tool" && item.call.artifact ? [item.call.artifact] : []);
1663
2567
  }
1664
- function useArtifactFollowUp(state, dispatch, services) {
1665
- const followed = useRef2(/* @__PURE__ */ new Map());
1666
- const alive = useRef2(true);
1667
- useEffect3(
2568
+ function useArtifactFollowUp(state, dispatch, services, opts = {}) {
2569
+ useAutoSave(state, dispatch, services, opts.save !== false && !autoSaveDisabled());
2570
+ const followed = useRef5(/* @__PURE__ */ new Map());
2571
+ const alive = useRef5(true);
2572
+ useEffect6(
1668
2573
  () => () => {
1669
2574
  alive.current = false;
1670
2575
  },
@@ -1672,7 +2577,7 @@ function useArtifactFollowUp(state, dispatch, services) {
1672
2577
  );
1673
2578
  const cards = liveArtifacts(state);
1674
2579
  const key = cards.map((c) => `${c.id}:${c.status}:${c.linkPending ? 1 : 0}:${c.released ? 1 : 0}`).join("|");
1675
- useEffect3(() => {
2580
+ useEffect6(() => {
1676
2581
  const send = (action) => {
1677
2582
  if (alive.current) dispatch(action);
1678
2583
  };
@@ -1707,6 +2612,7 @@ function useArtifactFollowUp(state, dispatch, services) {
1707
2612
  artifactId: card.id,
1708
2613
  status: artifact.status,
1709
2614
  title: artifact.title,
2615
+ ...!card.versionId && artifact.currentVersion?.id ? { versionId: artifact.currentVersion.id, versionNumber: artifact.currentVersion.number } : {},
1710
2616
  ...url ? { previewUrl: url } : {},
1711
2617
  ...artifact.error ? { failure: artifact.error } : {}
1712
2618
  });
@@ -1725,9 +2631,18 @@ function useArtifactFollowUp(state, dispatch, services) {
1725
2631
  // src/ui/screens/chat/useChatSession.ts
1726
2632
  var FLUSH_INTERVAL_MS = 50;
1727
2633
  var HISTORY_LIMIT = 20;
2634
+ function isRecord4(value) {
2635
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2636
+ }
2637
+ function resultFailure(result) {
2638
+ if (!isRecord4(result) || result.ok !== false) return null;
2639
+ const error = isRecord4(result.error) ? result.error : {};
2640
+ const text2 = typeof error.message === "string" ? error.message : typeof result.message === "string" ? result.message : "The tool refused this request";
2641
+ return { tone: "error", text: text2 };
2642
+ }
1728
2643
  function useChatSession(opts) {
1729
2644
  const { services, companyId } = opts;
1730
- const [state, dispatch] = useReducer2(
2645
+ const [state, dispatch] = useReducer3(
1731
2646
  chatReducer,
1732
2647
  opts,
1733
2648
  (o) => initialChatState({
@@ -1735,24 +2650,25 @@ function useChatSession(opts) {
1735
2650
  threadId: o.threadId,
1736
2651
  modelId: o.modelId,
1737
2652
  capabilities: o.capabilities ?? o.services.capabilityPrefs ?? DEFAULT_CAPABILITIES,
1738
- links: Boolean(o.services.hyperlinks)
2653
+ links: Boolean(o.services.hyperlinks),
2654
+ appUrl: o.services.appUrl
1739
2655
  })
1740
2656
  );
1741
- const stateRef = useRef3(state);
2657
+ const stateRef = useRef6(state);
1742
2658
  stateRef.current = state;
1743
- const buffer = useRef3([]);
1744
- const abortRef = useRef3(null);
1745
- const mounted = useRef3(true);
1746
- const [queue, setQueue] = useState3([]);
1747
- const [deciding, setDeciding] = useState3(null);
1748
- const [loading, setLoading] = useState3(
2659
+ const buffer = useRef6([]);
2660
+ const abortRef = useRef6(null);
2661
+ const mounted = useRef6(true);
2662
+ const [queue, setQueue] = useState4([]);
2663
+ const [deciding, setDeciding] = useState4(null);
2664
+ const [loading, setLoading] = useState4(
1749
2665
  opts.threadId ? { label: "Loading the thread\u2026", since: Date.now() } : null
1750
2666
  );
1751
- const flush = useCallback2(() => {
2667
+ const flush = useCallback4(() => {
1752
2668
  const events = buffer.current.splice(0);
1753
2669
  for (const event of events) dispatch(event);
1754
2670
  }, []);
1755
- useEffect4(() => {
2671
+ useEffect7(() => {
1756
2672
  if (!state.streaming) return void 0;
1757
2673
  const id = setInterval(() => {
1758
2674
  flush();
@@ -1760,16 +2676,16 @@ function useChatSession(opts) {
1760
2676
  }, FLUSH_INTERVAL_MS);
1761
2677
  return () => clearInterval(id);
1762
2678
  }, [state.streaming, flush]);
1763
- useEffect4(
2679
+ useEffect7(
1764
2680
  () => () => {
1765
2681
  mounted.current = false;
1766
2682
  abortRef.current?.abort();
1767
2683
  },
1768
2684
  []
1769
2685
  );
1770
- const send = useCallback2(
1771
- (text) => {
1772
- const message = text.trim();
2686
+ const send = useCallback4(
2687
+ (text2) => {
2688
+ const message = text2.trim();
1773
2689
  if (!message) return;
1774
2690
  if (stateRef.current.streaming) {
1775
2691
  setQueue((q) => [...q, message]);
@@ -1806,18 +2722,72 @@ function useChatSession(opts) {
1806
2722
  },
1807
2723
  [services, companyId, flush]
1808
2724
  );
1809
- const interrupt = useCallback2(() => {
2725
+ const runTool = useCallback4(
2726
+ (spec) => {
2727
+ if (stateRef.current.streaming) return false;
2728
+ const started = Date.now();
2729
+ dispatch({ type: "submit", text: spec.text, at: started });
2730
+ const controller = new AbortController();
2731
+ abortRef.current = controller;
2732
+ const payload = {
2733
+ id: `direct-${started}`,
2734
+ name: spec.name,
2735
+ args: spec.args,
2736
+ display: { title: spec.title },
2737
+ ...spec.cardType ? { cardType: spec.cardType } : {}
2738
+ };
2739
+ dispatch({ type: "tool-call", payload: { ...payload, status: "running", stage: "running" }, at: started });
2740
+ const settle = (event, notes) => {
2741
+ if (!mounted.current) return;
2742
+ const at = Date.now();
2743
+ dispatch({ type: "tool-result", payload: event, at });
2744
+ dispatch({ type: "finish", payload: { finishReason: "stop" }, at });
2745
+ for (const note of notes) dispatch({ type: "notice", ...note });
2746
+ };
2747
+ void (async () => {
2748
+ try {
2749
+ const outcome = await spec.run(controller.signal);
2750
+ if (controller.signal.aborted) throw new Error("aborted");
2751
+ if (!outcome.ok) {
2752
+ const failed = { ...payload, status: "failed", stage: "failed", result: { ok: false, error: { code: outcome.error.code, message: outcome.error.message } } };
2753
+ settle(failed, [{ tone: "error", text: outcome.error.message, ...outcome.error.hint ? { hint: outcome.error.hint } : {} }]);
2754
+ return;
2755
+ }
2756
+ if (outcome.approval) {
2757
+ const waiting = { ...payload, status: "failed", stage: "failed", result: { ok: false, error: { code: "APPROVAL_REQUIRED", message: outcome.approval.message } } };
2758
+ const hint = `Approve it with \`tabbio approvals approve ${outcome.approval.approvalId}\`, in /approvals on the home screen, or in the app.`;
2759
+ settle(waiting, [{ tone: "attention", text: outcome.approval.message, hint }]);
2760
+ return;
2761
+ }
2762
+ const refused = resultFailure(outcome.result);
2763
+ const completed = { ...payload, status: refused ? "failed" : "completed", stage: refused ? "failed" : "completed", result: outcome.result };
2764
+ settle(completed, refused ? [refused] : spec.after?.(outcome.result) ?? []);
2765
+ } catch (error) {
2766
+ const cancelled = controller.signal.aborted || isCliError(error) && error.exitCode === 130;
2767
+ const cli = toCliError(error);
2768
+ const failed = { ...payload, status: "failed", stage: "failed", result: { ok: false, error: { code: cli.code, message: cancelled ? "Stopped" : cli.message } } };
2769
+ const note = cancelled ? { tone: "attention", text: "Stopped" } : { tone: "error", text: cli.message, ...cli.hint ? { hint: cli.hint } : {} };
2770
+ settle(failed, [note]);
2771
+ } finally {
2772
+ if (abortRef.current === controller) abortRef.current = null;
2773
+ }
2774
+ })();
2775
+ return true;
2776
+ },
2777
+ []
2778
+ );
2779
+ const interrupt = useCallback4(() => {
1810
2780
  if (!abortRef.current) return false;
1811
2781
  abortRef.current.abort();
1812
2782
  return true;
1813
2783
  }, []);
1814
- useEffect4(() => {
2784
+ useEffect7(() => {
1815
2785
  if (state.streaming || state.approvals.length > 0 || queue.length === 0) return;
1816
2786
  const [next, ...rest] = queue;
1817
2787
  setQueue(rest);
1818
2788
  if (next) send(next);
1819
2789
  }, [state.streaming, state.approvals.length, queue, send]);
1820
- const decide = useCallback2(
2790
+ const decide = useCallback4(
1821
2791
  async (approval, decision) => {
1822
2792
  if (decision === "leave") {
1823
2793
  dispatch({ type: "approval-decided", approvalId: approval.approvalId, decision, at: Date.now() });
@@ -1844,7 +2814,7 @@ function useChatSession(opts) {
1844
2814
  },
1845
2815
  [services, companyId]
1846
2816
  );
1847
- const resume = useCallback2(
2817
+ const resume = useCallback4(
1848
2818
  async (threadId, title) => {
1849
2819
  setLoading({ label: "Loading the thread\u2026", since: Date.now() });
1850
2820
  try {
@@ -1863,8 +2833,8 @@ function useChatSession(opts) {
1863
2833
  },
1864
2834
  [services, companyId]
1865
2835
  );
1866
- const booted = useRef3(false);
1867
- useEffect4(() => {
2836
+ const booted = useRef6(false);
2837
+ useEffect7(() => {
1868
2838
  if (booted.current) return;
1869
2839
  booted.current = true;
1870
2840
  void (async () => {
@@ -1872,97 +2842,112 @@ function useChatSession(opts) {
1872
2842
  if (opts.initialMessage && mounted.current) send(opts.initialMessage);
1873
2843
  })();
1874
2844
  }, [opts.threadId, opts.initialMessage, resume, send]);
1875
- useArtifactFollowUp(state, dispatch, services);
1876
- return { state, dispatch, send, interrupt, decide, deciding, queue, setQueue, resume, loading };
2845
+ const saveArtifacts = opts.saveArtifacts !== false;
2846
+ useArtifactFollowUp(state, dispatch, services, { save: saveArtifacts });
2847
+ return { state, dispatch, send, runTool, interrupt, decide, deciding, queue, setQueue, resume, loading, saveArtifacts };
1877
2848
  }
1878
2849
 
1879
2850
  // src/ui/screens/chat/Chat.tsx
1880
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2851
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
1881
2852
  var DOUBLE_PRESS_MS = 2e3;
1882
- var ChatStatus = memo14(function ChatStatus2(props) {
1883
- useClockTick(props.since !== null);
1884
- const elapsed = props.since !== null ? formatElapsed(Date.now() - props.since) : null;
1885
- const hints = props.approval ? [
1886
- { key: "y", label: "approve" },
1887
- { key: "n", label: "reject" },
1888
- { key: "l", label: "later" }
1889
- ] : props.streaming ? [
1890
- { key: "enter", label: "queue" },
1891
- { key: "esc", label: "interrupt" },
1892
- { key: "/", label: "commands" }
1893
- ] : [
1894
- { key: "enter", label: "send" },
1895
- { key: "/", label: "commands" },
1896
- { key: "ctrl+c", label: "quit" }
1897
- ];
1898
- return /* @__PURE__ */ jsx16(StatusBar, { width: props.width, segments: [...props.segments, elapsed], hints });
1899
- });
2853
+ function saveHintText(dir = artifactsDir()) {
2854
+ return `Generated files are saved to ${displayPath(dir)} (/help for /save, /show, /open)`;
2855
+ }
1900
2856
  function Chat(props) {
1901
- const { services, width, height, hostedByHome = false, onExit } = props;
2857
+ const { services, width, height, hostedByHome = false, onExit, onAccountChange } = props;
1902
2858
  const gutter = pageMargin(useTerminalSize().columns);
1903
2859
  const session = useChatSession(props);
1904
2860
  const { state } = session;
1905
- const [editor, dispatchEditor] = useReducer3(editorReducer, emptyEditor);
1906
- const [paletteIndex, setPaletteIndex] = useState4(0);
1907
- const [overlay, setOverlay] = useState4(null);
1908
- const [expandTools, setExpandTools] = useState4(false);
1909
- const [hint, setHint] = useState4(null);
1910
- const lastCtrlC = useRef4(0);
1911
- const lastEsc = useRef4(0);
1912
- const commands = useMemo4(() => chatCommands({ hostedByHome }), [hostedByHome]);
1913
- const commandMatches = overlay ? [] : paletteMatches(commands, editor.value);
1914
- const argMatches = overlay || commandMatches.length ? [] : paletteArgMatches(commands, editor.value);
1915
- const matches = commandMatches.length ? commandMatches : argMatches;
2861
+ const [editor, dispatchEditor] = useReducer4(editorReducer, emptyEditor);
2862
+ const [paletteIndex, setPaletteIndex] = useState5(0);
2863
+ const [navigated, setNavigated] = useState5(false);
2864
+ const [overlay, setOverlay] = useState5(null);
2865
+ const [expandTools, setExpandTools] = useState5(false);
2866
+ const [hint, setHint] = useState5(null);
2867
+ const lastCtrlC = useRef7(0);
2868
+ const lastEsc = useRef7(0);
2869
+ const commands = useMemo5(() => chatCommands({ hostedByHome }), [hostedByHome]);
2870
+ const skills = useSkills(services, services.account.hasSession && editor.value.startsWith("/"));
2871
+ const view = overlay ? null : paletteView(commands, skills, editor.value, { ellipsis: theme.symbols.ellipsis, separator: theme.symbols.middot });
2872
+ const matches = view?.items ?? [];
1916
2873
  const selected = Math.min(paletteIndex, Math.max(0, matches.length - 1));
1917
2874
  const approval = !state.streaming ? state.approvals[0] : void 0;
1918
2875
  const composerActive = !overlay && !approval;
1919
- const [header] = useState4(() => ({
2876
+ const firstTurn = !state.entries.some((e) => e.kind === "user") && !state.turn;
2877
+ const saveHint = session.saveArtifacts && firstTurn ? saveHintText() : null;
2878
+ const [header] = useState5(() => ({
1920
2879
  kind: "header",
1921
2880
  id: "chat-header",
1922
2881
  text: `${theme.bold("Chat")} ${theme.dim(`${theme.symbols.middot} ${props.mode}${props.threadId ? ` ${theme.symbols.middot} resumed` : ""}`)}`,
1923
2882
  right: `profile ${services.profileName}`
1924
2883
  }));
1925
- const staticItems = useMemo4(() => [header, ...state.entries], [header, state.entries]);
1926
- useEffect5(() => {
2884
+ const staticItems = useMemo5(() => [header, ...state.entries], [header, state.entries]);
2885
+ useEffect8(() => {
1927
2886
  if (!hint) return void 0;
1928
2887
  const timer = setTimeout(() => setHint(null), DOUBLE_PRESS_MS);
1929
2888
  return () => clearTimeout(timer);
1930
2889
  }, [hint]);
1931
- const openThreads = useCallback3(() => {
2890
+ useEffect8(() => setNavigated(false), [editor.value]);
2891
+ const openThreads = useCallback5(() => {
1932
2892
  setOverlay({ kind: "threads", since: Date.now() });
1933
- services.listThreads({ mode: state.mode, companyId: props.companyId }).then((threads) => setOverlay((o) => o ? { ...o, threads } : o)).catch((error) => setOverlay((o) => o ? { ...o, error: toCliError(error).message } : o));
2893
+ services.listThreads({ mode: state.mode, companyId: props.companyId }).then((threads) => setOverlay((o) => o?.kind === "threads" ? { ...o, threads } : o)).catch((error) => setOverlay((o) => o?.kind === "threads" ? { ...o, error: toCliError(error).message } : o));
1934
2894
  }, [services, state.mode, props.companyId]);
1935
- const runCommand = useChatCommands({ session, services, companyId: props.companyId, hostedByHome, onExit, openThreads });
1936
- const submit2 = useCallback3(
2895
+ const openLogin = useCallback5((start) => setOverlay({ kind: "login", start }), []);
2896
+ const onLoginDone = useCallback5(
2897
+ (outcome) => {
2898
+ setOverlay(null);
2899
+ if (outcome.status === "done") {
2900
+ onAccountChange?.(outcome.change);
2901
+ session.dispatch({ type: "notice", tone: "success", text: outcome.change.message });
2902
+ for (const warning of outcome.change.warnings ?? []) session.dispatch({ type: "notice", tone: "attention", text: warning });
2903
+ } else if (outcome.status === "cancelled") session.dispatch({ type: "notice", tone: "info", text: "Sign-in cancelled" });
2904
+ else session.dispatch({ type: "notice", tone: "error", text: outcome.message, hint: outcome.hint });
2905
+ },
2906
+ [onAccountChange, session]
2907
+ );
2908
+ const runCommand = useChatCommands({ session, services, companyId: props.companyId, hostedByHome, onExit, openThreads, openLogin, onAccountChange });
2909
+ const submit2 = useCallback5(
1937
2910
  (value) => {
1938
- const text = value.trim();
1939
- if (!text) return;
1940
- const slash = parseSlash(text);
1941
- if (slash) {
1942
- const exact = commands.find((c) => c.name === slash.name);
1943
- const choice = exact ?? commandMatches[selected];
1944
- const picked = exact?.args ? argMatches[selected]?.completion : void 0;
1945
- if (choice?.args && !slash.arg && !picked) {
1946
- dispatchEditor({ type: "set", value: `/${choice.name} ` });
1947
- setPaletteIndex(0);
1948
- return;
1949
- }
1950
- let arg = exact ? slash.arg : "";
1951
- const known = exact?.args?.some((a) => a.value === arg.toLowerCase());
1952
- if (exact?.args && !known && picked) arg = parseSlash(picked)?.arg ?? arg;
1953
- dispatchEditor({ type: "remember", value: text });
2911
+ const argRows2 = view?.args ? matches : [];
2912
+ const res = resolveSubmit(value, { commands, skills, highlighted: view?.args ? void 0 : matches[selected], navigated });
2913
+ if (res.kind === "empty" || res.kind === "bare") return;
2914
+ if (res.kind === "loading") {
2915
+ setHint("Skills are still loading. Press enter again in a moment.");
2916
+ return;
2917
+ }
2918
+ const text2 = value.trim();
2919
+ const done = () => {
2920
+ dispatchEditor({ type: "remember", value: text2 });
1954
2921
  dispatchEditor({ type: "clear" });
1955
2922
  setPaletteIndex(0);
1956
- runCommand(choice?.name ?? slash.name, arg);
2923
+ };
2924
+ if (res.kind === "message" || res.kind === "skill") {
2925
+ done();
2926
+ session.send(res.kind === "skill" ? res.message : res.text);
1957
2927
  return;
1958
2928
  }
1959
- dispatchEditor({ type: "remember", value: text });
1960
- dispatchEditor({ type: "clear" });
1961
- session.send(text);
2929
+ if (res.kind === "unknown") {
2930
+ done();
2931
+ session.dispatch({ type: "notice", tone: "error", text: `Unknown command /${res.name}`, hint: "Type / to see the commands and your skills." });
2932
+ return;
2933
+ }
2934
+ const exact = res.picked ? void 0 : commands.find((c) => c.name === res.name);
2935
+ const choice = exact ?? commands.find((c) => c.name === res.name);
2936
+ const picked = exact?.args ? argRows2[selected]?.completion : void 0;
2937
+ if (choice?.args && !res.arg && !picked) {
2938
+ dispatchEditor({ type: "set", value: `/${choice.name} ` });
2939
+ setPaletteIndex(0);
2940
+ return;
2941
+ }
2942
+ let arg = exact ? res.arg : "";
2943
+ const known = exact?.args?.some((a) => a.value === arg.toLowerCase());
2944
+ if (exact?.args && !known && picked) arg = parseSlash(picked)?.arg ?? arg;
2945
+ done();
2946
+ runCommand(res.name, arg);
1962
2947
  },
1963
- [commandMatches, argMatches, selected, commands, runCommand, session]
2948
+ [view, matches, selected, commands, skills, navigated, runCommand, session]
1964
2949
  );
1965
- const onCtrlC = useCallback3(() => {
2950
+ const onCtrlC = useCallback5(() => {
1966
2951
  const now = Date.now();
1967
2952
  if (now - lastCtrlC.current < DOUBLE_PRESS_MS) {
1968
2953
  session.interrupt();
@@ -1976,21 +2961,22 @@ function Chat(props) {
1976
2961
  setHint("Press ctrl+c again to quit.");
1977
2962
  }
1978
2963
  }, [session, onExit, editor.value]);
1979
- useInput5((input, key) => {
2964
+ useInput7((input, key) => {
1980
2965
  if (key.ctrl && input === "c") onCtrlC();
1981
2966
  });
1982
- const onComposerKey = useCallback3(
2967
+ const onComposerKey = useCallback5(
1983
2968
  (input, key) => {
1984
2969
  if (key.ctrl && input === "c") return true;
1985
2970
  if (matches.length > 0) {
1986
2971
  if (key.upArrow || key.downArrow) {
1987
2972
  setPaletteIndex((i) => key.upArrow ? Math.max(0, i - 1) : Math.min(matches.length - 1, i + 1));
2973
+ setNavigated(true);
1988
2974
  return true;
1989
2975
  }
1990
2976
  if (key.tab) {
1991
2977
  const choice = matches[selected];
1992
2978
  if (choice?.completion) dispatchEditor({ type: "set", value: choice.completion });
1993
- else if (choice) dispatchEditor({ type: "set", value: `/${choice.name}${choice.usage ? " " : ""}` });
2979
+ else if (choice) dispatchEditor({ type: "set", value: `/${choice.name}${choice.usage || choice.skillId ? " " : ""}` });
1994
2980
  setPaletteIndex(0);
1995
2981
  return true;
1996
2982
  }
@@ -2001,7 +2987,7 @@ function Chat(props) {
2001
2987
  if (!editor.value) {
2002
2988
  if (hostedByHome) onExit("home");
2003
2989
  else setHint("Type /exit or press ctrl+c twice to quit.");
2004
- } else if (matches.length > 0 || now - lastEsc.current < 600) dispatchEditor({ type: "clear" });
2990
+ } else if (view?.open || now - lastEsc.current < 600) dispatchEditor({ type: "clear" });
2005
2991
  lastEsc.current = now;
2006
2992
  return true;
2007
2993
  }
@@ -2017,10 +3003,12 @@ function Chat(props) {
2017
3003
  }
2018
3004
  return false;
2019
3005
  },
2020
- [matches, selected, session, editor.value, state.streaming, state.mode, state.turn, runCommand, hostedByHome, onExit]
3006
+ [matches, selected, view, session, editor.value, state.streaming, state.mode, state.turn, runCommand, hostedByHome, onExit]
2021
3007
  );
2022
3008
  const cardRows = approval ? 14 + Math.min(9, Object.keys(approval.args).length) : 0;
2023
- const fixedRows = 4 + 1 + (hint ? 1 : 0) + session.queue.length + (matches.length ? Math.min(6, matches.length) : 0) + (overlay ? 12 : 0) + cardRows + 2;
3009
+ const paletteRows = view?.open ? Math.min(6, matches.length) + 2 : 0;
3010
+ const saveHintRows = saveHint ? Math.ceil(saveHint.length / Math.max(20, width)) : 0;
3011
+ const fixedRows = 4 + 1 + (hint ? 1 : 0) + saveHintRows + session.queue.length + paletteRows + (overlay ? 12 : 0) + cardRows + 2;
2024
3012
  const liveBudget = Math.max(3, height - fixedRows - 1);
2025
3013
  const placeholder = approval ? "Decide the approval above first" : state.streaming ? `Queue a follow-up${theme.symbols.ellipsis} (esc to interrupt)` : "Ask Tabbio anything, or / for commands";
2026
3014
  const segments = [
@@ -2030,11 +3018,11 @@ function Chat(props) {
2030
3018
  ...capabilitySegments(state.mode, state.capabilities),
2031
3019
  state.threadId ? shortId(state.threadId, 6, 3) : "new chat"
2032
3020
  ];
2033
- return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", width, children: [
2034
- /* @__PURE__ */ jsx16(Static, { items: staticItems, children: (item) => /* @__PURE__ */ jsx16(Box15, { paddingLeft: gutter, children: /* @__PURE__ */ jsx16(EntryView, { entry: item, width }) }, item.id) }),
2035
- state.turn ? /* @__PURE__ */ jsx16(LiveTurn, { turn: state.turn, width, expandTools, maxLines: liveBudget, streaming: state.streaming }) : null,
2036
- session.loading ? /* @__PURE__ */ jsx16(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx16(Working, { label: session.loading.label, since: session.loading.since }) }) : null,
2037
- approval ? /* @__PURE__ */ jsx16(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx16(
3021
+ return /* @__PURE__ */ jsxs15(Box17, { flexDirection: "column", width, children: [
3022
+ /* @__PURE__ */ jsx19(Static, { items: staticItems, children: (item) => /* @__PURE__ */ jsx19(Box17, { paddingLeft: gutter, children: /* @__PURE__ */ jsx19(EntryView, { entry: item, width }) }, item.id) }),
3023
+ state.turn ? /* @__PURE__ */ jsx19(LiveTurn, { turn: state.turn, width, expandTools, maxLines: liveBudget, streaming: state.streaming }) : null,
3024
+ session.loading ? /* @__PURE__ */ jsx19(Box17, { marginTop: 1, children: /* @__PURE__ */ jsx19(Working, { label: session.loading.label, since: session.loading.since }) }) : null,
3025
+ approval ? /* @__PURE__ */ jsx19(Box17, { marginTop: 1, children: /* @__PURE__ */ jsx19(
2038
3026
  ApprovalCard,
2039
3027
  {
2040
3028
  approvalId: approval.approvalId,
@@ -2048,9 +3036,10 @@ function Chat(props) {
2048
3036
  onDecision: (decision) => void session.decide(approval, decision)
2049
3037
  }
2050
3038
  ) }) : null,
2051
- overlay ? /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", marginTop: 1, children: [
2052
- /* @__PURE__ */ jsx16(Text15, { children: theme.bold("Recent chats") }),
2053
- overlay.error ? /* @__PURE__ */ jsx16(Text15, { children: theme.error(`${theme.symbols.error} ${overlay.error}`) }) : !overlay.threads ? /* @__PURE__ */ jsx16(Working, { label: "Loading chats\u2026", since: overlay.since }) : /* @__PURE__ */ jsx16(
3039
+ overlay?.kind === "login" ? /* @__PURE__ */ jsx19(Box17, { marginTop: 1, children: /* @__PURE__ */ jsx19(LoginPanel, { services, start: overlay.start, width, onDone: onLoginDone }) }) : null,
3040
+ overlay?.kind === "threads" ? /* @__PURE__ */ jsxs15(Box17, { flexDirection: "column", marginTop: 1, children: [
3041
+ /* @__PURE__ */ jsx19(Text17, { children: theme.bold("Recent chats") }),
3042
+ overlay.error ? /* @__PURE__ */ jsx19(Text17, { children: theme.error(`${theme.symbols.error} ${overlay.error}`) }) : !overlay.threads ? /* @__PURE__ */ jsx19(Working, { label: "Loading chats\u2026", since: overlay.since }) : /* @__PURE__ */ jsx19(
2054
3043
  SelectList,
2055
3044
  {
2056
3045
  items: overlay.threads.map((t) => ({
@@ -2069,11 +3058,11 @@ function Chat(props) {
2069
3058
  onCancel: () => setOverlay(null)
2070
3059
  }
2071
3060
  ),
2072
- overlay.error || !overlay.threads ? /* @__PURE__ */ jsx16(SelectEscape, { onClose: () => setOverlay(null) }) : null
3061
+ overlay.error || !overlay.threads ? /* @__PURE__ */ jsx19(SelectEscape, { onClose: () => setOverlay(null) }) : null
2073
3062
  ] }) : null,
2074
- session.queue.map((queued, i) => /* @__PURE__ */ jsx16(Text15, { wrap: "truncate-end", children: theme.dim(`${theme.symbols.arrowRight} queued: ${queued}`) }, `q-${i}`)),
2075
- matches.length > 0 ? /* @__PURE__ */ jsx16(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx16(Palette, { commands: matches, selected, width }) }) : null,
2076
- /* @__PURE__ */ jsx16(Box15, { marginTop: matches.length > 0 ? 0 : 1, children: /* @__PURE__ */ jsx16(
3063
+ session.queue.map((queued, i) => /* @__PURE__ */ jsx19(Text17, { wrap: "truncate-end", children: theme.dim(`${theme.symbols.arrowRight} queued: ${queued}`) }, `q-${i}`)),
3064
+ view?.open ? /* @__PURE__ */ jsx19(Box17, { marginTop: 1, children: /* @__PURE__ */ jsx19(Palette, { commands: matches, selected, width, footer: view.footer }) }) : null,
3065
+ /* @__PURE__ */ jsx19(Box17, { marginTop: view?.open ? 0 : 1, children: /* @__PURE__ */ jsx19(
2077
3066
  TextInput,
2078
3067
  {
2079
3068
  editor,
@@ -2086,45 +3075,233 @@ function Chat(props) {
2086
3075
  boxed: true
2087
3076
  }
2088
3077
  ) }),
2089
- hint ? /* @__PURE__ */ jsx16(Text15, { children: theme.attention(hint) }) : null,
2090
- /* @__PURE__ */ jsx16(
3078
+ hint ? /* @__PURE__ */ jsx19(Text17, { children: theme.attention(hint) }) : null,
3079
+ saveHint && !hint ? /* @__PURE__ */ jsx19(Text17, { wrap: "wrap", children: theme.dim(saveHint) }) : null,
3080
+ /* @__PURE__ */ jsx19(
2091
3081
  ChatStatus,
2092
3082
  {
2093
3083
  width,
2094
3084
  segments,
2095
3085
  since: state.streaming && state.turn ? state.turn.startedAt : null,
2096
3086
  streaming: state.streaming,
2097
- approval: Boolean(approval)
3087
+ approval: Boolean(approval),
3088
+ overlay: Boolean(overlay)
2098
3089
  }
2099
3090
  )
2100
3091
  ] });
2101
3092
  }
2102
- function capabilitySegments(mode, capabilities) {
2103
- if (mode === "employer") return [];
2104
- return ["research", "image", "result"].filter((key) => capabilities[key] !== DEFAULT_CAPABILITIES[key]).map((key) => `${key} ${capabilities[key]}`);
3093
+
3094
+ // src/ui/screens/home/Home.tsx
3095
+ import { Box as Box26, Text as Text26, useInput as useInput12 } from "ink";
3096
+ import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo8, useReducer as useReducer5, useRef as useRef10, useState as useState10 } from "react";
3097
+
3098
+ // src/ui/session-controls.ts
3099
+ import open from "open";
3100
+ var CATALOG_RELOAD_MS = 8e3;
3101
+ var created = /* @__PURE__ */ new Set();
3102
+ async function closeUiContexts() {
3103
+ const all = [...created];
3104
+ created.clear();
3105
+ await Promise.all(all.map((ctx) => ctx.close().catch(() => void 0)));
3106
+ }
3107
+ async function openInBrowser(url) {
3108
+ if (!hasDisplay()) return false;
3109
+ try {
3110
+ await open(url);
3111
+ return true;
3112
+ } catch {
3113
+ return false;
3114
+ }
2105
3115
  }
2106
- function SelectEscape({ onClose }) {
2107
- useInput5((input, key) => {
2108
- if (key.escape || input === "q") onClose();
3116
+ async function defaultReload(ctx) {
3117
+ const load = (async () => (await (await ctx.mcp()).listTools({ refresh: true })).filter((t) => !t.hidden).length)();
3118
+ const timeout = new Promise((resolve) => setTimeout(() => resolve(null), CATALOG_RELOAD_MS).unref());
3119
+ return Promise.race([load, timeout]).catch((error) => {
3120
+ debug(`catalog reload after sign-in failed: ${error.message}`);
3121
+ return null;
2109
3122
  });
2110
- return null;
3123
+ }
3124
+ function envNote() {
3125
+ return process.env.TABBIO_TOKEN || process.env.TABBIO_ACCESS_TOKEN ? "TABBIO_TOKEN / TABBIO_ACCESS_TOKEN is set and still wins for tool calls." : void 0;
3126
+ }
3127
+ function createSessionControls(deps) {
3128
+ const { ctx } = deps;
3129
+ const authClient = () => new ApiClient(ctx.profile, null, { persist: false, fetch: deps.fetch });
3130
+ const nextContext = () => {
3131
+ const next = deps.newContext ? deps.newContext() : createCommandContext();
3132
+ created.add(next);
3133
+ return next;
3134
+ };
3135
+ const adopt = async (toolCount) => {
3136
+ await ctx.close().catch(() => void 0);
3137
+ const next = nextContext();
3138
+ const count = toolCount !== void 0 ? toolCount : await (deps.reloadCatalog ?? defaultReload)(next);
3139
+ return deps.build(next, count === null ? {} : { toolCount: count });
3140
+ };
3141
+ const signedIn = async (payload) => {
3142
+ const creds = await saveAppSession(ctx, payload, { fetch: deps.fetch });
3143
+ const note = envNote();
3144
+ return {
3145
+ services: await adopt(),
3146
+ message: `Signed in as ${describeUser(creds.user)}`,
3147
+ ...note ? { warnings: [note] } : {},
3148
+ signedOut: false
3149
+ };
3150
+ };
3151
+ return {
3152
+ async loginWithBrowser({ signal, onWaiting }) {
3153
+ const payload = await loginWithBrowser(authClient(), {
3154
+ device: deviceLabel(),
3155
+ signal,
3156
+ openUrl: deps.openUrl ?? openInBrowser,
3157
+ onWaiting: (url, opened) => onWaiting(url, opened)
3158
+ });
3159
+ return signedIn(payload);
3160
+ },
3161
+ async loginWithEmail({ email, promptCode, onInvalidCode }) {
3162
+ const payload = await loginWithEmailOtp(authClient(), { email, promptCode, onInvalidCode });
3163
+ return signedIn(payload);
3164
+ },
3165
+ async loginWithToken(token) {
3166
+ const { visibleTools } = await saveTokenSession(ctx, token, { fetch: deps.fetch });
3167
+ const note = envNote();
3168
+ return {
3169
+ services: await adopt(visibleTools),
3170
+ message: `Token accepted ${theme.symbols.middot} ${visibleTools} tools. Chat and approvals need /login with the browser or an email code.`,
3171
+ ...note ? { warnings: [note] } : {},
3172
+ signedOut: false
3173
+ };
3174
+ },
3175
+ async logout() {
3176
+ const stored = loadStoredCredentials(ctx.profile.name);
3177
+ const result = stored ? await logoutProfile(ctx.profile, stored, { fetch: deps.fetch }) : { warnings: [], revokedSession: false };
3178
+ const note = envNote();
3179
+ const warnings = [...result.warnings, ...note ? [note] : []];
3180
+ return {
3181
+ services: await adopt(null),
3182
+ message: stored ? `Signed out of profile ${ctx.profile.name}` : `Not signed in (profile ${ctx.profile.name})`,
3183
+ ...warnings.length ? { warnings } : {},
3184
+ signedOut: true
3185
+ };
3186
+ }
3187
+ };
2111
3188
  }
2112
3189
 
2113
- // src/ui/screens/home/Home.tsx
2114
- import { Box as Box24, Text as Text24, useInput as useInput10 } from "ink";
2115
- import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo7, useReducer as useReducer4, useRef as useRef7, useState as useState9 } from "react";
3190
+ // src/ui/services.ts
3191
+ function accountIdentity(services) {
3192
+ const account = services.account;
3193
+ return `${services.profileName}:${account.signedIn ? account.hasSession ? "app" : "token" : "signed-out"}:${account.userId ?? account.email ?? account.name ?? ""}`;
3194
+ }
3195
+ function hostOf2(url) {
3196
+ try {
3197
+ return new URL(url).host;
3198
+ } catch {
3199
+ return url;
3200
+ }
3201
+ }
3202
+ function createUiServices(ctx, opts = {}) {
3203
+ const creds = ctx.api.credentials ?? ctx.credentials;
3204
+ const cached = readCachedCatalog(ctx.profile, creds);
3205
+ const saved = /* @__PURE__ */ new Map();
3206
+ let skills = null;
3207
+ return {
3208
+ version: CLI_VERSION,
3209
+ profileName: ctx.profile.name,
3210
+ apiHost: hostOf2(ctx.profile.apiUrl),
3211
+ account: {
3212
+ name: creds?.user?.name ?? null,
3213
+ email: creds?.user?.email ?? null,
3214
+ signedIn: Boolean(creds?.accessToken || creds?.mcpToken),
3215
+ hasSession: Boolean(creds?.accessToken),
3216
+ userId: creds?.user?.id
3217
+ },
3218
+ cachedToolCount: opts.toolCount ?? (cached ? cached.tools.filter((t) => !t.hidden).length : null),
3219
+ async loadPlan() {
3220
+ if (!creds?.accessToken) return null;
3221
+ const state = await ctx.api.json("/api/subscription/state");
3222
+ return state?.snapshot?.tier ?? null;
3223
+ },
3224
+ async loadPendingApprovals() {
3225
+ const state = await getMcpAccessState(ctx.api);
3226
+ return state?.pendingApprovals ?? [];
3227
+ },
3228
+ listThreads: (scope) => listThreads(ctx, scope),
3229
+ listModels: (scope) => listModels(ctx, scope),
3230
+ getThreadMessages: (threadId, scope) => getThreadMessages(ctx, threadId, scope),
3231
+ streamChat: (request, signal) => streamChat(ctx, request, { signal }),
3232
+ decideApproval: (approvalId, decision, scope) => decision === "approve" ? confirmApproval(ctx, approvalId, scope) : rejectApproval(ctx, approvalId, scope),
3233
+ async listTools(opts2) {
3234
+ const session = await ctx.mcp();
3235
+ return session.listTools(opts2);
3236
+ },
3237
+ async callTool(toolId, input, opts2) {
3238
+ const session = await ctx.mcp();
3239
+ const started = Date.now();
3240
+ const result = await session.callTool(toolId, input, opts2);
3241
+ return { ...result, durationMs: Date.now() - started };
3242
+ },
3243
+ capabilityPrefs: loadCapabilityPrefs(ctx.profile.name),
3244
+ saveCapabilityPrefs: (capabilities) => saveCapabilityPrefs(ctx.profile.name, capabilities),
3245
+ loadCapabilities: () => ctx.api.json("/api/agent/capabilities"),
3246
+ getArtifact: (artifactId) => getArtifact(ctx.api, artifactId),
3247
+ async artifactPreviewUrl(artifactId, kind) {
3248
+ return (await previewUrl(ctx.api, artifactId, { variant: previewVariantFor(kind) })).url;
3249
+ },
3250
+ hyperlinks: supportsHyperlinks(),
3251
+ appUrl: ctx.profile.appUrl,
3252
+ session: createSessionControls({ ctx, build: createUiServices }),
3253
+ loadSkills() {
3254
+ skills ??= (async () => {
3255
+ const session = await ctx.mcp();
3256
+ const result = await session.callTool("skills.list", { limit: 50 });
3257
+ if (!result.ok) throw result.error;
3258
+ return parseSkillsResult(result.result);
3259
+ })().catch((error) => {
3260
+ skills = null;
3261
+ throw error;
3262
+ });
3263
+ return skills;
3264
+ },
3265
+ openUrl: (url) => openInBrowser(url),
3266
+ async saveArtifact(artifactId, saveOpts) {
3267
+ const files = await saveArtifactToDisk(ctx, artifactId, saveOpts ?? {});
3268
+ if (files.length) saved.set(artifactId, files);
3269
+ return files;
3270
+ },
3271
+ savedFiles: (artifactId) => saved.get(artifactId),
3272
+ saveCvExport: (cvId, exportOpts) => saveCvExportToDisk(ctx, cvId, exportOpts ?? {}),
3273
+ readSavedPreview: (path, maxLines) => readSavedPreview(path, maxLines),
3274
+ async loadStatus() {
3275
+ const { collectStatus } = await import("./status-2C7BECKK.js");
3276
+ const report = await collectStatus(ctx);
3277
+ const who = report.user ? report.user.name ? `${report.user.name} <${report.user.email}>` : report.user.email : "not signed in";
3278
+ const rows = [
3279
+ ["Profile", `${report.profile.name} (${report.profile.source})`],
3280
+ ["API", report.profile.apiUrl],
3281
+ ["Account", who],
3282
+ ["Plan", report.plan],
3283
+ ["Session", report.session ? `${report.session.token}${report.session.refreshable ? " \xB7 auto-refresh" : ""}` : null],
3284
+ ["Tools via", report.mcp?.label],
3285
+ ["Approvals", report.pendingApprovals === null ? null : `${report.pendingApprovals} pending`],
3286
+ ["Catalog", report.catalog ? `${report.catalog.tools} tools${report.catalog.fresh ? "" : " (stale)"}` : null],
3287
+ ...report.problems.map((p) => ["Problem", p])
3288
+ ];
3289
+ return rows.filter((r) => Boolean(r[1])).map(([label, value]) => ({ label, value }));
3290
+ }
3291
+ };
3292
+ }
2116
3293
 
2117
3294
  // src/ui/screens/Approvals.tsx
2118
- import { Box as Box18, Text as Text18, useInput as useInput7 } from "ink";
2119
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "react";
3295
+ import { Box as Box20, Text as Text20, useInput as useInput9 } from "ink";
3296
+ import { useCallback as useCallback6, useEffect as useEffect9, useRef as useRef8, useState as useState7 } from "react";
2120
3297
 
2121
3298
  // src/ui/screens/ResultPanel.tsx
2122
- import { Box as Box17, Text as Text17, useInput as useInput6 } from "ink";
2123
- import { useMemo as useMemo5, useState as useState5 } from "react";
3299
+ import { Box as Box19, Text as Text19, useInput as useInput8 } from "ink";
3300
+ import { useMemo as useMemo6, useState as useState6 } from "react";
2124
3301
 
2125
3302
  // src/ui/components/Panel.tsx
2126
- import { Box as Box16, Text as Text16 } from "ink";
2127
- import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
3303
+ import { Box as Box18, Text as Text18 } from "ink";
3304
+ import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
2128
3305
  var BORDER = {
2129
3306
  round: { tl: "\u256D", tr: "\u256E", h: "\u2500" },
2130
3307
  classic: { tl: "+", tr: "+", h: "-" }
@@ -2136,12 +3313,12 @@ function toneColor(tone) {
2136
3313
  if (tone === "attention") return "yellow";
2137
3314
  return void 0;
2138
3315
  }
2139
- function paintBorder(tone, text) {
2140
- if (tone === "accent") return theme.accent(text);
2141
- if (tone === "success") return theme.success(text);
2142
- if (tone === "danger") return theme.danger(text);
2143
- if (tone === "attention") return theme.attention(text);
2144
- return theme.dim(text);
3316
+ function paintBorder(tone, text2) {
3317
+ if (tone === "accent") return theme.accent(text2);
3318
+ if (tone === "success") return theme.success(text2);
3319
+ if (tone === "danger") return theme.danger(text2);
3320
+ if (tone === "attention") return theme.attention(text2);
3321
+ return theme.dim(text2);
2145
3322
  }
2146
3323
  function panelTopLine(width, title = "", right = "", tone = "muted") {
2147
3324
  const chars2 = BORDER[theme.boxStyle];
@@ -2162,10 +3339,10 @@ function Panel({
2162
3339
  children
2163
3340
  }) {
2164
3341
  const color = toneColor(tone);
2165
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", width, children: [
2166
- /* @__PURE__ */ jsx17(Text16, { children: panelTopLine(width, title, right, tone) }),
2167
- /* @__PURE__ */ jsx17(
2168
- Box16,
3342
+ return /* @__PURE__ */ jsxs16(Box18, { flexDirection: "column", width, children: [
3343
+ /* @__PURE__ */ jsx20(Text18, { children: panelTopLine(width, title, right, tone) }),
3344
+ /* @__PURE__ */ jsx20(
3345
+ Box18,
2169
3346
  {
2170
3347
  borderStyle: theme.boxStyle,
2171
3348
  borderTop: false,
@@ -2181,11 +3358,11 @@ function Panel({
2181
3358
  }
2182
3359
 
2183
3360
  // src/ui/result-model.ts
2184
- function isRecord3(value) {
3361
+ function isRecord5(value) {
2185
3362
  return typeof value === "object" && value !== null && !Array.isArray(value);
2186
3363
  }
2187
3364
  function isRowList(value) {
2188
- return Array.isArray(value) && value.length > 0 && value.every(isRecord3);
3365
+ return Array.isArray(value) && value.length > 0 && value.every(isRecord5);
2189
3366
  }
2190
3367
  function tableBody(rows, summary, now) {
2191
3368
  const picked = pickColumns(rows);
@@ -2201,7 +3378,7 @@ function kvValue(value, now) {
2201
3378
  if (Array.isArray(value) && value.some((v) => typeof v === "object" && v !== null)) {
2202
3379
  return `${value.length} item${value.length === 1 ? "" : "s"}`;
2203
3380
  }
2204
- if (isRecord3(value)) {
3381
+ if (isRecord5(value)) {
2205
3382
  const json = safeJson(value, 0);
2206
3383
  return json.length > 100 ? `${json.slice(0, 99)}\u2026` : json;
2207
3384
  }
@@ -2238,7 +3415,7 @@ function bodyLength(body) {
2238
3415
  }
2239
3416
 
2240
3417
  // src/ui/screens/ResultPanel.tsx
2241
- import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
3418
+ import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
2242
3419
  function summaryLine(body) {
2243
3420
  switch (body.kind) {
2244
3421
  case "table":
@@ -2252,23 +3429,23 @@ function summaryLine(body) {
2252
3429
  }
2253
3430
  }
2254
3431
  function ErrorBody({ error }) {
2255
- return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
2256
- error.hint ? /* @__PURE__ */ jsx18(Text17, { wrap: "wrap", children: theme.dim(`${theme.symbols.arrowRight} ${error.hint}`) }) : null,
2257
- /* @__PURE__ */ jsx18(Text17, { children: theme.dim([error.requestId ? `request id ${error.requestId}` : null, `exit ${error.exitCode}`].filter(Boolean).join(` ${theme.symbols.middot} `)) })
3432
+ return /* @__PURE__ */ jsxs17(Box19, { flexDirection: "column", children: [
3433
+ error.hint ? /* @__PURE__ */ jsx21(Text19, { wrap: "wrap", children: theme.dim(`${theme.symbols.arrowRight} ${error.hint}`) }) : null,
3434
+ /* @__PURE__ */ jsx21(Text19, { children: theme.dim([error.requestId ? `request id ${error.requestId}` : null, `exit ${error.exitCode}`].filter(Boolean).join(` ${theme.symbols.middot} `)) })
2258
3435
  ] });
2259
3436
  }
2260
3437
  function ResultPanel(props) {
2261
3438
  const { title, result, error, durationMs, requestId, note, width, height, interactive } = props;
2262
- const body = useMemo5(() => describeResult(result), [result]);
2263
- const jsonLines = useMemo5(() => safeJson(result ?? null).split("\n"), [result]);
2264
- const [view, setView] = useState5("human");
2265
- const [offset, setOffset] = useState5(0);
3439
+ const body = useMemo6(() => describeResult(result), [result]);
3440
+ const jsonLines = useMemo6(() => safeJson(result ?? null).split("\n"), [result]);
3441
+ const [view, setView] = useState6("human");
3442
+ const [offset, setOffset] = useState6(0);
2266
3443
  const total = view === "json" ? jsonLines.length : bodyLength(body);
2267
3444
  const chrome = 3 + (note ? 1 : 0) + 3;
2268
3445
  const window = interactive ? Math.max(3, height - chrome - (body.kind === "table" ? 1 : 0)) : total;
2269
3446
  const maxOffset = Math.max(0, total - window);
2270
3447
  const clamped = Math.min(offset, maxOffset);
2271
- useInput6(
3448
+ useInput8(
2272
3449
  (input, key) => {
2273
3450
  if (key.escape || input === "q") props.onBack?.();
2274
3451
  else if (input === "j") {
@@ -2296,32 +3473,32 @@ function ResultPanel(props) {
2296
3473
  ...total > window ? [{ key: theme.symbols.upDown, label: "scroll" }] : [],
2297
3474
  { key: "q", label: "back" }
2298
3475
  ];
2299
- return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", width, children: [
2300
- /* @__PURE__ */ jsx18(Panel, { title: heading, right, width, tone: ok ? "muted" : "danger", children: /* @__PURE__ */ jsx18(Text17, { wrap: "truncate-end", children: error ? theme.error(error.message) : theme.dim(summaryLine(body)) }) }),
2301
- note ? /* @__PURE__ */ jsx18(Text17, { wrap: "truncate-end", children: note }) : null,
2302
- /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", paddingLeft: 2, width, children: [
2303
- error ? /* @__PURE__ */ jsx18(ErrorBody, { error }) : view === "json" ? jsonLines.slice(clamped, clamped + window).map((line, i) => /* @__PURE__ */ jsx18(Text17, { wrap: "truncate-end", children: line }, clamped + i)) : body.kind === "table" ? /* @__PURE__ */ jsx18(Table, { columns: body.columns, rows: body.rows, width: inner, offset: clamped, limit: window }) : body.kind === "kv" ? /* @__PURE__ */ jsx18(KeyValue, { rows: body.rows.slice(clamped, clamped + window), width: inner }) : body.kind === "text" ? interactive ? body.text.split("\n").slice(clamped, clamped + window).map((line, i) => /* @__PURE__ */ jsx18(Text17, { wrap: "truncate-end", children: line }, clamped + i)) : /* @__PURE__ */ jsx18(Markdown, { text: body.text, width: inner }) : /* @__PURE__ */ jsx18(Text17, { children: theme.dim("No output") }),
2304
- interactive && total > window ? /* @__PURE__ */ jsx18(Text17, { children: theme.dim(`${clamped + 1}\u2013${Math.min(total, clamped + window)} of ${total}`) }) : null
3476
+ return /* @__PURE__ */ jsxs17(Box19, { flexDirection: "column", width, children: [
3477
+ /* @__PURE__ */ jsx21(Panel, { title: heading, right, width, tone: ok ? "muted" : "danger", children: /* @__PURE__ */ jsx21(Text19, { wrap: "truncate-end", children: error ? theme.error(error.message) : theme.dim(summaryLine(body)) }) }),
3478
+ note ? /* @__PURE__ */ jsx21(Text19, { wrap: "truncate-end", children: note }) : null,
3479
+ /* @__PURE__ */ jsxs17(Box19, { flexDirection: "column", paddingLeft: 2, width, children: [
3480
+ error ? /* @__PURE__ */ jsx21(ErrorBody, { error }) : view === "json" ? jsonLines.slice(clamped, clamped + window).map((line, i) => /* @__PURE__ */ jsx21(Text19, { wrap: "truncate-end", children: line }, clamped + i)) : body.kind === "table" ? /* @__PURE__ */ jsx21(Table, { columns: body.columns, rows: body.rows, width: inner, offset: clamped, limit: window }) : body.kind === "kv" ? /* @__PURE__ */ jsx21(KeyValue, { rows: body.rows.slice(clamped, clamped + window), width: inner }) : body.kind === "text" ? interactive ? body.text.split("\n").slice(clamped, clamped + window).map((line, i) => /* @__PURE__ */ jsx21(Text19, { wrap: "truncate-end", children: line }, clamped + i)) : /* @__PURE__ */ jsx21(Markdown, { text: body.text, width: inner }) : /* @__PURE__ */ jsx21(Text19, { children: theme.dim("No output") }),
3481
+ interactive && total > window ? /* @__PURE__ */ jsx21(Text19, { children: theme.dim(`${clamped + 1}\u2013${Math.min(total, clamped + window)} of ${total}`) }) : null
2305
3482
  ] }),
2306
- interactive ? /* @__PURE__ */ jsx18(Box17, { marginTop: 1, children: /* @__PURE__ */ jsx18(KeyHints, { width, hints }) }) : null
3483
+ interactive ? /* @__PURE__ */ jsx21(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx21(KeyHints, { width, hints }) }) : null
2307
3484
  ] });
2308
3485
  }
2309
3486
 
2310
3487
  // src/ui/screens/Approvals.tsx
2311
- import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3488
+ import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
2312
3489
  function argsOf(item) {
2313
3490
  return typeof item.args === "object" && item.args !== null && !Array.isArray(item.args) ? item.args : {};
2314
3491
  }
2315
3492
  function Approvals({ services, width, height, onExit }) {
2316
- const [phase, setPhase] = useState6({ name: "loading", since: Date.now() });
2317
- const mounted = useRef5(true);
2318
- useEffect6(
3493
+ const [phase, setPhase] = useState7({ name: "loading", since: Date.now() });
3494
+ const mounted = useRef8(true);
3495
+ useEffect9(
2319
3496
  () => () => {
2320
3497
  mounted.current = false;
2321
3498
  },
2322
3499
  []
2323
3500
  );
2324
- const load = useCallback4(async () => {
3501
+ const load = useCallback6(async () => {
2325
3502
  if (!services.account.hasSession) {
2326
3503
  setPhase({ name: "failed", message: "Approvals need a full sign-in", hint: "Run `tabbio login`; a personal MCP token cannot approve." });
2327
3504
  return;
@@ -2335,10 +3512,10 @@ function Approvals({ services, width, height, onExit }) {
2335
3512
  if (mounted.current) setPhase({ name: "failed", message: cli.message, hint: cli.hint });
2336
3513
  }
2337
3514
  }, [services]);
2338
- useEffect6(() => {
3515
+ useEffect9(() => {
2339
3516
  void load();
2340
3517
  }, [load]);
2341
- const decide = useCallback4(
3518
+ const decide = useCallback6(
2342
3519
  async (decision) => {
2343
3520
  if (phase.name !== "card") return;
2344
3521
  if (decision === "leave") {
@@ -2362,7 +3539,7 @@ function Approvals({ services, width, height, onExit }) {
2362
3539
  },
2363
3540
  [phase, services]
2364
3541
  );
2365
- useInput7(
3542
+ useInput9(
2366
3543
  (input, key) => {
2367
3544
  if (phase.name === "done" && phase.decision !== "approve" && (key.return || key.escape || input === "q")) void load();
2368
3545
  else if (phase.name === "failed" && (key.escape || input === "q" || key.return)) onExit();
@@ -2371,27 +3548,27 @@ function Approvals({ services, width, height, onExit }) {
2371
3548
  { isActive: phase.name === "done" && phase.decision !== "approve" || phase.name === "failed" || phase.name === "loading" }
2372
3549
  );
2373
3550
  const count = phase.name === "list" || phase.name === "card" ? phase.items.length : null;
2374
- const header = /* @__PURE__ */ jsx19(Box18, { marginBottom: 1, children: /* @__PURE__ */ jsxs17(Text18, { children: [
3551
+ const header = /* @__PURE__ */ jsx22(Box20, { marginBottom: 1, children: /* @__PURE__ */ jsxs18(Text20, { children: [
2375
3552
  theme.bold("Approvals"),
2376
3553
  count !== null ? theme.dim(` ${theme.symbols.middot} ${count} waiting`) : ""
2377
3554
  ] }) });
2378
3555
  switch (phase.name) {
2379
3556
  case "loading":
2380
- return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
3557
+ return /* @__PURE__ */ jsxs18(Box20, { flexDirection: "column", width, children: [
2381
3558
  header,
2382
- /* @__PURE__ */ jsx19(Working, { label: "Loading approvals\u2026", since: phase.since })
3559
+ /* @__PURE__ */ jsx22(Working, { label: "Loading approvals\u2026", since: phase.since })
2383
3560
  ] });
2384
3561
  case "failed":
2385
- return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
3562
+ return /* @__PURE__ */ jsxs18(Box20, { flexDirection: "column", width, children: [
2386
3563
  header,
2387
- /* @__PURE__ */ jsx19(Text18, { children: theme.error(`${theme.symbols.error} ${phase.message}`) }),
2388
- phase.hint ? /* @__PURE__ */ jsx19(Text18, { wrap: "wrap", children: theme.dim(` ${theme.symbols.arrowRight} ${phase.hint}`) }) : null,
2389
- /* @__PURE__ */ jsx19(Box18, { marginTop: 1, children: /* @__PURE__ */ jsx19(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) })
3564
+ /* @__PURE__ */ jsx22(Text20, { children: theme.error(`${theme.symbols.error} ${phase.message}`) }),
3565
+ phase.hint ? /* @__PURE__ */ jsx22(Text20, { wrap: "wrap", children: theme.dim(` ${theme.symbols.arrowRight} ${phase.hint}`) }) : null,
3566
+ /* @__PURE__ */ jsx22(Box20, { marginTop: 1, children: /* @__PURE__ */ jsx22(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) })
2390
3567
  ] });
2391
3568
  case "list":
2392
- return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
3569
+ return /* @__PURE__ */ jsxs18(Box20, { flexDirection: "column", width, children: [
2393
3570
  header,
2394
- /* @__PURE__ */ jsx19(
3571
+ /* @__PURE__ */ jsx22(
2395
3572
  SelectList,
2396
3573
  {
2397
3574
  items: phase.items.map((item) => ({
@@ -2413,9 +3590,9 @@ function Approvals({ services, width, height, onExit }) {
2413
3590
  )
2414
3591
  ] });
2415
3592
  case "card":
2416
- return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
3593
+ return /* @__PURE__ */ jsxs18(Box20, { flexDirection: "column", width, children: [
2417
3594
  header,
2418
- /* @__PURE__ */ jsx19(
3595
+ /* @__PURE__ */ jsx22(
2419
3596
  ApprovalCard,
2420
3597
  {
2421
3598
  approvalId: phase.item.id,
@@ -2432,9 +3609,9 @@ function Approvals({ services, width, height, onExit }) {
2432
3609
  )
2433
3610
  ] });
2434
3611
  case "done":
2435
- return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
3612
+ return /* @__PURE__ */ jsxs18(Box20, { flexDirection: "column", width, children: [
2436
3613
  header,
2437
- phase.decision === "approve" ? /* @__PURE__ */ jsx19(
3614
+ phase.decision === "approve" ? /* @__PURE__ */ jsx22(
2438
3615
  ResultPanel,
2439
3616
  {
2440
3617
  title: sentenceCase(phase.item.toolName),
@@ -2446,41 +3623,41 @@ function Approvals({ services, width, height, onExit }) {
2446
3623
  interactive: true,
2447
3624
  onBack: () => void load()
2448
3625
  }
2449
- ) : /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", children: [
2450
- /* @__PURE__ */ jsx19(Text18, { children: approvalOutcomeText(phase.decision, sentenceCase(phase.item.toolName), phase.item.id, phase.durationMs) }),
2451
- /* @__PURE__ */ jsx19(Box18, { marginTop: 1, children: /* @__PURE__ */ jsx19(KeyHints, { width, hints: [{ key: "enter", label: "back to approvals" }] }) })
3626
+ ) : /* @__PURE__ */ jsxs18(Box20, { flexDirection: "column", children: [
3627
+ /* @__PURE__ */ jsx22(Text20, { children: approvalOutcomeText(phase.decision, sentenceCase(phase.item.toolName), phase.item.id, phase.durationMs) }),
3628
+ /* @__PURE__ */ jsx22(Box20, { marginTop: 1, children: /* @__PURE__ */ jsx22(KeyHints, { width, hints: [{ key: "enter", label: "back to approvals" }] }) })
2452
3629
  ] })
2453
3630
  ] });
2454
3631
  }
2455
3632
  }
2456
3633
 
2457
3634
  // src/ui/screens/ToolBrowser.tsx
2458
- import { Box as Box20, Text as Text20, useInput as useInput9 } from "ink";
2459
- import { memo as memo15, useMemo as useMemo6, useState as useState8 } from "react";
3635
+ import { Box as Box22, Text as Text22, useInput as useInput11 } from "ink";
3636
+ import { memo as memo16, useMemo as useMemo7, useState as useState9 } from "react";
2460
3637
 
2461
3638
  // src/ui/screens/ToolRun.tsx
2462
- import { Box as Box19, Text as Text19, useInput as useInput8 } from "ink";
2463
- import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef6, useState as useState7 } from "react";
2464
- import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
3639
+ import { Box as Box21, Text as Text21, useInput as useInput10 } from "ink";
3640
+ import { useCallback as useCallback7, useEffect as useEffect10, useRef as useRef9, useState as useState8 } from "react";
3641
+ import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
2465
3642
  function toolLabel(tool) {
2466
3643
  return tool.commandPath.join(" ") || tool.id;
2467
3644
  }
2468
3645
  function ToolRun({ tool, services, width, height, initialInput = {}, start, onExit }) {
2469
- const [phase, setPhase] = useState7({ name: "form", input: initialInput });
2470
- const mounted = useRef6(true);
2471
- const controller = useRef6(null);
2472
- useEffect7(
3646
+ const [phase, setPhase] = useState8({ name: "form", input: initialInput });
3647
+ const mounted = useRef9(true);
3648
+ const controller = useRef9(null);
3649
+ useEffect10(
2473
3650
  () => () => {
2474
3651
  mounted.current = false;
2475
3652
  controller.current?.abort();
2476
3653
  },
2477
3654
  []
2478
3655
  );
2479
- const withDefaults = useCallback5(
3656
+ const withDefaults = useCallback7(
2480
3657
  (input) => autoFilledProperties(tool, tool.inputSchema).includes("userId") && input.userId === void 0 && services.account.userId ? { ...input, userId: services.account.userId } : input,
2481
3658
  [tool, services.account.userId]
2482
3659
  );
2483
- const run = useCallback5(
3660
+ const run = useCallback7(
2484
3661
  async (input) => {
2485
3662
  const startedAt = Date.now();
2486
3663
  controller.current?.abort();
@@ -2503,13 +3680,13 @@ function ToolRun({ tool, services, width, height, initialInput = {}, start, onEx
2503
3680
  },
2504
3681
  [services, tool.id, withDefaults]
2505
3682
  );
2506
- const started = useRef6(false);
2507
- useEffect7(() => {
3683
+ const started = useRef9(false);
3684
+ useEffect10(() => {
2508
3685
  if (started.current || start !== "run") return;
2509
3686
  started.current = true;
2510
3687
  void run(initialInput);
2511
3688
  }, [start, run, initialInput]);
2512
- const decide = useCallback5(
3689
+ const decide = useCallback7(
2513
3690
  async (decision) => {
2514
3691
  if (phase.name !== "approval") return;
2515
3692
  const { approval, input, requestedAt } = phase;
@@ -2536,7 +3713,7 @@ function ToolRun({ tool, services, width, height, initialInput = {}, start, onEx
2536
3713
  },
2537
3714
  [phase, services, tool.title]
2538
3715
  );
2539
- useInput8(
3716
+ useInput10(
2540
3717
  (input, key) => {
2541
3718
  if (phase.name === "running" && key.escape) {
2542
3719
  controller.current?.abort();
@@ -2550,19 +3727,19 @@ function ToolRun({ tool, services, width, height, initialInput = {}, start, onEx
2550
3727
  { isActive: phase.name === "running" || phase.name === "error" || phase.name === "left" }
2551
3728
  );
2552
3729
  const label = toolLabel(tool);
2553
- const header = /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", marginBottom: 1, children: [
2554
- /* @__PURE__ */ jsxs18(Text19, { wrap: "truncate-end", children: [
3730
+ const header = /* @__PURE__ */ jsxs19(Box21, { flexDirection: "column", marginBottom: 1, children: [
3731
+ /* @__PURE__ */ jsxs19(Text21, { wrap: "truncate-end", children: [
2555
3732
  theme.bold(label),
2556
3733
  " ",
2557
3734
  theme.dim(`${theme.symbols.middot} ${oneLine(tool.title)}`)
2558
3735
  ] }),
2559
- tool.description ? /* @__PURE__ */ jsx20(Text19, { wrap: "truncate-end", children: theme.dim(oneLine(tool.description)) }) : null
3736
+ tool.description ? /* @__PURE__ */ jsx23(Text21, { wrap: "truncate-end", children: theme.dim(oneLine(tool.description)) }) : null
2560
3737
  ] });
2561
3738
  switch (phase.name) {
2562
3739
  case "form":
2563
- return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
3740
+ return /* @__PURE__ */ jsxs19(Box21, { flexDirection: "column", width, children: [
2564
3741
  header,
2565
- /* @__PURE__ */ jsx20(
3742
+ /* @__PURE__ */ jsx23(
2566
3743
  Form,
2567
3744
  {
2568
3745
  initial: createFormState(tool.inputSchema, phase.input, { skip: autoFilledProperties(tool, tool.inputSchema) }),
@@ -2575,15 +3752,15 @@ function ToolRun({ tool, services, width, height, initialInput = {}, start, onEx
2575
3752
  )
2576
3753
  ] });
2577
3754
  case "running":
2578
- return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
3755
+ return /* @__PURE__ */ jsxs19(Box21, { flexDirection: "column", width, children: [
2579
3756
  header,
2580
- /* @__PURE__ */ jsx20(Working, { label: `Running ${label}${theme.symbols.ellipsis}`, since: phase.startedAt }),
2581
- /* @__PURE__ */ jsx20(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx20(KeyHints, { width, hints: [{ key: "esc", label: "cancel" }] }) })
3757
+ /* @__PURE__ */ jsx23(Working, { label: `Running ${label}${theme.symbols.ellipsis}`, since: phase.startedAt }),
3758
+ /* @__PURE__ */ jsx23(Box21, { marginTop: 1, children: /* @__PURE__ */ jsx23(KeyHints, { width, hints: [{ key: "esc", label: "cancel" }] }) })
2582
3759
  ] });
2583
3760
  case "approval":
2584
- return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
3761
+ return /* @__PURE__ */ jsxs19(Box21, { flexDirection: "column", width, children: [
2585
3762
  header,
2586
- /* @__PURE__ */ jsx20(
3763
+ /* @__PURE__ */ jsx23(
2587
3764
  ApprovalCard,
2588
3765
  {
2589
3766
  approvalId: phase.approval.approvalId,
@@ -2601,7 +3778,7 @@ function ToolRun({ tool, services, width, height, initialInput = {}, start, onEx
2601
3778
  )
2602
3779
  ] });
2603
3780
  case "result":
2604
- return /* @__PURE__ */ jsx20(
3781
+ return /* @__PURE__ */ jsx23(
2605
3782
  ResultPanel,
2606
3783
  {
2607
3784
  title: label,
@@ -2618,18 +3795,18 @@ function ToolRun({ tool, services, width, height, initialInput = {}, start, onEx
2618
3795
  }
2619
3796
  );
2620
3797
  case "error":
2621
- return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
2622
- /* @__PURE__ */ jsx20(ResultPanel, { title: label, error: phase.error, durationMs: phase.durationMs, width, height, interactive: false }),
2623
- /* @__PURE__ */ jsx20(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx20(KeyHints, { width, hints: [{ key: "r", label: "retry" }, { key: "e", label: "edit input" }, { key: "q", label: "back" }] }) })
3798
+ return /* @__PURE__ */ jsxs19(Box21, { flexDirection: "column", width, children: [
3799
+ /* @__PURE__ */ jsx23(ResultPanel, { title: label, error: phase.error, durationMs: phase.durationMs, width, height, interactive: false }),
3800
+ /* @__PURE__ */ jsx23(Box21, { marginTop: 1, children: /* @__PURE__ */ jsx23(KeyHints, { width, hints: [{ key: "r", label: "retry" }, { key: "e", label: "edit input" }, { key: "q", label: "back" }] }) })
2624
3801
  ] });
2625
3802
  case "left":
2626
- return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
3803
+ return /* @__PURE__ */ jsxs19(Box21, { flexDirection: "column", width, children: [
2627
3804
  header,
2628
- /* @__PURE__ */ jsx20(Text19, { children: approvalOutcomeText("leave", sentenceCase(phase.approval.toolName || tool.title), phase.approval.approvalId) }),
2629
- /* @__PURE__ */ jsx20(Text19, { wrap: "wrap", children: theme.dim(
3805
+ /* @__PURE__ */ jsx23(Text21, { children: approvalOutcomeText("leave", sentenceCase(phase.approval.toolName || tool.title), phase.approval.approvalId) }),
3806
+ /* @__PURE__ */ jsx23(Text21, { wrap: "wrap", children: theme.dim(
2630
3807
  services.account.hasSession ? "Decide later in the Tabbio app or with `tabbio approvals`." : `Approve it in the Tabbio app, or run \`tabbio approvals wait ${shortId(phase.approval.approvalId, 12, 4)}\`.`
2631
3808
  ) }),
2632
- /* @__PURE__ */ jsx20(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx20(KeyHints, { width, hints: [{ key: "r", label: "run again" }, { key: "e", label: "edit input" }, { key: "q", label: "back" }] }) })
3809
+ /* @__PURE__ */ jsx23(Box21, { marginTop: 1, children: /* @__PURE__ */ jsx23(KeyHints, { width, hints: [{ key: "r", label: "run again" }, { key: "e", label: "edit input" }, { key: "q", label: "back" }] }) })
2633
3810
  ] });
2634
3811
  }
2635
3812
  }
@@ -2727,7 +3904,7 @@ function browserKeyAction(state, input, key, rows) {
2727
3904
  }
2728
3905
 
2729
3906
  // src/ui/screens/ToolBrowser.tsx
2730
- import { jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
3907
+ import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
2731
3908
  var TWO_PANE_MIN = fromTerminal(90);
2732
3909
  function badge(tool) {
2733
3910
  if (tool.kind === "workflow") return theme.dim("workflow");
@@ -2735,14 +3912,14 @@ function badge(tool) {
2735
3912
  if (tool.annotations.destructiveHint === true) return theme.danger("write");
2736
3913
  return tool.readOnly ? theme.dim("read") : "write";
2737
3914
  }
2738
- var ListRow = memo15(function ListRow2({ row, selected, width }) {
3915
+ var ListRow = memo16(function ListRow2({ row, selected, width }) {
2739
3916
  const pointer = selected ? theme.accent(theme.symbols.pointer) : " ";
2740
3917
  if (row.kind === "group") {
2741
3918
  const glyph2 = row.expanded ? theme.symbols.expanded : theme.symbols.collapsed;
2742
3919
  const count = theme.dim(row.matched === row.total ? String(row.total) : `${row.matched} of ${row.total}`);
2743
3920
  const name = truncate(row.group, Math.max(4, width - 12));
2744
3921
  const fill = Math.max(1, width - 4 - displayWidth(name) - displayWidth(count));
2745
- return /* @__PURE__ */ jsxs19(Text20, { wrap: "truncate-end", children: [
3922
+ return /* @__PURE__ */ jsxs20(Text22, { wrap: "truncate-end", children: [
2746
3923
  pointer,
2747
3924
  " ",
2748
3925
  theme.dim(glyph2),
@@ -2755,34 +3932,34 @@ var ListRow = memo15(function ListRow2({ row, selected, width }) {
2755
3932
  const label = row.tool.kind === "workflow" ? row.tool.action : row.tool.commandPath.join(" ");
2756
3933
  const tag = badge(row.tool);
2757
3934
  const labelWidth = Math.max(4, width - 4 - displayWidth(tag) - 1);
2758
- const text = padEnd(truncate(label, labelWidth), labelWidth);
2759
- return /* @__PURE__ */ jsxs19(Text20, { wrap: "truncate-end", children: [
3935
+ const text2 = padEnd(truncate(label, labelWidth), labelWidth);
3936
+ return /* @__PURE__ */ jsxs20(Text22, { wrap: "truncate-end", children: [
2760
3937
  pointer,
2761
3938
  " ",
2762
- selected ? theme.accent(text) : text,
3939
+ selected ? theme.accent(text2) : text2,
2763
3940
  " ",
2764
3941
  tag
2765
3942
  ] });
2766
3943
  });
2767
- var ToolDetail = memo15(function ToolDetail2({ tool, width, height }) {
2768
- const flags = useMemo6(() => describeSchema(tool.inputSchema), [tool]);
2769
- const example = useMemo6(() => exampleInvocation(tool, tool.inputSchema), [tool]);
3944
+ var ToolDetail = memo16(function ToolDetail2({ tool, width, height }) {
3945
+ const flags = useMemo7(() => describeSchema(tool.inputSchema), [tool]);
3946
+ const example = useMemo7(() => exampleInvocation(tool, tool.inputSchema), [tool]);
2770
3947
  const flagWidth = Math.min(28, Math.max(8, ...flags.map((f) => displayWidth(`${f.flag} <${f.type}>`))));
2771
3948
  const descLines = Math.max(1, Math.min(4, height - flags.length - 8));
2772
3949
  const lines = wrapWords(tool.description, width, descLines);
2773
3950
  const shownFlags = flags.slice(0, Math.max(1, height - 7 - lines.length));
2774
- return /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", width, children: [
2775
- /* @__PURE__ */ jsxs19(Box20, { width, children: [
2776
- /* @__PURE__ */ jsx21(Box20, { flexGrow: 1, children: /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.bold(tool.commandPath.join(" ")) }) }),
2777
- /* @__PURE__ */ jsx21(Text20, { children: badge(tool) })
3951
+ return /* @__PURE__ */ jsxs20(Box22, { flexDirection: "column", width, children: [
3952
+ /* @__PURE__ */ jsxs20(Box22, { width, children: [
3953
+ /* @__PURE__ */ jsx24(Box22, { flexGrow: 1, children: /* @__PURE__ */ jsx24(Text22, { wrap: "truncate-end", children: theme.bold(tool.commandPath.join(" ")) }) }),
3954
+ /* @__PURE__ */ jsx24(Text22, { children: badge(tool) })
2778
3955
  ] }),
2779
- /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: oneLine(tool.title) }),
2780
- /* @__PURE__ */ jsx21(Text20, { children: " " }),
2781
- lines.map((line, i) => /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.dim(line) }, i)),
2782
- /* @__PURE__ */ jsx21(Text20, { children: " " }),
2783
- /* @__PURE__ */ jsx21(Text20, { children: theme.dim("FLAGS") }),
2784
- flags.length === 0 ? /* @__PURE__ */ jsx21(Text20, { children: theme.dim(" none") }) : null,
2785
- shownFlags.map((flag) => /* @__PURE__ */ jsxs19(Text20, { wrap: "truncate-end", children: [
3956
+ /* @__PURE__ */ jsx24(Text22, { wrap: "truncate-end", children: oneLine(tool.title) }),
3957
+ /* @__PURE__ */ jsx24(Text22, { children: " " }),
3958
+ lines.map((line, i) => /* @__PURE__ */ jsx24(Text22, { wrap: "truncate-end", children: theme.dim(line) }, i)),
3959
+ /* @__PURE__ */ jsx24(Text22, { children: " " }),
3960
+ /* @__PURE__ */ jsx24(Text22, { children: theme.dim("FLAGS") }),
3961
+ flags.length === 0 ? /* @__PURE__ */ jsx24(Text22, { children: theme.dim(" none") }) : null,
3962
+ shownFlags.map((flag) => /* @__PURE__ */ jsxs20(Text22, { wrap: "truncate-end", children: [
2786
3963
  " ",
2787
3964
  padEnd(truncate(`${flag.flag} <${flag.type}>`, flagWidth), flagWidth),
2788
3965
  " ",
@@ -2790,10 +3967,10 @@ var ToolDetail = memo15(function ToolDetail2({ tool, width, height }) {
2790
3967
  " ",
2791
3968
  theme.dim(flag.description)
2792
3969
  ] }, flag.flag)),
2793
- flags.length > shownFlags.length ? /* @__PURE__ */ jsx21(Text20, { children: theme.dim(` +${flags.length - shownFlags.length} more`) }) : null,
2794
- /* @__PURE__ */ jsx21(Text20, { children: " " }),
2795
- /* @__PURE__ */ jsx21(Text20, { children: theme.dim("EXAMPLE") }),
2796
- /* @__PURE__ */ jsxs19(Text20, { wrap: "truncate-end", children: [
3970
+ flags.length > shownFlags.length ? /* @__PURE__ */ jsx24(Text22, { children: theme.dim(` +${flags.length - shownFlags.length} more`) }) : null,
3971
+ /* @__PURE__ */ jsx24(Text22, { children: " " }),
3972
+ /* @__PURE__ */ jsx24(Text22, { children: theme.dim("EXAMPLE") }),
3973
+ /* @__PURE__ */ jsxs20(Text22, { wrap: "truncate-end", children: [
2797
3974
  " ",
2798
3975
  example
2799
3976
  ] })
@@ -2801,15 +3978,15 @@ var ToolDetail = memo15(function ToolDetail2({ tool, width, height }) {
2801
3978
  });
2802
3979
  function ToolBrowser(props) {
2803
3980
  const { tools, services, width, height, onExit } = props;
2804
- const [state, setState] = useState8(() => {
3981
+ const [state, setState] = useState9(() => {
2805
3982
  const initial = initialBrowserState({ query: props.initialQuery, group: props.initialGroup });
2806
3983
  return initial.query ? applyBrowserAction(tools, { ...initial, query: "" }, { type: "type", text: initial.query }) : initial;
2807
3984
  });
2808
- const [running, setRunning] = useState8(null);
2809
- const rows = useMemo6(() => browserRows(tools, state), [tools, state]);
3985
+ const [running, setRunning] = useState9(null);
3986
+ const rows = useMemo7(() => browserRows(tools, state), [tools, state]);
2810
3987
  const cursor = Math.min(state.cursor, Math.max(0, rows.length - 1));
2811
3988
  const current = rows[cursor];
2812
- useInput9(
3989
+ useInput11(
2813
3990
  (input, key) => {
2814
3991
  const outcome = browserKeyAction({ ...state, cursor }, input, key, rows);
2815
3992
  if (!outcome) return;
@@ -2820,7 +3997,7 @@ function ToolBrowser(props) {
2820
3997
  { isActive: !running }
2821
3998
  );
2822
3999
  if (running) {
2823
- return /* @__PURE__ */ jsx21(ToolRun, { tool: running, services, width, height, start: "form", onExit: () => setRunning(null) });
4000
+ return /* @__PURE__ */ jsx24(ToolRun, { tool: running, services, width, height, start: "form", onExit: () => setRunning(null) });
2824
4001
  }
2825
4002
  const visibleCount = tools.filter((t) => !t.hidden).length;
2826
4003
  const groupCount = new Set(tools.filter((t) => !t.hidden).map((t) => t.group)).size;
@@ -2835,16 +4012,16 @@ function ToolBrowser(props) {
2835
4012
  twoPane ? `${theme.symbols.line.repeat(leftWidth + 1)}\u252C${theme.symbols.line.repeat(Math.max(0, rightWidth + 1))}` : theme.symbols.line.repeat(width)
2836
4013
  );
2837
4014
  const detailTool = current?.kind === "tool" ? current.tool : null;
2838
- return /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", width, children: [
2839
- /* @__PURE__ */ jsxs19(Box20, { width, children: [
2840
- /* @__PURE__ */ jsx21(Box20, { flexGrow: 1, children: /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: title }) }),
2841
- /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: filter })
4015
+ return /* @__PURE__ */ jsxs20(Box22, { flexDirection: "column", width, children: [
4016
+ /* @__PURE__ */ jsxs20(Box22, { width, children: [
4017
+ /* @__PURE__ */ jsx24(Box22, { flexGrow: 1, children: /* @__PURE__ */ jsx24(Text22, { wrap: "truncate-end", children: title }) }),
4018
+ /* @__PURE__ */ jsx24(Text22, { wrap: "truncate-end", children: filter })
2842
4019
  ] }),
2843
- /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.unicode ? rule : theme.dim("-".repeat(width)) }),
2844
- /* @__PURE__ */ jsxs19(Box20, { width, children: [
2845
- /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", width: leftWidth, children: [
2846
- rows.length === 0 ? /* @__PURE__ */ jsx21(Text20, { children: theme.dim(` No tools match "${state.query}"`) }) : null,
2847
- rows.slice(start, end).map((row, i) => /* @__PURE__ */ jsx21(
4020
+ /* @__PURE__ */ jsx24(Text22, { wrap: "truncate-end", children: theme.unicode ? rule : theme.dim("-".repeat(width)) }),
4021
+ /* @__PURE__ */ jsxs20(Box22, { width, children: [
4022
+ /* @__PURE__ */ jsxs20(Box22, { flexDirection: "column", width: leftWidth, children: [
4023
+ rows.length === 0 ? /* @__PURE__ */ jsx24(Text22, { children: theme.dim(` No tools match "${state.query}"`) }) : null,
4024
+ rows.slice(start, end).map((row, i) => /* @__PURE__ */ jsx24(
2848
4025
  ListRow,
2849
4026
  {
2850
4027
  row,
@@ -2854,11 +4031,11 @@ function ToolBrowser(props) {
2854
4031
  row.kind === "group" ? `g:${row.group}` : `t:${row.tool.id}`
2855
4032
  ))
2856
4033
  ] }),
2857
- twoPane ? /* @__PURE__ */ jsx21(Box20, { flexDirection: "column", marginLeft: 1, paddingLeft: 1, borderStyle: theme.unicode ? "single" : "classic", borderTop: false, borderRight: false, borderBottom: false, borderDimColor: true, width: rightWidth + 2, children: detailTool ? /* @__PURE__ */ jsx21(ToolDetail, { tool: detailTool, width: rightWidth, height: listHeight }) : /* @__PURE__ */ jsx21(Text20, { children: theme.dim(current?.kind === "group" ? `${current.group}: enter or \u2192 to open the group` : "Select a tool") }) }) : null
4034
+ twoPane ? /* @__PURE__ */ jsx24(Box22, { flexDirection: "column", marginLeft: 1, paddingLeft: 1, borderStyle: theme.unicode ? "single" : "classic", borderTop: false, borderRight: false, borderBottom: false, borderDimColor: true, width: rightWidth + 2, children: detailTool ? /* @__PURE__ */ jsx24(ToolDetail, { tool: detailTool, width: rightWidth, height: listHeight }) : /* @__PURE__ */ jsx24(Text22, { children: theme.dim(current?.kind === "group" ? `${current.group}: enter or \u2192 to open the group` : "Select a tool") }) }) : null
2858
4035
  ] }),
2859
- !twoPane && detailTool ? /* @__PURE__ */ jsx21(Box20, { marginTop: 1, children: /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.dim(oneLine(detailTool.description || detailTool.title)) }) }) : null,
2860
- /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.dim(theme.symbols.line.repeat(width)) }),
2861
- /* @__PURE__ */ jsx21(
4036
+ !twoPane && detailTool ? /* @__PURE__ */ jsx24(Box22, { marginTop: 1, children: /* @__PURE__ */ jsx24(Text22, { wrap: "truncate-end", children: theme.dim(oneLine(detailTool.description || detailTool.title)) }) }) : null,
4037
+ /* @__PURE__ */ jsx24(Text22, { wrap: "truncate-end", children: theme.dim(theme.symbols.line.repeat(width)) }),
4038
+ /* @__PURE__ */ jsx24(
2862
4039
  KeyHints,
2863
4040
  {
2864
4041
  width,
@@ -2876,7 +4053,14 @@ function ToolBrowser(props) {
2876
4053
 
2877
4054
  // src/ui/screens/home/home-model.ts
2878
4055
  var emptyHomeData = { plan: null, pendingApprovals: null, recent: null };
2879
- function homeCommands(data, toolCount) {
4056
+ function homeCommands(data, toolCount, access = { signedIn: true, hasSession: true }) {
4057
+ if (!access.signedIn) {
4058
+ return [
4059
+ { name: "login", usage: "/login [email|token]", description: "Sign in (browser, email code or token)" },
4060
+ { name: "help", description: "Keys and commands" },
4061
+ { name: "exit", description: "Quit tabbio" }
4062
+ ];
4063
+ }
2880
4064
  const waiting = data.pendingApprovals;
2881
4065
  return [
2882
4066
  { name: "chat", description: "Chat with Tabbio" },
@@ -2884,34 +4068,50 @@ function homeCommands(data, toolCount) {
2884
4068
  { name: "approvals", description: waiting === null ? "Approve or reject actions" : waiting > 0 ? `${waiting} waiting` : "None waiting" },
2885
4069
  { name: "status", description: "Account, tokens, catalog" },
2886
4070
  { name: "help", description: "Keys and commands" },
4071
+ { name: "login", usage: "/login [email|token]", description: access.hasSession ? "Switch account" : "Full sign-in for chat and approvals" },
4072
+ { name: "logout", description: "Sign out of this profile" },
2887
4073
  { name: "exit", description: "Quit tabbio" }
2888
4074
  ];
2889
4075
  }
2890
- function isHomeCommand(name) {
2891
- return ["chat", "tools", "approvals", "status", "help", "exit", "quit"].includes(name);
4076
+ function startCommands(commands, signedIn) {
4077
+ return commands.filter((c) => c.name !== "exit" && (!signedIn || c.name !== "login" && c.name !== "logout"));
4078
+ }
4079
+ var HOME_COMMAND_NAMES = ["chat", "tools", "approvals", "status", "login", "logout", "help", "exit", "quit"];
4080
+ var SIGN_IN_FIRST = "Sign in first: /login";
4081
+ function homeGate(name, access) {
4082
+ if (name === "login" || name === "help" || name === "exit" || name === "quit") return null;
4083
+ if (!access.signedIn) return SIGN_IN_FIRST;
4084
+ if ((name === "chat" || name === "approvals" || name === "message" || name === "skill") && !access.hasSession) {
4085
+ return `${name === "approvals" ? "Approvals need" : "Chat needs"} a full sign-in: /login (browser or email code)`;
4086
+ }
4087
+ return null;
2892
4088
  }
2893
4089
  var HOME_HELP_ROWS = [
2894
4090
  ["type + enter", "Start a chat with that message"],
2895
- ["/", "Command palette (chat, tools, approvals, status, help, exit)"],
4091
+ ["/", "Command palette: /chat /tools /approvals /status /login /logout /help /exit"],
4092
+ ["/login", "Sign in here: browser, /login email you@x.com, or /login token"],
4093
+ ["/logout", "Sign out of this profile (ends the session on the server)"],
4094
+ ["/<skill>", "Your Tabbio skills are listed in the palette; enter runs one in a chat"],
2896
4095
  ["tab", "Jump to recent chats"],
2897
- ["esc", "Close, go back, or interrupt"],
4096
+ ["esc", "Close, go back, or clear the composer"],
4097
+ ["q", "Back from lists and help"],
2898
4098
  ["ctrl+j", "New line in the composer (or \\ then enter)"],
2899
4099
  ["\u2191 \u2193", "Composer history, or move in lists"],
2900
- ["ctrl+c \xD72", "Quit"]
4100
+ ["ctrl+c twice", "Quit"]
2901
4101
  ];
2902
4102
 
2903
4103
  // src/ui/screens/home/HomeView.tsx
2904
- import { Box as Box23, Text as Text23 } from "ink";
2905
- import { memo as memo17 } from "react";
4104
+ import { Box as Box25, Text as Text25 } from "ink";
4105
+ import { memo as memo18 } from "react";
2906
4106
 
2907
4107
  // src/ui/components/Header.tsx
2908
- import { Box as Box22, Text as Text22 } from "ink";
2909
- import { memo as memo16 } from "react";
4108
+ import { Box as Box24, Text as Text24 } from "ink";
4109
+ import { memo as memo17 } from "react";
2910
4110
 
2911
4111
  // src/ui/components/Wordmark.tsx
2912
- import { Box as Box21, Text as Text21 } from "ink";
4112
+ import { Box as Box23, Text as Text23 } from "ink";
2913
4113
  import Gradient from "ink-gradient";
2914
- import { jsx as jsx22 } from "react/jsx-runtime";
4114
+ import { jsx as jsx25 } from "react/jsx-runtime";
2915
4115
  var WORDMARK_ROWS = [
2916
4116
  " \u2588 \u2588 \u2588 \u2580",
2917
4117
  "\u2580\u2588\u2580 \u2584\u2580\u2580\u2588 \u2588\u2580\u2580\u2584 \u2588\u2580\u2580\u2584 \u2588 \u2584\u2580\u2580\u2584",
@@ -2919,17 +4119,17 @@ var WORDMARK_ROWS = [
2919
4119
  ];
2920
4120
  var WORDMARK_WIDTH = 25;
2921
4121
  function Wordmark() {
2922
- if (!theme.unicode) return /* @__PURE__ */ jsx22(Text21, { children: theme.accentBold("tabbio") });
4122
+ if (!theme.unicode) return /* @__PURE__ */ jsx25(Text23, { children: theme.accentBold("tabbio") });
2923
4123
  const rows = WORDMARK_ROWS.map((row) => row.padEnd(WORDMARK_WIDTH));
2924
4124
  const gradient = theme.wordmarkGradient;
2925
4125
  if (gradient) {
2926
- return /* @__PURE__ */ jsx22(Gradient, { colors: [...gradient], children: /* @__PURE__ */ jsx22(Text21, { children: rows.join("\n") }) });
4126
+ return /* @__PURE__ */ jsx25(Gradient, { colors: [...gradient], children: /* @__PURE__ */ jsx25(Text23, { children: rows.join("\n") }) });
2927
4127
  }
2928
- return /* @__PURE__ */ jsx22(Box21, { flexDirection: "column", children: rows.map((row, i) => /* @__PURE__ */ jsx22(Text21, { children: theme.accent(row) }, i)) });
4128
+ return /* @__PURE__ */ jsx25(Box23, { flexDirection: "column", children: rows.map((row, i) => /* @__PURE__ */ jsx25(Text23, { children: theme.accent(row) }, i)) });
2929
4129
  }
2930
4130
 
2931
4131
  // src/ui/components/Header.tsx
2932
- import { jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
4132
+ import { jsx as jsx26, jsxs as jsxs21 } from "react/jsx-runtime";
2933
4133
  var WIDE_HEADER_MIN = fromTerminal(60);
2934
4134
  function oneLineHeader({ version, profile, account }) {
2935
4135
  const sep = theme.dim(` ${theme.symbols.middot} `);
@@ -2945,24 +4145,24 @@ function oneLineHeader({ version, profile, account }) {
2945
4145
  function capitalize(value) {
2946
4146
  return value ? value.charAt(0).toUpperCase() + value.slice(1) : value;
2947
4147
  }
2948
- var Header = memo16(function Header2(props) {
4148
+ var Header = memo17(function Header2(props) {
2949
4149
  const { version, width, tagline = "Your career, from the terminal" } = props;
2950
4150
  if (width < WIDE_HEADER_MIN || !theme.unicode) {
2951
- return /* @__PURE__ */ jsx23(Text22, { wrap: "truncate-end", children: oneLineHeader(props) });
4151
+ return /* @__PURE__ */ jsx26(Text24, { wrap: "truncate-end", children: oneLineHeader(props) });
2952
4152
  }
2953
4153
  const showTagline = width >= WORDMARK_WIDTH + 3 + tagline.length;
2954
- return /* @__PURE__ */ jsxs20(Box22, { children: [
2955
- /* @__PURE__ */ jsx23(Wordmark, {}),
2956
- /* @__PURE__ */ jsxs20(Box22, { flexDirection: "column", marginLeft: 3, children: [
2957
- /* @__PURE__ */ jsx23(Text22, { children: " " }),
2958
- /* @__PURE__ */ jsx23(Text22, { children: theme.bold(`tabbio ${version}`) }),
2959
- /* @__PURE__ */ jsx23(Text22, { wrap: "truncate-end", children: showTagline ? theme.dim(tagline) : "" })
4154
+ return /* @__PURE__ */ jsxs21(Box24, { children: [
4155
+ /* @__PURE__ */ jsx26(Wordmark, {}),
4156
+ /* @__PURE__ */ jsxs21(Box24, { flexDirection: "column", marginLeft: 3, children: [
4157
+ /* @__PURE__ */ jsx26(Text24, { children: " " }),
4158
+ /* @__PURE__ */ jsx26(Text24, { children: theme.bold(`tabbio ${version}`) }),
4159
+ /* @__PURE__ */ jsx26(Text24, { wrap: "truncate-end", children: showTagline ? theme.dim(tagline) : "" })
2960
4160
  ] })
2961
4161
  ] });
2962
4162
  });
2963
4163
 
2964
4164
  // src/ui/screens/home/HomeView.tsx
2965
- import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
4165
+ import { jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
2966
4166
  var TWO_COLUMN_MIN = fromTerminal(100);
2967
4167
  function AccountLine({ services, plan, width }) {
2968
4168
  const { account } = services;
@@ -2971,13 +4171,14 @@ function AccountLine({ services, plan, width }) {
2971
4171
  const left = `${account.signedIn ? theme.success(theme.symbols.dot) : theme.attention(theme.symbols.dot)} ${who}`;
2972
4172
  const right = theme.dim(`profile ${services.profileName} ${theme.symbols.middot} ${services.apiHost}`);
2973
4173
  const fill = width - displayWidth(left) - displayWidth(right);
2974
- return /* @__PURE__ */ jsx24(Text23, { wrap: "truncate-end", children: fill >= 2 ? `${left}${" ".repeat(fill)}${right}` : left });
4174
+ return /* @__PURE__ */ jsx27(Text25, { wrap: "truncate-end", children: fill >= 2 ? `${left}${" ".repeat(fill)}${right}` : left });
2975
4175
  }
2976
- var QuickActions = memo17(function QuickActions2({ commands, width, pending }) {
4176
+ var QuickActions = memo18(function QuickActions2(props) {
4177
+ const { commands, width, pending, signedIn } = props;
2977
4178
  const nameWidth = 13;
2978
- return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
2979
- /* @__PURE__ */ jsx24(Text23, { children: theme.dim("START") }),
2980
- commands.filter((c) => c.name !== "exit").map((c) => /* @__PURE__ */ jsxs21(Text23, { wrap: "truncate-end", children: [
4179
+ return /* @__PURE__ */ jsxs22(Box25, { flexDirection: "column", width, children: [
4180
+ /* @__PURE__ */ jsx27(Text25, { children: theme.dim("START") }),
4181
+ startCommands(commands, signedIn).map((c) => /* @__PURE__ */ jsxs22(Text25, { wrap: "truncate-end", children: [
2981
4182
  theme.accent(padEnd(`/${c.name}`, nameWidth)),
2982
4183
  truncate(c.description, Math.max(4, width - nameWidth - 3)),
2983
4184
  c.name === "approvals" && pending ? ` ${theme.attention(theme.symbols.dot)}` : ""
@@ -2986,23 +4187,23 @@ var QuickActions = memo17(function QuickActions2({ commands, width, pending }) {
2986
4187
  });
2987
4188
  function Recent(props) {
2988
4189
  const { data, focus, width, rows, services } = props;
2989
- const title = /* @__PURE__ */ jsx24(Text23, { children: theme.dim("RECENT CHATS") });
4190
+ const title = /* @__PURE__ */ jsx27(Text25, { children: theme.dim("RECENT CHATS") });
2990
4191
  if (!services.account.hasSession) {
2991
- return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
4192
+ return /* @__PURE__ */ jsxs22(Box25, { flexDirection: "column", width, children: [
2992
4193
  title,
2993
- /* @__PURE__ */ jsx24(Text23, { wrap: "truncate-end", children: theme.dim("Sign in with `tabbio login` to see your chats") })
4194
+ /* @__PURE__ */ jsx27(Text25, { wrap: "truncate-end", children: theme.dim(services.account.signedIn ? "Chats need a full sign-in: /login" : "Type /login to see your chats") })
2994
4195
  ] });
2995
4196
  }
2996
4197
  if (data.recent === null) {
2997
- return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
4198
+ return /* @__PURE__ */ jsxs22(Box25, { flexDirection: "column", width, children: [
2998
4199
  title,
2999
- /* @__PURE__ */ jsx24(Text23, { children: theme.dim(data.error ? `Could not load: ${data.error}` : `Loading${theme.symbols.ellipsis}`) })
4200
+ /* @__PURE__ */ jsx27(Text25, { children: theme.dim(data.error ? `Could not load: ${data.error}` : `Loading${theme.symbols.ellipsis}`) })
3000
4201
  ] });
3001
4202
  }
3002
- return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
4203
+ return /* @__PURE__ */ jsxs22(Box25, { flexDirection: "column", width, children: [
3003
4204
  title,
3004
- data.recent.length === 0 ? /* @__PURE__ */ jsx24(Text23, { children: theme.dim("No chats yet. Type below to start one.") }) : null,
3005
- data.recent.length > 0 ? /* @__PURE__ */ jsx24(
4205
+ data.recent.length === 0 ? /* @__PURE__ */ jsx27(Text25, { children: theme.dim("No chats yet. Type below to start one.") }) : null,
4206
+ data.recent.length > 0 ? /* @__PURE__ */ jsx27(
3006
4207
  SelectList,
3007
4208
  {
3008
4209
  items: data.recent.map((r) => ({ id: r.id, label: r.title, meta: r.when })),
@@ -3017,24 +4218,37 @@ function Recent(props) {
3017
4218
  ) : null
3018
4219
  ] });
3019
4220
  }
4221
+ function composerPlaceholder(signedOut, width) {
4222
+ if (signedOut) return "Type /login to sign in, or / for commands";
4223
+ return width < WIDE_HEADER_MIN ? "Ask Tabbio anything, or / for commands" : "Ask Tabbio anything, or type / for commands";
4224
+ }
3020
4225
  function HomeView(props) {
3021
4226
  const { services, data, commands, matches, width, height } = props;
3022
4227
  const wide = width >= TWO_COLUMN_MIN;
3023
4228
  const tall = height >= 26;
3024
4229
  const showWordmark = width >= WIDE_HEADER_MIN && tall;
3025
4230
  const showAccount = width >= WIDE_HEADER_MIN;
3026
- const recentRows = wide ? 5 : height >= 30 ? 3 : 0;
3027
- const colWidth = wide ? Math.floor((width - 4) / 2) : width;
4231
+ const signedOut = !services.account.signedIn;
4232
+ const recentRows = signedOut ? 0 : wide ? 5 : height >= 30 ? 3 : 0;
4233
+ const colWidth = wide && recentRows > 0 ? Math.floor((width - 4) / 2) : width;
3028
4234
  const account = { name: services.account.name, email: services.account.email, plan: data.plan };
3029
- return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
3030
- showWordmark ? /* @__PURE__ */ jsx24(Header, { version: services.version, profile: services.profileName, width, account }) : /* @__PURE__ */ jsx24(Header, { version: services.version, profile: services.profileName, width: Math.min(width, WIDE_HEADER_MIN - 1), account }),
3031
- showAccount ? /* @__PURE__ */ jsx24(Box23, { marginTop: 1, children: /* @__PURE__ */ jsx24(AccountLine, { services, plan: data.plan, width }) }) : null,
3032
- /* @__PURE__ */ jsxs21(Box23, { marginTop: 1, flexDirection: wide ? "row" : "column", width, children: [
3033
- /* @__PURE__ */ jsx24(QuickActions, { commands, width: colWidth, pending: data.pendingApprovals }),
3034
- recentRows > 0 ? /* @__PURE__ */ jsx24(Box23, { marginLeft: wide ? 4 : 0, marginTop: wide ? 0 : 1, children: /* @__PURE__ */ jsx24(Recent, { ...props, width: colWidth, rows: recentRows }) }) : null
4235
+ return /* @__PURE__ */ jsxs22(Box25, { flexDirection: "column", width, children: [
4236
+ showWordmark ? /* @__PURE__ */ jsx27(Header, { version: services.version, profile: services.profileName, width, account }) : /* @__PURE__ */ jsx27(Header, { version: services.version, profile: services.profileName, width: Math.min(width, WIDE_HEADER_MIN - 1), account }),
4237
+ showAccount ? /* @__PURE__ */ jsxs22(Box25, { marginTop: 1, flexDirection: "column", children: [
4238
+ /* @__PURE__ */ jsx27(AccountLine, { services, plan: data.plan, width }),
4239
+ signedOut ? /* @__PURE__ */ jsx27(Text25, { wrap: "truncate-end", children: theme.dim(" Type /login to sign in") }) : null
4240
+ ] }) : signedOut ? /* @__PURE__ */ jsx27(Box25, { marginTop: 1, children: /* @__PURE__ */ jsxs22(Text25, { wrap: "truncate-end", children: [
4241
+ theme.attention("Not signed in"),
4242
+ " ",
4243
+ theme.dim("Type /login to sign in")
4244
+ ] }) }) : null,
4245
+ /* @__PURE__ */ jsxs22(Box25, { marginTop: 1, flexDirection: wide ? "row" : "column", width, children: [
4246
+ /* @__PURE__ */ jsx27(QuickActions, { commands, width: colWidth, pending: data.pendingApprovals, signedIn: !signedOut }),
4247
+ recentRows > 0 ? /* @__PURE__ */ jsx27(Box25, { marginLeft: wide ? 4 : 0, marginTop: wide ? 0 : 1, children: /* @__PURE__ */ jsx27(Recent, { ...props, width: colWidth, rows: recentRows }) }) : null
3035
4248
  ] }),
3036
- matches.length > 0 ? /* @__PURE__ */ jsx24(Box23, { marginTop: 1, children: /* @__PURE__ */ jsx24(Palette, { commands: matches, selected: props.paletteIndex, width }) }) : null,
3037
- /* @__PURE__ */ jsx24(Box23, { marginTop: 1, children: /* @__PURE__ */ jsx24(
4249
+ props.modal ? /* @__PURE__ */ jsx27(Box25, { marginTop: 1, children: props.modal }) : null,
4250
+ !props.modal && (matches.length > 0 || props.paletteOpen) ? /* @__PURE__ */ jsx27(Box25, { marginTop: 1, children: /* @__PURE__ */ jsx27(Palette, { commands: matches, selected: props.paletteIndex, width, footer: props.paletteFooter }) }) : null,
4251
+ props.modal ? null : /* @__PURE__ */ jsx27(Box25, { marginTop: 1, children: /* @__PURE__ */ jsx27(
3038
4252
  TextInput,
3039
4253
  {
3040
4254
  editor: props.editor,
@@ -3043,16 +4257,20 @@ function HomeView(props) {
3043
4257
  onKey: props.onComposerKey,
3044
4258
  isActive: props.focus === "composer",
3045
4259
  width,
3046
- placeholder: width < WIDE_HEADER_MIN ? "Ask Tabbio anything, or / for commands" : "Ask Tabbio anything, or type / for commands",
4260
+ placeholder: composerPlaceholder(signedOut, width),
3047
4261
  boxed: true
3048
4262
  }
3049
4263
  ) }),
3050
- props.notice ? /* @__PURE__ */ jsx24(Text23, { wrap: "truncate-end", children: props.notice }) : null,
3051
- /* @__PURE__ */ jsx24(
4264
+ props.notice ? props.notice.split("\n").map((line, i) => /* @__PURE__ */ jsx27(Text25, { wrap: "wrap", children: line }, i)) : null,
4265
+ /* @__PURE__ */ jsx27(
3052
4266
  KeyHints,
3053
4267
  {
3054
4268
  width,
3055
- hints: [
4269
+ hints: props.modal ? [{ key: "esc", label: "cancel sign-in" }] : signedOut ? [
4270
+ { key: "/login", label: "sign in" },
4271
+ { key: "/", label: "commands" },
4272
+ { key: "ctrl+c", label: "quit" }
4273
+ ] : [
3056
4274
  { key: "enter", label: "send" },
3057
4275
  { key: "/", label: "commands" },
3058
4276
  ...services.account.hasSession && recentRows > 0 ? [{ key: "tab", label: "recent chats" }] : [],
@@ -3064,32 +4282,41 @@ function HomeView(props) {
3064
4282
  }
3065
4283
 
3066
4284
  // src/ui/screens/home/Home.tsx
3067
- import { jsx as jsx25, jsxs as jsxs22 } from "react/jsx-runtime";
4285
+ import { jsx as jsx28, jsxs as jsxs23 } from "react/jsx-runtime";
3068
4286
  var DOUBLE_PRESS_MS2 = 2e3;
4287
+ var NOTICE_MS = 6e3;
3069
4288
  function BackKeys({ onBack }) {
3070
- useInput10((input, key) => {
4289
+ useInput12((input, key) => {
3071
4290
  if (key.escape || input === "q" || key.return) onBack();
3072
4291
  });
3073
4292
  return null;
3074
4293
  }
4294
+ function accountNotice(change) {
4295
+ return [theme.success(`${theme.symbols.success} ${change.message}`), ...(change.warnings ?? []).map((w) => theme.attention(`${theme.symbols.attention} ${w}`))].join("\n");
4296
+ }
3075
4297
  function Home(props) {
3076
- const { services, width, height, onExit, mode = "seeker" } = props;
3077
- const [route, setRoute] = useState9({ name: "home" });
3078
- const [data, setData] = useState9(props.initialData ?? emptyHomeData);
3079
- const [editor, dispatchEditor] = useReducer4(editorReducer, emptyEditor);
3080
- const [paletteIndex, setPaletteIndex] = useState9(0);
3081
- const [focus, setFocus] = useState9("composer");
3082
- const [notice, setNotice] = useState9(null);
3083
- const lastCtrlC = useRef7(0);
3084
- const mounted = useRef7(true);
3085
- useEffect8(
4298
+ const { width, height, onExit, mode = "seeker", saveArtifacts = true } = props;
4299
+ const [services, setServices] = useState10(props.services);
4300
+ const [route, setRoute] = useState10({ name: "home" });
4301
+ const [data, setData] = useState10(props.initialData ?? emptyHomeData);
4302
+ const [editor, dispatchEditor] = useReducer5(editorReducer, emptyEditor);
4303
+ const [paletteIndex, setPaletteIndex] = useState10(0);
4304
+ const [navigated, setNavigated] = useState10(false);
4305
+ const [focus, setFocus] = useState10("composer");
4306
+ const [notice, setNotice] = useState10(props.initialNotice ?? null);
4307
+ const [login, setLogin] = useState10(null);
4308
+ const lastCtrlC = useRef10(0);
4309
+ const mounted = useRef10(true);
4310
+ useEffect11(
3086
4311
  () => () => {
3087
4312
  mounted.current = false;
3088
4313
  },
3089
4314
  []
3090
4315
  );
3091
- const loadData = useCallback6(async () => {
3092
- if (props.initialData || !services.account.hasSession) return;
4316
+ const { signedIn, hasSession } = services.account;
4317
+ const access = useMemo8(() => ({ signedIn, hasSession }), [signedIn, hasSession]);
4318
+ const loadData = useCallback8(async () => {
4319
+ if (props.initialData && services === props.services || !services.account.hasSession) return;
3093
4320
  const [plan, approvals, threads] = await Promise.allSettled([
3094
4321
  services.loadPlan(),
3095
4322
  services.loadPendingApprovals(),
@@ -3106,51 +4333,89 @@ function Home(props) {
3106
4333
  })) : null,
3107
4334
  error: threads.status === "rejected" ? toCliError(threads.reason).message : void 0
3108
4335
  });
3109
- }, [props.initialData, props.companyId, services, mode]);
3110
- useEffect8(() => {
4336
+ }, [props.initialData, props.services, props.companyId, services, mode]);
4337
+ useEffect11(() => {
3111
4338
  if (route.name === "home") void loadData();
3112
4339
  }, [route.name, loadData]);
3113
- useEffect8(() => {
4340
+ useEffect11(() => {
3114
4341
  if (!notice) return void 0;
3115
- const timer = setTimeout(() => setNotice(null), 4e3);
4342
+ const timer = setTimeout(() => setNotice(null), NOTICE_MS);
3116
4343
  return () => clearTimeout(timer);
3117
4344
  }, [notice]);
3118
- const commands = useMemo7(() => homeCommands(data, services.cachedToolCount), [data, services.cachedToolCount]);
3119
- const matches = paletteMatches(commands, editor.value);
4345
+ useEffect11(() => setNavigated(false), [editor.value]);
4346
+ const commands = useMemo8(() => homeCommands(data, services.cachedToolCount, access), [data, services.cachedToolCount, access]);
4347
+ const skills = useSkills(services, hasSession && editor.value.startsWith("/"));
4348
+ const view = paletteView(commands, hasSession ? skills : { status: "idle", skills: [] }, editor.value, { ellipsis: theme.symbols.ellipsis, separator: theme.symbols.middot });
4349
+ const matches = view.items;
3120
4350
  const selected = Math.min(paletteIndex, Math.max(0, matches.length - 1));
3121
- const goHome = useCallback6(() => setRoute({ name: "home" }), []);
3122
- const needSession = useCallback6(
3123
- (what) => {
3124
- if (services.account.hasSession) return true;
3125
- setNotice(theme.attention(`${theme.symbols.attention} ${what} needs a full sign-in. Run \`tabbio login\`.`));
3126
- return false;
4351
+ const goHome = useCallback8(() => setRoute({ name: "home" }), []);
4352
+ const warn = useCallback8((text2) => setNotice(theme.attention(`${theme.symbols.attention} ${text2}`)), []);
4353
+ const applyAccount = useCallback8((change, opts = {}) => {
4354
+ const switched = accountIdentity(services) !== accountIdentity(change.services);
4355
+ setServices(change.services);
4356
+ setData(emptyHomeData);
4357
+ if (opts.stay && switched) setRoute({ name: "chat" });
4358
+ if (!opts.stay) {
4359
+ setRoute({ name: "home" });
4360
+ setNotice(accountNotice(change));
4361
+ }
4362
+ }, [services]);
4363
+ const onLoginDone = useCallback8(
4364
+ (outcome) => {
4365
+ setLogin(null);
4366
+ if (outcome.status === "done") applyAccount(outcome.change);
4367
+ else if (outcome.status === "cancelled") setNotice(theme.dim("Sign-in cancelled"));
4368
+ else setNotice([theme.error(`${theme.symbols.error} ${outcome.message}`), ...outcome.hint ? [theme.dim(` ${outcome.hint}`)] : []].join("\n"));
3127
4369
  },
3128
- [services.account.hasSession]
4370
+ [applyAccount]
3129
4371
  );
3130
- const open = useCallback6(
3131
- (name) => {
4372
+ const open2 = useCallback8(
4373
+ (name, arg = "") => {
4374
+ const gate = homeGate(name, access);
4375
+ if (gate) {
4376
+ warn(gate);
4377
+ return;
4378
+ }
3132
4379
  switch (name) {
3133
4380
  case "chat":
3134
- if (needSession("Chat")) setRoute({ name: "chat" });
4381
+ setRoute({ name: "chat" });
3135
4382
  return;
3136
4383
  case "tools": {
3137
- if (!services.account.signedIn) {
3138
- setNotice(theme.attention(`${theme.symbols.attention} Sign in first: run \`tabbio login\`.`));
3139
- return;
3140
- }
3141
4384
  const since = Date.now();
3142
4385
  setRoute({ name: "tools", since });
3143
4386
  services.listTools().then((tools) => mounted.current && setRoute((r) => r.name === "tools" ? { ...r, tools } : r)).catch((error) => mounted.current && setRoute((r) => r.name === "tools" ? { ...r, error: toCliError(error).message } : r));
3144
4387
  return;
3145
4388
  }
3146
4389
  case "approvals":
3147
- if (needSession("Approvals")) setRoute({ name: "approvals" });
4390
+ setRoute({ name: "approvals" });
3148
4391
  return;
3149
4392
  case "status": {
3150
4393
  setRoute({ name: "status", since: Date.now() });
3151
4394
  services.loadStatus().then((rows) => mounted.current && setRoute((r) => r.name === "status" ? { ...r, rows } : r)).catch((error) => mounted.current && setRoute((r) => r.name === "status" ? { ...r, error: toCliError(error).message } : r));
3152
4395
  return;
3153
4396
  }
4397
+ case "login": {
4398
+ const start = parseLoginArgs(arg);
4399
+ if ("error" in start) setNotice(theme.error(`${theme.symbols.error} ${start.error}`));
4400
+ else {
4401
+ setNotice(null);
4402
+ setLogin(start);
4403
+ }
4404
+ return;
4405
+ }
4406
+ case "logout": {
4407
+ const session = services.session;
4408
+ if (!session) {
4409
+ warn("Signing out is not available here. Run `tabbio logout`.");
4410
+ return;
4411
+ }
4412
+ setNotice(theme.dim(`Signing out${theme.symbols.ellipsis}`));
4413
+ session.logout().then(
4414
+ (change) => mounted.current && applyAccount(change),
4415
+ (error) => mounted.current && setNotice(theme.error(`${theme.symbols.error} ${toCliError(error).message}`))
4416
+ );
4417
+ return;
4418
+ }
3154
4419
  case "help":
3155
4420
  setRoute({ name: "help" });
3156
4421
  return;
@@ -3162,37 +4427,45 @@ function Home(props) {
3162
4427
  setNotice(theme.error(`${theme.symbols.error} Unknown command /${name}`));
3163
4428
  }
3164
4429
  },
3165
- [needSession, services, onExit]
4430
+ [access, services, onExit, warn, applyAccount]
3166
4431
  );
3167
- const submit2 = useCallback6(
4432
+ const submit2 = useCallback8(
3168
4433
  (value) => {
3169
- const text = value.trim();
3170
- if (!text) return;
3171
- const slash = parseSlash(text);
3172
- dispatchEditor({ type: "remember", value: text });
4434
+ const res = resolveSubmit(value, { commands, skills, highlighted: matches[selected], navigated, aliases: HOME_COMMAND_NAMES });
4435
+ if (res.kind === "empty" || res.kind === "bare") return;
4436
+ if (res.kind === "loading") {
4437
+ warn(`Skills are still loading. Press enter again in a moment.`);
4438
+ return;
4439
+ }
4440
+ dispatchEditor({ type: "remember", value: value.trim() });
3173
4441
  dispatchEditor({ type: "clear" });
3174
4442
  setPaletteIndex(0);
3175
- if (slash) {
3176
- const exact = isHomeCommand(slash.name) ? slash.name : void 0;
3177
- open(exact ?? matches[selected]?.name ?? slash.name);
4443
+ if (res.kind === "unknown") {
4444
+ setNotice([theme.error(`${theme.symbols.error} Unknown command /${res.name}`), theme.dim(" Type / to see the commands.")].join("\n"));
3178
4445
  return;
3179
4446
  }
3180
- if (needSession("Chat")) setRoute({ name: "chat", initialMessage: text });
4447
+ if (res.kind === "command") {
4448
+ open2(res.name, res.arg);
4449
+ return;
4450
+ }
4451
+ const gate = homeGate(res.kind, access);
4452
+ if (gate) warn(gate);
4453
+ else setRoute({ name: "chat", initialMessage: res.kind === "skill" ? res.message : res.text });
3181
4454
  },
3182
- [open, matches, selected, needSession]
4455
+ [commands, skills, matches, selected, navigated, open2, access, warn]
3183
4456
  );
3184
- const onComposerKey = useCallback6(
4457
+ const onComposerKey = useCallback8(
3185
4458
  (input, key) => {
3186
4459
  if (key.ctrl && input === "c") return true;
3187
4460
  if (matches.length > 0 && (key.upArrow || key.downArrow)) {
3188
4461
  setPaletteIndex((i) => key.upArrow ? Math.max(0, i - 1) : Math.min(matches.length - 1, i + 1));
4462
+ setNavigated(true);
3189
4463
  return true;
3190
4464
  }
3191
4465
  if (key.tab) {
3192
- if (matches.length > 0) {
3193
- const choice = matches[selected];
3194
- if (choice) dispatchEditor({ type: "set", value: `/${choice.name}` });
3195
- } else if (data.recent?.length) setFocus("recent");
4466
+ const choice = matches[selected];
4467
+ if (choice) dispatchEditor({ type: "set", value: `/${choice.name}${choice.usage || choice.skillId ? " " : ""}` });
4468
+ else if (data.recent?.length && hasSession) setFocus("recent");
3196
4469
  return true;
3197
4470
  }
3198
4471
  if (key.escape) {
@@ -3201,9 +4474,9 @@ function Home(props) {
3201
4474
  }
3202
4475
  return false;
3203
4476
  },
3204
- [matches, selected, data.recent]
4477
+ [matches, selected, data.recent, hasSession]
3205
4478
  );
3206
- useInput10(
4479
+ useInput12(
3207
4480
  (input, key) => {
3208
4481
  if (!(key.ctrl && input === "c")) return;
3209
4482
  const now = Date.now();
@@ -3219,7 +4492,7 @@ function Home(props) {
3219
4492
  );
3220
4493
  switch (route.name) {
3221
4494
  case "chat":
3222
- return /* @__PURE__ */ jsx25(
4495
+ return /* @__PURE__ */ jsx28(
3223
4496
  Chat,
3224
4497
  {
3225
4498
  services,
@@ -3227,42 +4500,48 @@ function Home(props) {
3227
4500
  companyId: props.companyId,
3228
4501
  threadId: route.threadId,
3229
4502
  initialMessage: route.initialMessage,
4503
+ saveArtifacts,
3230
4504
  width,
3231
4505
  height,
3232
4506
  hostedByHome: true,
4507
+ onAccountChange: (change) => applyAccount(change, { stay: !change.signedOut }),
3233
4508
  onExit: (reason) => reason === "home" ? goHome() : onExit()
3234
- }
4509
+ },
4510
+ accountIdentity(services)
3235
4511
  );
3236
4512
  case "tools":
3237
- if (route.tools) return /* @__PURE__ */ jsx25(ToolBrowser, { tools: route.tools, services, width, height, onExit: goHome });
3238
- return /* @__PURE__ */ jsxs22(Box24, { flexDirection: "column", width, children: [
3239
- route.error ? /* @__PURE__ */ jsx25(Text24, { children: theme.error(`${theme.symbols.error} ${route.error}`) }) : /* @__PURE__ */ jsx25(Working, { label: "Loading tools\u2026", since: route.since }),
3240
- /* @__PURE__ */ jsx25(BackKeys, { onBack: goHome })
4513
+ if (route.tools) return /* @__PURE__ */ jsx28(ToolBrowser, { tools: route.tools, services, width, height, onExit: goHome });
4514
+ return /* @__PURE__ */ jsxs23(Box26, { flexDirection: "column", width, children: [
4515
+ route.error ? /* @__PURE__ */ jsx28(Text26, { children: theme.error(`${theme.symbols.error} ${route.error}`) }) : /* @__PURE__ */ jsx28(Working, { label: "Loading tools\u2026", since: route.since }),
4516
+ /* @__PURE__ */ jsx28(KeyHints, { width, hints: [{ key: "esc", label: "back" }] }),
4517
+ /* @__PURE__ */ jsx28(BackKeys, { onBack: goHome })
3241
4518
  ] });
3242
4519
  case "approvals":
3243
- return /* @__PURE__ */ jsx25(Approvals, { services, width, height, onExit: goHome });
4520
+ return /* @__PURE__ */ jsx28(Approvals, { services, width, height, onExit: goHome });
3244
4521
  case "status":
3245
- return /* @__PURE__ */ jsxs22(Box24, { flexDirection: "column", width, children: [
3246
- /* @__PURE__ */ jsx25(Text24, { children: theme.bold("Status") }),
3247
- /* @__PURE__ */ jsx25(Box24, { marginTop: 1, flexDirection: "column", children: route.rows ? /* @__PURE__ */ jsx25(KeyValue, { rows: route.rows, width }) : route.error ? /* @__PURE__ */ jsx25(Text24, { children: theme.error(`${theme.symbols.error} ${route.error}`) }) : /* @__PURE__ */ jsx25(Working, { label: "Checking\u2026", since: route.since }) }),
3248
- /* @__PURE__ */ jsx25(Box24, { marginTop: 1, children: /* @__PURE__ */ jsx25(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) }),
3249
- /* @__PURE__ */ jsx25(BackKeys, { onBack: goHome })
4522
+ return /* @__PURE__ */ jsxs23(Box26, { flexDirection: "column", width, children: [
4523
+ /* @__PURE__ */ jsx28(Text26, { children: theme.bold("Status") }),
4524
+ /* @__PURE__ */ jsx28(Box26, { marginTop: 1, flexDirection: "column", children: route.rows ? /* @__PURE__ */ jsx28(KeyValue, { rows: route.rows, width }) : route.error ? /* @__PURE__ */ jsx28(Text26, { children: theme.error(`${theme.symbols.error} ${route.error}`) }) : /* @__PURE__ */ jsx28(Working, { label: "Checking\u2026", since: route.since }) }),
4525
+ /* @__PURE__ */ jsx28(Box26, { marginTop: 1, children: /* @__PURE__ */ jsx28(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) }),
4526
+ /* @__PURE__ */ jsx28(BackKeys, { onBack: goHome })
3250
4527
  ] });
3251
4528
  case "help":
3252
- return /* @__PURE__ */ jsxs22(Box24, { flexDirection: "column", width, children: [
3253
- /* @__PURE__ */ jsx25(Text24, { children: theme.bold("Keys and commands") }),
3254
- /* @__PURE__ */ jsx25(Box24, { marginTop: 1, children: /* @__PURE__ */ jsx25(KeyValue, { rows: HOME_HELP_ROWS.map(([label, value]) => ({ label, value })), width }) }),
3255
- /* @__PURE__ */ jsx25(Box24, { marginTop: 1, children: /* @__PURE__ */ jsx25(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) }),
3256
- /* @__PURE__ */ jsx25(BackKeys, { onBack: goHome })
4529
+ return /* @__PURE__ */ jsxs23(Box26, { flexDirection: "column", width, children: [
4530
+ /* @__PURE__ */ jsx28(Text26, { children: theme.bold("Keys and commands") }),
4531
+ /* @__PURE__ */ jsx28(Box26, { marginTop: 1, children: /* @__PURE__ */ jsx28(KeyValue, { rows: HOME_HELP_ROWS.map(([label, value]) => ({ label, value })), width }) }),
4532
+ /* @__PURE__ */ jsx28(Box26, { marginTop: 1, children: /* @__PURE__ */ jsx28(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) }),
4533
+ /* @__PURE__ */ jsx28(BackKeys, { onBack: goHome })
3257
4534
  ] });
3258
4535
  default:
3259
- return /* @__PURE__ */ jsx25(
4536
+ return /* @__PURE__ */ jsx28(
3260
4537
  HomeView,
3261
4538
  {
3262
4539
  services,
3263
4540
  data,
3264
4541
  commands,
3265
4542
  matches,
4543
+ paletteOpen: view.open,
4544
+ paletteFooter: view.footer,
3266
4545
  paletteIndex: selected,
3267
4546
  editor,
3268
4547
  dispatchEditor,
@@ -3274,6 +4553,7 @@ function Home(props) {
3274
4553
  setRoute({ name: "chat", threadId: id });
3275
4554
  },
3276
4555
  onRecentCancel: () => setFocus("composer"),
4556
+ modal: login ? /* @__PURE__ */ jsx28(LoginPanel, { services, start: login, width, onDone: onLoginDone }) : null,
3277
4557
  notice,
3278
4558
  width,
3279
4559
  height
@@ -3282,83 +4562,8 @@ function Home(props) {
3282
4562
  }
3283
4563
  }
3284
4564
 
3285
- // src/ui/services.ts
3286
- function hostOf2(url) {
3287
- try {
3288
- return new URL(url).host;
3289
- } catch {
3290
- return url;
3291
- }
3292
- }
3293
- function createUiServices(ctx) {
3294
- const creds = ctx.api.credentials ?? ctx.credentials;
3295
- const cached = readCachedCatalog(ctx.profile, creds);
3296
- return {
3297
- version: CLI_VERSION,
3298
- profileName: ctx.profile.name,
3299
- apiHost: hostOf2(ctx.profile.apiUrl),
3300
- account: {
3301
- name: creds?.user?.name ?? null,
3302
- email: creds?.user?.email ?? null,
3303
- signedIn: Boolean(creds?.accessToken || creds?.mcpToken),
3304
- hasSession: Boolean(creds?.accessToken),
3305
- userId: creds?.user?.id
3306
- },
3307
- cachedToolCount: cached ? cached.tools.filter((t) => !t.hidden).length : null,
3308
- async loadPlan() {
3309
- if (!creds?.accessToken) return null;
3310
- const state = await ctx.api.json("/api/subscription/state");
3311
- return state?.snapshot?.tier ?? null;
3312
- },
3313
- async loadPendingApprovals() {
3314
- const state = await getMcpAccessState(ctx.api);
3315
- return state?.pendingApprovals ?? [];
3316
- },
3317
- listThreads: (scope) => listThreads(ctx, scope),
3318
- listModels: (scope) => listModels(ctx, scope),
3319
- getThreadMessages: (threadId, scope) => getThreadMessages(ctx, threadId, scope),
3320
- streamChat: (request, signal) => streamChat(ctx, request, { signal }),
3321
- decideApproval: (approvalId, decision, scope) => decision === "approve" ? confirmApproval(ctx, approvalId, scope) : rejectApproval(ctx, approvalId, scope),
3322
- async listTools(opts) {
3323
- const session = await ctx.mcp();
3324
- return session.listTools(opts);
3325
- },
3326
- async callTool(toolId, input, opts) {
3327
- const session = await ctx.mcp();
3328
- const started = Date.now();
3329
- const result = await session.callTool(toolId, input, opts);
3330
- return { ...result, durationMs: Date.now() - started };
3331
- },
3332
- capabilityPrefs: loadCapabilityPrefs(ctx.profile.name),
3333
- saveCapabilityPrefs: (capabilities) => saveCapabilityPrefs(ctx.profile.name, capabilities),
3334
- loadCapabilities: () => ctx.api.json("/api/agent/capabilities"),
3335
- getArtifact: (artifactId) => getArtifact(ctx.api, artifactId),
3336
- async artifactPreviewUrl(artifactId, kind) {
3337
- return (await previewUrl(ctx.api, artifactId, { variant: previewVariantFor(kind) })).url;
3338
- },
3339
- hyperlinks: supportsHyperlinks(),
3340
- async loadStatus() {
3341
- const { collectStatus } = await import("./status-I5UAQPBY.js");
3342
- const report = await collectStatus(ctx);
3343
- const who = report.user ? report.user.name ? `${report.user.name} <${report.user.email}>` : report.user.email : "not signed in";
3344
- const rows = [
3345
- ["Profile", `${report.profile.name} (${report.profile.source})`],
3346
- ["API", report.profile.apiUrl],
3347
- ["Account", who],
3348
- ["Plan", report.plan],
3349
- ["Session", report.session ? `${report.session.token}${report.session.refreshable ? " \xB7 auto-refresh" : ""}` : null],
3350
- ["Tools via", report.mcp?.label],
3351
- ["Approvals", report.pendingApprovals === null ? null : `${report.pendingApprovals} pending`],
3352
- ["Catalog", report.catalog ? `${report.catalog.tools} tools${report.catalog.fresh ? "" : " (stale)"}` : null],
3353
- ...report.problems.map((p) => ["Problem", p])
3354
- ];
3355
- return rows.filter((r) => Boolean(r[1])).map(([label, value]) => ({ label, value }));
3356
- }
3357
- };
3358
- }
3359
-
3360
4565
  // src/ui/entry.tsx
3361
- import { jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
4566
+ import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
3362
4567
  function unwrap(outcome) {
3363
4568
  if (outcome.status === "interrupted") throw interruptedError();
3364
4569
  return outcome.value;
@@ -3366,7 +4571,7 @@ function unwrap(outcome) {
3366
4571
  async function runBrowseTools(ctx, tools, options) {
3367
4572
  const services = createUiServices(ctx);
3368
4573
  unwrap(
3369
- await renderScreen("The tool browser", ({ finish, width, height }) => /* @__PURE__ */ jsx26(
4574
+ await renderScreen("The tool browser", ({ finish, width, height }) => /* @__PURE__ */ jsx29(
3370
4575
  ToolBrowser,
3371
4576
  {
3372
4577
  tools,
@@ -3384,23 +4589,23 @@ async function runToolForm(_ctx, tool, initial, options) {
3384
4589
  const skip = autoFilledProperties(tool, tool.inputSchema);
3385
4590
  const state = createFormState(tool.inputSchema, initial, { skip, missingRequired: options.missingRequired });
3386
4591
  return unwrap(
3387
- await renderScreen("The input form", ({ finish, width, height }) => /* @__PURE__ */ jsxs23(Box25, { flexDirection: "column", width, children: [
3388
- /* @__PURE__ */ jsxs23(Box25, { flexDirection: "column", marginBottom: 1, children: [
3389
- /* @__PURE__ */ jsxs23(Text25, { wrap: "truncate-end", children: [
4592
+ await renderScreen("The input form", ({ finish, width, height }) => /* @__PURE__ */ jsxs24(Box27, { flexDirection: "column", width, children: [
4593
+ /* @__PURE__ */ jsxs24(Box27, { flexDirection: "column", marginBottom: 1, children: [
4594
+ /* @__PURE__ */ jsxs24(Text27, { wrap: "truncate-end", children: [
3390
4595
  theme.bold(toolLabel(tool)),
3391
4596
  " ",
3392
4597
  theme.dim(`${theme.symbols.middot} ${oneLine(tool.title)}`)
3393
4598
  ] }),
3394
- options.missingRequired.length ? /* @__PURE__ */ jsx26(Text25, { wrap: "truncate-end", children: theme.dim(`Missing: ${options.missingRequired.join(", ")}`) }) : null
4599
+ options.missingRequired.length ? /* @__PURE__ */ jsx29(Text27, { wrap: "truncate-end", children: theme.dim(`Missing: ${options.missingRequired.join(", ")}`) }) : null
3395
4600
  ] }),
3396
- /* @__PURE__ */ jsx26(
4601
+ /* @__PURE__ */ jsx29(
3397
4602
  Form,
3398
4603
  {
3399
4604
  initial: state,
3400
4605
  width,
3401
4606
  height: Math.max(3, height - 5),
3402
4607
  isActive: true,
3403
- onSubmit: (input) => finish(input, /* @__PURE__ */ jsx26(Text25, { wrap: "truncate-end", children: theme.dim(`${theme.symbols.pointer} ${equivalentCommand(tool, input, skip)}`) })),
4608
+ onSubmit: (input) => finish(input, /* @__PURE__ */ jsx29(Text27, { wrap: "truncate-end", children: theme.dim(`${theme.symbols.pointer} ${equivalentCommand(tool, input, skip)}`) })),
3404
4609
  onCancel: () => finish(null)
3405
4610
  }
3406
4611
  )
@@ -3411,7 +4616,7 @@ async function runApprovalPrompt(ctx, approval) {
3411
4616
  const shownAt = Date.now();
3412
4617
  const title = sentenceCase(approval.toolName || "this action");
3413
4618
  return unwrap(
3414
- await renderScreen("The approval prompt", ({ finish, width }) => /* @__PURE__ */ jsx26(
4619
+ await renderScreen("The approval prompt", ({ finish, width }) => /* @__PURE__ */ jsx29(
3415
4620
  ApprovalCard,
3416
4621
  {
3417
4622
  approvalId: approval.approvalId,
@@ -3422,21 +4627,21 @@ async function runApprovalPrompt(ctx, approval) {
3422
4627
  context: `Writes to your account ${theme.symbols.middot} profile ${ctx.profile.name}`,
3423
4628
  isActive: true,
3424
4629
  width,
3425
- onDecision: (decision) => finish(decision, /* @__PURE__ */ jsx26(Text25, { children: approvalOutcomeText(decision, title, approval.approvalId, Date.now() - shownAt) }))
4630
+ onDecision: (decision) => finish(decision, /* @__PURE__ */ jsx29(Text27, { children: approvalOutcomeText(decision, title, approval.approvalId, Date.now() - shownAt) }))
3426
4631
  }
3427
4632
  ))
3428
4633
  );
3429
4634
  }
3430
4635
  function PrintOnce({ children, onPrinted }) {
3431
4636
  const { columns } = useTerminalSize();
3432
- useEffect9(() => {
4637
+ useEffect12(() => {
3433
4638
  onPrinted();
3434
4639
  }, [onPrinted]);
3435
- return /* @__PURE__ */ jsx26(Static2, { items: ["once"], children: (key) => /* @__PURE__ */ jsx26(Page, { columns, children }, key) });
4640
+ return /* @__PURE__ */ jsx29(Static2, { items: ["once"], children: (key) => /* @__PURE__ */ jsx29(Page, { columns, children }, key) });
3436
4641
  }
3437
4642
  async function runShowResult(_ctx, tool, result, meta) {
3438
4643
  unwrap(
3439
- await renderScreen("The result panel", ({ finish, width, height }) => /* @__PURE__ */ jsx26(PrintOnce, { onPrinted: () => finish(void 0), children: /* @__PURE__ */ jsx26(
4644
+ await renderScreen("The result panel", ({ finish, width, height }) => /* @__PURE__ */ jsx29(PrintOnce, { onPrinted: () => finish(void 0), children: /* @__PURE__ */ jsx29(
3440
4645
  ResultPanel,
3441
4646
  {
3442
4647
  title: toolLabel(tool),
@@ -3452,35 +4657,83 @@ async function runShowResult(_ctx, tool, result, meta) {
3452
4657
  }
3453
4658
  async function runHome(ctx, opts = {}) {
3454
4659
  const services = createUiServices(ctx);
3455
- await renderScreen(
3456
- "The home screen",
3457
- ({ finish, width, height }) => /* @__PURE__ */ jsx26(Home, { services, width, height, mode: opts.mode, companyId: opts.companyId, onExit: () => finish(void 0) }),
3458
- { ctrlC: "screen" }
3459
- );
4660
+ try {
4661
+ await renderScreen(
4662
+ "The home screen",
4663
+ ({ finish, width, height }) => /* @__PURE__ */ jsx29(
4664
+ Home,
4665
+ {
4666
+ services,
4667
+ width,
4668
+ height,
4669
+ mode: opts.mode,
4670
+ companyId: opts.companyId,
4671
+ saveArtifacts: opts.saveArtifacts,
4672
+ onExit: () => finish(void 0)
4673
+ }
4674
+ ),
4675
+ { ctrlC: "screen" }
4676
+ );
4677
+ } finally {
4678
+ await closeUiContexts();
4679
+ }
3460
4680
  }
3461
- async function runChat(ctx, opts) {
3462
- const services = createUiServices(ctx);
3463
- await renderScreen(
3464
- "Chat",
3465
- ({ finish, width, height }) => /* @__PURE__ */ jsx26(
3466
- Chat,
4681
+ function ChatHost(props) {
4682
+ const [services, setServices] = useState11(props.services);
4683
+ const [signedOut, setSignedOut] = useState11(null);
4684
+ const [sessionGeneration, setSessionGeneration] = useState11(0);
4685
+ if (signedOut) {
4686
+ return /* @__PURE__ */ jsx29(
4687
+ Home,
3467
4688
  {
3468
4689
  services,
3469
- mode: opts.mode,
3470
- companyId: opts.companyId,
3471
- threadId: opts.threadId,
3472
- modelId: opts.modelId,
3473
- initialMessage: opts.message,
3474
- capabilities: opts.capabilities,
3475
- width,
3476
- height,
3477
- onExit: () => finish(void 0)
4690
+ width: props.width,
4691
+ height: props.height,
4692
+ mode: props.mode,
4693
+ companyId: props.companyId,
4694
+ saveArtifacts: props.saveArtifacts,
4695
+ initialNotice: accountNotice(signedOut),
4696
+ onExit: props.onExit
3478
4697
  }
3479
- ),
3480
- { ctrlC: "screen" }
4698
+ );
4699
+ }
4700
+ return /* @__PURE__ */ jsx29(
4701
+ Chat,
4702
+ {
4703
+ services,
4704
+ mode: props.mode,
4705
+ companyId: props.companyId,
4706
+ threadId: sessionGeneration === 0 ? props.threadId : void 0,
4707
+ modelId: props.modelId,
4708
+ initialMessage: sessionGeneration === 0 ? props.message : void 0,
4709
+ capabilities: props.capabilities,
4710
+ saveArtifacts: props.saveArtifacts,
4711
+ width: props.width,
4712
+ height: props.height,
4713
+ onAccountChange: (change) => {
4714
+ if (accountIdentity(services) !== accountIdentity(change.services)) setSessionGeneration((generation) => generation + 1);
4715
+ setServices(change.services);
4716
+ if (change.signedOut) setSignedOut(change);
4717
+ },
4718
+ onExit: () => props.onExit()
4719
+ },
4720
+ sessionGeneration
3481
4721
  );
3482
4722
  }
4723
+ async function runChat(ctx, opts) {
4724
+ const services = createUiServices(ctx);
4725
+ try {
4726
+ await renderScreen(
4727
+ "Chat",
4728
+ ({ finish, width, height }) => /* @__PURE__ */ jsx29(ChatHost, { ...opts, services, width, height, onExit: () => finish(void 0) }),
4729
+ { ctrlC: "screen" }
4730
+ );
4731
+ } finally {
4732
+ await closeUiContexts();
4733
+ }
4734
+ }
3483
4735
  export {
4736
+ ChatHost,
3484
4737
  PrintOnce,
3485
4738
  runApprovalPrompt,
3486
4739
  runBrowseTools,
@@ -3489,4 +4742,4 @@ export {
3489
4742
  runShowResult,
3490
4743
  runToolForm
3491
4744
  };
3492
- //# sourceMappingURL=entry-WENOOT6W.js.map
4745
+ //# sourceMappingURL=entry-ZAXWZSEE.js.map