@k2b/cloud 0.40.0 → 0.41.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.
Files changed (75) hide show
  1. package/package.json +6 -3
  2. package/src/_internal/capability-sentences.ts +2 -1
  3. package/src/_internal/define-app.ts +13 -3
  4. package/src/_internal/heartbeat.ts +203 -14
  5. package/src/_internal/registry.ts +28 -1
  6. package/src/_internal/setup-lock.ts +103 -0
  7. package/src/ai/chat/action-card.ts +69 -3
  8. package/src/ai/chat/action-question.tsx +12 -1
  9. package/src/ai/chat/action-text.ts +288 -0
  10. package/src/ai/chat/tool-action-words.ts +31 -0
  11. package/src/ai/chat/tool-action.tsx +1 -26
  12. package/src/ai/executor.ts +10 -2
  13. package/src/ai/firecrawl-tools.ts +15 -2
  14. package/src/ai/index.ts +1 -0
  15. package/src/ai/migrate.ts +15 -0
  16. package/src/ai/open-questions.ts +172 -0
  17. package/src/ai/question-text.ts +8 -0
  18. package/src/ai/routes.ts +51 -5
  19. package/src/ai/store.ts +18 -0
  20. package/src/ai/testing.ts +36 -0
  21. package/src/ai/tool-sentences.ts +152 -34
  22. package/src/ai/tool-wording.ts +12 -6
  23. package/src/ai/tools.ts +8 -3
  24. package/src/ai/types.ts +16 -2
  25. package/src/api/app-approval-actions.ts +31 -7
  26. package/src/api/auth/schemas.ts +3 -0
  27. package/src/api/auth.ts +54 -18
  28. package/src/api/mcp.ts +27 -3
  29. package/src/api/me.ts +79 -3
  30. package/src/approvals/Approvals.tsx +25 -63
  31. package/src/approvals/approval-card.ts +68 -0
  32. package/src/contracts/app-approval.ts +3 -3
  33. package/src/contracts/capabilities.ts +1 -1
  34. package/src/contracts/shared.ts +3 -0
  35. package/src/server/middleware/auth.ts +4 -2
  36. package/src/services/accounts/app.ts +23 -3
  37. package/src/services/accounts/users.ts +12 -1
  38. package/src/services/approvals/device.ts +49 -33
  39. package/src/services/approvals/index.ts +1 -0
  40. package/src/services/approvals/list.ts +1 -1
  41. package/src/services/approvals/question.ts +68 -0
  42. package/src/services/audit/index.ts +1 -0
  43. package/src/services/auth-flows/ipa.ts +39 -2
  44. package/src/services/auth-flows/password-reset.ts +32 -6
  45. package/src/services/index.ts +2 -0
  46. package/src/services/pdf/markdown.ts +160 -2
  47. package/src/services/session/index.ts +203 -29
  48. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  49. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  50. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  51. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  52. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  53. package/src/ssr/HelpPanelView.island.tsx +27 -0
  54. package/src/ssr/LayoutHelp.tsx +182 -540
  55. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  56. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  57. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  58. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  59. package/src/ssr/help.ts +4 -3
  60. package/src/ssr/index.ts +1 -0
  61. package/src/ssr/islands/index.ts +3 -4
  62. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  63. package/src/ssr/layout-help-panel-document.ts +67 -0
  64. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  65. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  66. package/src/ssr/layout-help-panel-modules.ts +7 -0
  67. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  68. package/src/ssr/layout-help-panel-search.tsx +69 -0
  69. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  70. package/src/ssr/layout-help-panel-state.ts +141 -0
  71. package/src/ssr/layout-help-panel-types.ts +19 -0
  72. package/src/ssr/layout-help-views.tsx +484 -0
  73. package/src/ssr/panel-idle.ts +9 -0
  74. package/src/styles/approvals.css +6 -0
  75. package/src/styles/effects.css +15 -0
@@ -7,7 +7,8 @@ import type { User } from "../contracts/shared";
7
7
  import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
8
8
  import { appAppearanceStyle } from "./app-appearance";
9
9
  import Layout from "./Layout";
