@cortexkit/aft-opencode 0.42.0 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bg-notifications.d.ts +35 -0
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/config.d.ts +7 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/configure-warnings.d.ts +3 -3
- package/dist/configure-warnings.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6092 -9528
- package/dist/notifications.d.ts +2 -2
- package/dist/notifications.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +6 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +29 -15
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +10 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/subc-tool-schemas.d.ts +1 -1
- package/dist/subc-tool-schemas.d.ts.map +1 -1
- package/dist/tools/_shared.d.ts +12 -2
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/imports.d.ts.map +1 -1
- package/dist/tools/inspect.d.ts +0 -1
- package/dist/tools/inspect.d.ts.map +1 -1
- package/dist/tools/navigation.d.ts.map +1 -1
- package/dist/tools/permissions.d.ts +23 -13
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +0 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/refactoring.d.ts.map +1 -1
- package/dist/tools/safety.d.ts.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui/notification-socket.d.ts +41 -0
- package/dist/tui/notification-socket.d.ts.map +1 -0
- package/dist/tui.js +2460 -187
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -9
- package/src/shared/rpc-client.ts +9 -0
- package/src/shared/rpc-notifications.ts +97 -41
- package/src/shared/rpc-server.ts +286 -67
- package/src/tui/index.tsx +77 -125
- package/src/tui/notification-socket.ts +421 -0
- package/src/tui/sidebar.tsx +38 -54
- package/dist/patch-parser.d.ts +0 -33
- package/dist/patch-parser.d.ts.map +0 -1
- package/dist/shared/opencode-config-dir.d.ts +0 -16
- package/dist/shared/opencode-config-dir.d.ts.map +0 -1
- package/dist/shared/pty-cache.d.ts +0 -18
- package/dist/shared/pty-cache.d.ts.map +0 -1
- package/dist/shared/tui-config.d.ts +0 -2
- package/dist/shared/tui-config.d.ts.map +0 -1
- package/src/shared/opencode-config-dir.ts +0 -46
- package/src/shared/pty-cache.ts +0 -113
- package/src/shared/tui-config.ts +0 -58
package/src/tui/index.tsx
CHANGED
|
@@ -13,6 +13,14 @@ import {
|
|
|
13
13
|
formatSemanticIndexStatus,
|
|
14
14
|
formatSemanticRefreshing,
|
|
15
15
|
} from "../shared/status";
|
|
16
|
+
import {
|
|
17
|
+
createDebouncedStatusRefresh,
|
|
18
|
+
refreshAftTuiSocketScope,
|
|
19
|
+
type SocketNotification,
|
|
20
|
+
startAftTuiSocket,
|
|
21
|
+
stopAftTuiSocket,
|
|
22
|
+
subscribeStatusInvalidations,
|
|
23
|
+
} from "./notification-socket";
|
|
16
24
|
import {
|
|
17
25
|
createAftSidebarSlot,
|
|
18
26
|
formatCompressionSidebarRows,
|
|
@@ -50,53 +58,14 @@ function getSessionId(api: TuiPluginApi): string | null {
|
|
|
50
58
|
return null;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
interface TuiMessage {
|
|
54
|
-
id: number;
|
|
55
|
-
type: string;
|
|
56
|
-
payload: Record<string, unknown>;
|
|
57
|
-
sessionId?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const lastReceivedNotificationIdBySession = new Map<string, number>();
|
|
61
|
-
|
|
62
|
-
/** Poll for pending server → TUI notifications via RPC. */
|
|
63
|
-
async function consumeTuiMessages(client: AftRpcClient, sessionId: string): Promise<TuiMessage[]> {
|
|
64
|
-
try {
|
|
65
|
-
const result = await client.call<{ messages?: TuiMessage[] }>("consume-notifications", {
|
|
66
|
-
lastReceivedId: lastReceivedNotificationIdBySession.get(sessionId) ?? 0,
|
|
67
|
-
sessionId,
|
|
68
|
-
});
|
|
69
|
-
return (result.messages ?? []).map((message) => ({
|
|
70
|
-
id: message.id,
|
|
71
|
-
type: message.type,
|
|
72
|
-
payload: message.payload,
|
|
73
|
-
sessionId: message.sessionId,
|
|
74
|
-
}));
|
|
75
|
-
} catch {
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function markTuiMessagesReceived(sessionId: string, messages: TuiMessage[]): void {
|
|
81
|
-
const previous = lastReceivedNotificationIdBySession.get(sessionId) ?? 0;
|
|
82
|
-
const next = messages.reduce((max, message) => Math.max(max, message.id), previous);
|
|
83
|
-
if (next > previous) lastReceivedNotificationIdBySession.set(sessionId, next);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
61
|
// ---------------------------------------------------------------------------
|
|
87
|
-
// StatusDialog — themed, two-column JSX dialog.
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// proportional font with no column alignment; only TUI flex primitives
|
|
93
|
-
// (<box flexDirection="row" flexBasis={0}>) actually produce visible
|
|
94
|
-
// columns. This component owns its own RPC polling so it can re-render
|
|
95
|
-
// reactively as the status snapshot changes, with no parent re-mount.
|
|
62
|
+
// StatusDialog — themed, two-column JSX dialog. OpenCode renders DialogAlert
|
|
63
|
+
// text in a proportional font with no column alignment, so the status view uses
|
|
64
|
+
// TUI flex primitives instead of a padded monospace string. The component
|
|
65
|
+
// subscribes to server-pushed invalidations so it can re-render as the status
|
|
66
|
+
// snapshot changes, with no parent re-mount.
|
|
96
67
|
// ---------------------------------------------------------------------------
|
|
97
68
|
|
|
98
|
-
const POLL_INTERVAL_MS = 1500;
|
|
99
|
-
|
|
100
69
|
function formatCountShort(value: number | null | undefined): string {
|
|
101
70
|
if (value == null || !Number.isFinite(value)) return "—";
|
|
102
71
|
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
|
@@ -187,20 +156,19 @@ const StatusDialog = (props: StatusDialogProps) => {
|
|
|
187
156
|
const theme = createMemo(() => (props.api as any).theme.current as TuiThemeCurrent);
|
|
188
157
|
const t = () => theme();
|
|
189
158
|
|
|
190
|
-
// Reactive status signal — the dialog re-renders on
|
|
191
|
-
//
|
|
192
|
-
// and stops when it unmounts.
|
|
159
|
+
// Reactive status signal — the dialog re-renders on pushed status
|
|
160
|
+
// invalidations without remounting.
|
|
193
161
|
const [status, setStatus] = createSignal<AftStatusSnapshot | null>(props.initial);
|
|
194
162
|
const [error, setError] = createSignal<string | null>(props.initialError);
|
|
195
163
|
|
|
196
|
-
let
|
|
197
|
-
let
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
164
|
+
let refreshGeneration = 0;
|
|
165
|
+
let refreshController: AbortController | null = null;
|
|
166
|
+
const refreshStatus = async () => {
|
|
167
|
+
if (refreshController) return;
|
|
200
168
|
|
|
201
169
|
const controller = new AbortController();
|
|
202
|
-
const requestGeneration = ++
|
|
203
|
-
|
|
170
|
+
const requestGeneration = ++refreshGeneration;
|
|
171
|
+
refreshController = controller;
|
|
204
172
|
|
|
205
173
|
try {
|
|
206
174
|
const response = await props.client.call(
|
|
@@ -208,12 +176,13 @@ const StatusDialog = (props: StatusDialogProps) => {
|
|
|
208
176
|
{ sessionID: props.sessionID },
|
|
209
177
|
{ signal: controller.signal, accept: statusAcceptGate(props.directory) },
|
|
210
178
|
);
|
|
211
|
-
if (controller.signal.aborted || requestGeneration !==
|
|
179
|
+
if (controller.signal.aborted || requestGeneration !== refreshGeneration) return;
|
|
212
180
|
if ((response as Record<string, unknown>).success !== false) {
|
|
213
181
|
const snapshot = coerceAftStatus(response as Record<string, unknown>);
|
|
214
182
|
// Stale-while-revalidate: don't downgrade a good snapshot to a transient
|
|
215
|
-
// `not_initialized` (
|
|
216
|
-
//
|
|
183
|
+
// `not_initialized` (for example, while the bridge process respawns or
|
|
184
|
+
// a session directory lookup briefly misses) — it arrives as success:true
|
|
185
|
+
// and would blank the dialog until the next refresh.
|
|
217
186
|
const current = status();
|
|
218
187
|
if (
|
|
219
188
|
shouldSuppressUninitializedDowngrade(
|
|
@@ -227,22 +196,25 @@ const StatusDialog = (props: StatusDialogProps) => {
|
|
|
227
196
|
setError(null);
|
|
228
197
|
}
|
|
229
198
|
} catch {
|
|
230
|
-
if (controller.signal.aborted || requestGeneration !==
|
|
199
|
+
if (controller.signal.aborted || requestGeneration !== refreshGeneration) return;
|
|
231
200
|
// transient — keep showing last good snapshot
|
|
232
201
|
} finally {
|
|
233
|
-
if (
|
|
202
|
+
if (refreshController === controller) refreshController = null;
|
|
234
203
|
}
|
|
235
204
|
};
|
|
236
205
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
206
|
+
const statusDebouncer = createDebouncedStatusRefresh(refreshStatus, 200);
|
|
207
|
+
const unsubscribeStatusInvalidations = subscribeStatusInvalidations((event) => {
|
|
208
|
+
if (event.sessionId && event.sessionId !== props.sessionID) return;
|
|
209
|
+
statusDebouncer.schedule();
|
|
210
|
+
});
|
|
240
211
|
onCleanup(() => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
212
|
+
unsubscribeStatusInvalidations();
|
|
213
|
+
statusDebouncer.dispose();
|
|
214
|
+
refreshGeneration++;
|
|
215
|
+
if (refreshController) {
|
|
216
|
+
refreshController.abort();
|
|
217
|
+
refreshController = null;
|
|
246
218
|
}
|
|
247
219
|
});
|
|
248
220
|
|
|
@@ -588,7 +560,7 @@ async function showStatusDialog(api: TuiPluginApi): Promise<void> {
|
|
|
588
560
|
const client = getRpcClient(directory);
|
|
589
561
|
|
|
590
562
|
// Prime the dialog with one initial fetch so we don't show a blank
|
|
591
|
-
// skeleton — the component then
|
|
563
|
+
// skeleton — the component then listens for pushed invalidations.
|
|
592
564
|
let initial: AftStatusSnapshot | null = null;
|
|
593
565
|
let initialError: string | null = null;
|
|
594
566
|
try {
|
|
@@ -606,20 +578,15 @@ async function showStatusDialog(api: TuiPluginApi): Promise<void> {
|
|
|
606
578
|
initialError = "AFT is starting up. Status will refresh automatically...";
|
|
607
579
|
}
|
|
608
580
|
|
|
609
|
-
// The host
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
// entry's onClose on Esc AND clear() re-invokes every entry's onClose, so a
|
|
619
|
-
// clear()-based onClose recursed infinitely on close. The host pops the stack
|
|
620
|
-
// itself, and StatusDialog stops its own polling via onCleanup when unmounted,
|
|
621
|
-
// so no custom onClose is needed. `replace` resets size to "medium", so request
|
|
622
|
-
// "large" AFTER it.
|
|
581
|
+
// The dialog host already wraps every replace()'d element in its own centered
|
|
582
|
+
// frame and binds Esc/Ctrl-C to close it. Rendering our own dialog frame inside
|
|
583
|
+
// that host frame would misplace the content and focus; pass the bare component
|
|
584
|
+
// so the host frames it once.
|
|
585
|
+
// Do not pass an onClose that calls dialog.clear(). The dialog system calls
|
|
586
|
+
// every entry's onClose on Esc, and clear() would re-trigger them, causing
|
|
587
|
+
// infinite recursion. The host itself pops the dialog; StatusDialog cleans up
|
|
588
|
+
// its own subscriptions in onCleanup. `replace` resets size to "medium", so
|
|
589
|
+
// request "large" after the call.
|
|
623
590
|
api.ui.dialog.replace(() => (
|
|
624
591
|
<StatusDialog
|
|
625
592
|
api={api}
|
|
@@ -774,54 +741,39 @@ const tui: TuiPlugin = async (api) => {
|
|
|
774
741
|
// See https://github.com/cortexkit/aft/issues/33.
|
|
775
742
|
registerStatusCommand(api);
|
|
776
743
|
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
// Scope the drain to the TUI's active session so notifications tagged for a
|
|
782
|
-
// different session (served by the same RPC process) are not consumed here.
|
|
783
|
-
// Do not poll on non-session routes: a session-scoped action fetched while
|
|
784
|
-
// sessionless could otherwise be acked without being shown.
|
|
785
|
-
if (pollInFlight) return;
|
|
786
|
-
|
|
744
|
+
// Receive server → TUI dialog requests and status invalidations over one
|
|
745
|
+
// persistent WebSocket. This avoids repeated loopback HTTP connection setup,
|
|
746
|
+
// which caused idle CPU usage.
|
|
747
|
+
const handleNotification = async (message: SocketNotification): Promise<boolean> => {
|
|
787
748
|
const requestedSessionId = getSessionId(api);
|
|
788
|
-
if (!requestedSessionId) return;
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const orderedMessages = [...messages].sort((a, b) => a.id - b.id);
|
|
803
|
-
for (const message of orderedMessages) {
|
|
804
|
-
if (getSessionId(api) !== requestedSessionId) return;
|
|
805
|
-
if (message.sessionId && message.sessionId !== requestedSessionId) continue;
|
|
806
|
-
if (message.type !== "action") continue;
|
|
807
|
-
if (message.payload?.action !== "show-status-dialog") continue;
|
|
808
|
-
await showStatusDialog(api);
|
|
809
|
-
}
|
|
749
|
+
if (!requestedSessionId) return false;
|
|
750
|
+
if (message.sessionId && message.sessionId !== requestedSessionId) return false;
|
|
751
|
+
if (message.type !== "action") return false;
|
|
752
|
+
if (message.payload?.action !== "show-status-dialog") return false;
|
|
753
|
+
await showStatusDialog(api);
|
|
754
|
+
return true;
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
startAftTuiSocket({
|
|
758
|
+
getDirectory: () => api.state.path.directory ?? "",
|
|
759
|
+
getSessionId: () => getSessionId(api),
|
|
760
|
+
onNotification: handleNotification,
|
|
761
|
+
});
|
|
810
762
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
// Intentional: message polling should never crash the TUI.
|
|
816
|
-
})
|
|
817
|
-
.finally(() => {
|
|
818
|
-
pollInFlight = false;
|
|
819
|
-
});
|
|
820
|
-
}, 500);
|
|
763
|
+
const socketScopeUnsubs = [
|
|
764
|
+
api.event?.on?.("message.updated", () => refreshAftTuiSocketScope()),
|
|
765
|
+
api.event?.on?.("session.updated", () => refreshAftTuiSocketScope()),
|
|
766
|
+
].filter(Boolean);
|
|
821
767
|
|
|
822
768
|
api.lifecycle?.onDispose?.(() => {
|
|
823
|
-
|
|
824
|
-
|
|
769
|
+
stopAftTuiSocket();
|
|
770
|
+
for (const unsub of socketScopeUnsubs) {
|
|
771
|
+
try {
|
|
772
|
+
unsub();
|
|
773
|
+
} catch {
|
|
774
|
+
// Ignore unsubscribe errors during cleanup; socket shutdown is handled separately.
|
|
775
|
+
}
|
|
776
|
+
}
|
|
825
777
|
for (const client of rpcClients.values()) client.reset();
|
|
826
778
|
rpcClients.clear();
|
|
827
779
|
});
|