@hanzo/rooms 0.1.72 → 0.1.74

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 (110) hide show
  1. package/assets/personas/ada.md +187 -0
  2. package/assets/personas/adi_shamir.md +155 -0
  3. package/assets/personas/al_khwarizmi.md +168 -0
  4. package/assets/personas/al_kindi.md +172 -0
  5. package/assets/personas/alan_kay.md +154 -0
  6. package/assets/personas/analyst.md +138 -0
  7. package/assets/personas/andrew_yao.md +155 -0
  8. package/assets/personas/auguste_kerckhoffs.md +168 -0
  9. package/assets/personas/barbara_liskov.md +154 -0
  10. package/assets/personas/butler_lampson.md +156 -0
  11. package/assets/personas/charles_babbage.md +175 -0
  12. package/assets/personas/creative.md +141 -0
  13. package/assets/personas/curry.md +169 -0
  14. package/assets/personas/cynthia_dwork.md +158 -0
  15. package/assets/personas/david_chaum.md +157 -0
  16. package/assets/personas/david_parnas.md +153 -0
  17. package/assets/personas/dennis_ritchie.md +153 -0
  18. package/assets/personas/dijkstra.md +187 -0
  19. package/assets/personas/donald_knuth.md +156 -0
  20. package/assets/personas/douglas_engelbart.md +175 -0
  21. package/assets/personas/edgar_codd.md +151 -0
  22. package/assets/personas/engineer.md +140 -0
  23. package/assets/personas/feynman.md +178 -0
  24. package/assets/personas/finney.md +187 -0
  25. package/assets/personas/fred_brooks.md +152 -0
  26. package/assets/personas/geoffrey_hinton.md +194 -0
  27. package/assets/personas/herbert_simon.md +190 -0
  28. package/assets/personas/hickey.md +169 -0
  29. package/assets/personas/hopper.md +191 -0
  30. package/assets/personas/hubert_dreyfus.md +184 -0
  31. package/assets/personas/humberto_maturana.md +156 -0
  32. package/assets/personas/index.json +1 -0
  33. package/assets/personas/jcr_licklider.md +178 -0
  34. package/assets/personas/jobs.md +177 -0
  35. package/assets/personas/joe_armstrong.md +155 -0
  36. package/assets/personas/john_backus.md +179 -0
  37. package/assets/personas/john_mccarthy.md +177 -0
  38. package/assets/personas/jon_postel.md +149 -0
  39. package/assets/personas/joseph_weizenbaum.md +185 -0
  40. package/assets/personas/judea_pearl.md +192 -0
  41. package/assets/personas/karen_sparck_jones.md +154 -0
  42. package/assets/personas/konrad_zuse.md +175 -0
  43. package/assets/personas/kunihiko_fukushima.md +152 -0
  44. package/assets/personas/leslie_lamport.md +156 -0
  45. package/assets/personas/markov.md +171 -0
  46. package/assets/personas/mentor.md +141 -0
  47. package/assets/personas/nancy_lynch.md +154 -0
  48. package/assets/personas/niklaus_wirth.md +154 -0
  49. package/assets/personas/norbert_wiener.md +179 -0
  50. package/assets/personas/north-star.md +138 -0
  51. package/assets/personas/peter_naur.md +151 -0
  52. package/assets/personas/pike.md +165 -0
  53. package/assets/personas/radia_perlman.md +156 -0
  54. package/assets/personas/raj_reddy.md +154 -0
  55. package/assets/personas/ralph_merkle.md +152 -0
  56. package/assets/personas/richard_sutton.md +156 -0
  57. package/assets/personas/robin_milner.md +149 -0
  58. package/assets/personas/shafi_goldwasser.md +156 -0
  59. package/assets/personas/shannon.md +184 -0
  60. package/assets/personas/stafford_beer.md +157 -0
  61. package/assets/personas/taher_elgamal.md +180 -0
  62. package/assets/personas/tesla.md +181 -0
  63. package/assets/personas/tony_hoare.md +153 -0
  64. package/assets/personas/torvalds.md +188 -0
  65. package/assets/personas/tpope.md +163 -0
  66. package/assets/personas/turing.md +188 -0
  67. package/assets/personas/whitfield_diffie.md +159 -0
  68. package/assets/personas/zeekay.md +151 -0
  69. package/dist/App.js +26 -22
  70. package/dist/Chat.js +51 -38
  71. package/dist/Column.d.ts +3 -3
  72. package/dist/Column.js +5 -27
  73. package/dist/Me.d.ts +8 -3
  74. package/dist/Me.js +79 -108
  75. package/dist/Palette.d.ts +18 -0
  76. package/dist/Palette.js +131 -0
  77. package/dist/Rail.js +3 -15
  78. package/dist/Recents.d.ts +1 -2
  79. package/dist/Recents.js +9 -9
  80. package/dist/Shell.d.ts +7 -0
  81. package/dist/Shell.js +65 -46
  82. package/dist/Top.d.ts +17 -0
  83. package/dist/Top.js +58 -0
  84. package/dist/agents.d.ts +64 -0
  85. package/dist/agents.js +62 -0
  86. package/dist/crew.d.ts +26 -5
  87. package/dist/crew.js +208 -83
  88. package/dist/help.d.ts +43 -0
  89. package/dist/help.js +58 -0
  90. package/dist/lib/session.d.ts +5 -10
  91. package/dist/lib/session.js +6 -10
  92. package/dist/lib/web.d.ts +6 -0
  93. package/dist/lib/web.js +39 -7
  94. package/dist/recent.d.ts +2 -2
  95. package/dist/recent.js +3 -5
  96. package/dist/seek.d.ts +37 -0
  97. package/dist/seek.js +55 -0
  98. package/dist/team.js +3 -11
  99. package/dist/thread.js +7 -2
  100. package/dist/view.d.ts +44 -0
  101. package/dist/view.js +98 -0
  102. package/package.json +7 -7
  103. package/dist/Account.d.ts +0 -12
  104. package/dist/Account.js +0 -65
  105. package/dist/Find.d.ts +0 -7
  106. package/dist/Find.js +0 -82
  107. package/dist/Profile.d.ts +0 -4
  108. package/dist/Profile.js +0 -67
  109. package/dist/Standing.d.ts +0 -4
  110. package/dist/Standing.js +0 -16
