@hanzo/build 0.2.10 → 0.2.11

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 (136) hide show
  1. package/README.md +97 -25
  2. package/lib/api/agents.d.ts +80 -0
  3. package/lib/api/agents.js +148 -0
  4. package/lib/api/billing.d.ts +205 -0
  5. package/lib/api/billing.js +300 -0
  6. package/lib/api/call.d.ts +6 -0
  7. package/lib/api/call.js +11 -0
  8. package/lib/api/capabilities.d.ts +25 -0
  9. package/lib/api/capabilities.js +40 -0
  10. package/lib/api/coding.d.ts +31 -1
  11. package/lib/api/coding.js +50 -1
  12. package/lib/api/connectors.d.ts +90 -0
  13. package/lib/api/connectors.js +109 -0
  14. package/lib/api/consent.d.ts +22 -0
  15. package/lib/api/consent.js +28 -0
  16. package/lib/api/github.d.ts +13 -0
  17. package/lib/api/github.js +9 -0
  18. package/lib/api/harness.d.ts +44 -0
  19. package/lib/api/harness.js +348 -0
  20. package/lib/api/keys.d.ts +41 -0
  21. package/lib/api/keys.js +55 -0
  22. package/lib/api/machines.d.ts +56 -0
  23. package/lib/api/machines.js +84 -0
  24. package/lib/api/members.d.ts +47 -0
  25. package/lib/api/members.js +88 -0
  26. package/lib/api/memory.d.ts +25 -0
  27. package/lib/api/memory.js +34 -0
  28. package/lib/api/places.d.ts +2 -2
  29. package/lib/api/places.js +3 -26
  30. package/lib/api/plugins.d.ts +55 -0
  31. package/lib/api/plugins.js +62 -0
  32. package/lib/api/pref.d.ts +49 -0
  33. package/lib/api/pref.js +68 -0
  34. package/lib/api/profile.d.ts +21 -0
  35. package/lib/api/profile.js +40 -0
  36. package/lib/api/projects.d.ts +19 -2
  37. package/lib/api/projects.js +29 -4
  38. package/lib/api/provider.d.ts +42 -0
  39. package/lib/api/provider.js +58 -0
  40. package/lib/api/sessions.d.ts +32 -4
  41. package/lib/api/sessions.js +42 -6
  42. package/lib/api/skills.d.ts +70 -0
  43. package/lib/api/skills.js +90 -0
  44. package/lib/api/tools.d.ts +34 -0
  45. package/lib/api/tools.js +37 -0
  46. package/lib/api/turn.d.ts +84 -16
  47. package/lib/api/turn.js +221 -4
  48. package/lib/api/webhooks.d.ts +60 -0
  49. package/lib/api/webhooks.js +81 -0
  50. package/lib/ask.d.ts +18 -0
  51. package/lib/ask.js +69 -0
  52. package/lib/builder.js +39 -19
  53. package/lib/customize/agents.d.ts +2 -0
  54. package/lib/customize/agents.js +125 -0
  55. package/lib/customize/connectors.d.ts +2 -0
  56. package/lib/customize/connectors.js +197 -0
  57. package/lib/customize/index.d.ts +6 -0
  58. package/lib/customize/index.js +50 -0
  59. package/lib/customize/plugins.d.ts +2 -0
  60. package/lib/customize/plugins.js +93 -0
  61. package/lib/customize/skills.d.ts +2 -0
  62. package/lib/customize/skills.js +125 -0
  63. package/lib/customize/ui.d.ts +110 -0
  64. package/lib/customize/ui.js +99 -0
  65. package/lib/desk.d.ts +4 -1
  66. package/lib/desk.js +3 -16
  67. package/lib/find.d.ts +7 -0
  68. package/lib/find.js +77 -0
  69. package/lib/foot.d.ts +16 -0
  70. package/lib/foot.js +36 -0
  71. package/lib/forge.js +4 -4
  72. package/lib/git.d.ts +4 -0
  73. package/lib/git.js +6 -2
  74. package/lib/host.d.ts +5 -0
  75. package/lib/landing.d.ts +10 -0
  76. package/lib/landing.js +30 -14
  77. package/lib/markdown.d.ts +49 -0
  78. package/lib/markdown.js +123 -0
  79. package/lib/plans.d.ts +1 -0
  80. package/lib/plans.js +113 -0
  81. package/lib/prefs.d.ts +30 -0
  82. package/lib/prefs.js +83 -0
  83. package/lib/prose.d.ts +4 -0
  84. package/lib/prose.js +53 -0
  85. package/lib/route.d.ts +16 -1
  86. package/lib/route.js +32 -1
  87. package/lib/run.js +184 -32
  88. package/lib/section.js +2 -2
  89. package/lib/settings/account.d.ts +5 -0
  90. package/lib/settings/account.js +111 -0
  91. package/lib/settings/billing.d.ts +1 -0
  92. package/lib/settings/billing.js +104 -0
  93. package/lib/settings/capabilities.d.ts +1 -0
  94. package/lib/settings/capabilities.js +55 -0
  95. package/lib/settings/card.d.ts +31 -0
  96. package/lib/settings/card.js +105 -0
  97. package/lib/settings/code.d.ts +1 -0
  98. package/lib/settings/code.js +51 -0
  99. package/lib/settings/environments.d.ts +1 -0
  100. package/lib/settings/environments.js +53 -0
  101. package/lib/settings/general.d.ts +1 -0
  102. package/lib/settings/general.js +44 -0
  103. package/lib/settings/index.d.ts +4 -0
  104. package/lib/settings/index.js +22 -0
  105. package/lib/settings/integrations.d.ts +1 -0
  106. package/lib/settings/integrations.js +81 -0
  107. package/lib/settings/keys.d.ts +1 -0
  108. package/lib/settings/keys.js +102 -0
  109. package/lib/settings/machines.d.ts +1 -0
  110. package/lib/settings/machines.js +118 -0
  111. package/lib/settings/members.d.ts +1 -0
  112. package/lib/settings/members.js +67 -0
  113. package/lib/settings/memory.d.ts +1 -0
  114. package/lib/settings/memory.js +56 -0
  115. package/lib/settings/notifications.d.ts +1 -0
  116. package/lib/settings/notifications.js +97 -0
  117. package/lib/settings/privacy.d.ts +1 -0
  118. package/lib/settings/privacy.js +63 -0
  119. package/lib/settings/sections.d.ts +16 -0
  120. package/lib/settings/sections.js +32 -0
  121. package/lib/settings/ui.d.ts +49 -0
  122. package/lib/settings/ui.js +47 -0
  123. package/lib/settings/usage.d.ts +1 -0
  124. package/lib/settings/usage.js +114 -0
  125. package/lib/shelf.d.ts +6 -0
  126. package/lib/shelf.js +86 -16
  127. package/lib/switch.js +1 -2
  128. package/lib/transcript.d.ts +12 -0
  129. package/lib/transcript.js +56 -0
  130. package/lib/voice.d.ts +3 -1
  131. package/lib/voice.js +10 -4
  132. package/package.json +1 -1
  133. package/lib/account.d.ts +0 -14
  134. package/lib/account.js +0 -38
  135. package/lib/mcp.d.ts +0 -1
  136. package/lib/mcp.js +0 -28
