@nexus-ai-fs/tui 0.9.18
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/README.md +30 -0
- package/package.json +48 -0
- package/src/app.tsx +349 -0
- package/src/index.tsx +137 -0
- package/src/opentui-env.d.ts +61 -0
- package/src/panels/access/access-panel.tsx +597 -0
- package/src/panels/access/alert-list.tsx +77 -0
- package/src/panels/access/constraint-creator.tsx +128 -0
- package/src/panels/access/constraint-list.tsx +72 -0
- package/src/panels/access/credential-list.tsx +68 -0
- package/src/panels/access/delegation-chain-view.tsx +110 -0
- package/src/panels/access/delegation-completer.tsx +120 -0
- package/src/panels/access/delegation-creator.tsx +237 -0
- package/src/panels/access/delegation-list.tsx +74 -0
- package/src/panels/access/fraud-score-view.tsx +94 -0
- package/src/panels/access/manifest-creator.tsx +167 -0
- package/src/panels/access/manifest-list.tsx +105 -0
- package/src/panels/access/namespace-config-view.tsx +525 -0
- package/src/panels/access/permission-checker.tsx +231 -0
- package/src/panels/agents/agent-status-view.tsx +196 -0
- package/src/panels/agents/agents-panel.tsx +493 -0
- package/src/panels/agents/delegation-list.tsx +154 -0
- package/src/panels/agents/inbox-view.tsx +96 -0
- package/src/panels/agents/trajectories-tab.tsx +40 -0
- package/src/panels/api-console/api-console-panel.tsx +189 -0
- package/src/panels/api-console/codegen-viewer.tsx +36 -0
- package/src/panels/api-console/codegen.ts +112 -0
- package/src/panels/api-console/endpoint-list.tsx +57 -0
- package/src/panels/api-console/request-builder.tsx +69 -0
- package/src/panels/api-console/response-viewer.tsx +54 -0
- package/src/panels/connectors/available-tab.tsx +357 -0
- package/src/panels/connectors/connector-row.tsx +121 -0
- package/src/panels/connectors/connectors-panel.tsx +88 -0
- package/src/panels/connectors/error-parser.ts +116 -0
- package/src/panels/connectors/mounted-tab.tsx +179 -0
- package/src/panels/connectors/skills-tab.tsx +235 -0
- package/src/panels/connectors/template-generator.ts +211 -0
- package/src/panels/connectors/write-tab.tsx +514 -0
- package/src/panels/events/audit-tab.tsx +69 -0
- package/src/panels/events/audit-trail.tsx +75 -0
- package/src/panels/events/connector-detail.tsx +49 -0
- package/src/panels/events/connector-list.tsx +73 -0
- package/src/panels/events/connectors-tab.tsx +92 -0
- package/src/panels/events/event-replay.tsx +80 -0
- package/src/panels/events/events-panel.tsx +414 -0
- package/src/panels/events/events-tab.tsx +212 -0
- package/src/panels/events/lock-list.tsx +54 -0
- package/src/panels/events/locks-tab.tsx +103 -0
- package/src/panels/events/mcl-replay.tsx +77 -0
- package/src/panels/events/mcl-tab.tsx +83 -0
- package/src/panels/events/operations-tab-wrapper.tsx +62 -0
- package/src/panels/events/operations-tab.tsx +41 -0
- package/src/panels/events/replay-tab.tsx +76 -0
- package/src/panels/events/secrets-audit.tsx +64 -0
- package/src/panels/events/secrets-tab.tsx +75 -0
- package/src/panels/events/subscription-list.tsx +54 -0
- package/src/panels/events/subscriptions-tab.tsx +82 -0
- package/src/panels/files/file-aspects.tsx +93 -0
- package/src/panels/files/file-editor.tsx +160 -0
- package/src/panels/files/file-explorer-keybindings.ts +468 -0
- package/src/panels/files/file-explorer-panel.tsx +545 -0
- package/src/panels/files/file-lineage.tsx +163 -0
- package/src/panels/files/file-list-item.tsx +28 -0
- package/src/panels/files/file-metadata.tsx +62 -0
- package/src/panels/files/file-preview.tsx +108 -0
- package/src/panels/files/file-schema.tsx +89 -0
- package/src/panels/files/file-tree-node.tsx +44 -0
- package/src/panels/files/file-tree.tsx +169 -0
- package/src/panels/files/share-links-tab.tsx +33 -0
- package/src/panels/files/uploads-tab.tsx +45 -0
- package/src/panels/payments/approval-list.tsx +83 -0
- package/src/panels/payments/balance-card.tsx +43 -0
- package/src/panels/payments/budget-card.tsx +70 -0
- package/src/panels/payments/payments-panel.tsx +451 -0
- package/src/panels/payments/policy-list.tsx +64 -0
- package/src/panels/payments/reservation-list.tsx +78 -0
- package/src/panels/payments/transaction-list.tsx +103 -0
- package/src/panels/payments/transfer-form.tsx +109 -0
- package/src/panels/search/column-search.tsx +79 -0
- package/src/panels/search/knowledge-view.tsx +100 -0
- package/src/panels/search/memory-list.tsx +197 -0
- package/src/panels/search/playbook-list.tsx +77 -0
- package/src/panels/search/rlm-answer-view.tsx +105 -0
- package/src/panels/search/search-panel.tsx +405 -0
- package/src/panels/search/search-results.tsx +116 -0
- package/src/panels/stack/stack-panel.tsx +474 -0
- package/src/panels/versions/conflicts-tab.tsx +59 -0
- package/src/panels/versions/entry-detail.tsx +89 -0
- package/src/panels/versions/transaction-actions.tsx +34 -0
- package/src/panels/versions/transaction-list.tsx +90 -0
- package/src/panels/versions/versions-panel.tsx +276 -0
- package/src/panels/workflows/execution-list.tsx +102 -0
- package/src/panels/workflows/scheduler-view.tsx +135 -0
- package/src/panels/workflows/workflow-list.tsx +88 -0
- package/src/panels/workflows/workflows-panel.tsx +295 -0
- package/src/panels/zones/brick-detail.tsx +136 -0
- package/src/panels/zones/brick-list.tsx +56 -0
- package/src/panels/zones/cache-tab.tsx +118 -0
- package/src/panels/zones/drift-view.tsx +97 -0
- package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
- package/src/panels/zones/memories-tab.tsx +37 -0
- package/src/panels/zones/reindex-status.tsx +84 -0
- package/src/panels/zones/workspaces-tab.tsx +37 -0
- package/src/panels/zones/zone-list.tsx +73 -0
- package/src/panels/zones/zones-panel.tsx +559 -0
- package/src/services/command-runner.ts +303 -0
- package/src/shared/accessibility-announcements.ts +44 -0
- package/src/shared/action-registry.ts +466 -0
- package/src/shared/brick-states.ts +91 -0
- package/src/shared/command-palette.ts +35 -0
- package/src/shared/components/announcement-bar.tsx +30 -0
- package/src/shared/components/app-confirm-dialog.tsx +29 -0
- package/src/shared/components/breadcrumb.tsx +21 -0
- package/src/shared/components/brick-gate.tsx +60 -0
- package/src/shared/components/command-output.tsx +95 -0
- package/src/shared/components/command-palette.tsx +97 -0
- package/src/shared/components/confirm-dialog.tsx +61 -0
- package/src/shared/components/diff-viewer.tsx +219 -0
- package/src/shared/components/empty-state.tsx +36 -0
- package/src/shared/components/error-bar.tsx +60 -0
- package/src/shared/components/error-boundary.tsx +53 -0
- package/src/shared/components/help-overlay.tsx +99 -0
- package/src/shared/components/identity-switcher.tsx +168 -0
- package/src/shared/components/loading-indicator.tsx +40 -0
- package/src/shared/components/pagination-bar.tsx +68 -0
- package/src/shared/components/pre-connection-screen.tsx +398 -0
- package/src/shared/components/scroll-indicator.tsx +46 -0
- package/src/shared/components/side-nav-utils.ts +68 -0
- package/src/shared/components/side-nav.tsx +287 -0
- package/src/shared/components/spinner.tsx +26 -0
- package/src/shared/components/status-bar.tsx +117 -0
- package/src/shared/components/styled-text.tsx +72 -0
- package/src/shared/components/sub-tab-bar-utils.ts +100 -0
- package/src/shared/components/sub-tab-bar.tsx +40 -0
- package/src/shared/components/tab-bar-utils.ts +36 -0
- package/src/shared/components/tab-bar.tsx +50 -0
- package/src/shared/components/text-input.tsx +73 -0
- package/src/shared/components/tooltip.tsx +53 -0
- package/src/shared/components/virtual-list.tsx +93 -0
- package/src/shared/components/welcome-screen.tsx +111 -0
- package/src/shared/hooks/use-api.ts +10 -0
- package/src/shared/hooks/use-brick-available.ts +42 -0
- package/src/shared/hooks/use-confirm.ts +66 -0
- package/src/shared/hooks/use-connection-state.ts +67 -0
- package/src/shared/hooks/use-copy.ts +31 -0
- package/src/shared/hooks/use-fresh-server.ts +62 -0
- package/src/shared/hooks/use-keyboard.ts +58 -0
- package/src/shared/hooks/use-list-navigation.ts +106 -0
- package/src/shared/hooks/use-swr.ts +117 -0
- package/src/shared/hooks/use-tab-fallback.ts +32 -0
- package/src/shared/hooks/use-text-input.ts +113 -0
- package/src/shared/hooks/use-visible-tabs.ts +61 -0
- package/src/shared/lib/circular-buffer.ts +82 -0
- package/src/shared/lib/clipboard.ts +14 -0
- package/src/shared/nav-items.ts +73 -0
- package/src/shared/navigation.ts +110 -0
- package/src/shared/status-breadcrumb.ts +74 -0
- package/src/shared/syntax-style.ts +3 -0
- package/src/shared/tab-visibility.ts +15 -0
- package/src/shared/text-style.ts +23 -0
- package/src/shared/theme.ts +179 -0
- package/src/shared/utils/format-size.ts +20 -0
- package/src/shared/utils/format-text.ts +10 -0
- package/src/shared/utils/format-time.ts +72 -0
- package/src/shared/utils/lru-cache.ts +75 -0
- package/src/stores/access-store-types.ts +154 -0
- package/src/stores/access-store.ts +674 -0
- package/src/stores/agents-store.ts +404 -0
- package/src/stores/announcement-store.ts +46 -0
- package/src/stores/api-console-store.ts +476 -0
- package/src/stores/connectors-store.ts +434 -0
- package/src/stores/create-api-action.ts +140 -0
- package/src/stores/delegation-store.ts +300 -0
- package/src/stores/error-store.ts +102 -0
- package/src/stores/events-store.ts +163 -0
- package/src/stores/files-store.ts +630 -0
- package/src/stores/first-run-store.ts +34 -0
- package/src/stores/global-store.ts +255 -0
- package/src/stores/infra-store.ts +461 -0
- package/src/stores/knowledge-store.ts +358 -0
- package/src/stores/lineage-store.ts +126 -0
- package/src/stores/mcp-store.ts +147 -0
- package/src/stores/payments-store.ts +545 -0
- package/src/stores/search-store-types.ts +155 -0
- package/src/stores/search-store.ts +656 -0
- package/src/stores/share-link-store.ts +151 -0
- package/src/stores/stack-store.ts +352 -0
- package/src/stores/ui-store.ts +161 -0
- package/src/stores/upload-store.ts +131 -0
- package/src/stores/versions-store.ts +355 -0
- package/src/stores/workflows-store.ts +402 -0
- package/src/stores/workspace-store.ts +185 -0
- package/src/stores/zones-store.ts +378 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payments panel: tabbed layout for Balance, Reservations, Transactions,
|
|
3
|
+
* Policies, and Approvals views.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
7
|
+
import { usePaymentsStore } from "../../stores/payments-store.js";
|
|
8
|
+
import type { PaymentsTab } from "../../stores/payments-store.js";
|
|
9
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
10
|
+
import { useCopy } from "../../shared/hooks/use-copy.js";
|
|
11
|
+
import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
|
|
12
|
+
import { useTextInput } from "../../shared/hooks/use-text-input.js";
|
|
13
|
+
import { useConfirmStore } from "../../shared/hooks/use-confirm.js";
|
|
14
|
+
import { useApi } from "../../shared/hooks/use-api.js";
|
|
15
|
+
import { useUiStore } from "../../stores/ui-store.js";
|
|
16
|
+
import { BrickGate } from "../../shared/components/brick-gate.js";
|
|
17
|
+
import { statusColor } from "../../shared/theme.js";
|
|
18
|
+
import { BalanceCard } from "./balance-card.js";
|
|
19
|
+
import { ReservationList } from "./reservation-list.js";
|
|
20
|
+
import { TransferForm } from "./transfer-form.js";
|
|
21
|
+
import { TransactionList } from "./transaction-list.js";
|
|
22
|
+
import { PolicyList } from "./policy-list.js";
|
|
23
|
+
import { BudgetCard } from "./budget-card.js";
|
|
24
|
+
import { ApprovalList } from "./approval-list.js";
|
|
25
|
+
import { PAYMENTS_TABS } from "../../shared/navigation.js";
|
|
26
|
+
import { textStyle } from "../../shared/text-style.js";
|
|
27
|
+
|
|
28
|
+
const HELP_TEXT: Readonly<Record<string, string>> = {
|
|
29
|
+
balance: "Tab:switch tab t:transfer a:afford check r:refresh q:quit",
|
|
30
|
+
reservations: "j/k:navigate Tab:switch tab t:transfer r:refresh c:commit x:release q:quit",
|
|
31
|
+
transactions: "j/k:navigate ]:next [:prev i:verify integrity y:copy Tab:switch tab r:refresh",
|
|
32
|
+
policies: "j/k:navigate Tab:switch tab Shift+N:new d:delete b:budget r:refresh q:quit",
|
|
33
|
+
approvals: "j/k:navigate n:new request a:approve x:reject Tab:switch tab r:refresh q:quit",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default function PaymentsPanel(): React.ReactNode {
|
|
37
|
+
const client = useApi();
|
|
38
|
+
const confirm = useConfirmStore((s) => s.confirm);
|
|
39
|
+
const overlayActive = useUiStore((s) => s.overlayActive);
|
|
40
|
+
const { copy, copied } = useCopy();
|
|
41
|
+
const [showTransfer, setShowTransfer] = useState(false);
|
|
42
|
+
const [approvalInputMode, setApprovalInputMode] = useState(false);
|
|
43
|
+
const [approvalAmountBuffer, setApprovalAmountBuffer] = useState("");
|
|
44
|
+
const [approvalPurposeBuffer, setApprovalPurposeBuffer] = useState("");
|
|
45
|
+
const [approvalField, setApprovalField] = useState<"amount" | "purpose">("amount");
|
|
46
|
+
|
|
47
|
+
const balance = usePaymentsStore((s) => s.balance);
|
|
48
|
+
const balanceLoading = usePaymentsStore((s) => s.balanceLoading);
|
|
49
|
+
const reservations = usePaymentsStore((s) => s.reservations);
|
|
50
|
+
const selectedReservationIndex = usePaymentsStore((s) => s.selectedReservationIndex);
|
|
51
|
+
const reservationsLoading = usePaymentsStore((s) => s.reservationsLoading);
|
|
52
|
+
const transactions = usePaymentsStore((s) => s.transactions);
|
|
53
|
+
const transactionsLoading = usePaymentsStore((s) => s.transactionsLoading);
|
|
54
|
+
const selectedTransactionIndex = usePaymentsStore((s) => s.selectedTransactionIndex);
|
|
55
|
+
const policies = usePaymentsStore((s) => s.policies);
|
|
56
|
+
const policiesLoading = usePaymentsStore((s) => s.policiesLoading);
|
|
57
|
+
const budget = usePaymentsStore((s) => s.budget);
|
|
58
|
+
const budgetLoading = usePaymentsStore((s) => s.budgetLoading);
|
|
59
|
+
const activeTab = usePaymentsStore((s) => s.activeTab);
|
|
60
|
+
const error = usePaymentsStore((s) => s.error);
|
|
61
|
+
|
|
62
|
+
const fetchBalance = usePaymentsStore((s) => s.fetchBalance);
|
|
63
|
+
const transfer = usePaymentsStore((s) => s.transfer);
|
|
64
|
+
const commitReservation = usePaymentsStore((s) => s.commitReservation);
|
|
65
|
+
const releaseReservation = usePaymentsStore((s) => s.releaseReservation);
|
|
66
|
+
const transactionsHasMore = usePaymentsStore((s) => s.transactionsHasMore);
|
|
67
|
+
const transactionsCursorStack = usePaymentsStore((s) => s.transactionsCursorStack);
|
|
68
|
+
const integrityResult = usePaymentsStore((s) => s.integrityResult);
|
|
69
|
+
const fetchTransactions = usePaymentsStore((s) => s.fetchTransactions);
|
|
70
|
+
const fetchNextTransactions = usePaymentsStore((s) => s.fetchNextTransactions);
|
|
71
|
+
const fetchPrevTransactions = usePaymentsStore((s) => s.fetchPrevTransactions);
|
|
72
|
+
const verifyIntegrity = usePaymentsStore((s) => s.verifyIntegrity);
|
|
73
|
+
const fetchPolicies = usePaymentsStore((s) => s.fetchPolicies);
|
|
74
|
+
const fetchBudget = usePaymentsStore((s) => s.fetchBudget);
|
|
75
|
+
const deletePolicy = usePaymentsStore((s) => s.deletePolicy);
|
|
76
|
+
const checkAfford = usePaymentsStore((s) => s.checkAfford);
|
|
77
|
+
const affordResult = usePaymentsStore((s) => s.affordResult);
|
|
78
|
+
const createPolicy = usePaymentsStore((s) => s.createPolicy);
|
|
79
|
+
const approvals = usePaymentsStore((s) => s.approvals);
|
|
80
|
+
const approvalsLoading = usePaymentsStore((s) => s.approvalsLoading);
|
|
81
|
+
const selectedApprovalIndex = usePaymentsStore((s) => s.selectedApprovalIndex);
|
|
82
|
+
const fetchApprovals = usePaymentsStore((s) => s.fetchApprovals);
|
|
83
|
+
const requestApproval = usePaymentsStore((s) => s.requestApproval);
|
|
84
|
+
const approveRequest = usePaymentsStore((s) => s.approveRequest);
|
|
85
|
+
const rejectRequest = usePaymentsStore((s) => s.rejectRequest);
|
|
86
|
+
const setSelectedApprovalIndex = usePaymentsStore((s) => s.setSelectedApprovalIndex);
|
|
87
|
+
const setActiveTab = usePaymentsStore((s) => s.setActiveTab);
|
|
88
|
+
const setSelectedReservationIndex = usePaymentsStore(
|
|
89
|
+
(s) => s.setSelectedReservationIndex,
|
|
90
|
+
);
|
|
91
|
+
const setSelectedTransactionIndex = usePaymentsStore(
|
|
92
|
+
(s) => s.setSelectedTransactionIndex,
|
|
93
|
+
);
|
|
94
|
+
const [selectedPolicyIndex, setSelectedPolicyIndex] = useState(0);
|
|
95
|
+
|
|
96
|
+
// Clamp selectedPolicyIndex when policies list shrinks (e.g. after delete)
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (policies.length > 0 && selectedPolicyIndex >= policies.length) {
|
|
99
|
+
setSelectedPolicyIndex(Math.max(0, policies.length - 1));
|
|
100
|
+
}
|
|
101
|
+
}, [policies.length, selectedPolicyIndex]);
|
|
102
|
+
|
|
103
|
+
const handleTransferSubmit = useCallback(
|
|
104
|
+
async (to: string, amount: string, memo: string) => {
|
|
105
|
+
if (!client) return;
|
|
106
|
+
const ok = await confirm("Transfer funds?", `Transfer ${amount} credits to ${to}. This cannot be undone.`);
|
|
107
|
+
if (!ok) return;
|
|
108
|
+
transfer(to, amount, memo, client);
|
|
109
|
+
setShowTransfer(false);
|
|
110
|
+
},
|
|
111
|
+
[client, transfer, confirm],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const handleTransferCancel = useCallback(() => {
|
|
115
|
+
setShowTransfer(false);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
// Refresh current view based on active tab.
|
|
119
|
+
// Reservations are tracked locally, so no fetch is needed for that tab.
|
|
120
|
+
const refreshCurrentView = (): void => {
|
|
121
|
+
if (!client) return;
|
|
122
|
+
|
|
123
|
+
if (activeTab === "balance") {
|
|
124
|
+
fetchBalance(client);
|
|
125
|
+
} else if (activeTab === "transactions") {
|
|
126
|
+
fetchTransactions(client);
|
|
127
|
+
} else if (activeTab === "policies") {
|
|
128
|
+
fetchPolicies(client);
|
|
129
|
+
} else if (activeTab === "approvals") {
|
|
130
|
+
fetchApprovals(client);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Auto-fetch when tab changes
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
refreshCurrentView();
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
}, [activeTab, client]);
|
|
139
|
+
|
|
140
|
+
// Text input for afford check (numbers-only)
|
|
141
|
+
const affordInput = useTextInput({
|
|
142
|
+
onSubmit: (val) => {
|
|
143
|
+
if (val && client) checkAfford(val, client);
|
|
144
|
+
},
|
|
145
|
+
filter: (ch) => /[\d.]/.test(ch),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Text input for policy name creation
|
|
149
|
+
const policyInput = useTextInput({
|
|
150
|
+
onSubmit: (val) => {
|
|
151
|
+
if (val && client) createPolicy(val, {}, client);
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Shared list navigation (j/k/up/down/g/G) — switches per active tab
|
|
156
|
+
const listNav = listNavigationBindings({
|
|
157
|
+
getIndex: () => {
|
|
158
|
+
if (activeTab === "reservations") return selectedReservationIndex;
|
|
159
|
+
if (activeTab === "transactions") return selectedTransactionIndex;
|
|
160
|
+
if (activeTab === "policies") return selectedPolicyIndex;
|
|
161
|
+
if (activeTab === "approvals") return selectedApprovalIndex;
|
|
162
|
+
return 0;
|
|
163
|
+
},
|
|
164
|
+
setIndex: (i) => {
|
|
165
|
+
if (activeTab === "reservations") setSelectedReservationIndex(i);
|
|
166
|
+
else if (activeTab === "transactions") setSelectedTransactionIndex(i);
|
|
167
|
+
else if (activeTab === "policies") setSelectedPolicyIndex(i);
|
|
168
|
+
else if (activeTab === "approvals") setSelectedApprovalIndex(i);
|
|
169
|
+
},
|
|
170
|
+
getLength: () => {
|
|
171
|
+
if (activeTab === "reservations") return reservations.length;
|
|
172
|
+
if (activeTab === "transactions") return transactions.length;
|
|
173
|
+
if (activeTab === "policies") return policies.length;
|
|
174
|
+
if (activeTab === "approvals") return approvals.length;
|
|
175
|
+
return 0;
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Determine which input mode (if any) is active for useKeyboard routing
|
|
180
|
+
const anyInputActive = affordInput.active || policyInput.active || approvalInputMode;
|
|
181
|
+
|
|
182
|
+
useKeyboard(
|
|
183
|
+
overlayActive
|
|
184
|
+
? {}
|
|
185
|
+
: showTransfer
|
|
186
|
+
? {}
|
|
187
|
+
: affordInput.active
|
|
188
|
+
? affordInput.inputBindings
|
|
189
|
+
: policyInput.active
|
|
190
|
+
? policyInput.inputBindings
|
|
191
|
+
: approvalInputMode
|
|
192
|
+
? {
|
|
193
|
+
return: () => {
|
|
194
|
+
const amount = parseFloat(approvalAmountBuffer.trim());
|
|
195
|
+
const purpose = approvalPurposeBuffer.trim();
|
|
196
|
+
if (Number.isFinite(amount) && purpose && client) {
|
|
197
|
+
requestApproval(amount, purpose, client);
|
|
198
|
+
}
|
|
199
|
+
setApprovalInputMode(false);
|
|
200
|
+
setApprovalAmountBuffer("");
|
|
201
|
+
setApprovalPurposeBuffer("");
|
|
202
|
+
setApprovalField("amount");
|
|
203
|
+
},
|
|
204
|
+
escape: () => {
|
|
205
|
+
setApprovalInputMode(false);
|
|
206
|
+
setApprovalAmountBuffer("");
|
|
207
|
+
setApprovalPurposeBuffer("");
|
|
208
|
+
setApprovalField("amount");
|
|
209
|
+
},
|
|
210
|
+
backspace: () => {
|
|
211
|
+
if (approvalField === "amount") {
|
|
212
|
+
setApprovalAmountBuffer((b) => b.slice(0, -1));
|
|
213
|
+
} else {
|
|
214
|
+
setApprovalPurposeBuffer((b) => b.slice(0, -1));
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
tab: () => {
|
|
218
|
+
setApprovalField((f) => f === "amount" ? "purpose" : "amount");
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
: {
|
|
222
|
+
...listNav,
|
|
223
|
+
...subTabCycleBindings(visibleTabs, activeTab, setActiveTab),
|
|
224
|
+
t: () => {
|
|
225
|
+
setShowTransfer(true);
|
|
226
|
+
},
|
|
227
|
+
r: () => refreshCurrentView(),
|
|
228
|
+
c: () => {
|
|
229
|
+
if (activeTab !== "reservations" || !client) return;
|
|
230
|
+
const selected = reservations[selectedReservationIndex];
|
|
231
|
+
if (selected && selected.status === "pending") {
|
|
232
|
+
commitReservation(selected.id, client);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
x: async () => {
|
|
236
|
+
if (activeTab === "reservations" && client) {
|
|
237
|
+
const selected = reservations[selectedReservationIndex];
|
|
238
|
+
if (selected && selected.status === "pending") {
|
|
239
|
+
const ok = await confirm("Release reservation?", `Release reservation ${selected.id}. Reserved funds will be returned.`);
|
|
240
|
+
if (!ok) return;
|
|
241
|
+
releaseReservation(selected.id, client);
|
|
242
|
+
}
|
|
243
|
+
} else if (activeTab === "approvals" && client) {
|
|
244
|
+
const selected = approvals[selectedApprovalIndex];
|
|
245
|
+
if (selected && selected.status === "pending") {
|
|
246
|
+
const ok = await confirm("Reject approval?", `Reject spending approval request ${selected.id}.`);
|
|
247
|
+
if (!ok) return;
|
|
248
|
+
rejectRequest(selected.id, client);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
d: async () => {
|
|
253
|
+
if (activeTab !== "policies" || !client) return;
|
|
254
|
+
const selected = policies[selectedPolicyIndex];
|
|
255
|
+
if (selected) {
|
|
256
|
+
const ok = await confirm("Delete policy?", "Delete spending policy. This cannot be undone.");
|
|
257
|
+
if (!ok) return;
|
|
258
|
+
deletePolicy(selected.policy_id, client);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
b: () => {
|
|
262
|
+
if (activeTab !== "policies" || !client) return;
|
|
263
|
+
fetchBudget(client);
|
|
264
|
+
},
|
|
265
|
+
"]": () => {
|
|
266
|
+
if (activeTab !== "transactions" || !client) return;
|
|
267
|
+
fetchNextTransactions(client);
|
|
268
|
+
},
|
|
269
|
+
"[": () => {
|
|
270
|
+
if (activeTab !== "transactions" || !client) return;
|
|
271
|
+
fetchPrevTransactions(client);
|
|
272
|
+
},
|
|
273
|
+
i: () => {
|
|
274
|
+
if (activeTab !== "transactions" || !client) return;
|
|
275
|
+
const selected = transactions[selectedTransactionIndex];
|
|
276
|
+
if (selected) {
|
|
277
|
+
verifyIntegrity(selected.id, client);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
a: async () => {
|
|
281
|
+
if (activeTab === "balance") {
|
|
282
|
+
affordInput.activate();
|
|
283
|
+
} else if (activeTab === "approvals" && client) {
|
|
284
|
+
const selected = approvals[selectedApprovalIndex];
|
|
285
|
+
if (selected && selected.status === "pending") {
|
|
286
|
+
const ok = await confirm("Approve request?", `Approve spending request ${selected.id} for ${selected.amount}.`);
|
|
287
|
+
if (!ok) return;
|
|
288
|
+
approveRequest(selected.id, client);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
n: () => {
|
|
293
|
+
if (activeTab === "approvals") {
|
|
294
|
+
setApprovalInputMode(true);
|
|
295
|
+
setApprovalAmountBuffer("");
|
|
296
|
+
setApprovalPurposeBuffer("");
|
|
297
|
+
setApprovalField("amount");
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
"shift+n": () => {
|
|
301
|
+
if (activeTab === "policies") {
|
|
302
|
+
policyInput.activate();
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
y: () => {
|
|
306
|
+
if (activeTab === "transactions") {
|
|
307
|
+
const selected = transactions[selectedTransactionIndex];
|
|
308
|
+
if (selected) copy(selected.id);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
!overlayActive && anyInputActive
|
|
313
|
+
? affordInput.active
|
|
314
|
+
? affordInput.onUnhandled
|
|
315
|
+
: policyInput.active
|
|
316
|
+
? policyInput.onUnhandled
|
|
317
|
+
: approvalInputMode
|
|
318
|
+
? (keyName: string) => {
|
|
319
|
+
if (approvalField === "amount" && keyName.length === 1 && /[\d.]/.test(keyName)) {
|
|
320
|
+
setApprovalAmountBuffer((b) => b + keyName);
|
|
321
|
+
} else if (approvalField === "purpose" && keyName.length === 1) {
|
|
322
|
+
setApprovalPurposeBuffer((b) => b + keyName);
|
|
323
|
+
} else if (approvalField === "purpose" && keyName === "space") {
|
|
324
|
+
setApprovalPurposeBuffer((b) => b + " ");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
: undefined
|
|
328
|
+
: undefined,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<BrickGate brick="pay">
|
|
333
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
334
|
+
{/* Tab bar */}
|
|
335
|
+
<box height={1} width="100%">
|
|
336
|
+
<text>
|
|
337
|
+
{TAB_ORDER.map((tab) => {
|
|
338
|
+
const label = TAB_LABELS[tab];
|
|
339
|
+
return tab === activeTab ? `[${label}]` : ` ${label} `;
|
|
340
|
+
}).join(" ")}
|
|
341
|
+
</text>
|
|
342
|
+
</box>
|
|
343
|
+
|
|
344
|
+
{/* Afford check input */}
|
|
345
|
+
{affordInput.active && (
|
|
346
|
+
<box height={1} width="100%">
|
|
347
|
+
<text>{`Check afford amount: ${affordInput.buffer}\u2588 (Enter:check Escape:cancel)`}</text>
|
|
348
|
+
</box>
|
|
349
|
+
)}
|
|
350
|
+
|
|
351
|
+
{/* Policy create input */}
|
|
352
|
+
{policyInput.active && (
|
|
353
|
+
<box height={1} width="100%">
|
|
354
|
+
<text>{`New policy name: ${policyInput.buffer}\u2588 (Enter:create Escape:cancel)`}</text>
|
|
355
|
+
</box>
|
|
356
|
+
)}
|
|
357
|
+
|
|
358
|
+
{/* Approval request input (inline bar) */}
|
|
359
|
+
{approvalInputMode && (
|
|
360
|
+
<box height={1} width="100%">
|
|
361
|
+
<text>
|
|
362
|
+
{approvalField === "amount"
|
|
363
|
+
? `Amount: ${approvalAmountBuffer}\u2588 \u2502 Purpose: ${approvalPurposeBuffer} (Tab:next Enter:submit Esc:cancel)`
|
|
364
|
+
: `Amount: ${approvalAmountBuffer} \u2502 Purpose: ${approvalPurposeBuffer}\u2588 (Tab:next Enter:submit Esc:cancel)`}
|
|
365
|
+
</text>
|
|
366
|
+
</box>
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
{/* Error display — 404 means the pay API routes aren't registered */}
|
|
370
|
+
{error && (
|
|
371
|
+
<box height={1} width="100%">
|
|
372
|
+
<text>{error.includes("Not Found") || error.includes("404")
|
|
373
|
+
? "Payment APIs not available on this server. The pay brick is enabled but REST routes are not registered."
|
|
374
|
+
: `Error: ${error}`}</text>
|
|
375
|
+
</box>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
{/* Detail content */}
|
|
379
|
+
<box flexGrow={1} borderStyle="single">
|
|
380
|
+
{showTransfer ? (
|
|
381
|
+
<TransferForm
|
|
382
|
+
onSubmit={handleTransferSubmit}
|
|
383
|
+
onCancel={handleTransferCancel}
|
|
384
|
+
/>
|
|
385
|
+
) : (
|
|
386
|
+
<>
|
|
387
|
+
{activeTab === "balance" && (
|
|
388
|
+
<>
|
|
389
|
+
<BalanceCard balance={balance} loading={balanceLoading} />
|
|
390
|
+
{affordResult && (
|
|
391
|
+
<box height={1} width="100%" marginTop={1}>
|
|
392
|
+
<text>
|
|
393
|
+
{`Afford check: ${affordResult.can_afford ? "YES" : "NO"} (balance=${affordResult.balance} requested=${affordResult.requested})`}
|
|
394
|
+
</text>
|
|
395
|
+
</box>
|
|
396
|
+
)}
|
|
397
|
+
</>
|
|
398
|
+
)}
|
|
399
|
+
{activeTab === "reservations" && (
|
|
400
|
+
<ReservationList
|
|
401
|
+
reservations={reservations}
|
|
402
|
+
selectedIndex={selectedReservationIndex}
|
|
403
|
+
loading={reservationsLoading}
|
|
404
|
+
/>
|
|
405
|
+
)}
|
|
406
|
+
{activeTab === "transactions" && (
|
|
407
|
+
<TransactionList
|
|
408
|
+
transactions={transactions}
|
|
409
|
+
selectedIndex={selectedTransactionIndex}
|
|
410
|
+
loading={transactionsLoading}
|
|
411
|
+
hasMore={transactionsHasMore}
|
|
412
|
+
hasPrev={transactionsCursorStack.length > 0}
|
|
413
|
+
currentPage={transactionsCursorStack.length + 1}
|
|
414
|
+
integrityResult={integrityResult}
|
|
415
|
+
/>
|
|
416
|
+
)}
|
|
417
|
+
{activeTab === "policies" && (
|
|
418
|
+
<box flexDirection="column" height="100%" width="100%">
|
|
419
|
+
<BudgetCard budget={budget} loading={budgetLoading} />
|
|
420
|
+
<PolicyList
|
|
421
|
+
policies={policies}
|
|
422
|
+
selectedIndex={selectedPolicyIndex}
|
|
423
|
+
loading={policiesLoading}
|
|
424
|
+
/>
|
|
425
|
+
</box>
|
|
426
|
+
)}
|
|
427
|
+
{activeTab === "approvals" && (
|
|
428
|
+
<ApprovalList
|
|
429
|
+
approvals={approvals}
|
|
430
|
+
selectedIndex={selectedApprovalIndex}
|
|
431
|
+
loading={approvalsLoading}
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
</>
|
|
435
|
+
)}
|
|
436
|
+
</box>
|
|
437
|
+
|
|
438
|
+
{/* Help bar */}
|
|
439
|
+
<box height={1} width="100%">
|
|
440
|
+
{copied
|
|
441
|
+
? <text style={textStyle({ fg: "green" })}>Copied!</text>
|
|
442
|
+
: <text>
|
|
443
|
+
{showTransfer
|
|
444
|
+
? "Tab:next field Enter:submit Escape:cancel"
|
|
445
|
+
: HELP_TEXT[activeTab] ?? "j/k:navigate Tab:switch tab r:refresh q:quit"}
|
|
446
|
+
</text>}
|
|
447
|
+
</box>
|
|
448
|
+
</box>
|
|
449
|
+
</BrickGate>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy list: displays spending policy records with limits and enabled status.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { PolicyRecord } from "../../stores/payments-store.js";
|
|
7
|
+
import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
|
|
8
|
+
import { EmptyState } from "../../shared/components/empty-state.js";
|
|
9
|
+
|
|
10
|
+
interface PolicyListProps {
|
|
11
|
+
readonly policies: readonly PolicyRecord[];
|
|
12
|
+
readonly selectedIndex: number;
|
|
13
|
+
readonly loading: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shortId(id: string): string {
|
|
17
|
+
if (id.length <= 12) return id;
|
|
18
|
+
return `${id.slice(0, 8)}..`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function PolicyList({
|
|
22
|
+
policies,
|
|
23
|
+
selectedIndex,
|
|
24
|
+
loading,
|
|
25
|
+
}: PolicyListProps): React.ReactNode {
|
|
26
|
+
if (loading) {
|
|
27
|
+
return <LoadingIndicator message="Loading policies..." />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (policies.length === 0) {
|
|
31
|
+
return <EmptyState message="No policies yet." hint="Press Shift+N to create a policy." />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<scrollbox height="100%" width="100%">
|
|
36
|
+
{/* Header */}
|
|
37
|
+
<box height={1} width="100%">
|
|
38
|
+
<text>{" ID DAILY WEEKLY MONTHLY PER-TX ENABLED"}</text>
|
|
39
|
+
</box>
|
|
40
|
+
<box height={1} width="100%">
|
|
41
|
+
<text>{" ---------- ----------- ----------- ----------- ----------- -------"}</text>
|
|
42
|
+
</box>
|
|
43
|
+
|
|
44
|
+
{/* Rows */}
|
|
45
|
+
{policies.map((p, i) => {
|
|
46
|
+
const isSelected = i === selectedIndex;
|
|
47
|
+
const prefix = isSelected ? "> " : " ";
|
|
48
|
+
const enabled = p.enabled ? "yes" : "no";
|
|
49
|
+
const daily = (p.daily_limit ?? "-").padEnd(11);
|
|
50
|
+
const weekly = (p.weekly_limit ?? "-").padEnd(11);
|
|
51
|
+
const monthly = (p.monthly_limit ?? "-").padEnd(11);
|
|
52
|
+
const perTx = (p.per_tx_limit ?? "-").padEnd(11);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<box key={p.policy_id} height={1} width="100%">
|
|
56
|
+
<text>
|
|
57
|
+
{`${prefix}${shortId(p.policy_id).padEnd(10)} ${daily} ${weekly} ${monthly} ${perTx} ${enabled}`}
|
|
58
|
+
</text>
|
|
59
|
+
</box>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</scrollbox>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reservation list with status badges, amounts, and purposes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { Reservation } from "../../stores/payments-store.js";
|
|
7
|
+
import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
|
|
8
|
+
import { EmptyState } from "../../shared/components/empty-state.js";
|
|
9
|
+
|
|
10
|
+
interface ReservationListProps {
|
|
11
|
+
readonly reservations: readonly Reservation[];
|
|
12
|
+
readonly selectedIndex: number;
|
|
13
|
+
readonly loading: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const STATUS_BADGES: Readonly<Record<Reservation["status"], string>> = {
|
|
17
|
+
pending: "◐",
|
|
18
|
+
committed: "✓",
|
|
19
|
+
released: "○",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function shortId(id: string): string {
|
|
23
|
+
if (id.length <= 12) return id;
|
|
24
|
+
return `${id.slice(0, 8)}..`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatTimestamp(ts: string | null): string {
|
|
28
|
+
if (!ts) return "n/a";
|
|
29
|
+
try {
|
|
30
|
+
return new Date(ts).toLocaleString();
|
|
31
|
+
} catch {
|
|
32
|
+
return ts;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ReservationList({
|
|
37
|
+
reservations,
|
|
38
|
+
selectedIndex,
|
|
39
|
+
loading,
|
|
40
|
+
}: ReservationListProps): React.ReactNode {
|
|
41
|
+
if (loading) {
|
|
42
|
+
return <LoadingIndicator message="Loading reservations..." />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (reservations.length === 0) {
|
|
46
|
+
return <EmptyState message="No reservations yet." hint="Reservations are created during transfers." />;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<scrollbox height="100%" width="100%">
|
|
51
|
+
{/* Header */}
|
|
52
|
+
<box height={1} width="100%">
|
|
53
|
+
<text>{" ST ID AMOUNT STATUS PURPOSE EXPIRES"}</text>
|
|
54
|
+
</box>
|
|
55
|
+
<box height={1} width="100%">
|
|
56
|
+
<text>{" -- ---------- ----------- ---------- ------------------- -------"}</text>
|
|
57
|
+
</box>
|
|
58
|
+
|
|
59
|
+
{/* Rows */}
|
|
60
|
+
{reservations.map((r, i) => {
|
|
61
|
+
const isSelected = i === selectedIndex;
|
|
62
|
+
const badge = STATUS_BADGES[r.status] ?? "?";
|
|
63
|
+
const purpose = r.purpose.length > 19
|
|
64
|
+
? `${r.purpose.slice(0, 16)}...`
|
|
65
|
+
: r.purpose;
|
|
66
|
+
const prefix = isSelected ? "> " : " ";
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<box key={r.id} height={1} width="100%">
|
|
70
|
+
<text>
|
|
71
|
+
{`${prefix}${badge} ${shortId(r.id).padEnd(10)} ${r.amount.padEnd(11)} ${r.status.padEnd(10)} ${purpose.padEnd(19)} ${formatTimestamp(r.expires_at)}`}
|
|
72
|
+
</text>
|
|
73
|
+
</box>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</scrollbox>
|
|
77
|
+
);
|
|
78
|
+
}
|