package/dist/lib/web.js CHANGED
@@ -41,6 +41,9 @@ export const PAGES = 3;
41
41
  /** Results a search lists, and how many of them it reads. */
42
42
  const LISTED = 6;
43
43
  const READ = 2;
44
+ /** What Research lists and reads instead. */
45
+ const DEEP_LISTED = 10;
46
+ const DEEP_READ = 5;
44
47
  /** Characters of a page a search quotes, and a named page quotes. */
45
48
  const PAGE_CHARS = 3500;
46
49
  const CRAWL_CHARS = 6000;
@@ -53,6 +56,21 @@ const GATE = 8192;
53
56
  const SEARCH_MS = 20_000;
54
57
  const SKIM_MS = 10_000;
55
58
  const READ_MS = 25_000;
59
+ /**
60
+ * Research: beside `WEB`, a turn that carries this reads deeper — ten results
61
+ * listed, five pages read — whatever its shape, and the model is asked for a
62
+ * report that cites them.
63
+ */
64
+ export const RESEARCH = {
65
+ type: 'function',
66
+ function: {
67
+ name: 'research',
68
+ description: 'Research the question in depth on the live web and answer with a cited report.',
69
+ parameters: { type: 'object', properties: {}, required: [] },
70
+ },
71
+ };
72
+ /** The word a Research turn's model is given. */
73
+ const REPORT = 'Research mode: answer with a detailed report — a short direct answer first, then the evidence under headings — citing each claim with the numbered sources above as [n](url). Say plainly where the sources disagree or fall short.';
56
74
  /** The two tools that mark a turn as one that may use the web. */