package/lib/foot.js ADDED
@@ -0,0 +1,36 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The rail's foot: the card that offers Hanzo in Slack, and the menu the account
4
+ * row opens — who is signed in, Settings, Usage, the plans, help and logging out.
5
+ * Everything it opens is an address of this page, but help, which is the
6
+ * documentation.
7
+ */
8
+ import { Popover, SizableText, XStack, YStack } from '@hanzo/gui';
9
+ import { Gauge, LifeBuoy, LogOut, Settings, Slack as Mark, Sparkles, X } from '@hanzogui/lucide-icons-2';
10
+ import { MenuRow, MenuRule } from '@hanzo/ui/product';
11
+ import { useHost } from './host.js';
12
+ import { path } from './route.js';
13
+ export const DOCS = 'https://docs.hanzo.ai/docs/dev';
14
+ /** Hanzo in Slack is set up under Settings; the card goes there, and once dismissed stays gone. */
15
+ export function Slack({ onOpen, onDismiss }) {
16
+ return (_jsxs(XStack, { items: "center", gap: "$2.5", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", children: [_jsx(Mark, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: "Try Hanzo in Slack" }), _jsx(XStack, { render: "button", "aria-label": "Set up Hanzo in Slack", onPress: onOpen, cursor: "pointer", children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Set up" }) })] }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: onDismiss, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 14 }) })] }));
17
+ }
18
+ /**
19
+ * The account menu. The rail draws the account row and says when it was pressed,
20
+ * so the menu hangs off an anchor laid over the rail's foot: above it on the
21
+ * open rail, beside it on the collapsed one.
22
+ */
23
+ export function Who({ open, onOpenChange, narrow }) {
24
+ const host = useHost();
25
+ const go = (p) => {
26
+ onOpenChange(false);
27
+ host.go(p);
28
+ };
29
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 6, children: [_jsx(Popover.Anchor, { position: "absolute", l: 8, b: 8, width: narrow ? 48 : 256, height: 36, pointerEvents: "none" }), _jsx(Popover.Content, { role: "menu", "aria-label": "Account", items: "stretch", borderWidth: 1, elevation: "$2", px: "$1", py: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person?.email || host.person?.name || '' }), _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", icon: _jsx(Settings, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(MenuRow, { label: "Usage", icon: _jsx(Gauge, { size: 16 }), onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(MenuRow, { label: "View all plans", icon: _jsx(Sparkles, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(MenuRow, { label: "Get help", icon: _jsx(LifeBuoy, { size: 16 }), onPress: () => {
30
+ onOpenChange(false);
31
+ window.open(DOCS, '_blank', 'noopener,noreferrer');
32
+ } }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuRow, { label: "Log out", icon: _jsx(LogOut, { size: 16 }), onPress: () => {
33
+ onOpenChange(false);
34
+ host.signOut?.();
35
+ } })] })) : null] }) })] }));
36
+ }
package/lib/forge.js CHANGED
@@ -10,7 +10,6 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
10
  import { ChevronDown, GitBranch, Plus, Settings, Workflow } from '@hanzogui/lucide-icons-2';
11
11
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
12
12
  import { useEffect, useState } from 'react';
13
- import { revealAccount } from './account.js';
14
13
  import { add, arm, flows } from './api/auto.js';
15
14
  import { codebases, create } from './api/codebases.js';
16
15
  import { environments } from './api/environment.js';
@@ -19,7 +18,7 @@ import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './ch
19
18
  import { useKept, useRead } from './data.js';
20
19
  import { useHost, useTarget } from './host.js';
21
20
  import { path } from './route.js';
22
- import { Servers } from './mcp.js';
21
+ import { Customize } from './customize/index.js';
23
22
  import { Sync } from './sync.js';
