@fungi.computer/tourtois 0.3.1 → 0.4.0
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/AppWorkbench.js +7 -6
- package/dist/app-directory.js +4 -2
- package/dist/app-launch.d.ts +9 -2
- package/dist/app-launch.js +32 -2
- package/dist/app-menus.js +1 -1
- package/dist/app-surface.d.ts +5 -2
- package/dist/command-dialogs.d.ts +1 -1
- package/dist/command-dialogs.js +29 -37
- package/dist/desktop/Desktop.d.ts +0 -1
- package/dist/desktop/Desktop.js +12 -8
- package/dist/desktop-client.d.ts +2 -0
- package/dist/desktop-client.js +5 -3
- package/dist/shell-host.js +1 -0
- package/dist/status-bar.js +5 -3
- package/dist/useDirectoryCommands.d.ts +1 -1
- package/dist/useDirectoryCommands.js +12 -25
- package/package.json +9 -9
package/dist/AppWorkbench.js
CHANGED
|
@@ -11,7 +11,7 @@ import { addressKey } from "@fungi.computer/nacre/app-directory";
|
|
|
11
11
|
import { reconcileAppDirectory, reconcileAppWindowFromDirectory, } from "./app-directory-reconciliation.js";
|
|
12
12
|
import { useAppDirectory } from "./app-directory.js";
|
|
13
13
|
import { AppIcon } from "./app-icon.js";
|
|
14
|
-
import { appForRole, composeOpenRequests, createAppLaunchDescriptor, attachWindowComputer, launchComputerId, launchForComputer, launchForRoleIntent, requireHandledNavigation, resolveFileEditorWindow, } from "./app-launch.js";
|
|
14
|
+
import { appForRole, composeOpenRequests, createAppLaunchDescriptor, attachWindowComputer, launchComputerId, launchForComputer, findOpenFileCommand, launchForRoleIntent, requireHandledNavigation, resolveFileEditorWindow, } from "./app-launch.js";
|
|
15
15
|
import { useAppSurfaceHost, } from "./app-surface.js";
|
|
16
16
|
import { hasVisibleAppWindows, } from "./app-windows.js";
|
|
17
17
|
import { desktopVisibility } from "./desktop-visibility.js";
|
|
@@ -37,7 +37,7 @@ function phoneShowDesktop(phone, covered, host, onFailure) {
|
|
|
37
37
|
return undefined;
|
|
38
38
|
return () => {
|
|
39
39
|
void host
|
|
40
|
-
.execute("shell:desktop.show", { origin: "menu" })
|
|
40
|
+
.execute({ namespace: "shell", id: "desktop.show" }, { origin: "menu" })
|
|
41
41
|
.then((outcome) => {
|
|
42
42
|
if (outcome.status !== "handled") {
|
|
43
43
|
console.error("Phone Show desktop refused", outcome);
|
|
@@ -298,8 +298,10 @@ export function AppWorkbench({ paneLayerRef, phone, teamId, teamName, initialCom
|
|
|
298
298
|
layout.zoom(null);
|
|
299
299
|
focusWindow(launch.windowId);
|
|
300
300
|
appSurfaceHost.focus?.(launch.surfaceId);
|
|
301
|
-
const
|
|
302
|
-
|
|
301
|
+
const openFile = findOpenFileCommand(appSurfaceHost.snapshot(), launch.surfaceId);
|
|
302
|
+
if (!openFile)
|
|
303
|
+
throw new Error("The editor's open-file command is unavailable.");
|
|
304
|
+
const outcome = await appSurfaceHost.execute(openFile.command, {
|
|
303
305
|
origin: "programmatic",
|
|
304
306
|
arguments: input,
|
|
305
307
|
});
|
|
@@ -456,7 +458,6 @@ export function AppWorkbench({ paneLayerRef, phone, teamId, teamName, initialCom
|
|
|
456
458
|
};
|
|
457
459
|
return launcher.icon ? { ...item, icon: launcher.icon } : item;
|
|
458
460
|
}), ...extraLaunchers);
|
|
459
|
-
const inboxSurfaceId = useMemo(() => appForRole(directory, directoryRoles, "inbox")?.surfaceId ?? null, [directory, directoryRoles]);
|
|
460
461
|
const visibility = desktopVisibility({
|
|
461
462
|
hasVisibleWindows: hasVisibleAppWindows(appProjection) ||
|
|
462
463
|
order.some((id) => !appProjection.windows.some((window) => window.windowId === id)),
|
|
@@ -530,7 +531,7 @@ export function AppWorkbench({ paneLayerRef, phone, teamId, teamName, initialCom
|
|
|
530
531
|
openAppPermissions(activeWindow.launch.surfaceId);
|
|
531
532
|
} }, activeWindow.windowId)) : null, systemMenuItems: systemMenuItems }), noticeContainer
|
|
532
533
|
? createPortal(_jsx(Toaster, { id: "workbench-notices", position: "bottom-right", offset: 24, mobileOffset: 24, closeButton: true }), noticeContainer)
|
|
533
|
-
: null, _jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx(MaosaicoRegion, { layer: "desktop", style: { position: "absolute", inset: 0 }, "data-desktop-visibility": visibility, ref: desktopRef, "aria-hidden": covered ? true : undefined, children: _jsx(Desktop, { phone: phone, editing: desktopEditing, onEditingChange: setDesktopEditing,
|
|
534
|
+
: null, _jsxs("div", { className: "relative min-h-0 flex-1", children: [_jsx(MaosaicoRegion, { layer: "desktop", style: { position: "absolute", inset: 0 }, "data-desktop-visibility": visibility, ref: desktopRef, "aria-hidden": covered ? true : undefined, children: _jsx(Desktop, { phone: phone, editing: desktopEditing, onEditingChange: setDesktopEditing, onOwner: receiveDesktopOwner, launcherItems: launchableItems, onAllApps: () => {
|
|
534
535
|
setAppDialOpen(true);
|
|
535
536
|
}, paused: covered, apps: windowApps, needsUpdate: needsUpdate, onAppSettings: (app) => {
|
|
536
537
|
setSettingsLocation({
|
package/dist/app-directory.js
CHANGED
|
@@ -12,8 +12,10 @@ export function useAppDirectory(teamId, directoryReader) {
|
|
|
12
12
|
// result fresh for that handoff; explicit mutations still invalidate it.
|
|
13
13
|
staleTime: 5_000,
|
|
14
14
|
// Quiet open Apps need release availability too, not just after a request
|
|
15
|
-
// is refused.
|
|
16
|
-
|
|
15
|
+
// is refused. Every directory read costs the host a Registry read, and
|
|
16
|
+
// explicit mutations already invalidate this query, so the poll is a slow
|
|
17
|
+
// backstop. React Query pauses it while the page is hidden.
|
|
18
|
+
refetchInterval: 60_000,
|
|
17
19
|
});
|
|
18
20
|
return {
|
|
19
21
|
query,
|
package/dist/app-launch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppOpenInput } from "@fungi.computer/agar/apps";
|
|
2
|
-
import type
|
|
2
|
+
import { type WhistleMenuProjection, type WhistleSnapshot } from "@fungi.computer/whistle";
|
|
3
3
|
import { type TourtoisAppSurfaceHost } from "./app-surface.js";
|
|
4
4
|
import { createAppWindowManager, type AppLaunchDescriptor, type AppWindowManager, type AppWindowProjection } from "./app-windows.js";
|
|
5
5
|
import type { WorkbenchDirectory } from "@fungi.computer/nacre/workbench-directory";
|
|
@@ -30,8 +30,15 @@ export declare function sameOfferedComputers(left: AppLaunchDescriptor, right: A
|
|
|
30
30
|
export declare function launchComputerId(href: string): string | null;
|
|
31
31
|
/** Keep the admitted App identity and point its existing surface at one Computer. */
|
|
32
32
|
export declare function launchForComputer(launch: AppLaunchDescriptor, computerId: string): AppLaunchDescriptor;
|
|
33
|
+
/** An editor surface's admitted open-file row, once the App registered it. */
|
|
34
|
+
export declare function findOpenFileCommand(snapshot: WhistleSnapshot, surfaceId: string): WhistleMenuProjection | undefined;
|
|
33
35
|
/** Find the user's already-open editor by its admitted open-file command. */
|
|
34
36
|
export declare function findOpenFileEditor<Window extends Pick<AppWindowProjection, "launch">>(windows: readonly Window[], snapshot: WhistleSnapshot): Window | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve once a just-mounted App has registered a declared command: an App
|
|
39
|
+
* window is ready when its host connects, and registers its commands after.
|
|
40
|
+
*/
|
|
41
|
+
export declare function waitForSurfaceCommand(host: Pick<TourtoisAppSurfaceHost, "subscribe">, isRegistered: () => boolean): Promise<void>;
|
|
35
42
|
/**
|
|
36
43
|
* Select the Team's Files App for file navigation before it is mounted, or
|
|
37
44
|
* any App that declares it opens files when no Files App handles them.
|
|
@@ -41,7 +48,7 @@ export declare function resolveFileEditorWindow(options: {
|
|
|
41
48
|
apps: readonly AppDirectoryApp[];
|
|
42
49
|
roles: AppDirectoryRoles;
|
|
43
50
|
appWindows: ReturnType<typeof createAppWindowManager>;
|
|
44
|
-
appSurfaceHost: TourtoisAppSurfaceHost
|
|
51
|
+
appSurfaceHost: Pick<TourtoisAppSurfaceHost, "snapshot" | "subscribe">;
|
|
45
52
|
computerId: string;
|
|
46
53
|
computers: readonly ComputerOption[];
|
|
47
54
|
origin: string;
|
package/dist/app-launch.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { defineIdFamily } from "@fungi.computer/cairn/id";
|
|
2
|
+
import { sameWhistleCommand, } from "@fungi.computer/whistle";
|
|
2
3
|
import {} from "./app-surface.js";
|
|
3
4
|
import { AppError } from "@fungi.computer/agar";
|
|
4
5
|
import { createAppWindowManager, } from "./app-windows.js";
|
|
6
|
+
import { appSurfaceCommandKey } from "@fungi.computer/nacre/command-host";
|
|
5
7
|
/** The Team's effective App for a role when it can be launched now. */
|
|
6
8
|
export function appForRole(apps, roles, role) {
|
|
7
9
|
const surfaceId = roles[role]?.surfaceId;
|
|
@@ -146,9 +148,37 @@ export function launchForComputer(launch, computerId) {
|
|
|
146
148
|
href: href.pathname + href.search,
|
|
147
149
|
};
|
|
148
150
|
}
|
|
151
|
+
/** An editor surface's admitted open-file row, once the App registered it. */
|
|
152
|
+
export function findOpenFileCommand(snapshot, surfaceId) {
|
|
153
|
+
const key = appSurfaceCommandKey({ surfaceId, commandId: "open-file" });
|
|
154
|
+
return snapshot.menu.find((row) => sameWhistleCommand(row.command, key));
|
|
155
|
+
}
|
|
149
156
|
/** Find the user's already-open editor by its admitted open-file command. */
|
|
150
157
|
export function findOpenFileEditor(windows, snapshot) {
|
|
151
|
-
return windows.find((window) => snapshot
|
|
158
|
+
return windows.find((window) => findOpenFileCommand(snapshot, window.launch.surfaceId) !== undefined);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve once a just-mounted App has registered a declared command: an App
|
|
162
|
+
* window is ready when its host connects, and registers its commands after.
|
|
163
|
+
*/
|
|
164
|
+
export function waitForSurfaceCommand(host, isRegistered) {
|
|
165
|
+
if (isRegistered())
|
|
166
|
+
return Promise.resolve();
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const timeout = window.setTimeout(() => {
|
|
169
|
+
unsubscribe();
|
|
170
|
+
reject(new Error("The App did not register its declared command."));
|
|
171
|
+
}, 10_000);
|
|
172
|
+
const check = () => {
|
|
173
|
+
if (!isRegistered())
|
|
174
|
+
return;
|
|
175
|
+
window.clearTimeout(timeout);
|
|
176
|
+
unsubscribe();
|
|
177
|
+
resolve();
|
|
178
|
+
};
|
|
179
|
+
const unsubscribe = host.subscribe(check);
|
|
180
|
+
check();
|
|
181
|
+
});
|
|
152
182
|
}
|
|
153
183
|
/**
|
|
154
184
|
* Select the Team's Files App for file navigation before it is mounted, or
|
|
@@ -184,7 +214,7 @@ export async function resolveFileEditorWindow(options) {
|
|
|
184
214
|
.read()
|
|
185
215
|
.windows.find((window) => window.windowId === candidate.windowId);
|
|
186
216
|
const editor = mounted
|
|
187
|
-
?
|
|
217
|
+
? await waitForSurfaceCommand(options.appSurfaceHost, () => findOpenFileCommand(options.appSurfaceHost.snapshot(), mounted.launch.surfaceId) !== undefined).then(() => mounted, () => undefined)
|
|
188
218
|
: undefined;
|
|
189
219
|
if (editor)
|
|
190
220
|
return { launch: editor.launch, opened: true };
|
package/dist/app-menus.js
CHANGED
package/dist/app-surface.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type { WhistleCommand, WhistleExecutionInput, WhistleExecutionOutcome, WhistleMenuProjection, WhistleSnapshot } from "@fungi.computer/whistle";
|
|
2
|
+
import type { WhistleCommand, WhistleCommandKey, WhistleExecutionInput, WhistleExecutionOutcome, WhistleMenuProjection, WhistleSnapshot } from "@fungi.computer/whistle";
|
|
3
3
|
import type { AgentSessionSelection, AppNavigationState, AppStatusItem, FileNavigationInput } from "@fungi.computer/agar";
|
|
4
4
|
import type { AppOpenInput, AppRoleChoice } from "@fungi.computer/agar/apps";
|
|
5
5
|
import type { AppDeclaredCommand } from "@fungi.computer/puffball/app";
|
|
6
6
|
import type { AppWindowMount } from "./app-windows.js";
|
|
7
7
|
import type { WindowEvents } from "@fungi.computer/nacre/window-manager";
|
|
8
|
+
import type { AppSurfaceCommandHost } from "@fungi.computer/nacre/command-host";
|
|
8
9
|
/** What one mounted App may raise toward the shell. */
|
|
9
10
|
export type TourtoisAppMountEvents = Pick<WindowEvents, "onDeliveryPending"> & Readonly<{
|
|
10
11
|
onComputerChoice?: () => Promise<void>;
|
|
@@ -39,6 +40,8 @@ export type TourtoisAppSurfaceHost = Readonly<{
|
|
|
39
40
|
appTitle: string;
|
|
40
41
|
command: AppDeclaredCommand;
|
|
41
42
|
}>[], run: (surfaceId: string, commandId: string) => Promise<void>) => () => void;
|
|
43
|
+
/** The App command a snapshot row runs, mounted or from the directory. */
|
|
44
|
+
appCommand: AppSurfaceCommandHost["appCommand"];
|
|
42
45
|
/** Commands contributed by the currently focused App window. */
|
|
43
46
|
menu: () => readonly WhistleMenuProjection[];
|
|
44
47
|
/** The shell-owned semantic command graph, including every mounted App. */
|
|
@@ -49,7 +52,7 @@ export type TourtoisAppSurfaceHost = Readonly<{
|
|
|
49
52
|
*/
|
|
50
53
|
active: () => WhistleSnapshot;
|
|
51
54
|
/** Execute an active command or an exact host-owned programmatic action. */
|
|
52
|
-
execute: (
|
|
55
|
+
execute: (command: WhistleCommandKey, input: WhistleExecutionInput) => Promise<WhistleExecutionOutcome>;
|
|
53
56
|
/** Bind one mounted App's explicit Agent Session selection to the shell. */
|
|
54
57
|
reportAgentSessionSelection?: (teamId: string, mountId: string, selection: AgentSessionSelection | null) => Promise<void> | void;
|
|
55
58
|
/** Observe focused App and command-graph changes. */
|
|
@@ -22,7 +22,7 @@ export type PaletteApp = Readonly<{
|
|
|
22
22
|
}>;
|
|
23
23
|
/** Which-key list and `:` line for the focused window, drawn from Caps. */
|
|
24
24
|
export declare function CommandDialogs({ host, state, focusedWindow, directoryApps, onStatus, finalFocus, container: dialog, renderIcon, }: Readonly<{
|
|
25
|
-
host: Pick<AppSurfaceCommandHost, "execute"> | null;
|
|
25
|
+
host: Pick<AppSurfaceCommandHost, "appCommand" | "execute"> | null;
|
|
26
26
|
state: ShellCommandState;
|
|
27
27
|
focusedWindow: Readonly<{
|
|
28
28
|
launch: Readonly<{
|
package/dist/command-dialogs.js
CHANGED
|
@@ -8,12 +8,6 @@ import { leaderKeyMatches, leaderKeys, parseChord, } from "@fungi.computer/nacre
|
|
|
8
8
|
function isMac() {
|
|
9
9
|
return /Mac|iPhone|iPad/u.test(navigator.userAgent);
|
|
10
10
|
}
|
|
11
|
-
function appCommandId(surfaceId, commandId) {
|
|
12
|
-
return `${encodeURIComponent(surfaceId)}:${commandId}`;
|
|
13
|
-
}
|
|
14
|
-
function directoryCommandId(surfaceId, commandId) {
|
|
15
|
-
return `app-directory:${encodeURIComponent(surfaceId)}-${encodeURIComponent(commandId)}`;
|
|
16
|
-
}
|
|
17
11
|
function chordKey(key) {
|
|
18
12
|
switch (key) {
|
|
19
13
|
case "enter":
|
|
@@ -85,12 +79,26 @@ export function CommandDialogs({ host, state, focusedWindow, directoryApps, onSt
|
|
|
85
79
|
surfaceId: focusedWindow.launch.surfaceId,
|
|
86
80
|
}
|
|
87
81
|
: undefined;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
// The App a palette row belongs to, and the command its Release declares.
|
|
83
|
+
const paletteEntry = (command) => {
|
|
84
|
+
const target = host?.appCommand(command);
|
|
85
|
+
if (target === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
const app = target.surfaceId === focusedApp?.surfaceId
|
|
88
|
+
? focusedApp
|
|
89
|
+
: command.sourceId === appDirectorySourceId
|
|
90
|
+
? directoryApps.find((entry) => entry.surfaceId === target.surfaceId)
|
|
91
|
+
: undefined;
|
|
92
|
+
if (app === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
return {
|
|
95
|
+
app,
|
|
96
|
+
declared: app.commands.find((entry) => entry.id === target.commandId),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
92
99
|
const isFocusedCommand = (command) => command.scope === focusedApp?.surfaceId ||
|
|
93
|
-
|
|
100
|
+
(focusedApp !== undefined &&
|
|
101
|
+
paletteEntry(command)?.app.surfaceId === focusedApp.surfaceId);
|
|
94
102
|
const orderedPaletteRows = [
|
|
95
103
|
...palette.filter(isFocusedCommand),
|
|
96
104
|
...palette.filter((command) => command.sourceId === appDirectorySourceId && !isFocusedCommand(command)),
|
|
@@ -104,18 +112,11 @@ export function CommandDialogs({ host, state, focusedWindow, directoryApps, onSt
|
|
|
104
112
|
seenPaletteIds.add(command.commandId);
|
|
105
113
|
return true;
|
|
106
114
|
});
|
|
107
|
-
const appByCommandId = new Map();
|
|
108
|
-
if (focusedApp)
|
|
109
|
-
for (const command of focusedApp.commands)
|
|
110
|
-
appByCommandId.set(appCommandId(focusedApp.surfaceId, command.id), focusedApp);
|
|
111
|
-
for (const app of directoryApps)
|
|
112
|
-
for (const command of app.commands)
|
|
113
|
-
appByCommandId.set(directoryCommandId(app.surfaceId, command.id), app);
|
|
114
115
|
const mac = isMac();
|
|
115
|
-
const run = useCallback((
|
|
116
|
+
const run = useCallback((command, origin) => {
|
|
116
117
|
if (!host)
|
|
117
118
|
return;
|
|
118
|
-
void host.execute(
|
|
119
|
+
void host.execute(command, { origin }).then((outcome) => {
|
|
119
120
|
const message = outcomeMessage(outcome);
|
|
120
121
|
if (message !== null)
|
|
121
122
|
onStatus(message);
|
|
@@ -127,7 +128,7 @@ export function CommandDialogs({ host, state, focusedWindow, directoryApps, onSt
|
|
|
127
128
|
onStatus(row.availability.reason);
|
|
128
129
|
return;
|
|
129
130
|
}
|
|
130
|
-
run(row.
|
|
131
|
+
run(row.command, "keyboard");
|
|
131
132
|
}, [onStatus, run, setKeysOpen]);
|
|
132
133
|
useLayoutEffect(() => {
|
|
133
134
|
if (!keysOpen)
|
|
@@ -160,7 +161,7 @@ export function CommandDialogs({ host, state, focusedWindow, directoryApps, onSt
|
|
|
160
161
|
} })] }, group.label)))] }) }), _jsx(Dialog, { open: commandLineOpen, onOpenChange: setCommandLineOpen, children: _jsxs(DialogContent, { container: dialog, className: "tourtois-command-palette", finalFocus: finalFocus, children: [_jsx(DialogTitle, { className: "sr-only", children: "Command palette" }), _jsxs("div", { className: "tourtois-command-palette-heading", children: [focusedApp
|
|
161
162
|
? renderIcon(focusedApp.roles, focusedApp.icon ?? undefined)
|
|
162
163
|
: null, _jsx("span", { children: focusedTitle ?? "Commands" })] }), _jsx(Combobox, { activeTone: "primary", "aria-label": "Available commands", emptyMessage: "No matching commands", getKey: (command) => command.commandId, getLabel: (command) => command.title, getSearchText: (command) => {
|
|
163
|
-
const app =
|
|
164
|
+
const app = paletteEntry(command)?.app;
|
|
164
165
|
return `${command.title} ${command.category} ${command.description ?? ""} ${app?.name ?? ""} ${app ? `opens ${app.name}` : ""}`;
|
|
165
166
|
}, groupBy: (command) => {
|
|
166
167
|
if (isFocusedCommand(command))
|
|
@@ -169,26 +170,17 @@ export function CommandDialogs({ host, state, focusedWindow, directoryApps, onSt
|
|
|
169
170
|
return "Other apps";
|
|
170
171
|
return "Workspace";
|
|
171
172
|
}, inputProps: { "aria-label": "Search commands" }, items: paletteRows, placeholder: ":", renderItem: (command) => (_jsxs("div", { className: "tourtois-command-palette-item", children: [_jsx("span", { className: "tourtois-command-palette-item-icon", children: (() => {
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
173
|
+
const entry = paletteEntry(command);
|
|
174
|
+
if (entry === undefined)
|
|
174
175
|
return null;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
command.commandId);
|
|
179
|
-
return renderIcon(app.roles, declared?.sigil ?? app.icon ?? undefined);
|
|
180
|
-
})() }), _jsx("span", { className: "tourtois-command-palette-item-title", children: command.title }), command.sourceId === appDirectorySourceId ? (appByCommandId.get(command.commandId)?.name !==
|
|
181
|
-
focusedApp?.name ? (_jsxs("small", { children: ["opens", " ", appByCommandId.get(command.commandId)?.name ?? "App"] })) : null) : null, (() => {
|
|
182
|
-
const app = appByCommandId.get(command.commandId);
|
|
183
|
-
const declared = app?.commands.find((entry) => [
|
|
184
|
-
appCommandId(app.surfaceId, entry.id),
|
|
185
|
-
directoryCommandId(app.surfaceId, entry.id),
|
|
186
|
-
].includes(command.commandId));
|
|
176
|
+
return renderIcon(entry.app.roles, entry.declared?.sigil ?? entry.app.icon ?? undefined);
|
|
177
|
+
})() }), _jsx("span", { className: "tourtois-command-palette-item-title", children: command.title }), command.sourceId === appDirectorySourceId ? (paletteEntry(command)?.app.name !== focusedApp?.name ? (_jsxs("small", { children: ["opens ", paletteEntry(command)?.app.name ?? "App"] })) : null) : null, (() => {
|
|
178
|
+
const declared = paletteEntry(command)?.declared;
|
|
187
179
|
return (_jsx("span", { className: "tourtois-command-palette-shortcut", children: commandKeycaps(command.commandId, active, mac, declared).map((key, index) => key === "then" ? (_jsx("span", { children: "then" }, `${key}-${String(index)}`)) : (_jsx("kbd", { className: "kbd kbd-sm", children: key }, `${key}-${String(index)}`))) }));
|
|
188
180
|
})()] })), value: null, onValueChange: (_value, command) => {
|
|
189
181
|
if (!command)
|
|
190
182
|
return;
|
|
191
183
|
setCommandLineOpen(false);
|
|
192
|
-
run(command.
|
|
184
|
+
run(command.command, "palette");
|
|
193
185
|
} }), _jsxs("div", { className: "tourtois-command-palette-footer", children: [_jsx("span", { className: "tourtois-command-palette-close-hint", children: "Esc to close" }), _jsxs("span", { className: "tourtois-command-palette-leader-hint", children: [_jsx("kbd", { className: "kbd kbd-sm", children: mac ? "⌘ K" : "Ctrl K" }), _jsx("span", { children: "then" }), _jsx("kbd", { className: "kbd kbd-sm", children: "?" }), _jsx("span", { children: "for all keys" })] })] })] }) })] }));
|
|
194
186
|
}
|
|
@@ -8,7 +8,6 @@ import { type DesktopGillTransport } from "./row-gill.js";
|
|
|
8
8
|
export type DesktopProps = {
|
|
9
9
|
phone: boolean;
|
|
10
10
|
apps: WorkbenchDirectory["apps"];
|
|
11
|
-
inboxSurfaceId: string | null;
|
|
12
11
|
launcherItems: readonly AppBarItem[];
|
|
13
12
|
needsUpdate: NonNullable<WorkbenchDirectory["needsUpdate"]>;
|
|
14
13
|
onAppSettings: (app: WorkbenchDirectory["apps"][number]) => void;
|
package/dist/desktop/Desktop.js
CHANGED
|
@@ -48,14 +48,18 @@ export function Desktop(props) {
|
|
|
48
48
|
props.onOwner?.(null);
|
|
49
49
|
current?.close();
|
|
50
50
|
};
|
|
51
|
-
}, [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
}, [props.methods, props.storage, props.onOwner, retry]);
|
|
52
|
+
// The backend resolves the desktop against its own App catalog. One owner
|
|
53
|
+
// lives across directory polls; only a changed App set re-reads it.
|
|
54
|
+
const catalog = JSON.stringify(props.apps.map((app) => [app.surfaceId, app.releaseId, app.availability]));
|
|
55
|
+
const readCatalog = useRef(null);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (owner === null)
|
|
58
|
+
return;
|
|
59
|
+
if (readCatalog.current !== null && readCatalog.current !== catalog)
|
|
60
|
+
owner.invalidate();
|
|
61
|
+
readCatalog.current = catalog;
|
|
62
|
+
}, [owner, catalog]);
|
|
59
63
|
return (_jsxs("div", { className: "tourtois-desktop", "data-desktop-bento": true, children: [error ? (_jsxs("p", { role: "alert", children: [error, " ", _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
60
64
|
setRetry((value) => value + 1);
|
|
61
65
|
}, children: "Retry" })] })) : null, !owner && !error ? _jsx("p", { role: "status", children: "Loading desktop\u2026" }) : null, owner ? _jsx(ArrangedDesktop, { ...props, owner: owner }) : null] }));
|
package/dist/desktop-client.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type DesktopLayoutClient = Readonly<{
|
|
|
9
9
|
};
|
|
10
10
|
subscribe: (listener: () => void) => () => void;
|
|
11
11
|
refresh: () => Promise<void>;
|
|
12
|
+
/** Re-read in the background; a failure shows through `refreshFailure`. */
|
|
13
|
+
invalidate: () => void;
|
|
12
14
|
mutate: (method: DesktopMutation, input: WhistleJsonValue) => Promise<DesktopArrangeResult>;
|
|
13
15
|
close: () => void;
|
|
14
16
|
}>;
|
package/dist/desktop-client.js
CHANGED
|
@@ -85,10 +85,11 @@ export async function createDesktopLayoutClient({ storage, methods, }) {
|
|
|
85
85
|
});
|
|
86
86
|
return pending;
|
|
87
87
|
};
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// Handle background failures through the same idempotent state publisher.
|
|
89
|
+
const invalidate = () => {
|
|
90
90
|
void refresh().catch(publishRefreshFailure);
|
|
91
|
-
}
|
|
91
|
+
};
|
|
92
|
+
const unsubscribe = watch.subscribe(invalidate);
|
|
92
93
|
const close = () => {
|
|
93
94
|
closed = true;
|
|
94
95
|
retirePending?.();
|
|
@@ -116,6 +117,7 @@ export async function createDesktopLayoutClient({ storage, methods, }) {
|
|
|
116
117
|
};
|
|
117
118
|
},
|
|
118
119
|
refresh,
|
|
120
|
+
invalidate,
|
|
119
121
|
async mutate(method, input) {
|
|
120
122
|
const result = desktopArrangeResultSchema.parse(await methods.call(method, JSON.stringify(input)));
|
|
121
123
|
if (result.kind === "accepted" || result.code === "conflict")
|
package/dist/shell-host.js
CHANGED
|
@@ -243,6 +243,7 @@ export function createTourtoisShell(workbench) {
|
|
|
243
243
|
focus: commands.focus,
|
|
244
244
|
registerShellCommands: commands.registerShellCommands,
|
|
245
245
|
registerDirectoryCommands: commands.registerDirectoryCommands,
|
|
246
|
+
appCommand: commands.appCommand,
|
|
246
247
|
menu: commands.menu,
|
|
247
248
|
snapshot: commands.snapshot,
|
|
248
249
|
active: commands.active,
|
package/dist/status-bar.js
CHANGED
|
@@ -17,19 +17,21 @@ const statusIcons = {
|
|
|
17
17
|
* the Computer picker, never a raised key. Caps' status-bar chip replaces
|
|
18
18
|
* this composition when it ships.
|
|
19
19
|
*/
|
|
20
|
-
const StatusBarButton = forwardRef(function StatusBarButton({ label, icon, count, pressed, expanded, ...props }, ref) {
|
|
21
|
-
return (_jsxs(Button, { ...props, ref: ref, variant: "ghost", size: "xs", className: "caps-status-bar-picker tourtois-status-chip", ...(pressed === undefined ? {} : { "aria-pressed": pressed }), ...(expanded === undefined ? {} : { "aria-expanded": expanded }), children: [icon, _jsx("span", { className: "caps-status-bar-picker-label tourtois-status-chip-label", children: label }), count] }));
|
|
20
|
+
const StatusBarButton = forwardRef(function StatusBarButton({ label, icon, count, light, pressed, expanded, ...props }, ref) {
|
|
21
|
+
return (_jsxs(Button, { ...props, ref: ref, variant: "ghost", size: "xs", className: "caps-status-bar-picker tourtois-status-chip", ...(pressed === undefined ? {} : { "aria-pressed": pressed }), ...(expanded === undefined ? {} : { "aria-expanded": expanded }), children: [icon, light, _jsx("span", { className: "caps-status-bar-picker-label tourtois-status-chip-label", children: label }), count] }));
|
|
22
22
|
});
|
|
23
23
|
/** One App-declared status item, drawn by the shell as a flat status chip. */
|
|
24
24
|
export function WindowStatusItem({ item, container, onAction, anchorFor, }) {
|
|
25
25
|
const count = (item.kind === "button" || item.kind === "menu") &&
|
|
26
26
|
item.count !== undefined &&
|
|
27
27
|
item.count > 0 ? (_jsx(CountBadge, { count: item.count })) : null;
|
|
28
|
+
const light = item.kind === "button" &&
|
|
29
|
+
(item.tone !== undefined || item.busy === true) ? (_jsx(Status, { size: "compact", "aria-hidden": "true", tone: item.tone ?? "accent", motion: item.busy === true ? "pulse" : "none" })) : undefined;
|
|
28
30
|
switch (item.kind) {
|
|
29
31
|
case "text":
|
|
30
32
|
return item.tone === undefined ? (_jsx(AppStatusItem, { role: "status", children: item.text })) : (_jsx(Status, { size: "compact", tone: item.tone, label: item.text }));
|
|
31
33
|
case "button":
|
|
32
|
-
return (_jsx(StatusBarButton, { label: item.text, icon: item.icon === undefined ? undefined : statusIcons[item.icon], count: count, title: item.title ?? item.text, disabled: item.disabled, "aria-haspopup": item.opens, ...(item.expanded === undefined ? {} : { expanded: item.expanded }), onClick: (event) => {
|
|
34
|
+
return (_jsx(StatusBarButton, { label: item.text, icon: item.icon === undefined ? undefined : statusIcons[item.icon], count: count, light: light, title: item.title ?? item.text, disabled: item.disabled, "aria-haspopup": item.opens, ...(item.expanded === undefined ? {} : { expanded: item.expanded }), onClick: (event) => {
|
|
33
35
|
const anchor = anchorFor(event.currentTarget);
|
|
34
36
|
onAction(anchor === undefined
|
|
35
37
|
? { itemId: item.id }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TourtoisAppSurfaceHost } from "./app-surface.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type AppDirectoryApp } from "./app-launch.js";
|
|
3
3
|
import type { createAppWindowManager, AppLaunchDescriptor } from "./app-windows.js";
|
|
4
4
|
/** Keep declared commands addressable before their App has mounted. */
|
|
5
5
|
export declare function useDirectoryCommands({ host, directory, manager, launchFor, onFocus, onError, }: {
|
|
@@ -1,26 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return Promise.resolve();
|
|
6
|
-
return new Promise((resolve, reject) => {
|
|
7
|
-
const timeout = window.setTimeout(() => {
|
|
8
|
-
unsubscribe();
|
|
9
|
-
reject(new Error("The App did not register its declared command."));
|
|
10
|
-
}, 10_000);
|
|
11
|
-
const finish = () => {
|
|
12
|
-
window.clearTimeout(timeout);
|
|
13
|
-
unsubscribe();
|
|
14
|
-
resolve();
|
|
15
|
-
};
|
|
16
|
-
const check = () => {
|
|
17
|
-
if (isReady())
|
|
18
|
-
finish();
|
|
19
|
-
};
|
|
20
|
-
const unsubscribe = host.subscribe(check);
|
|
21
|
-
check();
|
|
22
|
-
});
|
|
23
|
-
}
|
|
2
|
+
import { waitForSurfaceCommand } from "./app-launch.js";
|
|
3
|
+
import { appSurfaceCommandKey } from "@fungi.computer/nacre/command-host";
|
|
4
|
+
import { sameWhistleCommand } from "@fungi.computer/whistle";
|
|
24
5
|
/** Keep declared commands addressable before their App has mounted. */
|
|
25
6
|
export function useDirectoryCommands({ host, directory, manager, launchFor, onFocus, onError, }) {
|
|
26
7
|
const advertisedCommands = useMemo(() => directory.flatMap((installation) => installation.commands.map((command) => ({
|
|
@@ -56,9 +37,15 @@ export function useDirectoryCommands({ host, directory, manager, launchFor, onFo
|
|
|
56
37
|
try {
|
|
57
38
|
await context.manager.open(launch);
|
|
58
39
|
context.onFocus(launch.windowId);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
40
|
+
const key = appSurfaceCommandKey({ surfaceId, commandId });
|
|
41
|
+
const registered = () => host
|
|
42
|
+
.active()
|
|
43
|
+
.palette.find((row) => sameWhistleCommand(row.command, key));
|
|
44
|
+
await waitForSurfaceCommand(host, () => registered() !== undefined);
|
|
45
|
+
const row = registered();
|
|
46
|
+
if (!row)
|
|
47
|
+
throw new Error("The App did not register its declared command.");
|
|
48
|
+
const outcome = await host.execute(row.command, {
|
|
62
49
|
origin: "palette",
|
|
63
50
|
});
|
|
64
51
|
if (outcome.status === "handled")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/tourtois",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"author": "Levi <levi@technomancy.dev>",
|
|
5
5
|
"description": "Tourtois: the trusted desktop shell for Oyster and Fungi, with host-supplied Settings sections",
|
|
6
6
|
"type": "module",
|
|
@@ -73,19 +73,19 @@
|
|
|
73
73
|
"LICENSE"
|
|
74
74
|
],
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@fungi.computer/agar": "0.
|
|
76
|
+
"@fungi.computer/agar": "0.5.0",
|
|
77
77
|
"@fungi.computer/cairn": "0.1.0",
|
|
78
|
-
"@fungi.computer/caps": "0.
|
|
78
|
+
"@fungi.computer/caps": "0.4.0",
|
|
79
79
|
"@fungi.computer/ear-tag": "0.1.0",
|
|
80
80
|
"@fungi.computer/gills": "0.2.0",
|
|
81
81
|
"@fungi.computer/maosaico": "0.1.0",
|
|
82
|
-
"@fungi.computer/parakeet": "0.
|
|
82
|
+
"@fungi.computer/parakeet": "0.5.0",
|
|
83
83
|
"@fungi.computer/puffball": "0.2.0",
|
|
84
|
-
"@fungi.computer/shiitake": "0.
|
|
84
|
+
"@fungi.computer/shiitake": "0.6.0",
|
|
85
85
|
"@fungi.computer/sigils": "0.2.0",
|
|
86
86
|
"@fungi.computer/sprite": "0.2.0",
|
|
87
87
|
"@fungi.computer/sprite-zdog": "0.2.0",
|
|
88
|
-
"@fungi.computer/whistle": "0.
|
|
88
|
+
"@fungi.computer/whistle": "0.3.0",
|
|
89
89
|
"@tanstack/history": "1.162.1",
|
|
90
90
|
"@tanstack/react-form": "1.33.2",
|
|
91
91
|
"@tanstack/react-query": "5.102.8",
|
|
@@ -96,16 +96,16 @@
|
|
|
96
96
|
"lucide-react": "0.460.0",
|
|
97
97
|
"react-dom": "18.3.1",
|
|
98
98
|
"zod": "4.4.3",
|
|
99
|
-
"@fungi.computer/nacre": "0.0
|
|
99
|
+
"@fungi.computer/nacre": "0.1.0"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
102
102
|
"react": "^18.3.1 || ^19.2.0"
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
|
-
"@fungi.computer/oyster": "0.
|
|
105
|
+
"@fungi.computer/oyster": "0.6.0",
|
|
106
106
|
"@cloudflare/workers-types": "5.20260828.1",
|
|
107
107
|
"@fungi.computer/forage": "0.2.0",
|
|
108
|
-
"@fungi.computer/mycelium": "0.2.
|
|
108
|
+
"@fungi.computer/mycelium": "0.2.3",
|
|
109
109
|
"@ladle/react": "5.1.1",
|
|
110
110
|
"@types/react": "18.3.3",
|
|
111
111
|
"@vitejs/plugin-react": "6.1.1",
|