10
- import { LayoutHelpPage, type LayoutHelpPageProps, LayoutHelpPanel, type LayoutHelpPanelProps } from "./LayoutHelp";
10
+ import { LayoutHelpPage, type LayoutHelpPageProps } from "./LayoutHelp";
11
+ import { LayoutHelpPanel, type LayoutHelpPanelProps } from "./LayoutHelpPanel.server";
11
12
  import { helpArticlePartsServer } from "./layout-help-article.server";
12
13
  import PanelLayout from "./PanelLayout";
13
14
  import { readPanelContext } from "./panel-context";
@@ -34,7 +35,8 @@ export const fixtureDocument: HelpDocumentPayload = {
34
35
  // A lead, the facts chips, and a section: everything the article head reads from the article.
35
36
  html: '<p>Share a Space so other people can see and edit its tasks.</p><h2 id="share">Share</h2><div class="help-steps"><ol><li>Open Space settings.</li><li>Add a person.</li></ol></div>',
36
37
  };
37
- const initialArticle = { payload: fixtureDocument, parts: helpArticlePartsServer(fixtureDocument.html) };
38
+ const { html, ...payload } = fixtureDocument;
39
+ const initialArticle = { payload, parts: helpArticlePartsServer(html) };
38
40
  const appearance: AppAppearance = { accent: "#16a34a", background: { from: "#16a34a", to: "#0284c7" } };
39
41
  const apps: CloudRuntime["apps"] = [
40
42
  {
@@ -65,7 +67,7 @@ const app = defineApp({
65
67
  routes: ["/"],
66
68
  });
67
69
  type Environment = { Variables: { runtime: CloudRuntime; user: User } };
68
- const PropsSnapshot = (props: { value: LayoutHelpPanelProps | LayoutHelpPageProps }) => (
70
+ const PropsSnapshot = (props: { value: LayoutHelpPageProps }) => (
69
71
  <script id="help-fixture-props" type="application/json" innerHTML={JSON.stringify(props.value).replace(/</g, "\\u003c")} />
70
72
  );
71
73
  export const server = new Hono<Environment>()
@@ -98,10 +100,7 @@ export const server = new Hono<Environment>()
98
100
  };
99
101
  return () => (
100
102
  <PanelLayout c={c} title="Help">
101
- <div id="help-fixture-root" class="flex h-full min-h-0 flex-1 flex-col">
102
- <LayoutHelpPanel {...props} />
103
- </div>
104
- <PropsSnapshot value={props} />
103
+ <LayoutHelpPanel {...props} />
105
104
  </PanelLayout>
106
105
  );
107
106
  }),
