@k2b/cloud 0.31.0 → 0.32.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/package.json +6 -4
- package/src/_internal/define-app.ts +3 -1
- package/src/ai/app-skills.ts +1 -7
- package/src/ai/audio-tool.ts +2 -1
- package/src/ai/browser.ts +109 -0
- package/src/ai/capabilities.ts +6 -2
- package/src/ai/chart-block.ts +431 -81
- package/src/ai/chat/action-sentences.ts +13 -7
- package/src/ai/chat/blocks.tsx +11 -7
- package/src/ai/chat/builtin-tools.tsx +9 -7
- package/src/ai/chat/chart-block.tsx +218 -44
- package/src/ai/chat/explorer-block.tsx +276 -0
- package/src/ai/chat/file-tools.tsx +40 -1
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/message-utils.ts +7 -0
- package/src/ai/chat/messages.ts +16 -0
- package/src/ai/chat/presentation.tsx +13 -3
- package/src/ai/chat/reference-text.tsx +34 -0
- package/src/ai/chat/tool-groups.ts +1 -0
- package/src/ai/chat/tool-names.ts +14 -0
- package/src/ai/chat/turn-view.tsx +17 -7
- package/src/ai/chat/view-block.browser-harness.tsx +35 -0
- package/src/ai/chat/view-block.tsx +417 -0
- package/src/ai/chat/visual-tools.tsx +32 -53
- package/src/ai/client-tool-results.ts +9 -0
- package/src/ai/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +87 -11
- package/src/ai/executor.ts +173 -31
- package/src/ai/file-error.ts +2 -0
- package/src/ai/file-tools.ts +27 -7
- package/src/ai/files-store.ts +60 -0
- package/src/ai/html-pdf-tool.ts +3 -2
- package/src/ai/http.ts +37 -12
- package/src/ai/index.ts +90 -0
- package/src/ai/local-tool-contracts.ts +244 -0
- package/src/ai/malformed-tool-call-repair.ts +144 -0
- package/src/ai/markdown-pdf-tool.ts +2 -1
- package/src/ai/migrate.ts +7 -0
- package/src/ai/openai-api-messages.ts +53 -0
- package/src/ai/openai-api-slots.ts +40 -0
- package/src/ai/openai-api.ts +383 -0
- package/src/ai/protocol.ts +6 -2
- package/src/ai/provider.ts +16 -4
- package/src/ai/request-context.ts +33 -0
- package/src/ai/routes.ts +96 -57
- package/src/ai/runtime.ts +48 -12
- package/src/ai/settings.ts +5 -0
- package/src/ai/skill-catalog.ts +20 -5
- package/src/ai/skill-format.ts +17 -7
- package/src/ai/skill-tool.ts +15 -5
- package/src/ai/skills-routes.ts +20 -6
- package/src/ai/skills.ts +26 -7
- package/src/ai/store.ts +25 -0
- package/src/ai/system-prompt.ts +69 -10
- package/src/ai/timeline.ts +3 -1
- package/src/ai/tools.ts +3 -0
- package/src/ai/types.ts +9 -0
- package/src/ai/view-block.ts +941 -0
- package/src/ai/vision-tool.ts +2 -1
- package/src/api/admin-identity.ts +6 -1
- package/src/api/app-approval-actions.ts +36 -0
- package/src/api/app-approval.ts +10 -2
- package/src/api/approvals-loader.ts +15 -0
- package/src/api/approvals.ts +120 -0
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +12 -3
- package/src/approvals/Approvals.browser-harness.tsx +16 -0
- package/src/approvals/Approvals.tsx +621 -0
- package/src/approvals/ApprovalsNavStatus.tsx +20 -0
- package/src/approvals/messages.ts +95 -0
- package/src/approvals/ui.ts +8 -0
- package/src/browser/app-approval.ts +32 -0
- package/src/browser/choose-files.tsx +26 -13
- package/src/browser/commands.ts +13 -4
- package/src/browser/file-provider-error.ts +11 -0
- package/src/browser/file-providers.ts +3 -12
- package/src/browser/files.tsx +1 -2
- package/src/browser/live-connection.ts +2 -3
- package/src/browser/provider-list.ts +2 -1
- package/src/browser/save-files.tsx +13 -2
- package/src/cli/account.ts +21 -2
- package/src/cli/commands.ts +4 -2
- package/src/contracts/app-approval.ts +120 -0
- package/src/contracts/approval-paths.ts +4 -0
- package/src/contracts/approvals.ts +81 -0
- package/src/contracts/command-path.ts +17 -0
- package/src/contracts/commands.ts +5 -15
- package/src/contracts/shared.ts +5 -0
- package/src/events/live-protocol.ts +2 -1
- package/src/events/live-server-message.ts +40 -0
- package/src/events/testing.ts +88 -0
- package/src/server/index.ts +2 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/request-timeout.ts +22 -0
- package/src/server/services/access.ts +4 -3
- package/src/services/app-approval.ts +103 -15
- package/src/services/approvals/cursor.ts +15 -0
- package/src/services/approvals/device.ts +112 -0
- package/src/services/approvals/index.ts +27 -11
- package/src/services/approvals/list.ts +120 -0
- package/src/services/approvals/migrate.ts +9 -1
- package/src/services/identity/models-only-auth.ts +25 -0
- package/src/services/notifications/email-frame.ts +16 -3
- package/src/services/service-account-credentials.ts +22 -10
- package/src/services/settings/core-settings.ts +27 -0
- package/src/shared/ai-platform-prompt.ts +26 -1
- package/src/shared/email-html.ts +4 -0
- package/src/shared/index.ts +6 -1
- package/src/shared/markdown/extensions/code.ts +14 -0
- package/src/shared/markdown/index.ts +4 -4
- package/src/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/page-trail.browser-harness.tsx +52 -3
- package/src/ssr/rail-navigation.ts +5 -3
- package/src/ssr/runtime.ts +6 -1
- package/src/styles/approvals.css +390 -0
- package/src/styles/global.css +5 -2
package/src/shared/email-html.ts
CHANGED
|
@@ -2,6 +2,7 @@ import sanitizeHtml from "sanitize-html";
|
|
|
2
2
|
|
|
3
3
|
export const EMAIL_HTML_TAGS = [
|
|
4
4
|
"a",
|
|
5
|
+
"blockquote",
|
|
5
6
|
"br",
|
|
6
7
|
"code",
|
|
7
8
|
"div",
|
|
@@ -12,6 +13,7 @@ export const EMAIL_HTML_TAGS = [
|
|
|
12
13
|
"li",
|
|
13
14
|
"ol",
|
|
14
15
|
"p",
|
|
16
|
+
"pre",
|
|
15
17
|
"span",
|
|
16
18
|
"strong",
|
|
17
19
|
"table",
|
|
@@ -26,6 +28,7 @@ export const EMAIL_HTML_TAGS = [
|
|
|
26
28
|
|
|
27
29
|
export const EMAIL_HTML_ALLOWED_ATTRIBUTES: Record<string, string[]> = {
|
|
28
30
|
a: ["href", "target", "style"],
|
|
31
|
+
blockquote: ["style"],
|
|
29
32
|
div: ["style"],
|
|
30
33
|
h1: ["style"],
|
|
31
34
|
h2: ["style"],
|
|
@@ -33,6 +36,7 @@ export const EMAIL_HTML_ALLOWED_ATTRIBUTES: Record<string, string[]> = {
|
|
|
33
36
|
li: ["style"],
|
|
34
37
|
ol: ["style"],
|
|
35
38
|
p: ["style"],
|
|
39
|
+
pre: ["style"],
|
|
36
40
|
span: ["style"],
|
|
37
41
|
table: ["style", "cellpadding", "cellspacing", "width", "align", "border"],
|
|
38
42
|
tbody: ["style"],
|
package/src/shared/index.ts
CHANGED
|
@@ -13,7 +13,12 @@ export * from "./account-display";
|
|
|
13
13
|
export * from "./account-session";
|
|
14
14
|
export * from "./ai-costs";
|
|
15
15
|
export type { AiPromptContextInput, AiToolPromptHint } from "./ai-platform-prompt";
|
|
16
|
-
export {
|
|
16
|
+
export {
|
|
17
|
+
AI_LOCAL_CLIENT_DEFAULT_INSTRUCTIONS,
|
|
18
|
+
AI_PLATFORM_PROMPT_TEMPLATE,
|
|
19
|
+
aiPromptContext,
|
|
20
|
+
renderAiPlatformPrompt,
|
|
21
|
+
} from "./ai-platform-prompt";
|
|
17
22
|
export type { AiChatQuotaSnapshot } from "./ai-quotas";
|
|
18
23
|
export * from "./ai-quotas";
|
|
19
24
|
export {
|
|
@@ -55,6 +55,20 @@ const renderCode = (source: string, language?: string): string => {
|
|
|
55
55
|
return highlighter ? highlighter(source) : escapeHtml(source);
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The label and diagram container need Cloud's styles and scripts; highlight spans only add to the email
|
|
60
|
+
* element budget. A bare `pre` lets the email design style blocks with `pre` and reserve `code` for inline code.
|
|
61
|
+
*/
|
|
62
|
+
export function plainCodeExtension(): MarkedExtension {
|
|
63
|
+
return {
|
|
64
|
+
renderer: {
|
|
65
|
+
code(token: Tokens.Code): string {
|
|
66
|
+
return `<pre>${escapeHtml(token.text)}</pre>\n`;
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
export function codeExtension(): MarkedExtension {
|
|
59
73
|
return {
|
|
60
74
|
renderer: {
|
|
@@ -9,7 +9,7 @@ import { markdownInfoBlocks } from "@k2b/ui";
|
|
|
9
9
|
import { Marked, type MarkedExtension } from "marked";
|
|
10
10
|
import sanitizeHtml from "sanitize-html";
|
|
11
11
|
import { markdownClient } from "./client";
|
|
12
|
-
import { codeExtension } from "./extensions/code";
|
|
12
|
+
import { codeExtension, plainCodeExtension } from "./extensions/code";
|
|
13
13
|
import { guidedHelpExtension } from "./extensions/guided-help";
|
|
14
14
|
import { imagesExtension } from "./extensions/images";
|
|
15
15
|
import { katexExtension } from "./extensions/katex";
|
|
@@ -36,12 +36,12 @@ const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: Ma
|
|
|
36
36
|
marked.use(infoBlocks);
|
|
37
37
|
marked.use(taskListExtension());
|
|
38
38
|
marked.use(tablesExtension());
|
|
39
|
-
// Plain
|
|
39
|
+
// Plain output keeps marked's own link renderer and renders code blocks as a bare `pre`.
|
|
40
40
|
// Cloud links are calm reference pills and quiet web links from `@k2b/ui`.
|
|
41
41
|
if (links === "widget") marked.use(linksExtension({ internalTarget: profile === "help" ? "_self" : "_blank", locale }));
|
|
42
42
|
marked.use(imagesExtension());
|
|
43
43
|
marked.use(katexExtension());
|
|
44
|
-
marked.use(codeExtension());
|
|
44
|
+
marked.use(links === "widget" ? codeExtension() : plainCodeExtension());
|
|
45
45
|
// Inline-style decorators come last so they run after structural tokenizers.
|
|
46
46
|
marked.use(markExtension());
|
|
47
47
|
marked.use(subSupExtension());
|
|
@@ -67,7 +67,7 @@ const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale
|
|
|
67
67
|
const marked = markedFor("content");
|
|
68
68
|
|
|
69
69
|
export type MarkdownRenderOptions = {
|
|
70
|
-
/** `"plain"` renders each link as an ordinary anchor around its text, for HTML read outside Cloud such as email. The default renders references to Cloud content as calm pills and web links as quiet text links. */
|
|
70
|
+
/** `"plain"` renders each link as an ordinary anchor around its text, for HTML read outside Cloud such as email. It also renders a code block, fenced or indented and including a Mermaid fence, as its source in a `pre` without the language label. The default renders references to Cloud content as calm pills and web links as quiet text links. */
|
|
71
71
|
links?: LinkStyle;
|
|
72
72
|
/** Request locale for the screen-reader names of untitled info blocks. Defaults to English on the server and the page language in the browser. */
|
|
73
73
|
locale?: string;
|
package/src/ssr/AppBadge.tsx
CHANGED
|
@@ -4,10 +4,10 @@ import { appBadgeCount } from "./app-badges";
|
|
|
4
4
|
import { railMessages } from "./rail-messages";
|
|
5
5
|
|
|
6
6
|
/** What a screen reader hears after the app name; empty without a count. Says no more than the "99+" shown. */
|
|
7
|
-
export const useAppBadgeDescription = (
|
|
7
|
+
export const useAppBadgeDescription = (input: () => { endpoint?: string; appId?: string }) => {
|
|
8
8
|
const locale = useLocale();
|
|
9
9
|
return () => {
|
|
10
|
-
const count = appBadgeCount(
|
|
10
|
+
const count = appBadgeCount(input());
|
|
11
11
|
const t = railMessages.resolve([locale()]).t;
|
|
12
12
|
return count > 99 ? t.badgeMany : count > 0 ? t.badge({ count }) : "";
|
|
13
13
|
};
|
|
@@ -18,8 +18,8 @@ export const useAppBadgeDescription = (endpoint: () => string | undefined) => {
|
|
|
18
18
|
* takes layout space, so appearing, growing to "99+", or clearing moves
|
|
19
19
|
* nothing. Decorative: the link carries the description.
|
|
20
20
|
*/
|
|
21
|
-
export function AppBadge(props: { endpoint?: string }) {
|
|
22
|
-
const count = () => appBadgeCount(props
|
|
21
|
+
export function AppBadge(props: { endpoint?: string; appId?: string }) {
|
|
22
|
+
const count = () => appBadgeCount(props);
|
|
23
23
|
return (
|
|
24
24
|
<Show when={count() > 0}>
|
|
25
25
|
<span class="cloud-app-badge" aria-hidden="true">
|
|
@@ -132,7 +132,7 @@ export const AppLaunchpadPanel = (
|
|
|
132
132
|
<div class="flex flex-wrap justify-center gap-3">
|
|
133
133
|
<For each={shortcuts()}>
|
|
134
134
|
{(shortcut) => {
|
|
135
|
-
const badge = useAppBadgeDescription(() => shortcut.badge);
|
|
135
|
+
const badge = useAppBadgeDescription(() => ({ endpoint: shortcut.badge, appId: shortcut.appId }));
|
|
136
136
|
return (
|
|
137
137
|
<a
|
|
138
138
|
onClick={follow}
|
|
@@ -141,7 +141,7 @@ export const AppLaunchpadPanel = (
|
|
|
141
141
|
>
|
|
142
142
|
<span class="relative grid h-9 w-9 place-items-center rounded-xl bg-[var(--ui-hover)] text-lg transition-colors group-hover:bg-[var(--ui-active)]">
|
|
143
143
|
<i class={shortcut.iconClass} aria-hidden="true" />
|
|
144
|
-
<AppBadge endpoint={shortcut.badge} />
|
|
144
|
+
<AppBadge endpoint={shortcut.badge} appId={shortcut.appId} />
|
|
145
145
|
</span>
|
|
146
146
|
<span class="max-w-full truncate text-[11px] font-medium text-primary dark:text-white">
|
|
147
147
|
{shortcut.label}
|
|
@@ -159,7 +159,7 @@ export const AppLaunchpadPanel = (
|
|
|
159
159
|
<div class="launchpad-apps-grid flex flex-wrap justify-center gap-x-4 gap-y-4 sm:gap-x-7 sm:gap-y-6">
|
|
160
160
|
<For each={apps()}>
|
|
161
161
|
{(app) => {
|
|
162
|
-
const badge = useAppBadgeDescription(() => app.badge);
|
|
162
|
+
const badge = useAppBadgeDescription(() => ({ endpoint: app.badge, appId: app.id }));
|
|
163
163
|
return (
|
|
164
164
|
<a
|
|
165
165
|
onClick={follow}
|
|
@@ -171,7 +171,7 @@ export const AppLaunchpadPanel = (
|
|
|
171
171
|
style={appIconStyle(app)}
|
|
172
172
|
>
|
|
173
173
|
<i class={app.iconClass} />
|
|
174
|
-
<AppBadge endpoint={app.badge} />
|
|
174
|
+
<AppBadge endpoint={app.badge} appId={app.id} />
|
|
175
175
|
</span>
|
|
176
176
|
<span class="max-w-full truncate text-[11px] font-medium text-primary sm:text-xs dark:text-white">
|
|
177
177
|
{app.label}
|
|
@@ -10,7 +10,7 @@ import { projectRailNavigation, type RailApp, type RailLink, railLinkActive } fr
|
|
|
10
10
|
function RailIcon(props: { link: RailLink; currentPath: string }) {
|
|
11
11
|
let target: HTMLAnchorElement | undefined;
|
|
12
12
|
const active = () => railLinkActive(props.link, props.currentPath);
|
|
13
|
-
const badge = useAppBadgeDescription(() => props.link.badge);
|
|
13
|
+
const badge = useAppBadgeDescription(() => ({ endpoint: props.link.badge, appId: props.link.appId }));
|
|
14
14
|
return (
|
|
15
15
|
<>
|
|
16
16
|
<a
|
|
@@ -23,7 +23,7 @@ function RailIcon(props: { link: RailLink; currentPath: string }) {
|
|
|
23
23
|
>
|
|
24
24
|
<i class={`${props.link.iconClass} text-base`} aria-hidden="true" />
|
|
25
25
|
<span class="sr-only">{props.link.label}</span>
|
|
26
|
-
<AppBadge endpoint={props.link.badge} />
|
|
26
|
+
<AppBadge endpoint={props.link.badge} appId={props.link.appId} />
|
|
27
27
|
</a>
|
|
28
28
|
<Tooltip target={() => target} content={props.link.label} placement="right" delay={0} />
|
|
29
29
|
</>
|
package/src/ssr/app-badges.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSignal } from "solid-js";
|
|
2
2
|
import { APP_BADGES_REFRESH_EVENT } from "../browser/app-badges";
|
|
3
|
+
import { APPROVAL_COUNTS_PATH } from "../contracts/approval-paths";
|
|
4
|
+
import type { ApprovalCounts } from "../contracts/approvals";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Shortest time between two scheduled reads in one tab, and the longest one
|
|
@@ -13,8 +15,20 @@ export const APP_BADGE_INTERVAL_MS = 60_000;
|
|
|
13
15
|
// count. Only the browser ever writes it; server renders always see no badge.
|
|
14
16
|
const [counts, setCounts] = createSignal<ReadonlyMap<string, number>>(new Map());
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
const [approvalCounts, setApprovalCounts] = createSignal<ApprovalCounts | null>(null);
|
|
19
|
+
|
|
20
|
+
/** Retain an SSR count until the first browser read; a failed read still resolves to zero. */
|
|
21
|
+
export const liveApprovalCount = (appId?: string): number | null => {
|
|
22
|
+
const current = approvalCounts();
|
|
23
|
+
if (!current) return null;
|
|
24
|
+
if (appId === undefined) return current.total;
|
|
25
|
+
return Object.hasOwn(current.apps, appId) ? (current.apps[appId] ?? 0) : 0;
|
|
26
|
+
};
|
|
27
|
+
export const approvalCount = (appId?: string): number => liveApprovalCount(appId) ?? 0;
|
|
28
|
+
|
|
29
|
+
/** Endpoint and approval counts for one app; 0 until the browser has read them. */
|
|
30
|
+
export const appBadgeCount = (input: { endpoint?: string; appId?: string }): number =>
|
|
31
|
+
(input.endpoint ? (counts().get(input.endpoint) ?? 0) : 0) + (input.appId ? approvalCount(input.appId) : 0);
|
|
18
32
|
|
|
19
33
|
/** A positive safe integer from an `AppNavBadge` body, else 0. */
|
|
20
34
|
export const parseAppBadge = (body: unknown): number => {
|
|
@@ -33,6 +47,33 @@ const readBadge = async (endpoint: string, signal: AbortSignal): Promise<number>
|
|
|
33
47
|
}
|
|
34
48
|
};
|
|
35
49
|
|
|
50
|
+
const isCount = (value: unknown, minimum: number): value is number =>
|
|
51
|
+
typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
52
|
+
|
|
53
|
+
/** What `ApprovalCountsSchema` accepts from a JSON body, without loading validators into the shell; a test compares both. */
|
|
54
|
+
export const parseApprovalCounts = (body: unknown): ApprovalCounts | null => {
|
|
55
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return null;
|
|
56
|
+
if (Object.keys(body).length !== 2 || !("total" in body) || !("apps" in body)) return null;
|
|
57
|
+
const { total, apps } = body;
|
|
58
|
+
if (!isCount(total, 0) || typeof apps !== "object" || apps === null || Array.isArray(apps)) return null;
|
|
59
|
+
// Like Zod's record, ignore a `__proto__` key that JSON.parse keeps as an own property.
|
|
60
|
+
const entries = Object.entries(apps).filter(([app]) => app !== "__proto__");
|
|
61
|
+
return entries.every(([, count]) => isCount(count, 1)) ? { total, apps: Object.fromEntries(entries) } : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const readApprovalCounts = async (signal: AbortSignal): Promise<ApprovalCounts> => {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(APPROVAL_COUNTS_PATH, { credentials: "same-origin", headers: { accept: "application/json" }, signal });
|
|
67
|
+
if (response.status === 200) {
|
|
68
|
+
const parsed = parseApprovalCounts(await response.json());
|
|
69
|
+
if (parsed) return parsed;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// A failing read clears approval counts just like an app badge.
|
|
73
|
+
}
|
|
74
|
+
return { total: 0, apps: {} };
|
|
75
|
+
};
|
|
76
|
+
|
|
36
77
|
let watching = false;
|
|
37
78
|
|
|
38
79
|
/**
|
|
@@ -45,8 +86,8 @@ let watching = false;
|
|
|
45
86
|
* read still running after the interval gives up. Returns a disposer.
|
|
46
87
|
*/
|
|
47
88
|
export const watchAppBadges = (endpoints: readonly string[]): (() => void) => {
|
|
48
|
-
const unique = [...new Set(endpoints)];
|
|
49
|
-
if (typeof window === "undefined" ||
|
|
89
|
+
const unique = [...new Set([...endpoints, APPROVAL_COUNTS_PATH])];
|
|
90
|
+
if (typeof window === "undefined" || watching) return () => {};
|
|
50
91
|
watching = true;
|
|
51
92
|
const stopped = new AbortController();
|
|
52
93
|
// Endpoints with a read in flight, mapped to whether one more read waits for it.
|
|
@@ -60,9 +101,12 @@ export const watchAppBadges = (endpoints: readonly string[]): (() => void) => {
|
|
|
60
101
|
return;
|
|
61
102
|
}
|
|
62
103
|
running.set(endpoint, false);
|
|
63
|
-
|
|
104
|
+
const signal = AbortSignal.any([stopped.signal, AbortSignal.timeout(APP_BADGE_INTERVAL_MS)]);
|
|
105
|
+
const result = endpoint === APPROVAL_COUNTS_PATH ? readApprovalCounts(signal) : readBadge(endpoint, signal);
|
|
106
|
+
void result.then((count) => {
|
|
64
107
|
if (stopped.signal.aborted) return;
|
|
65
|
-
setCounts((previous) => new Map(previous).set(endpoint, count));
|
|
108
|
+
if (typeof count === "number") setCounts((previous) => new Map(previous).set(endpoint, count));
|
|
109
|
+
else setApprovalCounts(count);
|
|
66
110
|
const queued = running.get(endpoint);
|
|
67
111
|
running.delete(endpoint);
|
|
68
112
|
if (queued && document.visibilityState !== "hidden") read(endpoint);
|
|
@@ -66,11 +66,11 @@ const Page = (props: { c: unknown; title: Title; children: JSX.Element }) => (
|
|
|
66
66
|
{props.children}
|
|
67
67
|
</Layout>
|
|
68
68
|
);
|
|
69
|
-
const AccountsWorkspace = (props: { children: JSX.Element }) => (
|
|
69
|
+
const AccountsWorkspace = (props: { children: JSX.Element; active?: boolean }) => (
|
|
70
70
|
<AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
|
|
71
71
|
<AppWorkspace.Sidebar id="accounts-navigation" label="Accounts">
|
|
72
72
|
<AppWorkspace.SidebarDesktop>
|
|
73
|
-
<AppWorkspace.SidebarItem href="/app/accounts/users" icon="ti ti-user" active>
|
|
73
|
+
<AppWorkspace.SidebarItem href="/app/accounts/users" icon="ti ti-user" active={props.active ?? true}>
|
|
74
74
|
Users
|
|
75
75
|
</AppWorkspace.SidebarItem>
|
|
76
76
|
</AppWorkspace.SidebarDesktop>
|
|
@@ -134,7 +134,8 @@ export const server = new Hono()
|
|
|
134
134
|
const uid = c.req.param("uid") ?? "";
|
|
135
135
|
return () => (
|
|
136
136
|
<Page c={c} title={[start, accountsRoot, { title: "Users", href: "/app/accounts/users" }, { title: uid }]}>
|
|
137
|
-
<AccountsWorkspace>
|
|
137
|
+
<AccountsWorkspace active={false}>
|
|
138
|
+
<h1 class="p-3">User Seven</h1>
|
|
138
139
|
<p class="p-3">Profile of {uid}</p>
|
|
139
140
|
<input aria-label="Note" class="m-3" />
|
|
140
141
|
</AccountsWorkspace>
|
|
@@ -142,6 +143,54 @@ export const server = new Hono()
|
|
|
142
143
|
);
|
|
143
144
|
}),
|
|
144
145
|
)
|
|
146
|
+
.get(
|
|
147
|
+
"/app/accounts/teams",
|
|
148
|
+
...probe.ssr((c) => () => (
|
|
149
|
+
<Page c={c} title={[start, accountsRoot, { title: "Directory", href: "/app/accounts/directory" }, { title: "Teams" }]}>
|
|
150
|
+
<AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
|
|
151
|
+
<AppWorkspace.Sidebar label="Directory">
|
|
152
|
+
<AppWorkspace.SidebarDesktop>
|
|
153
|
+
<AppWorkspace.SidebarItem href="/app/accounts/teams" active>
|
|
154
|
+
Teams
|
|
155
|
+
</AppWorkspace.SidebarItem>
|
|
156
|
+
</AppWorkspace.SidebarDesktop>
|
|
157
|
+
</AppWorkspace.Sidebar>
|
|
158
|
+
<AppWorkspace.Content>
|
|
159
|
+
<AppWorkspace.Main>
|
|
160
|
+
<input aria-label="Search teams" placeholder="Search Teams…" />
|
|
161
|
+
</AppWorkspace.Main>
|
|
162
|
+
</AppWorkspace.Content>
|
|
163
|
+
</AppWorkspace>
|
|
164
|
+
</Page>
|
|
165
|
+
)),
|
|
166
|
+
)
|
|
167
|
+
.get(
|
|
168
|
+
"/app/accounts/teams/engineering",
|
|
169
|
+
...probe.ssr((c) => () => (
|
|
170
|
+
<Page c={c} title={[start, accountsRoot, { title: "Teams", href: "/app/accounts/teams" }, { title: "Engineering" }]}>
|
|
171
|
+
<AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
|
|
172
|
+
<AppWorkspace.Sidebar label="Directory">
|
|
173
|
+
<AppWorkspace.SidebarDesktop>
|
|
174
|
+
<AppWorkspace.SidebarItem href="/app/accounts/teams" active="ancestor">
|
|
175
|
+
Teams
|
|
176
|
+
</AppWorkspace.SidebarItem>
|
|
177
|
+
</AppWorkspace.SidebarDesktop>
|
|
178
|
+
</AppWorkspace.Sidebar>
|
|
179
|
+
<AppWorkspace.Content>
|
|
180
|
+
<AppWorkspace.Main>Team members</AppWorkspace.Main>
|
|
181
|
+
</AppWorkspace.Content>
|
|
182
|
+
</AppWorkspace>
|
|
183
|
+
</Page>
|
|
184
|
+
)),
|
|
185
|
+
)
|
|
186
|
+
.get(
|
|
187
|
+
"/app/accounts/directory",
|
|
188
|
+
...probe.ssr((c) => () => (
|
|
189
|
+
<Page c={c} title={[{ title: "Directory" }]}>
|
|
190
|
+
<h1>Directory</h1>
|
|
191
|
+
</Page>
|
|
192
|
+
)),
|
|
193
|
+
)
|
|
145
194
|
.get(
|
|
146
195
|
"/app/files",
|
|
147
196
|
...probe.ssr((c) => {
|
|
@@ -12,7 +12,7 @@ export type RailApp = {
|
|
|
12
12
|
/** The app's `nav.badge` endpoint. */
|
|
13
13
|
badge?: string;
|
|
14
14
|
};
|
|
15
|
-
export type RailLink = Omit<RailApp, "defaultVisible"> & { exact?: boolean };
|
|
15
|
+
export type RailLink = Omit<RailApp, "defaultVisible"> & { appId?: string; exact?: boolean };
|
|
16
16
|
|
|
17
17
|
export const isRailAppVisible = (app: RailApp, settings: RailPreferences): boolean =>
|
|
18
18
|
Object.hasOwn(settings.visibility, app.id) ? (settings.visibility[app.id] ?? app.defaultVisible) : app.defaultVisible;
|
|
@@ -32,7 +32,7 @@ export const projectRailNavigation = (apps: readonly RailApp[], settings: RailSn
|
|
|
32
32
|
const app = catalog.get(shortcut.appId);
|
|
33
33
|
if (!app || pinned.has(app.id)) continue;
|
|
34
34
|
pinned.add(app.id);
|
|
35
|
-
shortcuts.push({ ...app, id: shortcut.id });
|
|
35
|
+
shortcuts.push({ ...app, id: shortcut.id, appId: app.id });
|
|
36
36
|
} else {
|
|
37
37
|
shortcuts.push({
|
|
38
38
|
id: shortcut.id,
|
|
@@ -46,7 +46,9 @@ export const projectRailNavigation = (apps: readonly RailApp[], settings: RailSn
|
|
|
46
46
|
}
|
|
47
47
|
return {
|
|
48
48
|
shortcuts,
|
|
49
|
-
apps: sortRailApps(apps, locale)
|
|
49
|
+
apps: sortRailApps(apps, locale)
|
|
50
|
+
.filter((app) => !pinned.has(app.id) && isRailAppVisible(app, settings))
|
|
51
|
+
.map((app) => ({ ...app, appId: app.id })),
|
|
50
52
|
};
|
|
51
53
|
};
|
|
52
54
|
|
package/src/ssr/runtime.ts
CHANGED
|
@@ -36,9 +36,14 @@ export const localizedAppName = (
|
|
|
36
36
|
appId: string,
|
|
37
37
|
fallback: string,
|
|
38
38
|
): string => {
|
|
39
|
+
return appNameIn(carrier, appId, getLocale(carrier as Parameters<typeof getLocale>[0]), fallback);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** An app's registered name in the locale of a recorded request. */
|
|
43
|
+
export const appNameIn = (carrier: RuntimeCarrier, appId: string, locale: string, fallback: string): string => {
|
|
39
44
|
const runtime = carrier.get("runtime");
|
|
40
45
|
const apps =
|
|
41
46
|
runtime && typeof runtime === "object" && Array.isArray((runtime as RuntimeContext).apps) ? (runtime as RuntimeContext).apps : [];
|
|
42
47
|
const app = apps.find((entry) => entry.id === appId);
|
|
43
|
-
return app ? resolveAppPresentation(app,
|
|
48
|
+
return app ? resolveAppPresentation(app, locale).name : fallback;
|
|
44
49
|
};
|