57
75
  export const WEB = [
58
76
  {
@@ -527,7 +545,7 @@ function pointed(text) {
527
545
  /** What the model is told of a page it was not given. */
528
546
  const withheld = (url) => `The person's message names ${label(url)}. It was not opened: opening an address like it can sign in, confirm, unsubscribe or spend a one-time link for them.`;
529
547
  /** Runs one search: the results, best first, and the text of the best pages. */
530
- async function search(call, q, lang, now) {
548
+ async function search(call, q, lang, now, deep = false) {
531
549
  let found;
532
550
  try {
533
551
  found = await call('/v1/websearch', lang ? { q, language: lang } : { q }, SEARCH_MS);
@@ -536,11 +554,11 @@ async function search(call, q, lang, now) {
536
554
  return unavailable(e);
537
555
  }
538
556
  const words = keywords(q);
539
- const ranked = rank(found?.results ?? [], words).slice(0, LISTED);
557
+ const ranked = rank(found?.results ?? [], words).slice(0, deep ? DEEP_LISTED : LISTED);
540
558
  const head = `Web search for "${q}", ${stamp(now)}.`;
541
559
  if (!ranked.length)
542
560
  return `${head}\nNothing was found.`;
543
- const pages = await Promise.all(ranked.slice(0, READ).map((r) => call('/v1/crawl', { url: r.url }, SKIM_MS)
561
+ const pages = await Promise.all(ranked.slice(0, deep ? DEEP_READ : READ).map((r) => call('/v1/crawl', { url: r.url }, SKIM_MS)
544
562
  .then((d) => (d?.success ? excerpt(d.data?.markdown ?? '', words, PAGE_CHARS) : ''))
545
563
  .catch(() => '')));
546
564
  const items = ranked.map((r, i) => {
@@ -563,6 +581,19 @@ async function read(call, url, words) {
563
581
  return `Could not read ${url}: ${e instanceof Error ? e.message : String(e)}.`;
564
582
  }
565
583
  }
584
+ /** A Research turn is looked up whatever its shape: its own words, searched. Files and pictures still stay home. */
585
+ function forced(messages, spoken) {
586
+ const last = messages.at(-1);
587
+ if (last?.role !== 'user')
588
+ return null;
589
+ const { text: said, carried } = told(textOf(last.content));
590
+ if (carried || pictured(last.content))
591
+ return null;
592
+ const plain = spell(said.trim().slice(0, GATE)).trim();
593
+ if (!plain)
594
+ return null;
595
+ return { pages: [], held: [], q: plain.slice(0, QUERY), lang: language(plain, spoken), about: keywords(plain) };
596
+ }
566
597
  /**
567
598
  * The lookup the last turn needs, or null. A short follow-up ("and in
568
599
  * Moscow?") is read with the question before it, the way the person meant it.
@@ -667,7 +698,8 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
667
698
  if (!asked.stream || !Array.isArray(asked.messages) || !WEB.every((t) => names.has(t.function.name)))
668
699
  return base(input, init);
669
700
  const { tools: _tools, tool_choice: _choice, ...bare } = asked;
670
- const look = lookup(asked.messages, spoken());
701
+ const deep = names.has(RESEARCH.function.name);
702
+ const look = lookup(asked.messages, spoken()) ?? (deep ? forced(asked.messages, spoken()) : null);
671
703
  if (!look)
672
704
  return base(input, { ...init, body: JSON.stringify(bare) });
673
705
  // The reader's stop, and the stream's own cancel, end every request below.
@@ -695,10 +727,10 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
695
727
  };
696
728
  const found = (await Promise.all([
697
729
  ...look.pages.map((u) => read(call, u, look.about)),
698
- ...(look.q ? [search(call, look.q, look.lang, now)] : []),
730
+ ...(look.q ? [search(call, look.q, look.lang, now, deep)] : []),
699
731
  ...look.held.map(async (u) => withheld(u)),
700
732
  ])).join('\n\n');
701
- const messages = primed(asked.messages, found, now);
733
+ const messages = primed(asked.messages, found, now, deep ? REPORT : '');
702
734
  const first = await send(messages);
703
735
  if (!streams(first))
704
736
  return first;
@@ -709,7 +741,7 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
709
741
  // results: asked once more, it answers. One that failed is not asked again.
710
742
  const { said, failed } = await relay(first, out);
711
743
  if (!said.trim() && !failed) {
712
- const again = await send(primed(asked.messages, found, now, SILENT));
744
+ const again = await send(primed(asked.messages, found, now, deep ? `${REPORT}\n\n${SILENT}` : SILENT));
713
745
  if (streams(again))
714
746
  await relay(again, out);
715
747
  else
package/dist/recent.d.ts CHANGED
@@ -56,12 +56,12 @@ export declare function runs(sessions: {
56
56
  updatedAt: string;
57
57
  }[]): Recent[];
58
58
  /**
59
- * The list as drawn: the rows the view and the words admit, sorted, and cut into
59
+ * The list as drawn: the rows the view admits, sorted, and cut into
60
60
  * headed groups in the order they read. Pinned rows come first, under Pinned,
61
61
  * whatever the grouping; a view grouped by None holds the rest in one group with
62
62
  * no heading.
63
63
  */
64
- export declare function arrange(rows: Recent[], view: View, words: string, now?: Date): {
64
+ export declare function arrange(rows: Recent[], view: View, now?: Date): {
65
65
  title: string;
66
66
  rows: Recent[];
67
67
  }[];
package/dist/recent.js CHANGED
@@ -106,15 +106,13 @@ function day(at, now) {
106
106
  }
107
107
  const newest = (a, b) => (a < b ? 1 : a > b ? -1 : 0);
108
108
  /**
109
- * The list as drawn: the rows the view and the words admit, sorted, and cut into
109
+ * The list as drawn: the rows the view admits, sorted, and cut into
110
110
  * headed groups in the order they read. Pinned rows come first, under Pinned,
111
111
  * whatever the grouping; a view grouped by None holds the rest in one group with
112
112
  * no heading.
113
113
  */
114
- export function arrange(rows, view, words, now = new Date()) {
115
- const w = words.trim().toLowerCase();
116
- const kept = rows.filter((r) => (!w || r.title.toLowerCase().includes(w)) &&
117
- (view.status === 'all' || r.status === view.status) &&
114
+ export function arrange(rows, view, now = new Date()) {
115
+ const kept = rows.filter((r) => (view.status === 'all' || r.status === view.status) &&
118
116
  (view.since === 'any' || now.getTime() - new Date(r.at).getTime() <= SPAN[view.since]));
119
117
  kept.sort((a, b) => view.sort === 'title' ? a.title.localeCompare(b.title) : view.sort === 'created' ? newest(a.began, b.began) : newest(a.at, b.at));
120
118
  const headOf = (r) => {
package/dist/seek.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import type { Press } from './chord.js';
2
+ /** A row the palette can list. */
3
+ export interface Hit {
4
+ id: string;
5
+ label: string;
6
+ /** Other words it is found by: a project's slug and repository, a skill's description. */
7
+ keywords?: string[];
8
+ /** When it last changed, ISO: the newest lead Recent chats. */
9
+ at?: string;
10
+ }
11
+ export interface Group<T extends Hit = Hit> {
12
+ id: string;
13
+ label: string;
14
+ items: T[];
15
+ }
16
+ /** The most rows one group shows for a query. */
17
+ export declare const CAP = 5;
18
+ /** The chats an empty field lists. */
19
+ export declare const RECENT = 8;
20
+ /**
21
+ * How well a row answers a query: 0 drops it, more ranks it higher. Every word
22
+ * of the query must be found, in the label or the row's other words; each counts
23
+ * most where the label starts with it, then where a word of the label does, then
24
+ * anywhere in the label, then in the other words alone. The label read whole
25
+ * outranks everything.
26
+ */
27
+ export declare function fit(row: Hit, query: string): number;
28
+ /** The rows a query keeps, best first, ties in the order they came, at most `cap`. */
29
+ export declare function rank<T extends Hit>(rows: T[], query: string, cap?: number): T[];
30
+ /**
31
+ * What the palette lists. An empty field: the newest chats, under Recent chats.
32
+ * A query: Chats, then each other group in its order, each cut to its best
33
+ * `CAP`, a group with nothing left out.
34
+ */
35
+ export declare function find<T extends Hit>(query: string, chats: T[], groups: Group<T>[]): Group<T>[];
36
+ /** Whether a key press opens the palette: ⌘K or Ctrl+K, once a press, from anywhere, a field included. */
37
+ export declare const summons: (e: Press) => boolean;
package/dist/seek.js ADDED
@@ -0,0 +1,55 @@
1
+ // The palette's decisions (Palette.tsx draws them): which rows a query keeps,
2
+ // in what order, under which heading, and which key press opens it. No React,
3
+ // so a spec reads them as they are.
4
+ /** The most rows one group shows for a query. */
5
+ export const CAP = 5;
6
+ /** The chats an empty field lists. */
7
+ export const RECENT = 8;
8
+ /**
9
+ * How well a row answers a query: 0 drops it, more ranks it higher. Every word
10
+ * of the query must be found, in the label or the row's other words; each counts
11
+ * most where the label starts with it, then where a word of the label does, then
12
+ * anywhere in the label, then in the other words alone. The label read whole
13
+ * outranks everything.
14
+ */
15
+ export function fit(row, query) {
16
+ const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
17
+ if (!terms.length)
18
+ return 1;
19
+ const label = row.label.toLowerCase();
20
+ const words = label.split(/[^\p{L}\p{N}]+/u);
21
+ const more = (row.keywords ?? []).join(' ').toLowerCase();
22
+ let total = label === terms.join(' ') ? 10 : 0;
23
+ for (const t of terms) {
24
+ const s = label.startsWith(t) ? 4 : words.some((w) => w.startsWith(t)) ? 3 : label.includes(t) ? 2 : more.includes(t) ? 1 : 0;
25
+ if (!s)
26
+ return 0;
27
+ total += s;
28
+ }
29
+ return total;
30
+ }
31
+ /** The rows a query keeps, best first, ties in the order they came, at most `cap`. */
32
+ export function rank(rows, query, cap = CAP) {
33
+ return rows
34
+ .map((row, i) => ({ row, i, score: fit(row, query) }))
35
+ .filter((x) => x.score > 0)
36
+ .sort((a, b) => b.score - a.score || a.i - b.i)
37
+ .slice(0, cap)
38
+ .map((x) => x.row);
39
+ }
40
+ /**
41
+ * What the palette lists. An empty field: the newest chats, under Recent chats.
42
+ * A query: Chats, then each other group in its order, each cut to its best
43
+ * `CAP`, a group with nothing left out.
44
+ */
45
+ export function find(query, chats, groups) {
46
+ if (!query.trim()) {
47
+ const newest = [...chats].sort((a, b) => (b.at ?? '').localeCompare(a.at ?? '')).slice(0, RECENT);
48
+ return newest.length ? [{ id: 'recent', label: 'Recent chats', items: newest }] : [];
49
+ }
50
+ return [{ id: 'chats', label: 'Chats', items: chats }, ...groups]
51
+ .map((g) => ({ ...g, items: rank(g.items, query) }))
52
+ .filter((g) => g.items.length);
53
+ }
54
+ /** Whether a key press opens the palette: ⌘K or Ctrl+K, once a press, from anywhere, a field included. */
55
+ export const summons = (e) => (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && !e.repeat && !e.isComposing && (e.key ?? '').toLowerCase() === 'k';
package/dist/team.js CHANGED
@@ -6,19 +6,11 @@
6
6
  // build (scripts/team.mjs), so the rooms keep no copy of a persona: moving the
7
7
  // pin is how a persona changes.
8
8
  //
9
- // The order is the order the crew is drawn in.
9
+ // Every member the package publishes, in the order the crew is drawn in
10
+ // (scripts/team.mjs `ORDER`): this is the default cast.
10
11
  import { parse } from '@hanzo/personas/team.js';
11
12
  import { FILES } from './team.gen.js';
12
- export const TEAM = [
13
- parse('dev', FILES.dev),
14
- parse('des', FILES.des),
15
- parse('vi', FILES.vi),
16
- parse('feynman', FILES.feynman),
17
- parse('einstein', FILES.einstein),
18
- parse('nora', FILES.nora),
19
- parse('maya', FILES.maya),
20
- parse('leo', FILES.leo),
21
- ];
13
+ export const TEAM = Object.entries(FILES).map(([id, text]) => parse(id, text));
22
14
  const BY_ID = new Map(TEAM.map((one) => [one.id, one]));
23
15
  /** The core member an agent name refers to, whatever its case, or undefined. */
24
16
  export const member = (name) => BY_ID.get(name?.toLowerCase() ?? '');
package/dist/thread.js CHANGED
@@ -33,6 +33,8 @@ import { attempt, bind, enlist, reread, snapshot, subscribe, sweep, tick, view }
33
33
  import { chord, named, said } from './chord.js';
34
34
  import { carry } from './stars.js';
35
35
  import { ShareDialog } from './Share.js';
36
+ /** Each client's last read of each shelf. */
37
+ const last = new WeakMap();
36
38
  /**
37
39
  * The member's conversations: pinned first, then most recently spoken in, the
38
40
  * archived ones left out — or, with `archived`, those alone — with every change
@@ -43,7 +45,8 @@ import { ShareDialog } from './Share.js';
43
45
  export function useThreadList(archived = false, seek = null) {
44
46
  const ai = useAi();
45
47
  // What was read, when, and from which shelf: the other shelf's rows are never drawn as this one's.
46
- const [read, setRead] = useState({ threads: [], at: 0, archived });
48
+ // A list mounted later (the palette) draws the client's last read of its shelf at once.
49
+ const [read, setRead] = useState(() => last.get(ai)?.[`${archived}`] ?? { threads: [], at: 0, archived });
47
50
  const [loading, setLoading] = useState(true);
48
51
  const [error, setError] = useState(null);
49
52
  const [asks, ask] = useState(0);
@@ -59,7 +62,9 @@ export function useThreadList(archived = false, seek = null) {
59
62
  if (control.signal.aborted)
60
63
  return;
61
64
  me.current.at = at;
62
- setRead({ threads, at, archived });
65
+ const got = { threads, at, archived };
66
+ last.set(ai, { ...last.get(ai), [`${archived}`]: got });
67
+ setRead(got);
63
68
  setError(null);
64
69
  setLoading(false);
65
70
  sweep();
package/dist/view.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /** The visual viewport, as `window.visualViewport` reports it. */
2
+ export interface Viewport {
3
+ height: number;
4
+ offsetTop: number;
5
+ scale: number;
6
+ }
7
+ /** Where the window stands, px. */
8
+ export interface Fit {
9
+ top: number;
10
+ height: number;
11
+ keyboard: number;
12
+ }
13
+ /**
14
+ * Where the window stands in a layout viewport `layout` px tall: null while the
15
+ * visual viewport is all of it, or zoomed in.
16
+ */
17
+ export declare function fit(layout: number, view: Viewport): Fit | null;
18
+ /** The window's top, for a style: the visual viewport's while a keyboard is up. */
19
+ export declare const TOP = "var(--view-top, 0px)";
20
+ /** The window's height, for a style: the visual viewport's while a keyboard is up. */
21
+ export declare const HEIGHT = "var(--view-height, 100dvh)";
22
+ /** The home indicator's inset, which the keyboard covers while it is up. */
23
+ export declare const FOOT = "max(0px, calc(var(--safe-bottom, 0px) - var(--keyboard, 0px)))";
24
+ interface Style {
25
+ setProperty(name: string, value: string): void;
26
+ removeProperty(name: string): string;
27
+ }
28
+ interface Source extends Viewport {
29
+ addEventListener(type: 'resize' | 'scroll', fn: () => void): void;
30
+ removeEventListener(type: 'resize' | 'scroll', fn: () => void): void;
31
+ }
32
+ interface Frame {
33
+ innerHeight: number;
34
+ requestAnimationFrame(fn: () => void): number;
35
+ cancelAnimationFrame(id: number): void;
36
+ }
37
+ /** Writes the window's place on `style` as `view` moves, once a frame; returns the stop. */
38
+ export declare function follow(style: Style, view: Source, frame: Frame): () => void;
39
+ /**
40
+ * The ref for the window: it follows the visual viewport while mounted. A
41
+ * browser without one leaves the window at 100dvh.
42
+ */
43
+ export declare function useView(): (node: unknown) => void;
44
+ export {};
package/dist/view.js ADDED
@@ -0,0 +1,98 @@
1
+ 'use client';
2
+ // The window kept inside what the reader can see, so a phone's keyboard never
3
+ // covers the composer.
4
+ //
5
+ // A phone's keyboard does not shrink the page: the layout viewport keeps its
6
+ // height and only the VISUAL viewport (`window.visualViewport`) loses the
7
+ // keyboard's height, and the browser pans it down the layout to show the field.
8
+ // So the app reads the visual viewport and stands the window in it: its top at
9
+ // the visual viewport's top, its height the visual viewport's. The bar stays at
10
+ // the top of what shows, the composer sits on the keyboard, and the transcript
11
+ // scrolls in what is left between them.
12
+ //
13
+ // const view = useView()
14
+ // <YStack ref={view} $max-md={{ position: 'fixed', t: TOP, height: HEIGHT }}>
15
+ //
16
+ // It writes three custom properties on the element, which every descendant
17
+ // reads, and nothing else: no state, so a keyboard opening re-renders nothing.
18
+ //
19
+ // --view-top px from the layout viewport's top to the visual one's
20
+ // --view-height px of the visual viewport
21
+ // --keyboard px the keyboard takes from the view; 0 when it is down
22
+ //
23
+ // While the visual viewport is the layout one, the first two are unset (the
24
+ // window keeps 100dvh) and --keyboard is 0. A pinch-zoom shrinks the visual
25
+ // viewport too, and the window does not chase it. Events are folded into one
26
+ // write per frame.
27
+ import { useCallback, useRef } from 'react';
28
+ /** A zoom past this is the reader's pinch, not a keyboard. */
29
+ const ZOOM = 1.01;
30
+ /**
31
+ * Where the window stands in a layout viewport `layout` px tall: null while the
32
+ * visual viewport is all of it, or zoomed in.
33
+ */
34
+ export function fit(layout, view) {
35
+ if (view.scale > ZOOM)
36
+ return null;
37
+ const height = Math.round(view.height);
38
+ const keyboard = Math.round(layout) - height;
39
+ if (keyboard < 1)
40
+ return null;
41
+ return { top: Math.max(0, Math.round(view.offsetTop)), height, keyboard };
42
+ }
43
+ /** The window's top, for a style: the visual viewport's while a keyboard is up. */
44
+ export const TOP = 'var(--view-top, 0px)';
45
+ /** The window's height, for a style: the visual viewport's while a keyboard is up. */
46
+ export const HEIGHT = 'var(--view-height, 100dvh)';
47
+ /** The home indicator's inset, which the keyboard covers while it is up. */
48
+ export const FOOT = 'max(0px, calc(var(--safe-bottom, 0px) - var(--keyboard, 0px)))';
49
+ /** Writes the window's place on `style` as `view` moves, once a frame; returns the stop. */
50
+ export function follow(style, view, frame) {
51
+ let queued = 0;
52
+ let last = null;
53
+ const write = () => {
54
+ queued = 0;
55
+ const at = fit(frame.innerHeight, view);
56
+ const key = at ? `${at.top}/${at.height}/${at.keyboard}` : '';
57
+ if (key === last)
58
+ return;
59
+ last = key;
60
+ if (at) {
61
+ style.setProperty('--view-top', `${at.top}px`);
62
+ style.setProperty('--view-height', `${at.height}px`);
63
+ style.setProperty('--keyboard', `${at.keyboard}px`);
64
+ }
65
+ else {
66
+ style.removeProperty('--view-top');
67
+ style.removeProperty('--view-height');
68
+ style.setProperty('--keyboard', '0px');
69
+ }
70
+ };
71
+ const queue = () => {
72
+ if (!queued)
73
+ queued = frame.requestAnimationFrame(write);
74
+ };
75
+ view.addEventListener('resize', queue);
76
+ view.addEventListener('scroll', queue);
77
+ write();
78
+ return () => {
79
+ view.removeEventListener('resize', queue);
80
+ view.removeEventListener('scroll', queue);
81
+ if (queued)
82
+ frame.cancelAnimationFrame(queued);
83
+ };
84
+ }
85
+ /**
86
+ * The ref for the window: it follows the visual viewport while mounted. A
87
+ * browser without one leaves the window at 100dvh.
88
+ */
89
+ export function useView() {
90
+ const stop = useRef(null);
91
+ return useCallback((node) => {
92
+ stop.current?.();
93
+ stop.current = null;
94
+ // A gui stack hands its host element through a ref typed for native too; on the web it is the element.
95
+ if (node instanceof HTMLElement && window.visualViewport)
96
+ stop.current = follow(node.style, window.visualViewport, window);
97
+ }, []);
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,14 +49,14 @@
49
49
  "peerDependencies": {
50
50
  "@hanzo/ai": ">=0.6.23",
51
51
  "@hanzo/appearance": ">=0.2.6",
52
- "@hanzo/build": ">=0.2.57",
52
+ "@hanzo/build": ">=0.2.61",
53
53
  "@hanzo/composer": ">=0.2.0",
54
54
  "@hanzo/event": ">=0.3.56",
55
55
  "@hanzo/gui": "^8.3.5",
56
- "@hanzo/iam": ">=0.21.28",
56
+ "@hanzo/iam": ">=0.21.33",
57
57
  "@hanzo/logo": ">=1.0.26",
58
58
  "@hanzo/studio": ">=0.1.0",
59
- "@hanzo/ui": ">=8.27.67",
59
+ "@hanzo/ui": ">=8.29.0",
60
60
  "@hanzo/usage": ">=0.1.18",
61
61
  "@hanzo/voice": ">=0.1.16",
62
62
  "@hanzogui/shell": "^8.3.20",
@@ -67,15 +67,15 @@
67
67
  "devDependencies": {
68
68
  "@hanzo/ai": "0.6.23",
69
69
  "@hanzo/appearance": "0.2.8",
70
- "@hanzo/build": "0.2.57",
70
+ "@hanzo/build": "0.2.61",
71
71
  "@hanzo/composer": "0.2.0",
72
72
  "@hanzo/design": "0.5.34",
73
73
  "@hanzo/event": "0.3.56",
74
74
  "@hanzo/gui": "8.3.5",
75
- "@hanzo/iam": "0.21.28",
75
+ "@hanzo/iam": "0.21.33",
76
76
  "@hanzo/logo": "1.0.26",
77
77
  "@hanzo/studio": "0.1.0",
78
- "@hanzo/ui": "8.27.67",
78
+ "@hanzo/ui": "8.29.0",
79
79
  "@hanzo/usage": "0.1.18",
80
80
  "@hanzo/voice": "0.1.16",
81
81
  "@hanzogui/config": "8.3.5",
package/dist/Account.d.ts DELETED
@@ -1,12 +0,0 @@
1
- /** The block at the foot of the sidebar: press it for the menu. */
2
- export declare function Account({ onToggleCollapse, onProfile, onAppearance, onUsage, onKeys, }?: {
3
- onToggleCollapse?: () => void;
4
- /** Opens the profile beside the room. Absent, the row goes to hanzo.id. */
5
- onProfile?: () => void;
6
- /** Opens the appearance settings. Absent, the row is not drawn. */
7
- onAppearance?: () => void;
8
- /** Opens the plan's usage settings. Absent, the row is not drawn. */
9
- onUsage?: () => void;
10
- /** Opens the API keys settings. Absent, the row is not drawn. */
11
- onKeys?: () => void;
12
- }): import("react").JSX.Element;
package/dist/Account.js DELETED
@@ -1,65 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- /**
4
- * WHO YOU ARE, at the foot of the sidebar: the person, and the person's menu.
5
- *
6
- * The plan leads it — @hanzo/build's `Meter` (Standing.tsx, loaded with the
7
- * menu), the block every account menu opens with: the plan by its family and its
8
- * windows as shares, the free allowance on Free, the balance only with no plan. Then profile, appearance, usage, billing,
9
- * API keys, security and sign out — the account, and nothing that belongs to the
10
- * workspace. Which workspace you stand in is the switcher at the TOP of the
11
- * sidebar, and what it may spend is its Billing: a plan never sits beside a
12
- * balance here.
13
- */
14
- import { lazy, Suspense, useEffect, useRef, useState } from 'react';
15
- import { Activity, ChevronsUpDown, CreditCard, KeyRound, LogOut, PanelLeftClose, Palette, Shield, UserRound } from 'lucide-react';
16
- import { XStack, YStack, Text, Box } from '@hanzo/ui';
17
- import { sheet } from '@hanzo/ui/glass';
18
- import { useIam } from '@hanzo/iam/react';
19
- import { useSignOut } from './lib/signout.js';
20
- import { useAccount } from './lib/account.js';
21
- import { Face } from './cast.js';
22
- import { iam } from './lib/api.js';
23
- import { link } from './lib/tags.js';
24
- import { showSettings } from './open.js';
25
- import { site } from './where.js';
26
- /** The plan at the menu's head, loaded with the menu. */
27
- const Standing = lazy(() => import('./Standing.js').then((m) => ({ default: m.Standing })));
28
- function Row({ icon, onPress, children, }) {
29
- return (_jsx(Box, { render: "button", onClick: onPress, width: "100%", px: "$3", py: "$2", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", children: [icon, _jsx(Text, { fontSize: "$2", color: "$ink", children: children })] }) }));
30
- }
31
- /** The block at the foot of the sidebar: press it for the menu. */
32
- export function Account({ onToggleCollapse, onProfile, onAppearance, onUsage, onKeys, } = {}) {
33
- const { user } = useIam();
34
- const logout = useSignOut();
35
- const { user: account } = useAccount();
36
- const [open, setOpen] = useState(false);
37
- const holder = useRef(null);
38
- useEffect(() => {
39
- if (!open)
40
- return;
41
- const away = (e) => {
42
- if (holder.current && !holder.current.contains(e.target))
43
- setOpen(false);
44
- };
45
- const esc = (e) => e.key === 'Escape' && setOpen(false);
46
- document.addEventListener('mousedown', away);
47
- document.addEventListener('keydown', esc);
48
- return () => {
49
- document.removeEventListener('mousedown', away);
50
- document.removeEventListener('keydown', esc);
51
- };
52
- }, [open]);
53
- const called = account?.name || user?.displayName || user?.name || user?.email || 'You';
54
- const email = user?.email || '';
55
- const face = account?.avatar ?? user?.avatar;
56
- const act = (fn) => () => {
57
- setOpen(false);
58
- fn();
59
- };
60
- return (_jsxs(YStack
61
- // @ts-expect-error — ref is forwarded to the DOM node by the primitive.
62
- , {
63
- // @ts-expect-error — ref is forwarded to the DOM node by the primitive.
64
- ref: holder, position: "relative", borderTopWidth: 1, borderColor: "$borderColor", children: [open ? (_jsxs(YStack, { role: "menu", "aria-label": "Account", position: "absolute", b: "calc(100% + 6px)", l: "$2", r: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", ...sheet(2), py: "$2", gap: "$1", z: "var(--z-modal)", children: [_jsx(Suspense, { fallback: null, children: _jsx(Standing, { onPlans: act(() => window.location.assign(site('/pricing'))), onBilling: act(() => showSettings('billing')) }) }), _jsx(Box, { height: 1, bg: "$borderColor", my: "$1" }), _jsx(Row, { icon: _jsx(UserRound, { size: 15, "aria-hidden": true }), onPress: act(() => (onProfile ? onProfile() : window.location.assign(link(`${iam()}/account`)))), children: "Profile" }), onAppearance ? (_jsx(Row, { icon: _jsx(Palette, { size: 15, "aria-hidden": true }), onPress: act(onAppearance), children: "Appearance" })) : null, onUsage ? (_jsx(Row, { icon: _jsx(Activity, { size: 15, "aria-hidden": true }), onPress: act(onUsage), children: "Usage" })) : null, _jsx(Row, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), onPress: act(() => showSettings('billing')), children: "Billing" }), onKeys ? (_jsx(Row, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), onPress: act(onKeys), children: "API keys" })) : null, _jsx(Row, { icon: _jsx(Shield, { size: 15, "aria-hidden": true }), onPress: act(() => window.location.assign(link(`${iam()}/account/security`))), children: "Security" }), _jsx(Box, { height: 1, bg: "$borderColor", my: "$1" }), _jsx(Row, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), onPress: act(() => void logout()), children: "Sign out" })] })) : null, _jsxs(XStack, { items: "center", width: "100%", children: [_jsx(Box, { render: "button", onClick: () => setOpen((v) => !v), "aria-expanded": open, "aria-haspopup": "menu", "aria-label": "Account", flex: 1, minW: 0, px: "$3", py: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Face, { src: face, name: called, size: 22 }), _jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, children: called }), email && email !== called ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: email })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true })] }) }), onToggleCollapse ? (_jsx(Box, { render: "button", onClick: onToggleCollapse, "aria-label": "Collapse sidebar", p: "$2", mr: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(PanelLeftClose, { size: 16, "aria-hidden": true }) })) : null] })] }));
65
- }
package/dist/Find.d.ts DELETED
@@ -1,7 +0,0 @@
1
- import { type OrgCommandItem } from "@hanzogui/shell";
2
- export declare function Find({ doors, onClose, navigate, }: {
3
- /** Where the shell can go without asking anyone: its rooms and its panes. */
4
- doors: OrgCommandItem[];
5
- onClose: () => void;
6
- navigate?: (route: string) => void;
7
- }): import("react").JSX.Element;
package/dist/Find.js DELETED
@@ -1,82 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- /**
4
- * ⌘K, in the workspace.
5
- *
6
- * The estate's palette — `OrgCommandPalette`, the same frame the marketing
7
- * pages open — so the chord means one thing wherever a reader presses it. Here
8
- * it used to focus the column's filter box, which left the shortcut that finds
9
- * anything on every other page of this site narrowing one list in the app: a
10
- * conversation in the room next door was unreachable from a control offering to
11
- * search the workspace.
12
- *
13
- * MOUNTED ONLY WHILE OPEN. Its rows are read from the platform, and a palette
14
- * that stayed mounted would fetch every conversation and every project on each
15
- * load of a room that draws neither.
16
- */
17
- import { dev } from "./lib/host.js";
18
- import { useEffect, useState } from "react";
19
- import { OrgCommandPalette } from "@hanzogui/shell";
20
- import { useThreads } from "@hanzo/ai/react";
21
- import { useAi } from "./lib/ai.js";
22
- import { useOpen } from "./open.js";
23
- export function Find({ doors, onClose, navigate, }) {
24
- const { threads } = useThreads();
25
- const { client } = useAi();
26
- const { open } = useOpen();
27
- const [projects, setProjects] = useState([]);
28
- useEffect(() => {
29
- if (!client)
30
- return;
31
- let live = true;
32
- client.http
33
- .json({ path: "/v1/projects" })
34
- .then((found) => live && setProjects(found))
35
- .catch(() => {
36
- // A palette that cannot read the projects still finds the rooms.
37
- });
38
- return () => {
39
- live = false;
40
- };
41
- }, [client]);
42
- const rows = [
43
- ...doors,
44
- // A conversation and a project are OPENED, not navigated to: the address is
45
- // the room, and which one is open belongs to the store — so a href would
46
- // land the reader in /chat with whatever they had before still on screen.
47
- ...threads.map((t) => ({
48
- id: `thread-${t.id}`,
49
- title: t.title || "Untitled",
50
- category: "Conversations",
51
- action: () => {
52
- open(t.id);
53
- navigate?.("/chat");
54
- },
55
- })),
56
- ...projects.map((p) => ({
57
- id: `project-${p.id}`,
58
- title: p.name,
59
- category: "Projects",
60
- action: () => {
61
- navigate?.(dev(p.name));
62
- },
63
- })),
64
- ];
65
- return (_jsx(OrgCommandPalette, { open: true, currentAppId: "chat", commands: rows, onOpenChange: (next) => {
66
- if (!next)
67
- onClose();
68
- }, onNavigate: (href, external) => {
69
- if (external)
70
- window.open(href, "_blank", "noopener");
71
- else
72
- navigate?.(href);
73
- onClose();
74
- },
75
- // THIS ROOM IS THE AI. A question nobody indexed opens a conversation
76
- // here, rather than a second copy of the product the reader is in.
77
- onAsk: (question) => {
78
- open(null);
79
- navigate?.(`/chat?q=${encodeURIComponent(question)}`);
80
- onClose();
81
- } }));
82
- }