@docyrus/docyrus 0.0.5 → 0.0.7

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/tui.mjs CHANGED
@@ -4,7 +4,7 @@ import { createRoot } from "@opentui/react";
4
4
 
5
5
  // src/tui/opentui/DocyrusOpenTuiApp.tsx
6
6
  import { useKeyboard as useKeyboard2, useRenderer } from "@opentui/react";
7
- import { useCallback as useCallback2, useEffect, useMemo, useState as useState2 } from "react";
7
+ import { useCallback as useCallback2, useEffect, useMemo, useRef, useState as useState2 } from "react";
8
8
 
9
9
  // src/services/tuiProcessExecutor.ts
10
10
  import { spawnSync } from "node:child_process";
@@ -202,21 +202,6 @@ function preventDefault(event) {
202
202
  event.preventDefault();
203
203
  }
204
204
  }
205
- function resolveSectionJumpKey(keyName, shift) {
206
- if (keyName === "s") {
207
- return "shortcuts";
208
- }
209
- if (keyName === "h") {
210
- return "history";
211
- }
212
- if (keyName === "m") {
213
- return "messages";
214
- }
215
- if (keyName === "?" || keyName === "/" && shift) {
216
- return "help";
217
- }
218
- return null;
219
- }
220
205
  function useCommandInput(options) {
221
206
  const [history, setHistory] = useState([]);
222
207
  const [historyCursor, setHistoryCursor] = useState(null);
@@ -273,12 +258,13 @@ function useCommandInput(options) {
273
258
  return;
274
259
  }
275
260
  const keyName = keyEvent.name;
261
+ const hotkeysEnabled = options.hotkeysEnabled !== false;
276
262
  if (keyEvent.ctrl && keyName === "l") {
277
263
  preventDefault(keyEvent);
278
264
  options.onClear();
279
265
  return;
280
266
  }
281
- if (keyName === "tab") {
267
+ if (hotkeysEnabled && keyName === "tab") {
282
268
  preventDefault(keyEvent);
283
269
  if (keyEvent.shift) {
284
270
  options.onPreviousSection();
@@ -287,12 +273,12 @@ function useCommandInput(options) {
287
273
  options.onNextSection();
288
274
  return;
289
275
  }
290
- if (keyName === "[") {
276
+ if (hotkeysEnabled && keyName === "[") {
291
277
  preventDefault(keyEvent);
292
278
  options.onPreviousSection();
293
279
  return;
294
280
  }
295
- if (keyName === "]") {
281
+ if (hotkeysEnabled && keyName === "]") {
296
282
  preventDefault(keyEvent);
297
283
  options.onNextSection();
298
284
  return;
@@ -307,18 +293,10 @@ function useCommandInput(options) {
307
293
  recallHistory("down");
308
294
  return;
309
295
  }
310
- if (options.input.length === 0) {
311
- const jumpSection = resolveSectionJumpKey(keyName, Boolean(keyEvent.shift));
312
- if (jumpSection) {
313
- preventDefault(keyEvent);
314
- options.onJumpSection(jumpSection);
315
- return;
316
- }
317
- }
318
296
  if (options.busy) {
319
297
  return;
320
298
  }
321
- if (options.input.length === 0 && /^[1-6]$/.test(keyName)) {
299
+ if (hotkeysEnabled && options.input.length === 0 && /^[1-6]$/.test(keyName)) {
322
300
  preventDefault(keyEvent);
323
301
  options.onShortcut(Number(keyName));
324
302
  }
@@ -328,6 +306,188 @@ function useCommandInput(options) {
328
306
  };
329
307
  }
330
308
 
309
+ // src/tui/opentui/dataSourceCatalog.ts
310
+ function isRecord(value) {
311
+ return typeof value === "object" && value !== null && !Array.isArray(value);
312
+ }
313
+ function toNonEmptyString(value) {
314
+ if (typeof value !== "string") {
315
+ return void 0;
316
+ }
317
+ const trimmed = value.trim();
318
+ return trimmed.length > 0 ? trimmed : void 0;
319
+ }
320
+ function toStringValue(value, fallback) {
321
+ return toNonEmptyString(value) || fallback;
322
+ }
323
+ function getArrayCandidates(payload) {
324
+ if (Array.isArray(payload)) {
325
+ return payload;
326
+ }
327
+ if (!isRecord(payload)) {
328
+ return [];
329
+ }
330
+ const directData = payload.data;
331
+ if (Array.isArray(directData)) {
332
+ return directData;
333
+ }
334
+ if (isRecord(directData) && Array.isArray(directData.data)) {
335
+ return directData.data;
336
+ }
337
+ if (Array.isArray(payload.items)) {
338
+ return payload.items;
339
+ }
340
+ if (Array.isArray(payload.dataSources)) {
341
+ return payload.dataSources;
342
+ }
343
+ return [];
344
+ }
345
+ function compareByName(a, b) {
346
+ return a.localeCompare(b, void 0, { sensitivity: "base" });
347
+ }
348
+ function getDataSourceDisplayName(dataSource) {
349
+ return dataSource.title || dataSource.name || dataSource.slug;
350
+ }
351
+ function extractAppsFromPayload(payload) {
352
+ const result = [];
353
+ const seen = /* @__PURE__ */ new Set();
354
+ for (const candidate of getArrayCandidates(payload)) {
355
+ if (!isRecord(candidate)) {
356
+ continue;
357
+ }
358
+ const id = toNonEmptyString(candidate.id);
359
+ const slug = toNonEmptyString(candidate.slug);
360
+ if (!id || !slug) {
361
+ continue;
362
+ }
363
+ const name = toStringValue(candidate.name, slug);
364
+ const dedupeKey = `${id}::${slug}`;
365
+ if (seen.has(dedupeKey)) {
366
+ continue;
367
+ }
368
+ seen.add(dedupeKey);
369
+ result.push({ id, slug, name });
370
+ }
371
+ result.sort((a, b) => compareByName(a.name, b.name));
372
+ return result;
373
+ }
374
+ function extractDataSourcesFromPayload(payload) {
375
+ const result = [];
376
+ const seen = /* @__PURE__ */ new Set();
377
+ for (const candidate of getArrayCandidates(payload)) {
378
+ if (!isRecord(candidate)) {
379
+ continue;
380
+ }
381
+ const id = toNonEmptyString(candidate.id);
382
+ const slug = toNonEmptyString(candidate.slug);
383
+ if (!id || !slug) {
384
+ continue;
385
+ }
386
+ const title = toNonEmptyString(candidate.title);
387
+ const name = toStringValue(candidate.name, slug);
388
+ const appSlug = toNonEmptyString(candidate.app_slug);
389
+ const tenantAppId = toNonEmptyString(candidate.tenant_app_id);
390
+ const dedupeKey = `${id}::${slug}`;
391
+ if (seen.has(dedupeKey)) {
392
+ continue;
393
+ }
394
+ seen.add(dedupeKey);
395
+ result.push({
396
+ id,
397
+ name,
398
+ title,
399
+ slug,
400
+ app_slug: appSlug,
401
+ tenant_app_id: tenantAppId
402
+ });
403
+ }
404
+ result.sort((a, b) => compareByName(getDataSourceDisplayName(a), getDataSourceDisplayName(b)));
405
+ return result;
406
+ }
407
+ function groupDataSourcesByApp(params) {
408
+ const { apps, dataSources } = params;
409
+ const appById = /* @__PURE__ */ new Map();
410
+ const appBySlug = /* @__PURE__ */ new Map();
411
+ const groups = /* @__PURE__ */ new Map();
412
+ for (const app of apps) {
413
+ appById.set(app.id, app);
414
+ appBySlug.set(app.slug, app);
415
+ }
416
+ for (const dataSource of dataSources) {
417
+ const byId = dataSource.tenant_app_id ? appById.get(dataSource.tenant_app_id) : void 0;
418
+ const bySlug = dataSource.app_slug ? appBySlug.get(dataSource.app_slug) : void 0;
419
+ const matchedApp = byId || bySlug;
420
+ const appKey = matchedApp ? matchedApp.id : dataSource.app_slug ? `slug:${dataSource.app_slug}` : "unknown";
421
+ const group = groups.get(appKey) || {
422
+ appKey,
423
+ appId: matchedApp?.id || dataSource.tenant_app_id,
424
+ appName: matchedApp?.name || dataSource.app_slug || "Unknown App",
425
+ appSlug: matchedApp?.slug || dataSource.app_slug,
426
+ dataSources: []
427
+ };
428
+ group.dataSources.push(dataSource);
429
+ groups.set(appKey, group);
430
+ }
431
+ const list = Array.from(groups.values());
432
+ for (const group of list) {
433
+ group.dataSources.sort((a, b) => compareByName(getDataSourceDisplayName(a), getDataSourceDisplayName(b)));
434
+ }
435
+ list.sort((a, b) => compareByName(a.appName, b.appName));
436
+ return list;
437
+ }
438
+ function filterDataSources(groups, query) {
439
+ const normalizedQuery = query.trim().toLowerCase();
440
+ if (normalizedQuery.length === 0) {
441
+ return groups;
442
+ }
443
+ return groups.map((group) => {
444
+ const filteredDataSources = group.dataSources.filter((dataSource) => {
445
+ const haystack = [
446
+ dataSource.title,
447
+ dataSource.name,
448
+ dataSource.slug
449
+ ].filter((value) => Boolean(value)).join(" ").toLowerCase();
450
+ return haystack.includes(normalizedQuery);
451
+ });
452
+ return {
453
+ ...group,
454
+ dataSources: filteredDataSources
455
+ };
456
+ }).filter((group) => group.dataSources.length > 0);
457
+ }
458
+ function flattenTreeRows(params) {
459
+ const rows = [];
460
+ const alwaysExpanded = params.query.trim().length > 0;
461
+ for (const group of params.groups) {
462
+ const expanded = alwaysExpanded || params.expandedAppKeys.has(group.appKey);
463
+ rows.push({
464
+ id: `app:${group.appKey}`,
465
+ kind: "app",
466
+ appKey: group.appKey,
467
+ appName: group.appName,
468
+ appSlug: group.appSlug,
469
+ expanded
470
+ });
471
+ if (!expanded) {
472
+ continue;
473
+ }
474
+ for (const dataSource of group.dataSources) {
475
+ const dataSourceName = getDataSourceDisplayName(dataSource);
476
+ rows.push({
477
+ id: `ds:${group.appKey}:${dataSource.id}`,
478
+ kind: "datasource",
479
+ appKey: group.appKey,
480
+ appName: group.appName,
481
+ appSlug: group.appSlug || dataSource.app_slug,
482
+ dataSourceId: dataSource.id,
483
+ dataSourceName,
484
+ dataSourceSlug: dataSource.slug
485
+ });
486
+ }
487
+ }
488
+ return rows;
489
+ }
490
+
331
491
  // src/tui/opentui/renderResult.tsx
332
492
  import { RGBA, SyntaxStyle } from "@opentui/core";
333
493
 
@@ -479,26 +639,26 @@ var SHORTCUTS = [
479
639
  label: "insert: ds list <appSlug> <dataSourceSlug>"
480
640
  }
481
641
  ];
482
- var SECTION_ORDER = ["shortcuts", "history", "messages", "help"];
642
+ var SECTION_ORDER = ["shortcuts", "datasources", "history", "messages", "help"];
483
643
  var FOCUS_ORDER = ["input", "left", "result"];
484
644
  var ESC_ARM_TIMEOUT_MS = 1400;
485
- function isRecord(value) {
645
+ function isRecord2(value) {
486
646
  return typeof value === "object" && value !== null && !Array.isArray(value);
487
647
  }
488
648
  function extractMetadata(payload) {
489
- if (!isRecord(payload)) {
649
+ if (!isRecord2(payload)) {
490
650
  return {};
491
651
  }
492
652
  const environmentValue = payload.environment;
493
653
  const contextValue = payload.context;
494
- const environment = isRecord(environmentValue) && typeof environmentValue.id === "string" && typeof environmentValue.name === "string" ? { id: environmentValue.id, name: environmentValue.name } : void 0;
654
+ const environment = isRecord2(environmentValue) && typeof environmentValue.id === "string" && typeof environmentValue.name === "string" ? { id: environmentValue.id, name: environmentValue.name } : void 0;
495
655
  if (contextValue === null) {
496
656
  return {
497
657
  environment,
498
658
  context: null
499
659
  };
500
660
  }
501
- const context = isRecord(contextValue) && typeof contextValue.email === "string" && typeof contextValue.tenantDisplay === "string" ? {
661
+ const context = isRecord2(contextValue) && typeof contextValue.email === "string" && typeof contextValue.tenantDisplay === "string" ? {
502
662
  email: contextValue.email,
503
663
  tenantDisplay: contextValue.tenantDisplay
504
664
  } : void 0;
@@ -535,6 +695,17 @@ function isEscapeKey(keyName) {
535
695
  function isEnterKey(keyName) {
536
696
  return keyName === "enter" || keyName === "return";
537
697
  }
698
+ function isSlashKey(keyName) {
699
+ return keyName === "/" || keyName === "slash";
700
+ }
701
+ function toCatalogErrorMessage(command, result) {
702
+ const details = result.error?.details || result.error?.message || "Unknown error.";
703
+ if (details.includes("Architect.Read.All") || details.includes("Architect.ReadWrite.All")) {
704
+ return `${command} failed: missing required scope Architect.Read.All or Architect.ReadWrite.All.`;
705
+ }
706
+ const message = result.error?.message || "Command failed.";
707
+ return `${command} failed: ${message}`;
708
+ }
538
709
  function DocyrusOpenTuiApp(props) {
539
710
  const renderer = useRenderer();
540
711
  const [input, setInput] = useState2("");
@@ -551,11 +722,36 @@ function DocyrusOpenTuiApp(props) {
551
722
  const [selectedHistoryIndex, setSelectedHistoryIndex] = useState2(0);
552
723
  const [isExitConfirmOpen, setIsExitConfirmOpen] = useState2(false);
553
724
  const [isEscArmed, setIsEscArmed] = useState2(false);
725
+ const [dataSourceSearch, setDataSourceSearch] = useState2("");
726
+ const [dataSourcePanelFocus, setDataSourcePanelFocus] = useState2("tree");
727
+ const [selectedDataSourceRowIndex, setSelectedDataSourceRowIndex] = useState2(0);
728
+ const [expandedAppKeys, setExpandedAppKeys] = useState2([]);
729
+ const [appsCatalog, setAppsCatalog] = useState2([]);
730
+ const [dataSourcesCatalog, setDataSourcesCatalog] = useState2([]);
731
+ const [isCatalogLoading, setIsCatalogLoading] = useState2(false);
732
+ const [catalogError, setCatalogError] = useState2(null);
733
+ const [catalogLoadedAt, setCatalogLoadedAt] = useState2(null);
734
+ const catalogScopeRef = useRef("");
554
735
  const executor = useMemo(() => {
555
736
  return createTuiProcessExecutor({
556
737
  executionConfig: props.executionConfig
557
738
  });
558
739
  }, [props.executionConfig]);
740
+ const applyMetadataFromPayload = useCallback2((payload) => {
741
+ const metadata = extractMetadata(payload);
742
+ if (metadata.environment) {
743
+ setEnvironment(metadata.environment);
744
+ }
745
+ if (metadata.context !== void 0) {
746
+ setContext(metadata.context);
747
+ }
748
+ }, []);
749
+ const appendSystemMessages = useCallback2((messages) => {
750
+ if (messages.length === 0) {
751
+ return;
752
+ }
753
+ setSystemMessages((previous) => [...previous, ...messages]);
754
+ }, []);
559
755
  const applyCommandResult = useCallback2((result, command) => {
560
756
  const entry = {
561
757
  id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
@@ -567,18 +763,12 @@ function DocyrusOpenTuiApp(props) {
567
763
  setActiveEntryId(entry.id);
568
764
  setSelectedHistoryIndex(0);
569
765
  if (result.messages && result.messages.length > 0) {
570
- setSystemMessages((previous) => [...previous, ...result.messages || []]);
766
+ appendSystemMessages(result.messages);
571
767
  }
572
768
  if (result.ok && result.data !== void 0) {
573
- const metadata = extractMetadata(result.data);
574
- if (metadata.environment) {
575
- setEnvironment(metadata.environment);
576
- }
577
- if (metadata.context !== void 0) {
578
- setContext(metadata.context);
579
- }
769
+ applyMetadataFromPayload(result.data);
580
770
  }
581
- }, []);
771
+ }, [appendSystemMessages, applyMetadataFromPayload]);
582
772
  const executeArgs = useCallback2(async (args, commandText) => {
583
773
  setIsRunning(true);
584
774
  try {
@@ -588,6 +778,13 @@ function DocyrusOpenTuiApp(props) {
588
778
  setIsRunning(false);
589
779
  }
590
780
  }, [applyCommandResult, executor]);
781
+ const runInternalArgs = useCallback2(async (args) => {
782
+ const result = await executor.runCliCommand(args);
783
+ if (result.ok && result.data !== void 0) {
784
+ applyMetadataFromPayload(result.data);
785
+ }
786
+ return result;
787
+ }, [applyMetadataFromPayload, executor]);
591
788
  const executeLine = useCallback2((line) => {
592
789
  let args;
593
790
  try {
@@ -615,7 +812,7 @@ function DocyrusOpenTuiApp(props) {
615
812
  }, [applyCommandResult, executeArgs]);
616
813
  const applyShortcut = useCallback2((shortcutId) => {
617
814
  const action = SHORTCUTS.find((shortcut) => shortcut.id === shortcutId);
618
- if (!action || isRunning) {
815
+ if (!action || isRunning || isCatalogLoading) {
619
816
  return;
620
817
  }
621
818
  if (action.template) {
@@ -626,7 +823,107 @@ function DocyrusOpenTuiApp(props) {
626
823
  if (action.run) {
627
824
  void executeArgs(parseCommandLine(action.run), action.run);
628
825
  }
629
- }, [executeArgs, isRunning]);
826
+ }, [executeArgs, isCatalogLoading, isRunning]);
827
+ const groupedDataSourceItems = useMemo(() => {
828
+ return groupDataSourcesByApp({
829
+ apps: appsCatalog,
830
+ dataSources: dataSourcesCatalog
831
+ });
832
+ }, [appsCatalog, dataSourcesCatalog]);
833
+ const filteredDataSourceGroups = useMemo(() => {
834
+ return filterDataSources(groupedDataSourceItems, dataSourceSearch);
835
+ }, [dataSourceSearch, groupedDataSourceItems]);
836
+ const dataSourceCountByAppKey = useMemo(() => {
837
+ const result = /* @__PURE__ */ new Map();
838
+ for (const group of filteredDataSourceGroups) {
839
+ result.set(group.appKey, group.dataSources.length);
840
+ }
841
+ return result;
842
+ }, [filteredDataSourceGroups]);
843
+ const expandedAppKeySet = useMemo(() => {
844
+ return new Set(expandedAppKeys);
845
+ }, [expandedAppKeys]);
846
+ const dataSourceRows = useMemo(() => {
847
+ return flattenTreeRows({
848
+ groups: filteredDataSourceGroups,
849
+ expandedAppKeys: expandedAppKeySet,
850
+ query: dataSourceSearch
851
+ });
852
+ }, [dataSourceSearch, expandedAppKeySet, filteredDataSourceGroups]);
853
+ const selectedDataSourceRow = useMemo(() => {
854
+ if (dataSourceRows.length === 0) {
855
+ return null;
856
+ }
857
+ return dataSourceRows[selectedDataSourceRowIndex] || dataSourceRows[0] || null;
858
+ }, [dataSourceRows, selectedDataSourceRowIndex]);
859
+ const toggleAppExpansion = useCallback2((appKey) => {
860
+ setExpandedAppKeys((previous) => {
861
+ if (previous.includes(appKey)) {
862
+ return previous.filter((key) => key !== appKey);
863
+ }
864
+ return [...previous, appKey];
865
+ });
866
+ }, []);
867
+ const loadDataSourceCatalog = useCallback2(async () => {
868
+ if (isCatalogLoading) {
869
+ return;
870
+ }
871
+ setIsCatalogLoading(true);
872
+ setCatalogError(null);
873
+ try {
874
+ const nextMessages = [];
875
+ const appsResult = await runInternalArgs(["apps", "list"]);
876
+ const dataSourcesResult = await runInternalArgs(["curl", "/dev/data-sources"]);
877
+ const nextApps = appsResult.ok && appsResult.data !== void 0 ? extractAppsFromPayload(appsResult.data) : [];
878
+ const nextDataSources = dataSourcesResult.ok && dataSourcesResult.data !== void 0 ? extractDataSourcesFromPayload(dataSourcesResult.data) : [];
879
+ if (!appsResult.ok) {
880
+ nextMessages.push(toCatalogErrorMessage("apps list", appsResult));
881
+ }
882
+ if (!dataSourcesResult.ok) {
883
+ nextMessages.push(toCatalogErrorMessage("curl /dev/data-sources", dataSourcesResult));
884
+ }
885
+ setAppsCatalog(nextApps);
886
+ setDataSourcesCatalog(nextDataSources);
887
+ setCatalogLoadedAt(Date.now());
888
+ const nextGroups = groupDataSourcesByApp({
889
+ apps: nextApps,
890
+ dataSources: nextDataSources
891
+ });
892
+ setExpandedAppKeys((previous) => {
893
+ if (previous.length > 0) {
894
+ const allowed = new Set(nextGroups.map((group) => group.appKey));
895
+ return previous.filter((key) => allowed.has(key));
896
+ }
897
+ return nextGroups.map((group) => group.appKey);
898
+ });
899
+ if (nextMessages.length > 0) {
900
+ appendSystemMessages(nextMessages);
901
+ setCatalogError(nextMessages[0] || "Failed to load catalog.");
902
+ } else {
903
+ setCatalogError(null);
904
+ }
905
+ } finally {
906
+ setIsCatalogLoading(false);
907
+ }
908
+ }, [appendSystemMessages, isCatalogLoading, runInternalArgs]);
909
+ const runSelectedDataSourceRow = useCallback2(async (row) => {
910
+ if (!row || isRunning || isCatalogLoading) {
911
+ return;
912
+ }
913
+ if (row.kind === "app") {
914
+ toggleAppExpansion(row.appKey);
915
+ return;
916
+ }
917
+ if (!row.appSlug || !row.dataSourceSlug) {
918
+ appendSystemMessages([
919
+ "Cannot execute ds get because selected row is missing appSlug or dataSourceSlug."
920
+ ]);
921
+ return;
922
+ }
923
+ const command = `ds get ${row.appSlug} ${row.dataSourceSlug}`;
924
+ setFocusedPanel("result");
925
+ await executeArgs(["ds", "get", row.appSlug, row.dataSourceSlug], command);
926
+ }, [appendSystemMessages, executeArgs, isCatalogLoading, isRunning, toggleAppExpansion]);
630
927
  const setSectionByIndex = useCallback2((index) => {
631
928
  const safeIndex = index < 0 ? SECTION_ORDER.length - 1 : index >= SECTION_ORDER.length ? 0 : index;
632
929
  const section = SECTION_ORDER[safeIndex];
@@ -676,11 +973,13 @@ function DocyrusOpenTuiApp(props) {
676
973
  return [...previous, message];
677
974
  });
678
975
  }, []);
976
+ const isDataSourceSearchFocused = activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "search" && !isRunning && !isExitConfirmOpen;
679
977
  const { submitInput } = useCommandInput({
680
- busy: isRunning,
978
+ busy: isRunning || isCatalogLoading,
681
979
  input,
682
980
  isInputFocused: focusedPanel === "input",
683
981
  isModalOpen: isExitConfirmOpen,
982
+ hotkeysEnabled: !isRunning && !isDataSourceSearchFocused,
684
983
  setInput,
685
984
  onSubmit: executeLine,
686
985
  onClear: () => {
@@ -691,11 +990,7 @@ function DocyrusOpenTuiApp(props) {
691
990
  },
692
991
  onShortcut: applyShortcut,
693
992
  onNextSection: nextSection,
694
- onPreviousSection: previousSection,
695
- onJumpSection: (section) => {
696
- setActiveSection(section);
697
- setFocusedPanel("left");
698
- }
993
+ onPreviousSection: previousSection
699
994
  });
700
995
  useKeyboard2((keyEvent) => {
701
996
  const keyName = keyEvent.name;
@@ -715,10 +1010,23 @@ function DocyrusOpenTuiApp(props) {
715
1010
  }
716
1011
  return;
717
1012
  }
1013
+ if (activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "search" && isEscapeKey(keyName)) {
1014
+ preventDefault2(keyEvent);
1015
+ setDataSourcePanelFocus("tree");
1016
+ return;
1017
+ }
1018
+ if (activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "tree" && isSlashKey(keyName)) {
1019
+ preventDefault2(keyEvent);
1020
+ setDataSourcePanelFocus("search");
1021
+ return;
1022
+ }
718
1023
  if (keyEvent.ctrl && keyName === "c") {
719
1024
  renderer.destroy();
720
1025
  return;
721
1026
  }
1027
+ if (isRunning) {
1028
+ return;
1029
+ }
722
1030
  if (isEscapeKey(keyName)) {
723
1031
  preventDefault2(keyEvent);
724
1032
  if (focusedPanel !== "input") {
@@ -733,6 +1041,13 @@ function DocyrusOpenTuiApp(props) {
733
1041
  openExitConfirmation();
734
1042
  return;
735
1043
  }
1044
+ if (activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "tree" && keyName === "space") {
1045
+ preventDefault2(keyEvent);
1046
+ if (selectedDataSourceRow?.kind === "app") {
1047
+ toggleAppExpansion(selectedDataSourceRow.appKey);
1048
+ }
1049
+ return;
1050
+ }
736
1051
  if (keyName === "left" || keyName === "right") {
737
1052
  if (focusedPanel === "input" && input.length > 0) {
738
1053
  return;
@@ -746,7 +1061,12 @@ function DocyrusOpenTuiApp(props) {
746
1061
  }
747
1062
  return;
748
1063
  }
749
- if (keyName === "r" && activeSection === "history" && focusedPanel === "left" && input.length === 0 && !isRunning) {
1064
+ if (keyName === "r" && activeSection === "datasources" && focusedPanel === "left" && dataSourcePanelFocus === "tree" && !isCatalogLoading) {
1065
+ preventDefault2(keyEvent);
1066
+ void loadDataSourceCatalog();
1067
+ return;
1068
+ }
1069
+ if (keyName === "r" && activeSection === "history" && focusedPanel === "left" && input.length === 0 && !isRunning && !isCatalogLoading) {
750
1070
  const entry = entries[selectedHistoryIndex];
751
1071
  if (entry) {
752
1072
  void runHistoryEntry(entry);
@@ -768,7 +1088,7 @@ function DocyrusOpenTuiApp(props) {
768
1088
  };
769
1089
  }, [isEscArmed]);
770
1090
  useEffect(() => {
771
- if (!isRunning) {
1091
+ if (!isRunning && !isCatalogLoading) {
772
1092
  return;
773
1093
  }
774
1094
  const timer = setInterval(() => {
@@ -777,7 +1097,7 @@ function DocyrusOpenTuiApp(props) {
777
1097
  return () => {
778
1098
  clearInterval(timer);
779
1099
  };
780
- }, [isRunning]);
1100
+ }, [isCatalogLoading, isRunning]);
781
1101
  useEffect(() => {
782
1102
  if (selectedHistoryIndex < entries.length) {
783
1103
  return;
@@ -785,6 +1105,43 @@ function DocyrusOpenTuiApp(props) {
785
1105
  const nextIndex = Math.max(entries.length - 1, 0);
786
1106
  setSelectedHistoryIndex(nextIndex);
787
1107
  }, [entries.length, selectedHistoryIndex]);
1108
+ useEffect(() => {
1109
+ if (selectedDataSourceRowIndex < dataSourceRows.length) {
1110
+ return;
1111
+ }
1112
+ const nextIndex = Math.max(dataSourceRows.length - 1, 0);
1113
+ setSelectedDataSourceRowIndex(nextIndex);
1114
+ }, [dataSourceRows.length, selectedDataSourceRowIndex]);
1115
+ useEffect(() => {
1116
+ if (activeSection !== "datasources" && dataSourcePanelFocus !== "tree") {
1117
+ setDataSourcePanelFocus("tree");
1118
+ }
1119
+ }, [activeSection, dataSourcePanelFocus]);
1120
+ useEffect(() => {
1121
+ const scopeKey = `${environment?.id || ""}::${context?.tenantDisplay || ""}`;
1122
+ if (catalogScopeRef.current.length === 0) {
1123
+ catalogScopeRef.current = scopeKey;
1124
+ return;
1125
+ }
1126
+ if (catalogScopeRef.current === scopeKey) {
1127
+ return;
1128
+ }
1129
+ catalogScopeRef.current = scopeKey;
1130
+ if (catalogLoadedAt !== null) {
1131
+ setCatalogLoadedAt(null);
1132
+ setAppsCatalog([]);
1133
+ setDataSourcesCatalog([]);
1134
+ setExpandedAppKeys([]);
1135
+ setSelectedDataSourceRowIndex(0);
1136
+ setCatalogError(null);
1137
+ }
1138
+ }, [catalogLoadedAt, context?.tenantDisplay, environment?.id]);
1139
+ useEffect(() => {
1140
+ if (activeSection !== "datasources" || catalogLoadedAt !== null || isCatalogLoading) {
1141
+ return;
1142
+ }
1143
+ void loadDataSourceCatalog();
1144
+ }, [activeSection, catalogLoadedAt, isCatalogLoading, loadDataSourceCatalog]);
788
1145
  const spinner = useMemo(() => {
789
1146
  return ["|", "/", "-", "\\"][spinnerFrame] || "|";
790
1147
  }, [spinnerFrame]);
@@ -803,8 +1160,8 @@ function DocyrusOpenTuiApp(props) {
803
1160
  ...SECTION_ORDER.filter((section) => section !== activeSection)
804
1161
  ];
805
1162
  return ordered.map((section) => ({
806
- name: section === "shortcuts" ? "Shortcuts" : section === "history" ? "History" : section === "messages" ? "Messages" : "Help",
807
- description: section === "shortcuts" ? "Quick actions" : section === "history" ? "Past runs" : section === "messages" ? "System output" : "Key map",
1163
+ name: section === "shortcuts" ? "Shortcuts" : section === "datasources" ? "DataSources" : section === "history" ? "History" : section === "messages" ? "Messages" : "Help",
1164
+ description: section === "shortcuts" ? "Quick actions" : section === "datasources" ? "App tree" : section === "history" ? "Past runs" : section === "messages" ? "System output" : "Key map",
808
1165
  value: section
809
1166
  }));
810
1167
  }, [activeSection]);
@@ -822,12 +1179,30 @@ function DocyrusOpenTuiApp(props) {
822
1179
  value: entry.id
823
1180
  }));
824
1181
  }, [entries]);
1182
+ const dataSourceOptions = useMemo(() => {
1183
+ return dataSourceRows.map((row) => {
1184
+ if (row.kind === "app") {
1185
+ const itemCount = dataSourceCountByAppKey.get(row.appKey) || 0;
1186
+ return {
1187
+ name: `${row.expanded ? "[-]" : "[+]"} ${row.appName}${row.appSlug ? ` (${row.appSlug})` : ""}`,
1188
+ description: `${itemCount} data source${itemCount === 1 ? "" : "s"}`,
1189
+ value: row.id
1190
+ };
1191
+ }
1192
+ const dataSourceLabel = row.dataSourceName || row.dataSourceSlug || row.dataSourceId || "Unknown";
1193
+ return {
1194
+ name: ` - ${dataSourceLabel}${row.dataSourceSlug ? ` (${row.dataSourceSlug})` : ""}`,
1195
+ description: row.appSlug ? `app: ${row.appSlug}` : "missing app slug",
1196
+ value: row.id
1197
+ };
1198
+ });
1199
+ }, [dataSourceCountByAppKey, dataSourceRows]);
825
1200
  const renderLeftSection = () => {
826
1201
  if (activeSection === "shortcuts") {
827
1202
  return /* @__PURE__ */ jsx2(
828
1203
  "select",
829
1204
  {
830
- focused: focusedPanel === "left" && !isExitConfirmOpen,
1205
+ focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning,
831
1206
  options: shortcutOptions,
832
1207
  selectedIndex: selectedShortcutIndex,
833
1208
  onChange: (index) => {
@@ -847,6 +1222,53 @@ function DocyrusOpenTuiApp(props) {
847
1222
  }
848
1223
  );
849
1224
  }
1225
+ if (activeSection === "datasources") {
1226
+ const treeFocused = focusedPanel === "left" && dataSourcePanelFocus === "tree" && !isRunning && !isExitConfirmOpen;
1227
+ const searchFocused = focusedPanel === "left" && dataSourcePanelFocus === "search" && !isRunning && !isExitConfirmOpen;
1228
+ return /* @__PURE__ */ jsxs2("box", { flexDirection: "column", flexGrow: 1, children: [
1229
+ /* @__PURE__ */ jsx2("box", { border: true, borderStyle: "single", borderColor: searchFocused ? "#7aa2f7" : "#414868", paddingX: 1, children: /* @__PURE__ */ jsx2(
1230
+ "input",
1231
+ {
1232
+ value: dataSourceSearch,
1233
+ placeholder: "Search data sources...",
1234
+ onChange: (value) => {
1235
+ setDataSourceSearch(value);
1236
+ setSelectedDataSourceRowIndex(0);
1237
+ },
1238
+ onSubmit: () => {
1239
+ setDataSourcePanelFocus("tree");
1240
+ },
1241
+ focused: searchFocused,
1242
+ style: {
1243
+ flexGrow: 1
1244
+ }
1245
+ }
1246
+ ) }),
1247
+ /* @__PURE__ */ jsx2("box", { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx2("text", { fg: "gray", children: "/ to search, Esc to tree, Space toggle app, Enter run ds get, r refresh" }) }),
1248
+ isCatalogLoading && /* @__PURE__ */ jsx2("text", { fg: "cyan", children: "Loading DataSources catalog..." }),
1249
+ catalogError && /* @__PURE__ */ jsx2("text", { fg: "red", children: catalogError }),
1250
+ dataSourceOptions.length === 0 ? /* @__PURE__ */ jsx2("text", { fg: "gray", children: dataSourceSearch.trim().length > 0 ? `No data sources match "${dataSourceSearch}".` : "No data sources available for this tenant." }) : /* @__PURE__ */ jsx2(
1251
+ "select",
1252
+ {
1253
+ focused: treeFocused,
1254
+ options: dataSourceOptions,
1255
+ selectedIndex: selectedDataSourceRowIndex,
1256
+ onChange: (index) => {
1257
+ setSelectedDataSourceRowIndex(index);
1258
+ },
1259
+ onSelect: (index) => {
1260
+ setSelectedDataSourceRowIndex(index);
1261
+ const row = dataSourceRows[index] || null;
1262
+ void runSelectedDataSourceRow(row);
1263
+ },
1264
+ showScrollIndicator: true,
1265
+ style: {
1266
+ flexGrow: 1
1267
+ }
1268
+ }
1269
+ )
1270
+ ] });
1271
+ }
850
1272
  if (activeSection === "history") {
851
1273
  if (entries.length === 0) {
852
1274
  return /* @__PURE__ */ jsx2("text", { fg: "gray", children: "No command history yet." });
@@ -856,7 +1278,7 @@ function DocyrusOpenTuiApp(props) {
856
1278
  /* @__PURE__ */ jsx2(
857
1279
  "select",
858
1280
  {
859
- focused: focusedPanel === "left" && !isExitConfirmOpen,
1281
+ focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning,
860
1282
  options: historyOptions,
861
1283
  selectedIndex: selectedHistoryIndex,
862
1284
  onChange: (index, option) => {
@@ -880,12 +1302,12 @@ function DocyrusOpenTuiApp(props) {
880
1302
  ] });
881
1303
  }
882
1304
  if (activeSection === "messages") {
883
- return /* @__PURE__ */ jsxs2("scrollbox", { focused: focusedPanel === "left" && !isExitConfirmOpen, style: { height: "100%" }, children: [
1305
+ return /* @__PURE__ */ jsxs2("scrollbox", { focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning, style: { height: "100%" }, children: [
884
1306
  systemMessages.length === 0 && /* @__PURE__ */ jsx2("text", { fg: "gray", children: "No messages yet." }),
885
1307
  systemMessages.map((line, index) => /* @__PURE__ */ jsx2("text", { children: line }, `message-${index}`))
886
1308
  ] });
887
1309
  }
888
- return /* @__PURE__ */ jsxs2("scrollbox", { focused: focusedPanel === "left" && !isExitConfirmOpen, style: { height: "100%" }, children: [
1310
+ return /* @__PURE__ */ jsxs2("scrollbox", { focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning, style: { height: "100%" }, children: [
889
1311
  /* @__PURE__ */ jsx2("text", { fg: "cyan", children: "Key Bindings" }),
890
1312
  /* @__PURE__ */ jsx2("text", { children: "Left/Right: switch focused panel" }),
891
1313
  /* @__PURE__ */ jsx2("text", { children: "Up/Down in left lists: selection" }),
@@ -894,8 +1316,11 @@ function DocyrusOpenTuiApp(props) {
894
1316
  /* @__PURE__ */ jsx2("text", { children: "Up/Down in input: command history" }),
895
1317
  /* @__PURE__ */ jsx2("text", { children: "Tab / Shift+Tab: next/prev left section" }),
896
1318
  /* @__PURE__ */ jsx2("text", { children: "[ / ]: next/prev left section" }),
897
- /* @__PURE__ */ jsx2("text", { children: "s/h/m/?: jump to section" }),
898
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" }),
899
1324
  /* @__PURE__ */ jsx2("text", { children: "Ctrl+L: clear command and message history" }),
900
1325
  /* @__PURE__ */ jsx2("text", { children: "Esc (other panel): focus command input" }),
901
1326
  /* @__PURE__ */ jsx2("text", { children: "Esc then Esc: open exit confirmation" }),
@@ -913,7 +1338,7 @@ function DocyrusOpenTuiApp(props) {
913
1338
  ] }),
914
1339
  /* @__PURE__ */ jsxs2("text", { children: [
915
1340
  "status: ",
916
- isRunning ? `${spinner} running` : "idle"
1341
+ isRunning ? `${spinner} running` : isCatalogLoading ? `${spinner} loading-catalog` : "idle"
917
1342
  ] }),
918
1343
  /* @__PURE__ */ jsxs2("text", { children: [
919
1344
  "account: ",
@@ -940,10 +1365,12 @@ function DocyrusOpenTuiApp(props) {
940
1365
  /* @__PURE__ */ jsx2(
941
1366
  "tab-select",
942
1367
  {
1368
+ focused: focusedPanel === "left" && !isExitConfirmOpen && !isRunning,
943
1369
  options: sectionOptions,
944
1370
  onChange: (_index, option) => {
945
1371
  if (typeof option?.value === "string") {
946
1372
  setActiveSection(option.value);
1373
+ setDataSourcePanelFocus("tree");
947
1374
  }
948
1375
  }
949
1376
  }
@@ -960,7 +1387,10 @@ function DocyrusOpenTuiApp(props) {
960
1387
  borderColor: focusedPanel === "result" ? "#9ece6a" : "#3b4261",
961
1388
  padding: 1,
962
1389
  flexGrow: 1,
963
- children: renderResult({
1390
+ children: isRunning ? /* @__PURE__ */ jsxs2("box", { width: "100%", height: "100%", alignItems: "center", justifyContent: "center", flexDirection: "column", children: [
1391
+ /* @__PURE__ */ jsx2("text", { fg: "cyan", children: spinner }),
1392
+ /* @__PURE__ */ jsx2("text", { fg: "gray", children: "Running command..." })
1393
+ ] }) : renderResult({
964
1394
  entry: activeEntry,
965
1395
  isFocused: focusedPanel === "result" && !isExitConfirmOpen
966
1396
  })
@@ -989,7 +1419,7 @@ function DocyrusOpenTuiApp(props) {
989
1419
  onSubmit: (value) => {
990
1420
  submitInput(typeof value === "string" ? value : void 0);
991
1421
  },
992
- focused: !isRunning && focusedPanel === "input" && !isExitConfirmOpen
1422
+ focused: !isRunning && !isCatalogLoading && focusedPanel === "input" && !isExitConfirmOpen
993
1423
  }
994
1424
  )
995
1425
  ]