24
23
  import { grants } from './api/github.js';
25
24
  function Head({ title, says }) {
@@ -43,8 +42,9 @@ export function Forge({ screen }) {
43
42
  return _jsx(Automations, {});
44
43
  if (screen === 'sync')
45
44
  return _jsx(Sync, {});
45
+ // The fleet's native servers are Connectors → Discover now; the old address lands there.
46
46
  if (screen === 'mcp')
47
- return _jsx(Servers, {});
47
+ return _jsx(Customize, { tab: "connectors", view: "discover" });
48
48
  if (screen === 'codebases')
49
49
  return _jsx(Codebases, {});
50
50
  if (screen === 'projects')
@@ -177,7 +177,7 @@ function Codebases() {
177
177
  pinCodebase(host.org, c);
178
178
  host.go('');
179
179
  };
180
- return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", shrink: 0, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => revealAccount(), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", shrink: 0, "$max-md": { display: 'none' }, children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1, "$max-md": { display: 'none' } }), _jsx(YStack, { width: 240, minW: 0, shrink: 1, "$max-md": { width: 'auto', flex: 1 }, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: "Environment" }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: envOf.get(c.name) === 'ready' ? 'Ready' : envOf.get(c.name) === 'proposed' ? 'To review' : '—' }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
180
+ return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", shrink: 0, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => host.go('-/settings/environments'), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", shrink: 0, "$max-md": { display: 'none' }, children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1, "$max-md": { display: 'none' } }), _jsx(YStack, { width: 240, minW: 0, shrink: 1, "$max-md": { width: 'auto', flex: 1 }, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: "Environment" }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: envOf.get(c.name) === 'ready' ? 'Ready' : envOf.get(c.name) === 'proposed' ? 'To review' : '—' }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
181
181
  const row = await create(t, name, description);
182
182
  pinCodebase(host.org, row);
183
183
  setMaking(false);
package/lib/git.d.ts CHANGED
@@ -3,3 +3,7 @@ export declare function Git({ session, title, live }: {
3
3
  title: string;
4
4
  live: boolean;
5
5
  }): import("react").JSX.Element;
6
+ /** A unified diff's lines, coloured by what each does: the Git tab's, and a transcript's edit card's. */
7
+ export declare function Patch({ text }: {
8
+ text: string;
9
+ }): import("react").JSX.Element;
package/lib/git.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * A run's Git tab: what it pushed, read from the forge. Diff is the net change
4
4
  * from its base, Review is the pull request it opened and what reviewers said,
@@ -44,7 +44,11 @@ export function Git({ session, title, live }) {
44
44
  function File({ change: f, first }) {
45
45
  const [open, setOpen] = useState(false);
46
46
  const lines = f.patch ? f.patch.split('\n') : [];
47
- return (_jsxs(YStack, { borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", children: [_jsxs(XStack, { render: "button", "aria-label": `${open ? 'Hide' : 'Show'} ${f.path}`, onPress: () => setOpen(!open), items: "center", gap: "$2", px: "$3", py: "$2", hoverStyle: { bg: '$hover' }, children: [open ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 }), _jsx(SizableText, { size: "$1", color: "$soft", width: 12, children: f.status[0].toUpperCase() }), _jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: f.from ? `${f.from} → ${f.path}` : f.path }), _jsx(SizableText, { size: "$1", color: "$green10", children: `+${f.additions}` }), _jsx(SizableText, { size: "$1", color: "$red10", children: `−${f.deletions}` })] }), open ? (_jsxs(YStack, { px: "$3", pb: "$2", overflow: "scroll", children: [lines.length ? (lines.map((l, i) => (_jsx(SizableText, { size: "$1", style: mono, color: l.startsWith('@@') ? '$soft' : l.startsWith('+') ? '$green10' : l.startsWith('-') ? '$red10' : '$ink', children: l || ' ' }, i)))) : (_jsx(SizableText, { size: "$1", color: "$soft", children: f.truncated ? 'Too large to show here.' : 'Binary file' })), lines.length && f.truncated ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The rest of this file\u2019s change is too large to show here." })) : null] })) : null] }));
47
+ return (_jsxs(YStack, { borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", children: [_jsxs(XStack, { render: "button", "aria-label": `${open ? 'Hide' : 'Show'} ${f.path}`, onPress: () => setOpen(!open), items: "center", gap: "$2", px: "$3", py: "$2", hoverStyle: { bg: '$hover' }, children: [open ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 }), _jsx(SizableText, { size: "$1", color: "$soft", width: 12, children: f.status[0].toUpperCase() }), _jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", numberOfLines: 1, style: { textAlign: 'left' }, children: f.from ? `${f.from} → ${f.path}` : f.path }), _jsx(SizableText, { size: "$1", color: "$green10", children: `+${f.additions}` }), _jsx(SizableText, { size: "$1", color: "$red10", children: `−${f.deletions}` })] }), open ? (_jsxs(YStack, { px: "$3", pb: "$2", overflow: "scroll", children: [lines.length ? (_jsx(Patch, { text: f.patch })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: f.truncated ? 'Too large to show here.' : 'Binary file' })), lines.length && f.truncated ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "The rest of this file\u2019s change is too large to show here." })) : null] })) : null] }));
48
+ }
49
+ /** A unified diff's lines, coloured by what each does: the Git tab's, and a transcript's edit card's. */
50
+ export function Patch({ text }) {
51
+ return (_jsx(_Fragment, { children: text.split('\n').map((l, i) => (_jsx(SizableText, { size: "$1", style: mono, color: l.startsWith('@@') ? '$soft' : l.startsWith('+') ? '$green10' : l.startsWith('-') ? '$red10' : '$ink', children: l || ' ' }, i))) }));
48
52
  }
