@kabmo-labs/kabmo-ui 0.0.0-stage → 0.0.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/package.json +38 -4
- package/src/components.tsx +163 -0
- package/src/fetch.test.tsx +167 -0
- package/src/fetch.ts +173 -0
- package/src/index.tsx +87 -0
- package/src/panel-host.tsx +49 -0
- package/src/panels.ts +206 -0
- package/src/participant.test.tsx +238 -0
- package/src/participant.ts +186 -0
- package/src/plugin-id.tsx +79 -0
- package/src/repo-picker.tsx +212 -0
- package/src/repo.test.tsx +218 -0
- package/src/repo.ts +72 -0
- package/src/services.ts +309 -0
- package/src/session-value.test.tsx +162 -0
- package/src/sidebar.test.tsx +190 -0
- package/src/sidebar.ts +136 -0
- package/README.md +0 -3
package/src/sidebar.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The sidebar hooks: what lets a plugin read the session list and act on a
|
|
8
|
+
* session.
|
|
9
|
+
*
|
|
10
|
+
* bb calls this "threads"; Kabmo calls the same thing sessions. One
|
|
11
|
+
* concept under two names, and these hooks use Kabmo's word because that
|
|
12
|
+
* is what the host's own routes and types say.
|
|
13
|
+
*
|
|
14
|
+
* **EXPERIMENTAL.** The `experimental_` prefix is the marker bb uses for the
|
|
15
|
+
* same two hooks, and it means the shape may move between minor versions — a
|
|
16
|
+
* plugin that uses them opts into that. The stable participants (`useRpc`,
|
|
17
|
+
* `useRealtime`, `useComposer`) carry no prefix.
|
|
18
|
+
*
|
|
19
|
+
* Both go through `requireServices()`, so a plugin outside the app throws naming
|
|
20
|
+
* the real problem, and a host built before this seam answers an honest empty
|
|
21
|
+
* state rather than a broken one — the same rule `useRealtime` follows for an
|
|
22
|
+
* older host that cannot publish a topic.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { useCallback, useEffect, useState } from 'preact/hooks';
|
|
26
|
+
import { requireServices } from './services';
|
|
27
|
+
import type { SidebarThread, SidebarThreadActions, SidebarThreadsState } from '@kabmo-labs/kabmo-sdk';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The sessions the sidebar is showing.
|
|
31
|
+
*
|
|
32
|
+
* `stale` is surfaced rather than hidden, for the reason `useRealtime` gives: a
|
|
33
|
+
* list shown as current when the socket is down is worse than one that says so.
|
|
34
|
+
*/
|
|
35
|
+
export function experimental_useSidebarThreads(): SidebarThreadsState {
|
|
36
|
+
const [state, setState] = useState<{ value: SidebarThread[]; stale: boolean }>(() => ({
|
|
37
|
+
value: [],
|
|
38
|
+
stale: false,
|
|
39
|
+
}));
|
|
40
|
+
/** True until the host has answered once, so a view can hold its placeholder. */
|
|
41
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const services = requireServices();
|
|
45
|
+
if (!services.subscribeSidebarThreads) {
|
|
46
|
+
// An older host cannot supply this. Reporting "loaded, empty" is the
|
|
47
|
+
// honest answer: there is nothing to wait for.
|
|
48
|
+
setIsLoading(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Seed from the host's current value BEFORE subscribing, so a component
|
|
52
|
+
// that mounted between two pushes does not render empty until the next one.
|
|
53
|
+
const seeded = services.sidebarThreads?.();
|
|
54
|
+
if (seeded) {
|
|
55
|
+
setState(seeded);
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
}
|
|
58
|
+
return services.subscribeSidebarThreads((next) => {
|
|
59
|
+
setState(next);
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
});
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const refresh = useCallback(() => {
|
|
65
|
+
requireServices().refreshSidebarThreads?.();
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return { threads: state.value, isLoading, stale: state.stale, refresh };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The session actions.
|
|
73
|
+
*
|
|
74
|
+
* The host owns the writes, so this is a thin pass-through — but the pass-through
|
|
75
|
+
* is the point: a plugin gets the SAME routes the sidebar's own controls call,
|
|
76
|
+
* with the server's own refusal wording, rather than a second implementation
|
|
77
|
+
* that would drift.
|
|
78
|
+
*/
|
|
79
|
+
export function experimental_useSidebarThreadActions(): SidebarThreadActions {
|
|
80
|
+
const [error, setError] = useState<string | null>(null);
|
|
81
|
+
|
|
82
|
+
const actions = requireServices().sidebarActions?.();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run one mutation, capturing a refusal's reason.
|
|
86
|
+
*
|
|
87
|
+
* TWO sources, because the host reports a refusal two ways and both are real:
|
|
88
|
+
* a `false` answer is the host saying no (its routes answer the refusal as a
|
|
89
|
+
* value), and the reason for it is on the actions object the host injected —
|
|
90
|
+
* while a THROW is a transport or programming failure with no host wording.
|
|
91
|
+
* Reading only the throw is what made a refused rename look like a silent
|
|
92
|
+
* no-op; reading only the false would lose the reason.
|
|
93
|
+
*
|
|
94
|
+
* The error clears on the next attempt, so a stale reason does not sit over a
|
|
95
|
+
* success.
|
|
96
|
+
*/
|
|
97
|
+
const run = useCallback(
|
|
98
|
+
async (operation: () => Promise<boolean>): Promise<boolean> => {
|
|
99
|
+
setError(null);
|
|
100
|
+
try {
|
|
101
|
+
const ok = await operation();
|
|
102
|
+
// A refusal carries its reason on the host's actions object, which is
|
|
103
|
+
// where the route's own wording lands.
|
|
104
|
+
if (!ok) setError(actions?.error ?? null);
|
|
105
|
+
return ok;
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[actions],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
open: useCallback((sessionId: string) => actions?.open(sessionId), [actions]),
|
|
116
|
+
rename: useCallback(
|
|
117
|
+
(sessionId: string, title: string) => run(() => actions?.rename(sessionId, title) ?? Promise.resolve(false)),
|
|
118
|
+
[actions, run],
|
|
119
|
+
),
|
|
120
|
+
setArchived: useCallback(
|
|
121
|
+
(sessionId: string, archived: boolean) =>
|
|
122
|
+
run(() => actions?.setArchived(sessionId, archived) ?? Promise.resolve(false)),
|
|
123
|
+
[actions, run],
|
|
124
|
+
),
|
|
125
|
+
markSeen: useCallback(
|
|
126
|
+
(sessionId: string) => run(() => actions?.markSeen(sessionId) ?? Promise.resolve(false)),
|
|
127
|
+
[actions, run],
|
|
128
|
+
),
|
|
129
|
+
remove: useCallback(
|
|
130
|
+
(sessionId: string) => run(() => actions?.remove(sessionId) ?? Promise.resolve(false)),
|
|
131
|
+
[actions, run],
|
|
132
|
+
),
|
|
133
|
+
openNew: useCallback((options?: { folderId?: string }) => actions?.openNew(options), [actions]),
|
|
134
|
+
error,
|
|
135
|
+
};
|
|
136
|
+
}
|
package/README.md
DELETED