@docyrus/docyrus 0.0.8 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/docyrus",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
package/tui.mjs CHANGED
@@ -3,6 +3,7 @@ import { createCliRenderer } from "@opentui/core";
3
3
  import { createRoot } from "@opentui/react";
4
4
 
5
5
  // src/tui/opentui/DocyrusOpenTuiApp.tsx
6
+ import { RGBA as RGBA2 } from "@opentui/core";
6
7
  import { useKeyboard as useKeyboard2, useRenderer } from "@opentui/react";
7
8
  import { useCallback as useCallback2, useEffect, useMemo, useRef, useState as useState2 } from "react";
8
9
 
@@ -259,12 +260,14 @@ function useCommandInput(options) {
259
260
  }
260
261
  const keyName = keyEvent.name;
261
262
  const hotkeysEnabled = options.hotkeysEnabled !== false;
263
+ const sectionTabEnabled = hotkeysEnabled && options.sectionTabEnabled !== false;
264
+ const inputUpHistoryEnabled = options.inputUpHistoryEnabled !== false;
262
265
  if (keyEvent.ctrl && keyName === "l") {
263
266
  preventDefault(keyEvent);
264
267
  options.onClear();
265
268
  return;
266
269
  }
267
- if (hotkeysEnabled && keyName === "tab") {
270
+ if (sectionTabEnabled && keyName === "tab") {
268
271
  preventDefault(keyEvent);
269
272
  if (keyEvent.shift) {
270
273
  options.onPreviousSection();
@@ -283,7 +286,7 @@ function useCommandInput(options) {
283
286
  options.onNextSection();
284
287
  return;
285
288
  }
286
- if (options.isInputFocused && keyName === "up") {
289
+ if (inputUpHistoryEnabled && options.isInputFocused && keyName === "up") {
287
290
  preventDefault(keyEvent);
288
291
  recallHistory("up");
289
292
  return;
@@ -539,6 +542,10 @@ function serializeResultDocument(document) {
539
542
 
540
543
  // src/tui/opentui/renderResult.tsx
541
544
  import { jsx, jsxs } from "@opentui/react/jsx-runtime";
545
+ var RESULT_VIEW_OPTIONS = [
546
+ { name: "PRETTY", description: "Markdown view", value: "pretty" },
547
+ { name: "JSON", description: "Raw JSON view", value: "json" }
548
+ ];
542
549
  var jsonSyntaxStyle = null;
543
550
  function getJsonSyntaxStyle() {
544
551
  if (jsonSyntaxStyle) {
@@ -572,26 +579,227 @@ function getJsonSyntaxStyle() {
572
579
  });
573
580
  return jsonSyntaxStyle;
574
581
  }
582
+ function isRecord2(value) {
583
+ return typeof value === "object" && value !== null && !Array.isArray(value);
584
+ }
585
+ function escapeMd(value) {
586
+ return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, "<br/>");
587
+ }
588
+ function inlineValue(value) {
589
+ if (value === null) {
590
+ return "`null`";
591
+ }
592
+ if (value === void 0) {
593
+ return "`undefined`";
594
+ }
595
+ if (typeof value === "string") {
596
+ return escapeMd(value);
597
+ }
598
+ if (typeof value === "number" || typeof value === "boolean") {
599
+ return `\`${String(value)}\``;
600
+ }
601
+ if (Array.isArray(value)) {
602
+ return `\`[${value.length} items]\``;
603
+ }
604
+ if (isRecord2(value)) {
605
+ return `\`{${Object.keys(value).length} keys}\``;
606
+ }
607
+ return escapeMd(String(value));
608
+ }
609
+ function headingForDepth(depth) {
610
+ const safeDepth = Math.min(6, Math.max(2, depth + 2));
611
+ return "#".repeat(safeDepth);
612
+ }
613
+ function renderObjectMarkdown(record, depth, sectionTitle) {
614
+ const entries = Object.entries(record);
615
+ if (entries.length === 0) {
616
+ return `${headingForDepth(depth)} ${sectionTitle}
617
+
618
+ _Empty object_
619
+ `;
620
+ }
621
+ const lines = [];
622
+ lines.push(`${headingForDepth(depth)} ${sectionTitle}`);
623
+ lines.push("");
624
+ lines.push("| Key | Value |");
625
+ lines.push("| --- | --- |");
626
+ const nestedSections = [];
627
+ for (const [key, value] of entries) {
628
+ const escapedKey = escapeMd(key);
629
+ if (Array.isArray(value) || isRecord2(value)) {
630
+ lines.push(`| ${escapedKey} | ${inlineValue(value)} |`);
631
+ nestedSections.push(renderAnyMarkdown(value, depth + 1, key));
632
+ continue;
633
+ }
634
+ lines.push(`| ${escapedKey} | ${inlineValue(value)} |`);
635
+ }
636
+ if (nestedSections.length > 0) {
637
+ lines.push("");
638
+ lines.push(...nestedSections);
639
+ }
640
+ return `${lines.join("\n")}
641
+ `;
642
+ }
643
+ function renderArrayMarkdown(values, depth, sectionTitle) {
644
+ const lines = [];
645
+ lines.push(`${headingForDepth(depth)} ${sectionTitle}`);
646
+ lines.push("");
647
+ if (values.length === 0) {
648
+ lines.push("_Empty array_");
649
+ return `${lines.join("\n")}
650
+ `;
651
+ }
652
+ const firstItem = values[0];
653
+ if (isRecord2(firstItem)) {
654
+ const headers = Object.keys(firstItem);
655
+ lines.push(`| ${headers.map(escapeMd).join(" | ")} |`);
656
+ lines.push(`| ${headers.map(() => "---").join(" | ")} |`);
657
+ const nestedSections2 = [];
658
+ values.forEach((item, index) => {
659
+ if (!isRecord2(item)) {
660
+ lines.push(`| ${escapeMd(String(item))} |`);
661
+ return;
662
+ }
663
+ const rowCells = headers.map((header) => {
664
+ const cellValue = item[header];
665
+ if (Array.isArray(cellValue) || isRecord2(cellValue)) {
666
+ nestedSections2.push(renderAnyMarkdown(cellValue, depth + 1, `${sectionTitle}[${index}].${header}`));
667
+ }
668
+ return inlineValue(cellValue);
669
+ });
670
+ lines.push(`| ${rowCells.join(" | ")} |`);
671
+ });
672
+ if (nestedSections2.length > 0) {
673
+ lines.push("");
674
+ lines.push(...nestedSections2);
675
+ }
676
+ return `${lines.join("\n")}
677
+ `;
678
+ }
679
+ lines.push("| Index | Value |");
680
+ lines.push("| --- | --- |");
681
+ const nestedSections = [];
682
+ values.forEach((value, index) => {
683
+ if (Array.isArray(value) || isRecord2(value)) {
684
+ lines.push(`| ${index} | ${inlineValue(value)} |`);
685
+ nestedSections.push(renderAnyMarkdown(value, depth + 1, `${sectionTitle}[${index}]`));
686
+ return;
687
+ }
688
+ lines.push(`| ${index} | ${inlineValue(value)} |`);
689
+ });
690
+ if (nestedSections.length > 0) {
691
+ lines.push("");
692
+ lines.push(...nestedSections);
693
+ }
694
+ return `${lines.join("\n")}
695
+ `;
696
+ }
697
+ function renderAnyMarkdown(value, depth, sectionTitle) {
698
+ if (Array.isArray(value)) {
699
+ return renderArrayMarkdown(value, depth, sectionTitle);
700
+ }
701
+ if (isRecord2(value)) {
702
+ return renderObjectMarkdown(value, depth, sectionTitle);
703
+ }
704
+ return `${headingForDepth(depth)} ${sectionTitle}
705
+
706
+ ${inlineValue(value)}
707
+ `;
708
+ }
709
+ function resolvePrettyPayload(document) {
710
+ const payload = document.payload;
711
+ if (!isRecord2(payload)) {
712
+ return payload;
713
+ }
714
+ const status = typeof payload.status === "number" ? payload.status : void 0;
715
+ const success = typeof payload.success === "boolean" ? payload.success : void 0;
716
+ if (typeof status === "number") {
717
+ if (status >= 400) {
718
+ if ("error" in payload) {
719
+ return payload.error;
720
+ }
721
+ if (isRecord2(payload.data) && "error" in payload.data) {
722
+ return payload.data.error;
723
+ }
724
+ return payload;
725
+ }
726
+ if ("data" in payload) {
727
+ return payload.data;
728
+ }
729
+ return payload;
730
+ }
731
+ if (success === true) {
732
+ if ("data" in payload) {
733
+ return payload.data;
734
+ }
735
+ return payload;
736
+ }
737
+ if (success === false) {
738
+ if ("error" in payload) {
739
+ return payload.error;
740
+ }
741
+ return payload;
742
+ }
743
+ if (document.ok && "data" in payload) {
744
+ return payload.data;
745
+ }
746
+ if (!document.ok && "error" in payload) {
747
+ return payload.error;
748
+ }
749
+ return payload;
750
+ }
751
+ function buildPrettyMarkdown(document) {
752
+ const payload = resolvePrettyPayload(document);
753
+ return renderAnyMarkdown(payload, 0, document.ok ? "Data" : "Error");
754
+ }
575
755
  function renderResult(params) {
576
756
  const {
577
757
  entry,
578
- isFocused
758
+ isFocused,
759
+ viewMode,
760
+ onViewModeChange,
761
+ resultScrollRef
579
762
  } = params;
580
763
  if (!entry) {
581
764
  return /* @__PURE__ */ jsx("text", { fg: "yellow", children: "No command executed yet." });
582
765
  }
583
766
  const document = buildResultDocument(entry);
584
767
  const json = serializeResultDocument(document);
768
+ const markdown = buildPrettyMarkdown(document);
769
+ const viewOptions = viewMode === "pretty" ? RESULT_VIEW_OPTIONS : [RESULT_VIEW_OPTIONS[1], RESULT_VIEW_OPTIONS[0]];
585
770
  return /* @__PURE__ */ jsxs("box", { flexDirection: "column", width: "100%", height: "100%", children: [
586
- /* @__PURE__ */ jsx("box", { paddingBottom: 1, children: /* @__PURE__ */ jsxs("text", { fg: document.ok ? "green" : "red", children: [
587
- document.ok ? "OK" : "ERR",
588
- " ",
589
- document.command,
590
- " [",
591
- document.durationMs,
592
- "ms]"
593
- ] }) }),
594
- /* @__PURE__ */ jsx("box", { flexGrow: 1, width: "100%", height: "100%", children: /* @__PURE__ */ jsx("scrollbox", { focused: isFocused, style: { height: "100%" }, children: /* @__PURE__ */ jsx(
771
+ /* @__PURE__ */ jsxs("box", { paddingBottom: 1, flexDirection: "column", children: [
772
+ /* @__PURE__ */ jsxs("text", { fg: document.ok ? "green" : "red", children: [
773
+ document.ok ? "OK" : "ERR",
774
+ " ",
775
+ document.command,
776
+ " [",
777
+ document.durationMs,
778
+ "ms]"
779
+ ] }),
780
+ /* @__PURE__ */ jsx(
781
+ "tab-select",
782
+ {
783
+ options: viewOptions,
784
+ onChange: (_index, option) => {
785
+ if (option?.value === "json") {
786
+ onViewModeChange("json");
787
+ return;
788
+ }
789
+ onViewModeChange("pretty");
790
+ },
791
+ onSelect: (_index, option) => {
792
+ if (option?.value === "json") {
793
+ onViewModeChange("json");
794
+ return;
795
+ }
796
+ onViewModeChange("pretty");
797
+ },
798
+ focused: false
799
+ }
800
+ )
801
+ ] }),
802
+ /* @__PURE__ */ jsx("box", { flexGrow: 1, width: "100%", height: "100%", children: /* @__PURE__ */ jsx("scrollbox", { ref: resultScrollRef, focused: isFocused, scrollX: true, scrollY: true, style: { height: "100%" }, children: viewMode === "json" ? /* @__PURE__ */ jsx(
595
803
  "code",
596
804
  {
597
805
  width: "100%",
@@ -601,6 +809,18 @@ function renderResult(params) {
601
809
  streaming: false,
602
810
  conceal: false
603
811
  }
812
+ ) : /* @__PURE__ */ jsx(
813
+ "markdown",
814
+ {
815
+ content: markdown,
816
+ syntaxStyle: getJsonSyntaxStyle(),
817
+ streaming: false,
818
+ tableOptions: {
819
+ widthMode: "content",
820
+ wrapMode: "none",
821
+ borders: true
822
+ }
823
+ }
604
824
  ) }) })
605
825
  ] });
606
826
  }
@@ -642,23 +862,23 @@ var SHORTCUTS = [
642
862
  var SECTION_ORDER = ["shortcuts", "datasources", "history", "messages", "help"];
643
863
  var FOCUS_ORDER = ["input", "left", "result"];
644
864
  var ESC_ARM_TIMEOUT_MS = 1400;
645
- function isRecord2(value) {
865
+ function isRecord3(value) {
646
866
  return typeof value === "object" && value !== null && !Array.isArray(value);
647
867
  }
648
868
  function extractMetadata(payload) {
649
- if (!isRecord2(payload)) {
869
+ if (!isRecord3(payload)) {
650
870
  return {};
651
871
  }
652
872
  const environmentValue = payload.environment;
653
873
  const contextValue = payload.context;
654
- const environment = isRecord2(environmentValue) && typeof environmentValue.id === "string" && typeof environmentValue.name === "string" ? { id: environmentValue.id, name: environmentValue.name } : void 0;
874
+ const environment = isRecord3(environmentValue) && typeof environmentValue.id === "string" && typeof environmentValue.name === "string" ? { id: environmentValue.id, name: environmentValue.name } : void 0;
655
875
  if (contextValue === null) {
656
876
  return {
657
877
  environment,
658
878
  context: null
659
879
  };
660
880
  }
661
- const context = isRecord2(contextValue) && typeof contextValue.email === "string" && typeof contextValue.tenantDisplay === "string" ? {
881
+ const context = isRecord3(contextValue) && typeof contextValue.email === "string" && typeof contextValue.tenantDisplay === "string" ? {
662
882
  email: contextValue.email,
663
883
  tenantDisplay: contextValue.tenantDisplay
664
884
  } : void 0;
@@ -706,6 +926,17 @@ function toCatalogErrorMessage(command, result) {
706
926
  const message = result.error?.message || "Command failed.";
707
927
  return `${command} failed: ${message}`;
708
928
  }
929
+ function isScrollAtBottom(scrollBox) {
930
+ if (!scrollBox) {
931
+ return true;
932
+ }
933
+ const viewportHeight = scrollBox.viewport.height;
934
+ const maxScrollTop = Math.max(0, scrollBox.scrollHeight - viewportHeight);
935
+ if (maxScrollTop <= 0) {
936
+ return true;
937
+ }
938
+ return scrollBox.scrollTop >= maxScrollTop - 1;
939
+ }
709
940
  function DocyrusOpenTuiApp(props) {
710
941
  const renderer = useRenderer();
711
942
  const [input, setInput] = useState2("");
@@ -722,6 +953,7 @@ function DocyrusOpenTuiApp(props) {
722
953
  const [selectedHistoryIndex, setSelectedHistoryIndex] = useState2(0);
723
954
  const [isExitConfirmOpen, setIsExitConfirmOpen] = useState2(false);
724
955
  const [isEscArmed, setIsEscArmed] = useState2(false);
956
+ const [resultViewMode, setResultViewMode] = useState2("pretty");
725
957
  const [dataSourceSearch, setDataSourceSearch] = useState2("");
726
958
  const [dataSourcePanelFocus, setDataSourcePanelFocus] = useState2("tree");
727
959
  const [selectedDataSourceRowIndex, setSelectedDataSourceRowIndex] = useState2(0);
@@ -732,6 +964,9 @@ function DocyrusOpenTuiApp(props) {
732
964
  const [catalogError, setCatalogError] = useState2(null);
733
965
  const [catalogLoadedAt, setCatalogLoadedAt] = useState2(null);
734
966
  const catalogScopeRef = useRef("");
967
+ const resultScrollRef = useRef(null);
968
+ const leftScrollRef = useRef(null);
969
+ const commandInputRef = useRef(null);
735
970
  const executor = useMemo(() => {
736
971
  return createTuiProcessExecutor({
737
972
  executionConfig: props.executionConfig
@@ -980,6 +1215,7 @@ function DocyrusOpenTuiApp(props) {
980
1215
  isInputFocused: focusedPanel === "input",
981
1216
  isModalOpen: isExitConfirmOpen,
982
1217
  hotkeysEnabled: !isRunning && !isDataSourceSearchFocused,
1218
+ sectionTabEnabled: focusedPanel !== "result",
983
1219
  setInput,
984
1220
  onSubmit: executeLine,
985
1221
  onClear: () => {
@@ -1024,9 +1260,44 @@ function DocyrusOpenTuiApp(props) {
1024
1260
  renderer.destroy();
1025
1261
  return;
1026
1262
  }
1263
+ if (focusedPanel === "result" && keyName === "tab" && !isRunning && !isCatalogLoading) {
1264
+ preventDefault2(keyEvent);
1265
+ setResultViewMode((previous) => previous === "pretty" ? "json" : "pretty");
1266
+ return;
1267
+ }
1027
1268
  if (isRunning) {
1028
1269
  return;
1029
1270
  }
1271
+ if (keyName === "down") {
1272
+ if (focusedPanel === "result") {
1273
+ if (isScrollAtBottom(resultScrollRef.current)) {
1274
+ preventDefault2(keyEvent);
1275
+ setIsEscArmed(false);
1276
+ setFocusedPanel("input");
1277
+ }
1278
+ return;
1279
+ }
1280
+ if (focusedPanel === "left") {
1281
+ let shouldSwitchToInput = false;
1282
+ if (activeSection === "shortcuts") {
1283
+ shouldSwitchToInput = shortcutOptions.length === 0 || selectedShortcutIndex >= shortcutOptions.length - 1;
1284
+ } else if (activeSection === "history") {
1285
+ shouldSwitchToInput = historyOptions.length === 0 || selectedHistoryIndex >= historyOptions.length - 1;
1286
+ } else if (activeSection === "datasources") {
1287
+ if (dataSourcePanelFocus === "tree") {
1288
+ shouldSwitchToInput = dataSourceOptions.length === 0 || selectedDataSourceRowIndex >= dataSourceOptions.length - 1;
1289
+ }
1290
+ } else if (activeSection === "messages" || activeSection === "help") {
1291
+ shouldSwitchToInput = isScrollAtBottom(leftScrollRef.current);
1292
+ }
1293
+ if (shouldSwitchToInput) {
1294
+ preventDefault2(keyEvent);
1295
+ setIsEscArmed(false);
1296
+ setFocusedPanel("input");
1297
+ return;
1298
+ }
1299
+ }
1300
+ }
1030
1301
  if (isEscapeKey(keyName)) {
1031
1302
  preventDefault2(keyEvent);
1032
1303
  if (focusedPanel !== "input") {
@@ -1049,7 +1320,44 @@ function DocyrusOpenTuiApp(props) {
1049
1320
  return;
1050
1321
  }
1051
1322
  if (keyName === "left" || keyName === "right") {
1052
- if (focusedPanel === "input" && input.length > 0) {
1323
+ if (focusedPanel === "input") {
1324
+ const cursorOffset = commandInputRef.current?.cursorOffset ?? input.length;
1325
+ const inputLength = input.length;
1326
+ const atStart = cursorOffset <= 0;
1327
+ const atEnd = cursorOffset >= inputLength;
1328
+ if (keyName === "left" && atStart) {
1329
+ preventDefault2(keyEvent);
1330
+ setIsEscArmed(false);
1331
+ setFocusedPanel("left");
1332
+ return;
1333
+ }
1334
+ if (keyName === "right" && atEnd) {
1335
+ preventDefault2(keyEvent);
1336
+ setIsEscArmed(false);
1337
+ setFocusedPanel("result");
1338
+ return;
1339
+ }
1340
+ return;
1341
+ }
1342
+ if (focusedPanel === "result") {
1343
+ const resultScroll = resultScrollRef.current;
1344
+ if (!resultScroll) {
1345
+ preventDefault2(keyEvent);
1346
+ setIsEscArmed(false);
1347
+ setFocusedPanel(keyName === "left" ? "left" : "input");
1348
+ return;
1349
+ }
1350
+ const viewportWidth = resultScroll.viewport.width;
1351
+ const maxScrollLeft = Math.max(0, resultScroll.scrollWidth - viewportWidth);
1352
+ const hasHorizontalScroll = maxScrollLeft > 0;
1353
+ const isAtStart = resultScroll.scrollLeft <= 0;
1354
+ const isAtEnd = resultScroll.scrollLeft >= maxScrollLeft - 1;
1355
+ if (!hasHorizontalScroll || keyName === "left" && isAtStart || keyName === "right" && isAtEnd) {
1356
+ preventDefault2(keyEvent);
1357
+ setIsEscArmed(false);
1358
+ setFocusedPanel(keyName === "left" ? "left" : "input");
1359
+ return;
1360
+ }
1053
1361
  return;
1054
1362
  }
1055
1363
  preventDefault2(keyEvent);
@@ -1302,35 +1610,105 @@ function DocyrusOpenTuiApp(props) {
1302
1610
  ] });
1303
1611
  }
1304
1612
  if (activeSection === "messages") {
1305
- return /* @__PURE__ */ jsxs2("scrollbox", { focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning, style: { height: "100%" }, children: [
1306
- systemMessages.length === 0 && /* @__PURE__ */ jsx2("text", { fg: "gray", children: "No messages yet." }),
1307
- systemMessages.map((line, index) => /* @__PURE__ */ jsx2("text", { children: line }, `message-${index}`))
1308
- ] });
1613
+ return /* @__PURE__ */ jsxs2(
1614
+ "scrollbox",
1615
+ {
1616
+ ref: leftScrollRef,
1617
+ focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning,
1618
+ style: { height: "100%" },
1619
+ children: [
1620
+ systemMessages.length === 0 && /* @__PURE__ */ jsx2("text", { fg: "gray", children: "No messages yet." }),
1621
+ systemMessages.map((line, index) => /* @__PURE__ */ jsx2("text", { children: line }, `message-${index}`))
1622
+ ]
1623
+ }
1624
+ );
1309
1625
  }
1310
- return /* @__PURE__ */ jsxs2("scrollbox", { focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning, style: { height: "100%" }, children: [
1311
- /* @__PURE__ */ jsx2("text", { fg: "cyan", children: "Key Bindings" }),
1312
- /* @__PURE__ */ jsx2("text", { children: "Left/Right: switch focused panel" }),
1313
- /* @__PURE__ */ jsx2("text", { children: "Up/Down in left lists: selection" }),
1314
- /* @__PURE__ */ jsx2("text", { children: "Up/Down in result: scroll" }),
1315
- /* @__PURE__ */ jsx2("text", { children: "Enter: run command" }),
1316
- /* @__PURE__ */ jsx2("text", { children: "Up/Down in input: command history" }),
1317
- /* @__PURE__ */ jsx2("text", { children: "Tab / Shift+Tab: next/prev left section" }),
1318
- /* @__PURE__ */ jsx2("text", { children: "[ / ]: next/prev left section" }),
1319
- /* @__PURE__ */ jsx2("text", { children: "1..6: run shortcut" }),
1320
- /* @__PURE__ */ jsx2("text", { children: "/ in DataSources: focus search" }),
1321
- /* @__PURE__ */ jsx2("text", { children: "Esc in DataSources search: return to tree" }),
1322
- /* @__PURE__ */ jsx2("text", { children: "Space in DataSources tree: toggle app folder" }),
1323
- /* @__PURE__ */ jsx2("text", { children: "Enter in DataSources tree: run ds get or toggle app" }),
1324
- /* @__PURE__ */ jsx2("text", { children: "Ctrl+L: clear command and message history" }),
1325
- /* @__PURE__ */ jsx2("text", { children: "Esc (other panel): focus command input" }),
1326
- /* @__PURE__ */ jsx2("text", { children: "Esc then Esc: open exit confirmation" }),
1327
- /* @__PURE__ */ jsx2("text", { children: "Enter (confirm): exit, Esc (confirm): cancel" }),
1328
- /* @__PURE__ */ jsx2("text", { children: "Ctrl+C: force quit" })
1329
- ] });
1626
+ return /* @__PURE__ */ jsxs2(
1627
+ "scrollbox",
1628
+ {
1629
+ ref: leftScrollRef,
1630
+ focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning,
1631
+ style: { height: "100%" },
1632
+ children: [
1633
+ /* @__PURE__ */ jsx2("text", { fg: "cyan", children: "Key Bindings" }),
1634
+ /* @__PURE__ */ jsx2("text", { children: "Input Left/Right: caret move, edge switches panel" }),
1635
+ /* @__PURE__ */ jsx2("text", { children: "Left/Right (non-input/result): switch focused panel" }),
1636
+ /* @__PURE__ */ jsx2("text", { children: "Up/Down in left lists: selection" }),
1637
+ /* @__PURE__ */ jsx2("text", { children: "Up/Down in result: vertical scroll" }),
1638
+ /* @__PURE__ */ jsx2("text", { children: "Left/Right in result: horizontal scroll (edge switches panel)" }),
1639
+ /* @__PURE__ */ jsx2("text", { children: "Down at bottom in left/result: focus command input" }),
1640
+ /* @__PURE__ */ jsx2("text", { children: "Enter: run command" }),
1641
+ /* @__PURE__ */ jsx2("text", { children: "Up/Down in input: command history" }),
1642
+ /* @__PURE__ */ jsx2("text", { children: "Tab in result panel: toggle PRETTY/JSON" }),
1643
+ /* @__PURE__ */ jsx2("text", { children: "Tab / Shift+Tab (outside result): next/prev left section" }),
1644
+ /* @__PURE__ */ jsx2("text", { children: "[ / ]: next/prev left section" }),
1645
+ /* @__PURE__ */ jsx2("text", { children: "1..6: run shortcut" }),
1646
+ /* @__PURE__ */ jsx2("text", { children: "/ in DataSources: focus search" }),
1647
+ /* @__PURE__ */ jsx2("text", { children: "Esc in DataSources search: return to tree" }),
1648
+ /* @__PURE__ */ jsx2("text", { children: "Space in DataSources tree: toggle app folder" }),
1649
+ /* @__PURE__ */ jsx2("text", { children: "Enter in DataSources tree: run ds get or toggle app" }),
1650
+ /* @__PURE__ */ jsx2("text", { children: "Ctrl+L: clear command and message history" }),
1651
+ /* @__PURE__ */ jsx2("text", { children: "Esc (other panel): focus command input" }),
1652
+ /* @__PURE__ */ jsx2("text", { children: "Esc then Esc: open exit confirmation" }),
1653
+ /* @__PURE__ */ jsx2("text", { children: "Enter (confirm): exit, Esc (confirm): cancel" }),
1654
+ /* @__PURE__ */ jsx2("text", { children: "Ctrl+C: force quit" })
1655
+ ]
1656
+ }
1657
+ );
1330
1658
  };
1331
1659
  return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", width: "100%", height: "100%", padding: 1, children: [
1332
1660
  /* @__PURE__ */ jsxs2("box", { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, paddingX: 1, children: [
1333
- /* @__PURE__ */ jsx2("ascii-font", { text: "DOCYRUS", font: "tiny", color: "#7aa2f7" }),
1661
+ /* @__PURE__ */ jsxs2(
1662
+ "box",
1663
+ {
1664
+ style: {
1665
+ position: "relative",
1666
+ width: 50,
1667
+ height: 6
1668
+ },
1669
+ children: [
1670
+ /* @__PURE__ */ jsx2(
1671
+ "box",
1672
+ {
1673
+ style: {
1674
+ position: "absolute",
1675
+ left: 1,
1676
+ top: 0
1677
+ },
1678
+ children: /* @__PURE__ */ jsx2(
1679
+ "ascii-font",
1680
+ {
1681
+ text: "DOCYRUS",
1682
+ font: "block",
1683
+ color: RGBA2.fromHex("#3f4bff")
1684
+ }
1685
+ )
1686
+ }
1687
+ ),
1688
+ /* @__PURE__ */ jsx2(
1689
+ "box",
1690
+ {
1691
+ style: {
1692
+ position: "absolute",
1693
+ left: 0,
1694
+ top: 0
1695
+ },
1696
+ children: /* @__PURE__ */ jsx2(
1697
+ "ascii-font",
1698
+ {
1699
+ text: "DOCYRUS",
1700
+ font: "block",
1701
+ color: [
1702
+ RGBA2.fromHex("#ef4444"),
1703
+ RGBA2.fromHex("#ef4444")
1704
+ ]
1705
+ }
1706
+ )
1707
+ }
1708
+ )
1709
+ ]
1710
+ }
1711
+ ),
1334
1712
  /* @__PURE__ */ jsxs2("box", { flexDirection: "column", alignItems: "flex-end", justifyContent: "center", children: [
1335
1713
  /* @__PURE__ */ jsxs2("text", { children: [
1336
1714
  "env: ",
@@ -1392,7 +1770,10 @@ function DocyrusOpenTuiApp(props) {
1392
1770
  /* @__PURE__ */ jsx2("text", { fg: "gray", children: "Running command..." })
1393
1771
  ] }) : renderResult({
1394
1772
  entry: activeEntry,
1395
- isFocused: focusedPanel === "result" && !isExitConfirmOpen
1773
+ isFocused: focusedPanel === "result" && !isExitConfirmOpen,
1774
+ viewMode: resultViewMode,
1775
+ onViewModeChange: setResultViewMode,
1776
+ resultScrollRef
1396
1777
  })
1397
1778
  }
1398
1779
  )
@@ -1410,6 +1791,7 @@ function DocyrusOpenTuiApp(props) {
1410
1791
  /* @__PURE__ */ jsx2(
1411
1792
  "input",
1412
1793
  {
1794
+ ref: commandInputRef,
1413
1795
  style: { flexGrow: 1 },
1414
1796
  value: input,
1415
1797
  placeholder: "Type a command...",