49
53
  function label(v) {
50
54
  return v === 'diff' ? 'Diff' : v === 'review' ? 'Review' : 'Commits';
package/lib/host.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { type ReactNode } from 'react';
11
11
  import type { Target } from './api/call.ts';
12
+ import type { Theme } from './api/pref.ts';
12
13
  export interface Person {
13
14
  name: string;
14
15
  email: string;
@@ -26,6 +27,10 @@ export interface Host {
26
27
  memberships?: readonly string[];
27
28
  /** Stay on this page and scope every read to `org`. */
28
29
  chooseOrg?: (org: string) => void;
30
+ /** The page's theme as last chosen, for a host that can change it. */
31
+ theme?: Theme;
32
+ /** Paint the page in `theme`. A host without it keeps its own, and Settings offers no choice. */
33
+ chooseTheme?: (theme: Theme) => void;
29
34
  /** Who is signed in, or null. */
30
35
  person: Person | null;
31
36
  /** Whether the person administers `org`: an admin publishes to main, anyone else opens a review. */
package/lib/landing.d.ts CHANGED
@@ -8,6 +8,16 @@ export declare const MODES: readonly [{
8
8
  readonly label: 'Plan';
9
9
  readonly hint: 'Plans the change and writes nothing';
10
10
  }];
11
+ export declare const EFFORTS: readonly [{
12
+ readonly id: 'low';
13
+ readonly label: 'Low';
14
+ }, {
15
+ readonly id: 'medium';
16
+ readonly label: 'Medium';
17
+ }, {
18
+ readonly id: 'high';
19
+ readonly label: 'High';
20
+ }];
11
21
  export declare function Landing({ onStarted }: {
12
22
  onStarted: (session: string) => void;
13
23
  }): import("react").JSX.Element;
package/lib/landing.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * New: the empty state. A heading at the top of the column, and at the foot of
4
- * the pane the composer — where the run goes (the sandbox or one of the org's
5
- * machines), which repository and branch it starts from, and the ask itself.
4
+ * the pane the composer — where the run goes (Cloud: the sandbox with the
5
+ * codebase's environment; or one of the org's machines, under remote control),
6
+ * which repository and branch it starts from, and the ask itself.
6
7
  * Under it: attach, dictate, the mode, and the model and effort on the right.
7
8
  *
8
9
  * Sending starts a coding run and opens it. The choices a person made here are
@@ -13,7 +14,7 @@ import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
13
14
  import { Button } from '@hanzo/ui';
14
15
  import { ModeSelect } from '@hanzo/ui/agents';
15
16
  import { Composer, EmptyPrompt } from '@hanzo/ui/chat';
16
- import { BranchSelect, ChipSelect, HanzoMark, RepoSelect } from '@hanzo/ui/product';
17
+ import { BranchSelect, ChipSelect, FooterLink, HanzoMark, RepoSelect } from '@hanzo/ui/product';
17
18
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
18
19
  import { start, unhonoured } from './api/coding.js';
19
20
  import { asRepo, chosen, codebases, one } from './api/codebases.js';
@@ -24,6 +25,7 @@ import { ready, SANDBOX } from './api/places.js';
24
25
  import { isForge } from './choice.js';
25
26
  import { useKept, usePlaces, useRead } from './data.js';
26
27
  import { useHost, useTarget } from './host.js';
28
+ import { usePrefs } from './prefs.js';
27
29
  import { Publish } from './publish.js';
28
30
  import { path } from './route.js';
29
31
  import { Attach, compose, Dictate, Files } from './tools.js';
@@ -32,7 +34,7 @@ export const MODES = [
32
34
  { id: 'build', label: 'Build', hint: 'Edits, commits and pushes a branch' },
33
35
  { id: 'plan', label: 'Plan', hint: 'Plans the change and writes nothing' },
34
36
  ];
35
- const EFFORTS = [
37
+ export const EFFORTS = [
36
38
  { id: 'low', label: 'Low' },
37
39
  { id: 'medium', label: 'Medium' },
38
40
  { id: 'high', label: 'High' },
@@ -43,7 +45,10 @@ export function Landing({ onStarted }) {
43
45
  const host = useHost();
44
46
  const t = useTarget();
45
47
  const signed = Boolean(host.person);
46
- const [kept, keep] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, FIRST);
48
+ const { prefs } = usePrefs();
49
+ const [stored, keep] = useKept(`hanzo.build.new.${host.org ?? 'none'}`, null);
50
+ // Nothing chosen here yet: start from the person's coding defaults (Settings → Code).
51
+ const kept = stored ?? { ...FIRST, ...prefs.code };
47
52
  const set = (patch) => keep({ ...kept, ...patch });
48
53
  const [draft, setDraft] = useState(kept.ask || '');
49
54
  const [files, setFiles] = useState([]);
@@ -51,6 +56,9 @@ export function Landing({ onStarted }) {
51
56
  const [note, setNote] = useState('');
52
57
  const [adding, setAdding] = useState(null);
53
58
  const [offering, setOffering] = useState(false);
59
+ // The repository list is read again each time it opens; Refresh reads it again while open.
60
+ const [picking, setPicking] = useState(false);
61
+ const [round, setRound] = useState(0);
54
62
  // A codebase or an issue hands its words over once. Left in the kept choice,
55
63
  // every later visit to New would put them back.
56
64
  useEffect(() => {
@@ -122,21 +130,29 @@ export function Landing({ onStarted }) {
122
130
  const run = await start(t, { prompt: SETUP, repo: codebase, mode: 'setup' });
123
131
  onStarted(run.session);
124
132
  };
133
+ // Cloud is the sandbox, set up by the chosen codebase's environment.
134
+ const setUp = codebase && env.value
135
+ ? env.value.state === 'ready'
136
+ ? `${codebase} environment`
137
+ : env.value.state === 'proposed'
138
+ ? `${codebase} environment proposed`
139
+ : `no ${codebase} environment yet`
140
+ : '';
125
141
  const placeItems = useMemo(() => (places.value.length ? places.value : [SANDBOX]).map((p) => ({
126
142
  id: p.id || 'sandbox',
127
- label: p.label,
128
- hint: p.id ? [p.status, p.capacity].filter(Boolean).join(' · ') : 'The platform’s own sandbox',
143
+ label: p.id ? p.label : 'Cloud',
144
+ hint: p.id ? ['Remote control', p.status, p.capacity].filter(Boolean).join(' · ') : ['Hanzo sandbox', setUp].filter(Boolean).join(' · '),
129
145
  disabled: !ready(p),
130
146
  place: p,
131
- })), [places.value]);
147
+ })), [places.value, setUp]);
132
148
  const branch = kept.branch || kept.repo?.default_branch || 'main';
133
- const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Default', chosen: placeItems.find((i) => i.place.id === place.id) ?? null, items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsx(SizableText, { size: "$1", color: "$soft", children: "A run on a machine uses that machine\u2019s checkout and credentials." }) }), _jsx(RepoSelect, { value: isForge(kept.repo) ? kept.repo : null, onChange: (r) => {
149
+ const head = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: "Where the run runs", icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : 'Cloud', chosen: placeItems.find((i) => i.place.id === place.id) ?? null, items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: "Search machines\u2026", loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: "Set up remote control" }), _jsxs(SizableText, { size: "$1", color: "$soft", children: ["Run", ' ', _jsx(SizableText, { size: "$1", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: "hanzo link" }), ' ', "on a machine: it joins this list, and a run there uses its checkout and credentials."] })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) ? kept.repo : null, onChange: (r) => {
134
150
  const row = known.current.get(r.name) ?? chosen(r);
135
151
  set({ repo: row, branch: row.default_branch });
136
152
  }, load: loadRepos, troubleshoot: {
137
- label: 'Browse codebases',
138
- onPress: () => host.go(path({ kind: 'screen', screen: 'codebases' })),
139
- }, note: "Repositories on the forge. Type to search.", action: {
153
+ label: 'Missing a GitHub repository? Bring it onto the forge',
154
+ onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })),
155
+ }, connect: _jsx(XStack, { justify: "flex-end", children: _jsx(FooterLink, { label: "Refresh list", onPress: () => setRound((n) => n + 1) }) }), note: "Repositories on the forge. Type to search.", action: {
140
156
  label: 'Add to project',
141
157
  onPress: (r) => {
142
158
  const row = known.current.get(r.name);
@@ -147,9 +163,9 @@ export function Landing({ onStarted }) {
147
163
  name: r.name,
148
164
  });
149
165
  },