@@ -0,0 +1,131 @@
1
+ import { basename, resolve } from "node:path";
2
+ import tailwind from "bun-plugin-tailwind";
3
+ import { Hono } from "hono";
4
+ import { toJSON } from "seroval";
5
+ import { createPanelRenderer, panelFixtureProps, panelIslandEntries } from "../../../../scripts/fixtures/help-panel";
6
+ import { buildIslandClosure } from "../../../../scripts/fixtures/island-closure";
7
+ import { islandIdFromFile } from "../../../ssr/src/island-id";
8
+ import { helpArticlePartsServer } from "./layout-help-article.server";
9
+
10
+ /**
11
+ * Build the real islands together, with the production mount runtime and shared ESM chunks. One module script imports
12
+ * every island at once, as @k2b/ssr's loader does, so a held island does not hold the others.
13
+ */
14
+ export async function helpPanelServerBrowserFixture() {
15
+ const root = resolve(import.meta.dir, "../../../..");
16
+ const renderer = await createPanelRenderer();
17
+ const files: Record<string, string> = {};
18
+ const entrypoints = panelIslandEntries.map((entry) => {
19
+ const id = islandIdFromFile(entry, root);
20
+ const virtual = resolve(import.meta.dir, `${id}.ts`);
21
+ files[virtual] =
22
+ `import { mount } from ${JSON.stringify(resolve(root, "packages/ssr/src/mount.ts"))}; import C from ${JSON.stringify(entry)}; mount(C, 'solid-island[data-id="${id}"]'); document.documentElement.dataset[${JSON.stringify(`mounted${id}`)}] = "true";`;
23
+ return virtual;
24
+ });
25
+ try {
26
+ const build = await buildIslandClosure(entrypoints, files);
27
+ if (!build.metafile) throw new Error("Missing browser fixture metafile");
28
+ const outputMeta = new Map(Object.entries(build.metafile.outputs).map(([path, output]) => [basename(path), output]));
29
+ const eager = new Set<string>();
30
+ const visit = (name: string) => {
31
+ if (eager.has(name)) return;
32
+ eager.add(name);
33
+ for (const edge of outputMeta.get(name)?.imports ?? []) if (edge.kind !== "dynamic-import") visit(basename(edge.path));
34
+ };
35
+ const scripts = entrypoints.map((entry) => {
36
+ const output = [...outputMeta].find(([, meta]) => meta.entryPoint && resolve(meta.entryPoint) === entry);
37
+ if (!output) throw new Error(`Missing browser island entry ${entry}`);
38
+ visit(output[0]);
39
+ return output[0];
40
+ });
41
+ const outputs = new Map(await Promise.all(build.outputs.map(async (output) => [basename(output.path), await output.text()] as const)));
42
+ const dynamicChunks = [...outputMeta.keys()].filter((name) => !eager.has(name));
43
+ const chunkFor = (suffix: string) => {
44
+ const found = [...outputMeta]
45
+ .filter(([, meta]) => Object.entries(meta.inputs).some(([input, details]) => input.endsWith(suffix) && details.bytesInOutput > 0))
46
+ .map(([name]) => name);
47
+ if (!found.length) throw new Error(`Missing chunk for ${suffix}`);
48
+ return found;
49
+ };
50
+ const styles = await Bun.build({ entrypoints: [resolve(root, "styles.css")], plugins: [tailwind] });
51
+ if (!styles.success) throw new AggregateError(styles.logs, "Could not compile panel styles");
52
+ const css = await styles.outputs[0]!.text();
53
+ const initialArticle = {
54
+ payload: {
55
+ id: "share",
56
+ title: "Share a Space",
57
+ locale: "en",
58
+ kind: "task" as const,
59
+ needs: "edit" as const,
60
+ markdown: "Share a Space.\n\n## Invite\n\nInvite people.",
61
+ related: [],
62
+ },
63
+ parts: helpArticlePartsServer(
64
+ '<p>Share a Space.</p><h2 id="invite">Invite</h2><p>Invite people.</p><a href="/help/apps/spaces/roles">Roles</a>',
65
+ ),
66
+ };
67
+ const runtimeId = islandIdFromFile(panelIslandEntries.at(-1)!, root);
68
+ const runtimeProps = JSON.stringify(toJSON({ provider: "fixture", scope: "/panel" }))
69
+ .replaceAll("&", "&amp;")
70
+ .replaceAll('"', "&quot;");
71
+ const app = new Hono()
72
+ // Split chunks import each other by relative paths from the build's output layout; serve every file by name.
73
+ .get("/:path{.+\\.js}", (c) => {
74
+ const content = outputs.get(basename(c.req.path));
75
+ return content === undefined ? c.notFound() : c.body(content, 200, { "content-type": "text/javascript" });
76
+ })
77
+ .get("/", (c) =>
78
+ c.html(
79
+ `<!doctype html><html lang="en"${themeClass(c.req.query("theme"))}><head><style>${css}</style></head><body class="k2b-ui" style="margin:0"><div class="cloud-app-canvas" data-panel="open" data-panel-sheet="open" style="position:relative;display:flex;height:100dvh"><main data-cloud-app-area style="flex:1">Host app</main><aside class="cloud-panel" data-state="ready"><div class="cloud-panel__card"><header class="cloud-panel__head">Help</header><div class="cloud-panel__body"><iframe class="cloud-panel__frame" title="Help" src="/panel?${new URLSearchParams({ ...(c.req.query("topic") ? { topic: "share" } : {}), theme: c.req.query("theme") ?? "light" })}"></iframe></div></div></aside></div></body></html>`,
80
+ ),
81
+ )
82
+ .get("/panel", (c) =>
83
+ c.html(
84
+ `<!doctype html><html lang="en"${themeClass(c.req.query("theme"))}><head><style>${css}</style><style>html,body{margin:0;height:100%}*,*::before,*::after{animation:none!important;transition:none!important}</style></head><body class="k2b-ui"><div class="cloud-panel-page" data-cloud-panel-page><script id="cloud-panel-context" type="application/json">{"v":1,"seq":0,"pageId":"22222222-2222-4222-8222-222222222222","app":"spaces","help":["share"]}</script><solid-island data-id="${runtimeId}" data-props="${runtimeProps}"></solid-island>${renderer.render({ ...panelFixtureProps, ...(c.req.query("topic") ? { initialTopic: "share", initialArticle } : {}) })}</div><script>document.documentElement.dataset.serverGeometry=JSON.stringify((${panelGeometry.toString()})());</script><script type="module">${JSON.stringify(scripts)}.forEach((file)=>import("/assets/"+file))</script></body></html>`,
85
+ ),
86
+ )
87
+ .get("/api/help/search", (c) => c.json({ ids: ["share"] }))
88
+ .get("/api/help/:id", (c) =>
89
+ c.json({
90
+ ...initialArticle.payload,
91
+ id: c.req.param("id"),
92
+ html: '<p>Fixture.</p><h2 id="invite">Invite</h2><p>Invite people.</p>',
93
+ }),
94
+ );
95
+ const server = Bun.serve({ port: 0, fetch: app.fetch });
96
+ return {
97
+ origin: `http://localhost:${server.port}`,
98
+ dynamicChunks,
99
+ searchChunks: chunkFor("/layout-help-panel-search.tsx"),
100
+ menuChunks: chunkFor("/layout-help-panel-menu.tsx"),
101
+ islandChunks: new Map(panelIslandEntries.map((entry, index) => [basename(entry), scripts[index]!])),
102
+ ids: panelIslandEntries.map((entry) => islandIdFromFile(entry, root)),
103
+ cleanup: () => {
104
+ server.stop(true);
105
+ renderer.cleanup();
106
+ },
107
+ };
108
+ } catch (error) {
109
+ renderer.cleanup();
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /** Cloud's theme script sets `dark` on the root; the fixture renders that class on the server for both documents. */
115
+ const themeClass = (theme: string | undefined) => (theme === "dark" ? ' class="dark"' : "");
116
+
117
+ /** First-frame geometry is the assertion subject, so callers also take it while action chunks are held. */
118
+ export function panelGeometry() {
119
+ return [
120
+ "[data-help-head]",
121
+ "[data-help-search]",
122
+ "[data-help-results]",
123
+ "[data-help-tiles] button",
124
+ "[data-help-server-article] [data-help-article-head]",
125
+ ].map((selector) => {
126
+ const element = document.querySelector<HTMLElement>(selector);
127
+ if (!element || element.closest("[hidden]")) return null;
128
+ const rect = element.getBoundingClientRect();
129
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
130
+ });
131
+ }
@@ -0,0 +1,115 @@
1
+ import { MarkdownView, ScrollArea, TableOfContents, useLocale } from "@k2b/ui";
2
+ import { Show } from "solid-js";
3
+ import HelpPanelArticle from "./HelpPanelArticle.island";
4
+ import HelpPanelBack from "./HelpPanelBack.island";
5
+ import HelpPanelMenu from "./HelpPanelMenu.island";
6
+ import HelpPanelSearch from "./HelpPanelSearch.island";
7
+ import HelpPanelShortcuts from "./HelpPanelShortcuts.island";
8
+ import HelpPanelView from "./HelpPanelView.island";
9
+ import { helpMessages } from "./help-messages";
10
+ import { HelpPanelSectionTrigger } from "./layout-help-panel-controls";
11
+ import type { HelpPanelData } from "./layout-help-panel-state";
12
+ import type { LayoutHelpPanelProps } from "./layout-help-panel-types";
13
+ import { helpStart } from "./layout-help-start";
14
+ import { HelpArticleView, HelpHead, HelpNotAvailableView, HelpStartView } from "./layout-help-views";
15
+
16
+ export type { LayoutHelpPanelProps } from "./layout-help-panel-types";
17
+
18
+ /** Static provider content survives island mounting; only the controls own browser state. */
19
+ export function LayoutHelpPanel(props: LayoutHelpPanelProps) {
20
+ const locale = useLocale();
21
+ const t = () => helpMessages.resolve([locale()]).t;
22
+ const topic = props.documents.find((topic) => topic.id === props.initialTopic);
23
+ const article = topic && props.initialArticle?.payload.id === topic.id ? props.initialArticle : undefined;
24
+ const start = helpStart(props.documents, props.view);
25
+ const data: HelpPanelData = {
26
+ // The legacy reader searched and copied its topics in start order; keep that order for every manifest.
27
+ documents: [...props.documents]
28
+ .sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title))
29
+ .map(({ id, title, description, icon, kind, order, url, searchUrl, locale }) => ({
30
+ id,
31
+ title,
32
+ description,
33
+ icon,
34
+ kind,
35
+ order,
36
+ url,
37
+ searchUrl,
38
+ locale,
39
+ })),
40
+ appId: props.app?.id,
41
+ pageBase: props.pageBase,
42
+ article: article?.payload,
43
+ };
44
+ return (
45
+ <div
46
+ class="help-shell app-accent-scope flex h-full min-h-0 flex-col"
47
+ data-surface="panel"
48
+ style={props.appearance}
49
+ data-help-initial-topic={topic?.id}
50
+ >
51
+ <Show when={props.state !== "unavailable"}>
52
+ <HelpHead
53
+ row
54
+ back={!!topic}
55
+ tile={props.app?.icon ?? "ti ti-help-circle"}
56
+ title={props.app?.name ?? t().help}
57
+ lead={<HelpPanelBack back={!!topic} tile={props.app?.icon ?? "ti ti-help-circle"} />}
58
+ menu={<HelpPanelMenu />}
59
+ closable={false}
60
+ onBack={() => {}}
61
+ onClose={() => {}}
62
+ query=""
63
+ onQuery={() => {}}
64
+ searchRef={() => {}}
65
+ />
66
+ </Show>
67
+ <ScrollArea class="min-h-0 flex-1 p-4 sm:p-5">
68
+ <Show when={props.state !== "unavailable"} fallback={<HelpNotAvailableView />}>
69
+ <div class="help-start help-measure" data-help-results="" hidden={!!topic}>
70
+ <div data-help-start-content="">
71
+ <HelpPanelView topics={start.view} />
72
+ <HelpStartView
73
+ start={{ ...start, view: [] }}
74
+ empty={props.documents.length === 0}
75
+ failed={props.state === "failed"}
76
+ onOpen={() => {}}
77
+ onReload={() => {}}
78
+ shortcuts={<HelpPanelShortcuts appName={props.app?.name} platform={props.platform} />}
79
+ />
80
+ </div>
81
+ <HelpPanelSearch />
82
+ </div>
83
+ <Show when={article && topic}>
84
+ {article && topic && (
85
+ <article class="help-article" data-surface="panel" data-help-server-article="">
86
+ <HelpArticleView
87
+ surface="panel"
88
+ topic={topic}
89
+ appName={props.app?.name}
90
+ appId={props.app?.id}
91
+ parts={article.parts}
92
+ document={{ ...article.payload, html: "" }}
93
+ Markdown={MarkdownView}
94
+ showTools={article.parts.sections.length > 0}
95
+ sectionMenu={article.parts.sections.length ? <HelpPanelSectionTrigger label={t().onThisPage} /> : null}
96
+ copyButton={null}
97
+ loading={null}
98
+ failure={null}
99
+ toc={<TableOfContents items={article.parts.sections} title={t().onThisPage} />}
100
+ copyAction={null}
101
+ headingRef={() => {}}
102
+ contentRef={() => {}}
103
+ onBack={() => {}}
104
+ />
105
+ </article>
106
+ )}
107
+ </Show>
108
+ <HelpPanelArticle topic={article ? undefined : topic} />
109
+ </Show>
110
+ <span class="sr-only" aria-live="polite" data-help-announcement="" />
111
+ </ScrollArea>
112
+ <script type="application/json" data-help-panel-data="" innerHTML={JSON.stringify(data).replace(/</g, "\\u003c")} />
113
+ </div>
114
+ );
115
+ }