150
- }, placeholder: "Codebase", disabled: !signed }), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
166
+ }, placeholder: "Codebase", disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null, _jsx(Files, { files: files, onFiles: setFiles })] }));
151
167
  const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === kept.model)?.label ?? 'Enso', chosen: catalog.value.find((m) => m.id === kept.model) ?? null, items: catalog.value, onChange: (m) => set({ model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
152
- return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", px: "$4", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, flex: 1, minH: 0, children: [_jsx(EmptyPrompt, { title: "What\u2019s up next?", mark: _jsx(HanzoMark, { size: 18 }), column: COLUMN }), _jsx(YStack, { flex: 1 }), _jsxs(YStack, { pb: "$2", gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
168
+ return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", px: "$4", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, flex: 1, minH: 0, children: [_jsx(EmptyPrompt, { title: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?', mark: _jsx(HanzoMark, { size: 18 }), column: COLUMN }), _jsx(YStack, { flex: 1 }), _jsxs(YStack, { pb: "$2", gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
153
169
  ? `A proposed environment for ${codebase} is waiting for review.`
154
170
  : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, open: offering, onOpenChange: setOffering, onStart: setup, onSaved: () => {
155
171
  setOffering(false);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The markdown an agent writes, read into blocks and inline pieces: paragraphs,
3
+ * headings, lists, quotes, rules, tables and fenced code; inline code, bold,
4
+ * italics and links. What prose.tsx draws, as plain values.
5
+ *
6
+ * A link keeps an address only when it is http(s); anything else, and every
7
+ * image, is its words.
8
+ */
9
+ export type Block = {
10
+ kind: 'paragraph';
11
+ text: string;
12
+ } | {
13
+ kind: 'heading';
14
+ level: number;
15
+ text: string;
16
+ } | {
17
+ kind: 'code';
18
+ lang: string;
19
+ text: string;
20
+ } | {
21
+ kind: 'list';
22
+ ordered: boolean;
23
+ start: number;
24
+ items: {
25
+ text: string;
26
+ depth: number;
27
+ }[];
28
+ } | {
29
+ kind: 'quote';
30
+ text: string;
31
+ } | {
32
+ kind: 'rule';
33
+ } | {
34
+ kind: 'table';
35
+ head: string[];
36
+ rows: string[][];
37
+ };
38
+ /** The blocks of a markdown text, in order. */
39
+ export declare function blocks(md: string): Block[];
40
+ export type Span = {
41
+ kind: 'text' | 'code' | 'strong' | 'em';
42
+ text: string;
43
+ } | {
44
+ kind: 'link';
45
+ text: string;
46
+ href: string;
47
+ };
48
+ /** A line's inline pieces. A link to anything but http(s) is its words. */
49
+ export declare function spans(text: string): Span[];
@@ -0,0 +1,123 @@
1
+ const FENCE = /^\s{0,3}(```+|~~~+)\s*([\w+#.-]*)\s*$/;
2
+ const HEADING = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
3
+ const ITEM = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/;
4
+ const RULE = /^\s{0,3}([-*_])(\s*\1){2,}\s*$/;
5
+ const QUOTE = /^\s{0,3}>\s?(.*)$/;
6
+ const ROW = /^\s*\|(.*)\|\s*$/;
7
+ const DIVIDER = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
8
+ const cells = (line) => (ROW.exec(line)?.[1] ?? line).split('|').map((c) => c.trim());
9
+ /** The blocks of a markdown text, in order. */
10
+ export function blocks(md) {
11
+ const lines = md.replace(/\r\n?/g, '\n').split('\n');
12
+ const out = [];
13
+ let para = [];
14
+ const flush = () => {
15
+ if (para.length)
16
+ out.push({ kind: 'paragraph', text: para.join(' ').trim() });
17
+ para = [];
18
+ };
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i];
21
+ const fence = FENCE.exec(line);
22
+ if (fence) {
23
+ flush();
24
+ const body = [];
25
+ i++;
26
+ while (i < lines.length && !lines[i].trim().startsWith(fence[1]))
27
+ body.push(lines[i++]);
28
+ out.push({ kind: 'code', lang: fence[2] ?? '', text: body.join('\n') });
29
+ continue;
30
+ }
31
+ if (!line.trim()) {
32
+ flush();
33
+ continue;
34
+ }
35
+ const heading = HEADING.exec(line);
36
+ if (heading) {
37
+ flush();
38
+ out.push({ kind: 'heading', level: heading[1].length, text: heading[2] });
39
+ continue;
40
+ }
41
+ if (RULE.test(line)) {
42
+ flush();
43
+ out.push({ kind: 'rule' });
44
+ continue;
45
+ }
46
+ if (ROW.test(line) && DIVIDER.test(lines[i + 1] ?? '')) {
47
+ flush();
48
+ const head = cells(line);
49
+ const rows = [];
50
+ i += 2;
51
+ while (i < lines.length && ROW.test(lines[i]))
52
+ rows.push(cells(lines[i++]));
53
+ i--;
54
+ out.push({ kind: 'table', head, rows });
55
+ continue;
56
+ }
57
+ const quote = QUOTE.exec(line);
58
+ if (quote) {
59
+ flush();
60
+ const body = [quote[1]];
61
+ while (i + 1 < lines.length && QUOTE.test(lines[i + 1]))
62
+ body.push(QUOTE.exec(lines[++i])[1]);
63
+ out.push({ kind: 'quote', text: body.join(' ').trim() });
64
+ continue;
65
+ }
66
+ const item = ITEM.exec(line);
67
+ if (item) {
68
+ flush();
69
+ const ordered = /\d/.test(item[2]);
70
+ const items = [];
71
+ const start = ordered ? parseInt(item[2], 10) : 1;
72
+ let j = i;
73
+ while (j < lines.length) {
74
+ const m = ITEM.exec(lines[j]);
75
+ if (m) {
76
+ items.push({ text: m[3], depth: Math.min(3, Math.floor(m[1].replace(/\t/g, ' ').length / 2)) });
77
+ j++;
78
+ continue;
79
+ }
80
+ // A line indented under an item continues it.
81
+ if (lines[j].trim() && /^\s{2,}/.test(lines[j]) && items.length) {
82
+ items[items.length - 1].text += ` ${lines[j].trim()}`;
83
+ j++;
84
+ continue;
85
+ }
86
+ break;
87
+ }
88
+ out.push({ kind: 'list', ordered, start, items });
89
+ i = j - 1;
90
+ continue;
91
+ }
92
+ para.push(line.trim());
93
+ }
94
+ flush();
95
+ return out;
96
+ }
97
+ const INLINE = /(`+)([^`]+?)\1|!?\[([^\]]+)\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?\)|\*\*(.+?)\*\*|__(.+?)__|\*([^*\s](?:[^*]*[^*\s])?)\*|(?<![\w])_([^_\s](?:[^_]*[^_\s])?)_(?![\w])|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;
98
+ const safe = (href) => (/^https?:\/\/[^\s]+$/i.test(href) ? href : '');
99
+ /** A line's inline pieces. A link to anything but http(s) is its words. */
100
+ export function spans(text) {
101
+ const out = [];
102
+ let at = 0;
103
+ for (const m of text.matchAll(INLINE)) {
104
+ if (m.index > at)
105
+ out.push({ kind: 'text', text: text.slice(at, m.index) });
106
+ if (m[2] !== undefined)
107
+ out.push({ kind: 'code', text: m[2] });
108
+ else if (m[3] !== undefined) {
109
+ const href = safe(m[4]);
110
+ out.push(href && !m[0].startsWith('!') ? { kind: 'link', text: m[3], href } : { kind: 'text', text: m[3] });
111
+ }
112
+ else if (m[5] !== undefined || m[6] !== undefined)
113
+ out.push({ kind: 'strong', text: (m[5] ?? m[6]) });
114
+ else if (m[7] !== undefined || m[8] !== undefined)
115
+ out.push({ kind: 'em', text: (m[7] ?? m[8]) });
116
+ else if (m[9] !== undefined)
117
+ out.push({ kind: 'link', text: m[9], href: m[9] });
118
+ at = m.index + m[0].length;
119
+ }
120
+ if (at < text.length)
121
+ out.push({ kind: 'text', text: text.slice(at) });
122
+ return out;
123
+ }
package/lib/plans.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function Plans(): import("react").JSX.Element;
package/lib/plans.js ADDED
@@ -0,0 +1,113 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Plans: every plan this brand sells, priced as the checkout will charge, with
4
+ * the one the organization is on marked, and the move to another.
5
+ *
6
+ * Buying charges a saved card for the plan's first period at the catalog's
7
+ * price; the platform sells one paid plan at a time, so a move between paid
8
+ * plans is the current one ending first. Going back to Free is ending the paid
9
+ * plan at the close of the period already paid for.
10
+ */
11
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
12
+ import { Check, ChevronLeft } from '@hanzogui/lucide-icons-2';
13
+ import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
14
+ import { useState } from 'react';
15
+ import { cancel, chosen, current, label, methods, money, plans, subscribe, subscriptions } from './api/billing.js';
16
+ import { useRead } from './data.js';
17
+ import { useHost, useTarget } from './host.js';
18
+ import { path } from './route.js';
19
+ import { AddCard } from './settings/card.js';
20
+ import { day } from './settings/ui.js';
21
+ /** What a plan costs a month on a term, and what the term charges at once. */
22
+ function price(p, term) {
23
+ const month = term === 'year' && p.yearly ? p.yearly : p.monthly;
24
+ return { month, now: term === 'year' && p.yearly ? p.yearly * 12 : p.monthly };
25
+ }
26
+ export function Plans() {
27
+ const host = useHost();
28
+ const t = useTarget();
29
+ const signed = Boolean(host.person);
30
+ const list = useRead(() => plans(t), [], [t]);
31
+ const subs = useRead(signed ? () => subscriptions(t) : null, [], [t, signed]);
32
+ const cards = useRead(signed ? () => methods(t) : null, [], [t, signed]);
33
+ const [term, setTerm] = useState('month');
34
+ const [buying, setBuying] = useState(null);
35
+ const [leaving, setLeaving] = useState(false);
36
+ const [adding, setAdding] = useState(false);
37
+ const [working, setWorking] = useState(false);
38
+ const [note, setNote] = useState('');
39
+ const on = current(subs.value);
40
+ const card = chosen(cards.value);
41
+ const yearly = list.value.some((p) => p.yearly > 0);
42
+ const free = (p) => p.monthly === 0 && !p.sales;
43
+ const mine = (p) => signed && (on ? p.id === on.plan : free(p));
44
+ // Until the plan and the cards are read, no move is offered: it would be a guess.
45
+ const settled = signed && !(subs.loading && !subs.value.length) && !(cards.loading && !cards.value.length);
46
+ const act = async (what) => {
47
+ setWorking(true);
48
+ setNote('');
49
+ try {
50
+ setNote(await what());
51
+ subs.reload();
52
+ }
53
+ catch (e) {
54
+ setNote(e instanceof Error ? e.message : 'That did not work');
55
+ }
56
+ finally {
57
+ setWorking(false);
58
+ setBuying(null);
59
+ setLeaving(false);
60
+ }
61
+ };
62
+ /** The one thing a card can do, or a line saying why it does nothing. */
63
+ const move = (p) => {
64
+ if (mine(p)) {
65
+ return (_jsx(Button, { size: "sm", variant: "outline", disabled: true, children: "Current plan" }));
66
+ }
67
+ if (p.sales)
68
+ return _jsx(Line, { children: "Priced with our team for your organization." });
69
+ if (!signed)
70
+ return _jsx(Line, { children: "Sign in to choose a plan." });
71
+ if (!host.admin)
72
+ return _jsx(Line, { children: "An org admin changes the plan." });
73
+ if (!settled)
74
+ return null;
75
+ if (free(p)) {
76
+ if (!on)
77
+ return null;
78
+ if (on.ending)
79
+ return _jsx(Line, { children: `You move to ${p.name} when ${on.name} ends on ${day(on.ends)}.` });
80
+ return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setLeaving(true), children: `Switch to ${p.name}` }));
81
+ }
82
+ if (on && on.price > 0)
83
+ return _jsx(Line, { children: `One paid plan at a time: ${on.name} ends before ${p.name} begins.` });
84
+ if (!card) {
85
+ return (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: "Add a card to upgrade" }));
86
+ }
87
+ return (_jsx(Button, { size: "sm", variant: p.popular ? 'primary' : 'outline', onPress: () => setBuying(p), children: `Upgrade to ${p.name}` }));
88
+ };
89
+ return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: 1100, mx: "auto", px: "$5", py: "$5", gap: "$5", children: [_jsxs(XStack, { render: "button", "aria-label": "Back to billing", items: "center", gap: "$1", self: "flex-start", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: [_jsx(ChevronLeft, { size: 16 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Billing" })] }), _jsxs(XStack, { items: "flex-end", gap: "$4", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 240, gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$7", color: "$ink", children: "Plans that grow with you" }), _jsx(SizableText, { size: "$2", color: "$soft", children: !signed
90
+ ? 'Sign in to see the plan you are on.'
91
+ : on
92
+ ? `${host.org ?? 'This organization'} is on ${on.name}.`
93
+ : `${host.org ?? 'This organization'} is on the free plan.` })] }), yearly ? (_jsx(XStack, { role: "radiogroup", "aria-label": "Billing period", p: "$1", gap: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: ['month', 'year'].map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": term === k, "aria-label": k === 'month' ? 'Monthly' : 'Yearly', onPress: () => setTerm(k), px: "$3", py: "$1.5", rounded: "$10", bg: term === k ? '$hover' : 'transparent', children: _jsx(SizableText, { size: "$2", color: term === k ? '$ink' : '$soft', children: k === 'month' ? 'Monthly' : 'Yearly' }) }, k))) })) : null] }), list.error ? (_jsx(Line, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Line, { children: "Reading plans\u2026" })) : list.value.length === 0 ? (_jsx(Line, { children: "This brand sells no plans." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$4", children: list.value.map((p) => {
94
+ const cost = price(p, term);
95
+ return (_jsxs(YStack, { "aria-label": `${p.name} plan`, flex: 1, minW: 240, maxW: 360, "$max-sm": { maxW: '100%' }, gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: mine(p) || p.popular ? '$ink' : '$borderColor', bg: "$panel", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, size: "$5", color: "$ink", children: p.name }), mine(p) ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Current" })) : p.popular ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Popular" })) : null] }), p.description ? (_jsx(SizableText, { size: "$2", color: "$soft", children: p.description })) : null, _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$7", color: "$ink", children: p.sales ? 'Custom' : cost.month === 0 ? 'Free' : money(cost.month, p.currency) }), _jsx(SizableText, { size: "$1", color: "$soft", children: p.sales
96
+ ? 'Talk to us'
97
+ : cost.month === 0
98
+ ? 'Forever'
99
+ : `${p.perSeat ? 'per seat, ' : ''}a month${term === 'year' && p.yearly ? `, ${money(cost.now, p.currency)} billed yearly` : ''}` })] }), _jsx(XStack, { children: move(p) }), p.features.length ? (_jsx(YStack, { gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: p.features.map((f) => (_jsxs(XStack, { gap: "$2", items: "flex-start", children: [_jsx(YStack, { pt: 2, children: _jsx(Check, { size: 14 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$soft", children: f })] }, f))) })) : null] }, p.id));
100
+ }) })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null] }), _jsx(Dialog, { open: Boolean(buying), onOpenChange: (o) => (o ? undefined : setBuying(null)), children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: buying ? `Upgrade to ${buying.name}` : 'Upgrade' }), buying && card ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${label(card)} is charged ${money(price(buying, term).now, buying.currency)} now, and again every ${term} until you cancel.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(null), children: "Not now" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void act(async () => {
101
+ await subscribe(t, { plan: buying.id, interval: yearly && buying.yearly ? term : 'month', method: card.id });
102
+ return `${host.org ?? 'This organization'} is on ${buying.name}`;
103
+ }), children: "Upgrade" })] })] })) : null] }) }), _jsx(Dialog, { open: leaving, onOpenChange: setLeaving, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Switch to the free plan?" }), on ? (_jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${on.name} stays on until ${day(on.ends) || 'the end of this period'}, and nothing is charged after that.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setLeaving(false), children: `Keep ${on.name}` }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => void act(async () => {
104
+ await cancel(t, on.id);
105
+ return `${on.name} ends on ${day(on.ends)}`;
106
+ }), children: "Switch" })] })] })) : null] }) }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
107
+ cards.reload();
108
+ setNote(`${label(m)} is saved`);
109
+ } })] }));
110
+ }
111
+ function Line({ children }) {
112
+ return (_jsx(SizableText, { size: "$2", color: "$soft", children: children }));
113
+ }
package/lib/prefs.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The person's own settings, read once for the whole builder and shared by
3
+ * everything that follows them: Settings writes them, New starts from the
4
+ * coding defaults and says the name, dictation listens in the chosen language,
5
+ * and the page takes the theme, the text size and the motion.
6
+ *
7
+ * The last document read is kept in this browser, per org, so the page is
8
+ * drawn the person's way before the platform answers. A save shows at once and
9
+ * is taken back, key by key, if the platform refuses it.
10
+ */
11
+ import { type ReactNode } from 'react';
12
+ import { type Patch, type Prefs } from './api/pref.ts';
13
+ export interface PrefState {
14
+ prefs: Prefs;
15
+ /** The first read is still in flight. */
16
+ loading: boolean;
17
+ error: Error | null;
18
+ /** Merge a change in and keep it. Throws the platform's refusal. */
19
+ save: (patch: Patch) => Promise<void>;
20
+ }
21
+ export declare function PrefsProvider({ children }: {
22
+ children: ReactNode;
23
+ }): import("react").JSX.Element;
24
+ export declare function usePrefs(): PrefState;
25
+ /** The type scale each text size is drawn at. Medium is the design system's own. */
26
+ export declare const SCALE: {
27
+ readonly small: 0.9;
28
+ readonly medium: 1;
29
+ readonly large: 1.15;
30
+ };