paseo-aydo 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +142 -2
  3. package/client/AttachmentPill.tsx +145 -0
  4. package/client/Avatar.tsx +94 -0
  5. package/client/BotDialogs.tsx +364 -0
  6. package/client/BotSidebar.tsx +831 -0
  7. package/client/BotsNav.tsx +65 -0
  8. package/client/BotsSurface.tsx +907 -0
  9. package/client/ChatPane.tsx +237 -0
  10. package/client/Markdown.tsx +315 -0
  11. package/client/Splash.tsx +38 -0
  12. package/client/approvals.ts +11 -0
  13. package/client/auto-start.ts +9 -0
  14. package/client/chat/Composer.tsx +722 -0
  15. package/client/chat/FindBar.tsx +115 -0
  16. package/client/chat/Permission.tsx +152 -0
  17. package/client/chat/Stream.tsx +478 -0
  18. package/client/chat/composer/CommandMenu.tsx +94 -0
  19. package/client/chat/composer/ContextMeter.tsx +105 -0
  20. package/client/chat/composer/drafts.ts +103 -0
  21. package/client/chat/composer/height.ts +51 -0
  22. package/client/chat/composer/logic.ts +309 -0
  23. package/client/chat/composer/storage.ts +97 -0
  24. package/client/chat/follow.ts +26 -0
  25. package/client/chat/stream/ApprovalCard.tsx +154 -0
  26. package/client/chat/stream/PlanCard.tsx +79 -0
  27. package/client/chat/stream/ProposalCard.tsx +309 -0
  28. package/client/chat/stream/QuestionForm.tsx +313 -0
  29. package/client/chat/stream/RoutineRunCard.tsx +71 -0
  30. package/client/chat/stream/details.tsx +358 -0
  31. package/client/chat/stream/model.ts +469 -0
  32. package/client/chat/stream/question.ts +103 -0
  33. package/client/chat/stream/rows.tsx +465 -0
  34. package/client/chat/stream/ui.tsx +331 -0
  35. package/client/chat-engine.ts +20 -0
  36. package/client/data.ts +176 -0
  37. package/client/draft.ts +16 -0
  38. package/client/i18n/messages.ts +5 -0
  39. package/client/i18n/use-t.ts +9 -0
  40. package/client/intent.ts +16 -0
  41. package/client/keyboard.ts +92 -0
  42. package/client/library/LibraryList.tsx +180 -0
  43. package/client/library/LibraryView.tsx +287 -0
  44. package/client/library/McpPage.tsx +158 -0
  45. package/client/library/McpSheets.tsx +206 -0
  46. package/client/library/SkillPage.tsx +159 -0
  47. package/client/library/SkillSheets.tsx +197 -0
  48. package/client/library/parts.tsx +86 -0
  49. package/client/native-agent-view.ts +28 -0
  50. package/client/native.ts +177 -0
  51. package/client/nav-hosts.ts +14 -0
  52. package/client/navigation.ts +33 -0
  53. package/client/panel/AgentSections.tsx +366 -0
  54. package/client/panel/ApprovalsList.tsx +62 -0
  55. package/client/panel/BotPanel.tsx +306 -0
  56. package/client/panel/IdentitySection.tsx +137 -0
  57. package/client/panel/KnowledgeSections.tsx +389 -0
  58. package/client/panel/LibraryPicker.tsx +63 -0
  59. package/client/panel/MaintenanceSection.tsx +103 -0
  60. package/client/panel/MemoryActivity.tsx +160 -0
  61. package/client/panel/OverviewSections.tsx +205 -0
  62. package/client/panel/RememberedProjects.tsx +42 -0
  63. package/client/panel/RoutinesSection.tsx +490 -0
  64. package/client/panel/controls.tsx +517 -0
  65. package/client/panel/maintenance-text.ts +32 -0
  66. package/client/panel/picture.tsx +76 -0
  67. package/client/paseo.ts +11 -0
  68. package/client/sent-attachments.ts +31 -0
  69. package/client/settings/BotsSettings.tsx +219 -0
  70. package/client/speech.ts +97 -0
  71. package/client/teams/TeamMap.tsx +191 -0
  72. package/client/teams/TeamSheet.tsx +136 -0
  73. package/client/teams/TeamTabs.tsx +134 -0
  74. package/client/theme.ts +41 -0
  75. package/client/typography.ts +123 -0
  76. package/client/ui/Columns.tsx +120 -0
  77. package/client/ui/Menu.tsx +236 -0
  78. package/client/ui/SearchableSelect.tsx +158 -0
  79. package/client/ui/Tooltip.ts +197 -0
  80. package/client/useBotSettings.ts +48 -0
  81. package/client/useChat.ts +194 -0
  82. package/client/web.ts +444 -0
  83. package/client/workspace-choice.ts +56 -0
  84. package/index.client.tsx +96 -0
  85. package/index.server.ts +296 -0
  86. package/package.json +44 -4
  87. package/paseo-plugin.json +7 -0
  88. package/server/activity.ts +123 -0
  89. package/server/approvals-file.ts +46 -0
  90. package/server/approvals-store.ts +181 -0
  91. package/server/approvals.ts +221 -0
  92. package/server/ask-store.ts +102 -0
  93. package/server/asks.ts +326 -0
  94. package/server/bot-home.ts +121 -0
  95. package/server/chat-send.ts +54 -0
  96. package/server/chats.ts +61 -0
  97. package/server/commands.ts +86 -0
  98. package/server/file-tools.ts +411 -0
  99. package/server/history-files.ts +82 -0
  100. package/server/host-caps.ts +15 -0
  101. package/server/host.ts +85 -0
  102. package/server/identity.ts +69 -0
  103. package/server/journal.ts +174 -0
  104. package/server/library.ts +200 -0
  105. package/server/maintenance.ts +46 -0
  106. package/server/managed-files.ts +93 -0
  107. package/server/mcp-probe.ts +279 -0
  108. package/server/mcp-sources.ts +34 -0
  109. package/server/memory.ts +194 -0
  110. package/server/paseo.ts +6 -0
  111. package/server/project-grants.ts +69 -0
  112. package/server/prompt.ts +91 -0
  113. package/server/proposal-notices.ts +72 -0
  114. package/server/proposals.ts +74 -0
  115. package/server/relay-state.ts +38 -0
  116. package/server/relay.ts +159 -0
  117. package/server/reports.ts +199 -0
  118. package/server/scheduler.ts +335 -0
  119. package/server/setup-files.ts +85 -0
  120. package/server/share.ts +236 -0
  121. package/server/tidy.ts +285 -0
  122. package/server/tools/approvals.ts +60 -0
  123. package/server/tools/bots.ts +231 -0
  124. package/server/tools/chats.ts +76 -0
  125. package/server/tools/index.ts +11 -0
  126. package/server/tools/mcp.ts +92 -0
  127. package/server/tools/routines.ts +27 -0
  128. package/server/tools/setup.ts +53 -0
  129. package/server/tools/skills.ts +23 -0
  130. package/server/tools/workspaces.ts +26 -0
  131. package/server/turn-facts.ts +48 -0
  132. package/server/turn-timeline.ts +47 -0
  133. package/server/uploads.ts +13 -0
  134. package/server/workspaces.ts +170 -0
  135. package/shared/activity.ts +234 -0
  136. package/shared/approvals.ts +108 -0
  137. package/shared/attachments.ts +81 -0
  138. package/shared/avatar.ts +567 -0
  139. package/shared/bot-tools.ts +73 -0
  140. package/shared/bot.ts +794 -0
  141. package/shared/changes.ts +847 -0
  142. package/shared/chat.ts +237 -0
  143. package/shared/commands.ts +9 -0
  144. package/shared/diff.ts +96 -0
  145. package/shared/file-tools.ts +60 -0
  146. package/shared/folder-access.ts +36 -0
  147. package/shared/groups.ts +162 -0
  148. package/shared/host-caps.ts +16 -0
  149. package/shared/identity.ts +106 -0
  150. package/shared/layout.ts +27 -0
  151. package/shared/library.ts +161 -0
  152. package/shared/locales/en.json +1340 -0
  153. package/shared/locales/zh-CN.json +1312 -0
  154. package/shared/markdown.ts +888 -0
  155. package/shared/memory-maintenance.ts +167 -0
  156. package/shared/pages.ts +19 -0
  157. package/shared/paseo-tools.ts +30 -0
  158. package/shared/pixel.ts +156 -0
  159. package/shared/playbooks.ts +36 -0
  160. package/shared/proposals.ts +70 -0
  161. package/shared/reports.ts +157 -0
  162. package/shared/routines.ts +380 -0
  163. package/shared/rpc.ts +402 -0
  164. package/shared/setup.ts +89 -0
  165. package/shared/sidebar.ts +125 -0
  166. package/shared/skills.ts +75 -0
  167. package/shared/speech.ts +32 -0
  168. package/shared/team-logo.ts +325 -0
  169. package/shared/templates.ts +91 -0
  170. package/shared/time.ts +16 -0
  171. package/shared/tools-mode.ts +37 -0
  172. package/shared/tools.ts +481 -0
  173. package/shared/transcript.ts +45 -0
  174. package/shared/translate.ts +38 -0
  175. package/shared/uuid.ts +11 -0
  176. package/shared/version.ts +5 -0
  177. package/shared/workspaces.ts +63 -0
  178. package/tsconfig.json +25 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ondrej Rehacek
4
+ Copyright (c) 2026 quickwind
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="docs/bots.svg" width="512" alt="Six pixel-art assistants">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ # Aydo, offline edition
6
+
7
+ Aydo: personal assistants for [Paseo](https://paseo.sh) and Irix: give each assistant instructions, a model, memory, skills and tools, then chat with it in its own workspace or let it run routines.
8
+
9
+ Aydo is based on [oliexe/paseo-bots](https://github.com/oliexe/paseo-bots) 0.2.0 (MIT, by Ondrej Rehacek) for machines that must not send data to cloud services. **The plugin makes no network connections of its own.** Everything that needed one is gone, and a test keeps it that way.
10
+
11
+ ## What is different
12
+
13
+ | Upstream feature | Offline edition |
14
+ | --- | --- |
15
+ | Connected apps through Composio (Gmail, Slack, Notion, 1,000+ more) | Removed, with its relay proxy, API key, sign-in cards and the roles built on it (Inbox, Watch, Ops) |
16
+ | Avatar pictures drawn by OpenAI | Removed; pixel-art faces, and pictures you upload |
17
+ | Pictures from an image address | Removed; a picture is kept only as an uploaded `data:` image |
18
+ | Importing skills from GitHub or a link | Imports from **a folder on this computer** instead |
19
+ | Testing MCP servers at a remote address (HTTP, SSE) | The plugin only tests programs and `http://localhost` addresses. A remote server (for example one inside your company) is imported and can be turned on without a test; the assistant's agent is the one that connects to it |
20
+ | GitHub link on the start screen | Removed |
21
+
22
+ Everything else is unchanged: assistants, memory, routines and webhooks, teams, the skill and MCP library, `/learn`, Paseo's own tools for assistants, and sharing assistants and teams as files.
23
+
24
+ ## What this does not control
25
+
26
+ - **The assistants' own agents.** An assistant runs on a provider (Claude Code, for instance) that talks to its own service. That is the provider's traffic, not the plugin's.
27
+ - **MCP servers.** The agent connects to the servers you give an assistant, remote ones included; the plugin never does. A server may itself reach the internet. Add only servers you trust.
28
+ - **Links in a chat.** A link in an assistant's reply opens in your browser only when you press it.
29
+ - **The plugin runs unsandboxed**, like every Paseo plugin. Read the code before installing it.
30
+
31
+ ## Screenshots
32
+
33
+ | | |
34
+ | --- | --- |
35
+ | **Assistants** | **Assistant settings** |
36
+ | ![Aydo](docs/splash.png) | ![Assistant settings](docs/bot.png) |
37
+ | **Skills** | **MCP servers** |
38
+ | ![Skills](docs/skills.png) | ![MCP servers](docs/mcp.png) |
39
+ | **Teams** | |
40
+ | ![Teams](docs/teams.png) | |
41
+
42
+ ## Install
43
+
44
+ Get the plugin folder onto the machine (the offline package unzips into one), then install it from that folder.
45
+
46
+ ``bash
47
+ irix plugin install /path/to/aydo # Irix
48
+ paseo plugin install /path/to/aydo # Paseo
49
+ ``
50
+
51
+ Turn on **Settings → Plugins → Enable plugins** first. Requires Paseo 0.13 or later. Aydo shows in English and Chinese (zh-CN), following the Paseo/Irix language setting. Irix installs plugins from a local folder only, which is what this edition is for.
52
+
53
+ ## Usage
54
+
55
+ ### Assistants
56
+
57
+ 1. Open **Aydo** in Paseo's sidebar and press **New assistant**. Start blank, from a role, from one of your presets, or paste an assistant or team file under **Import**.
58
+ 2. Chat with it. Every chat is a thread in the assistant's workspace; **Open in Paseo** in a chat's menu shows it in Paseo's own view. On Irix builds that offer the embedded chat (`ui.AgentView`), an open chat is Irix's own chat view inside the Assistants page, so the model, mode and thinking pickers, slash commands and other plugins' cards work there too; the first message of a new chat is still typed in the plugin's own box, because it builds the assistant's system prompt. On Paseo without it, the plugin's own chat window is used.
59
+ 3. Open its settings with the panel button at the top right of a chat, or **Open assistant settings** in the assistant's menu. **Identity** holds the name, avatar and voice, **Soul** its standing instructions, **Model** the provider and model, and **Permissions** the approval mode.
60
+
61
+ Assistants can do the setting up too. Ask one in a chat, for example "Create a research team of three assistants with Scout as Chief of Staff", and it shows every change on one card; **Apply changes** makes them. An assistant can create, edit and delete assistants, teams, routines, library skills and MCP servers, and change what new assistants start with. Each assistant's earlier settings stay under **History** in its settings.
62
+
63
+ **Devin.** Devin has no system prompt, so the plugin writes each assistant's identity (name, standing instructions, memory, skills, team) to `.devin/rules/bot-identity.md` in the assistant's own folder, as an always-on rule that Devin reads when a chat starts. It is rewritten when the settings or the memory change and before each chat, so both a chat started in Assistants and a plain new chat in the assistant's workspace know who the assistant is. The file is generated: edit the assistant's settings, not the file. Over 32 KB it is split into several files, and the assistant's Overview says so. An assistant with its own working folder gets nothing written there; its Devin chats carry the identity in the first message instead. Its tools are files too; see **Assistant tools without MCP** below.
64
+
65
+ **Assistant tools without MCP.** The plugin's tools (reading the setup, proposing changes, routines and skills, asking another assistant, searching past chats) normally come to an assistant as an MCP server. Where MCP is not allowed (Devin here), they come as files in the assistant's own folder instead. Settings > Plugins > Aydo > **Default: tools come as** sets the default for every assistant, MCP or files; an assistant can choose its own under **Agent > Tools come as**. An assistant on Devin or Antigravity starts with its own choice, files.
66
+
67
+ - `setup/` describes the assistants and the setup, and the format of the proposal files; it is kept current by the plugin.
68
+ - `proposals/`: the assistant writes one JSON file (`changes`, `routine` or `skill`) as the last step of a turn; when the turn ends the plugin checks it and posts the same confirmation card as the tool would. A file that is not accepted is reported back to the assistant, with the reason in `proposals/done/`.
69
+ - `outbox/`: the assistant writes an `ask` file to ask another assistant. With "Ask first" the user approves a card; the other assistant answers in a chat of its own and the answer arrives in the asking chat as a message. A chat started by another assistant cannot ask further. A question unanswered for 30 minutes is closed. A follow-up to the same assistant from the same chat goes into the chat that assistant already has with it (an hour of quiet starts a new one); if that chat is still on the last question, the follow-up waits its turn. `ask_bot` does the same.
70
+ - `history/`: a transcript of each recent chat, for the assistant to search with its own file tools.
71
+
72
+ A file belongs to the chat whose turn wrote it (the turn's tool calls name the file), so two chats of one assistant do not take each other's files. An assistant with its own working folder writes into its home folder, which Devin may ask to approve. A plain new chat in an assistant's folder counts as that assistant's chat.
73
+
74
+ **Set up with the assistant.** An assistant made with **Blank assistant** opens with a short interview: it asks about what you want from it, how you work and what it must never do, then suggests three names (the last question), proposes its name, title, description and instructions on one card and writes a starting `MEMORY.md` once you confirm. After you press **Apply changes** the card says which model the assistant uses and how memory tidy is set, and where to change them. Every proposal card says what happened after you press a button, and shows why on the card when it did not work. It does not change its provider, model or approval mode; you set those under **Model** and **Permissions**. You can start the interview again from **Overview > Set up with the assistant**. Under Settings > Plugins > Aydo, **Set up with the assistant** holds the prompt (leave it empty for the built-in one; name a skill in it to have the assistant use it, if the skill is on for that assistant) and a switch to stop blank assistants from starting it by themselves. Assistants made from a role or preset never start it by themselves.
75
+
76
+ ### Memory
77
+
78
+ An assistant updates its `MEMORY.md` when it learns something worth keeping, for example when you say "Remember that I prefer short answers". New chats start with it.
79
+
80
+ In the assistant's settings, **Memory** shows `MEMORY.md` and its topic files, which you can edit, a **Daily log** of its chats, and **Changes**, where **Undo** puts a file back the way it was.
81
+
82
+ **Maintenance.** Under **Memory > Maintenance** two switches keep memory useful as it grows (on for new assistants, off for existing ones):
83
+
84
+ - **Save notes when a chat fills up.** When a chat passes the chosen share of its context window, the app asks it, once, to write what is worth keeping to `memory/inbox.md`. You see that message and a one-line reply in the chat. A provider that does not report context use is never asked.
85
+ - **Tidy memory automatically.** On an interval (it also runs after a laptop wakes), the app starts a "Memory tidy" chat that moves what is durable from the inbox and the daily log into `MEMORY.md`, merges duplicates, drops what is out of date and empties the inbox. It skips when there is nothing new, waits while you are chatting with the assistant, runs at most six times a day, and backs off for a day if memory stays over budget. **Tidy now** runs it at once. The model is Paseo's Metadata generation setting by default, or the assistant's own. New Devin assistants start with tidy off, because every run spends your Devin quota.
86
+
87
+ Every change shows under **Changes** with undo.
88
+
89
+ ### Routines
90
+
91
+ 1. In the assistant's settings, open **Routines** and press **New routine**.
92
+ 2. Give it a name and a prompt. The prompt is the first message of each run.
93
+ 3. Pick when it repeats: a preset such as **Weekdays 9:00** or your own cron expression, **Once**, or **When its webhook is called**.
94
+ 4. To collect results in one place, pick a chat under **Post results to**. Each run still gets its own chat.
95
+
96
+ Assistants can propose routines too: ask in a chat, then press **Create routine** on the card. A webhook routine's URL is under **Copy webhook URL** in its menu; `POST` to it from this computer.
97
+
98
+ ### Teams
99
+
100
+ 1. Press **Team map** at the bottom of the assistant list, then **New team**.
101
+ 2. Name the team, switch on its members, pick a **Lead** as its Chief of Staff, and add shared instructions for every member. The team gets a pixel-art logo; **Reroll** draws another, or use your own picture.
102
+ 3. Send your requests to the Chief of Staff. It hands parts of the work to its teammates.
103
+
104
+ With teams, the Aydo screen gets a tab for each team, and **Other assistants** for assistants without one (a menu on phones); **+** next to the tabs adds a team, and right-clicking a tab edits it. A team can let its members ask each other without you approving every question (off by default; an assistant set to never contact others still may not), and its Chief of Staff can ask several teammates at once. A team file carries your assistants and their teams: find it in **Settings → Plugins**, under **Assistants** in the paseo-bots menu.
105
+
106
+ Whether an assistant may ask others for help is set in its settings under **Permissions → Contact other assistants**: **Ask first**, **Allowed** or **Off**.
107
+
108
+ ### Skills & Tools
109
+
110
+ Press **Skills & Tools** at the bottom of the assistant list.
111
+
112
+ - **Skills**: press **+** to import skills from a folder on this computer (the full path; every `SKILL.md` under it comes in, up to 30) or write a new one. Open an imported skill and press **Review** to turn it on. **Update** on a skill reads its folder again.
113
+ - **MCP servers**: press **+**, then **New server**, or **Import config** to pick up the servers set up in Claude Code, Claude Desktop or Cursor. Press **Test and turn on** on the server's page; a remote server has no test and is turned on with its switch.
114
+
115
+ Then give them to an assistant in its settings: skills under **Skills**, MCP servers under **Access**.
116
+
117
+ ### Learning
118
+
119
+ After a task, send `/learn` in the chat, or `/learn <what to focus on>`. The assistant writes up what worked as a skill and shows it as a card; press **Save skill** to add it to Skills & Tools and turn it on for that assistant.
120
+
121
+ ### Paseo tools
122
+
123
+ Assistants get Paseo's own tools, so you can ask one in a chat to start other agents, open workspaces and terminals, set up schedules or use the browser. An assistant's **Access** settings show whether they're on, and **Turn on** there switches them on for every agent on the host.
124
+
125
+ ## Building the offline package
126
+
127
+ ``bash
128
+ npm ci --ignore-scripts
129
+ npm run typecheck && npm test
130
+ node scripts/pack-offline.mjs --out ../aydo-offline
131
+ ``
132
+
133
+ The package is a zip of the plugin's source files only (the daemon compiles them when the plugin starts). It has no runtime dependencies, so installing it needs no npm and no internet.
134
+
135
+ `tests/offline.test.ts` scans the whole source for network code (`fetch`, sockets, WebSocket, a remote address in code, an image loaded from an address) and fails if it finds any outside the one MCP probe, which refuses everything but this computer (remote MCP servers are the agent's to connect to). Run it after every change.
136
+
137
+ ## Staying close to upstream
138
+
139
+ `upstream` is the original repository. Merge it into the `offline` branch from time to time; the removed features will conflict, and the test above will tell you if a merge brings network code back.
140
+
141
+ ## License
142
+
143
+ MIT, as upstream. See `LICENSE`. The design follows OpenMausBot (Apache-2.0); see `NOTICE`.
@@ -0,0 +1,145 @@
1
+ import type { PluginTheme } from "@getpaseo/plugin";
2
+ import { Icon, Modal, ScrollView } from "@getpaseo/plugin/client/react-native";
3
+ import { useState, type ReactNode } from "react";
4
+ import { ActivityIndicator, Image, Platform, Pressable, Text, View, useWindowDimensions } from "react-native";
5
+ import { getFileTypeLabel, type ComposerAttachment } from "../shared/attachments";
6
+ import { MONO_FONT, MONO_PROPS, nativeTokens } from "./native";
7
+ import { code, codeLine, ui } from "./typography";
8
+ import { tooltip } from "./ui/Tooltip";
9
+ import { useT } from "./i18n/use-t";
10
+
11
+ type Colors = PluginTheme["colors"];
12
+
13
+ // Paseo's attachment pill (components/attachment-pill.tsx): every body is 48 high inside a
14
+ // radius-6 frame with a borderAccent border; images are 48x48 thumbnails, the rest a
15
+ // labelled body (18 icon slot with FileText 14, name over the file type) up to 260 wide.
16
+ // The remove button sits on the top-left corner and, on desktop, only shows on hover.
17
+ const CONTENT_HEIGHT = 48;
18
+
19
+ interface AttachmentPillProps {
20
+ colors: Colors;
21
+ attachment: ComposerAttachment;
22
+ onRemove?(): void;
23
+ disabled?: boolean;
24
+ /** Paseo always shows the remove button on phones and compact layouts. Defaults to native platforms. */
25
+ alwaysShowRemove?: boolean;
26
+ }
27
+
28
+ export function AttachmentPill({ colors, attachment, onRemove, disabled, alwaysShowRemove }: AttachmentPillProps) {
29
+ const { t } = useT();
30
+ const [open, setOpen] = useState(false);
31
+ const [bodyHovered, setBodyHovered] = useState(false);
32
+ const [closeHovered, setCloseHovered] = useState(false);
33
+ const showRemove = (alwaysShowRemove ?? Platform.OS !== "web") || bodyHovered || closeHovered;
34
+ const canOpen = attachment.kind !== "file";
35
+ const openLabel = attachment.kind === "image" ? t("attachment.openImage") : t("attachment.openFile", { name: attachment.name });
36
+ const removeLabel = attachment.kind === "image" ? t("attachment.removeImage") : t("attachment.removeFile");
37
+
38
+ return (
39
+ <View style={{ position: "relative" }}>
40
+ <Pressable
41
+ accessibilityRole={canOpen ? "button" : undefined}
42
+ accessibilityLabel={canOpen ? openLabel : attachment.name}
43
+ disabled={!canOpen}
44
+ onPress={() => setOpen(true)}
45
+ onHoverIn={() => setBodyHovered(true)}
46
+ onHoverOut={() => setBodyHovered(false)}
47
+ style={frameStyle(colors)}
48
+ >
49
+ {attachment.kind === "image" ? (
50
+ <Image source={{ uri: imageUri(attachment) }} style={{ width: CONTENT_HEIGHT, height: CONTENT_HEIGHT }} />
51
+ ) : (
52
+ <AttachmentLabel colors={colors} icon={<Icon name="FileText" size={14} color={colors.foregroundMuted} />} title={attachment.name} subtitle={subtitleFor(attachment)} />
53
+ )}
54
+ </Pressable>
55
+ {onRemove ? (
56
+ <Pressable
57
+ accessibilityRole="button"
58
+ accessibilityLabel={removeLabel}
59
+ {...tooltip(t("attachment.remove"))}
60
+ onPress={onRemove}
61
+ disabled={disabled || !showRemove}
62
+ onHoverIn={() => setCloseHovered(true)}
63
+ onHoverOut={() => setCloseHovered(false)}
64
+ hitSlop={8}
65
+ style={{
66
+ position: "absolute",
67
+ top: -8,
68
+ left: -8,
69
+ width: 24,
70
+ height: 24,
71
+ borderRadius: 12,
72
+ alignItems: "center",
73
+ justifyContent: "center",
74
+ backgroundColor: colors.surface2,
75
+ borderWidth: 1,
76
+ borderColor: colors.border,
77
+ zIndex: 1,
78
+ opacity: showRemove ? 1 : 0,
79
+ }}
80
+ >
81
+ <Icon name="X" size={12} color={colors.foregroundMuted} />
82
+ </Pressable>
83
+ ) : null}
84
+ {open ? <AttachmentPreview colors={colors} attachment={attachment} onClose={() => setOpen(false)} /> : null}
85
+ </View>
86
+ );
87
+ }
88
+
89
+ /** A file still being read or uploaded: Paseo's pending pill with a spinner in the icon slot and no remove button. */
90
+ export function PendingAttachmentPill({ colors, name }: { colors: Colors; name: string }) {
91
+ const { t } = useT();
92
+ return (
93
+ <View accessibilityLabel={t("attachment.attaching", { name })} style={frameStyle(colors)}>
94
+ <AttachmentLabel colors={colors} icon={<ActivityIndicator size="small" color={colors.foregroundMuted} />} title={name} subtitle={getFileTypeLabel(name) ?? ""} />
95
+ </View>
96
+ );
97
+ }
98
+
99
+ function frameStyle(colors: Colors) {
100
+ return { borderRadius: 6, borderWidth: 1, borderColor: nativeTokens(colors).borderAccent, overflow: "hidden" as const };
101
+ }
102
+
103
+ function imageUri(attachment: Extract<ComposerAttachment, { kind: "image" }>): string {
104
+ return `data:${attachment.mimeType};base64,${attachment.data}`;
105
+ }
106
+
107
+ function subtitleFor(attachment: ComposerAttachment): string {
108
+ return getFileTypeLabel(attachment.name) ?? (attachment.kind === "text" ? "TXT" : "");
109
+ }
110
+
111
+ function AttachmentLabel({ colors, icon, title, subtitle }: { colors: Colors; icon: ReactNode; title: string; subtitle: string }) {
112
+ return (
113
+ <View style={{ height: CONTENT_HEIGHT, maxWidth: 260, flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 12, backgroundColor: colors.surface1 }}>
114
+ <View style={{ width: 18, alignItems: "center", justifyContent: "center" }}>{icon}</View>
115
+ <View style={{ minWidth: 0, flexShrink: 1 }}>
116
+ <Text numberOfLines={1} style={{ fontSize: ui(14), color: colors.foreground }}>
117
+ {title}
118
+ </Text>
119
+ <Text numberOfLines={1} style={{ fontSize: ui(12), color: colors.foregroundMuted }}>
120
+ {subtitle}
121
+ </Text>
122
+ </View>
123
+ </View>
124
+ );
125
+ }
126
+
127
+ /** Paseo's attachment lightbox for images; pasted and attached text opens as a read-only preview. */
128
+ function AttachmentPreview({ colors, attachment, onClose }: { colors: Colors; attachment: ComposerAttachment; onClose(): void }) {
129
+ const { height } = useWindowDimensions();
130
+ return (
131
+ <Modal title={attachment.name} open onOpenChange={(value) => !value && onClose()}>
132
+ <Modal.Content scrollable={attachment.kind !== "text"}>
133
+ {attachment.kind === "image" ? (
134
+ <Image accessibilityLabel={attachment.name} source={{ uri: imageUri(attachment) }} resizeMode="contain" style={{ width: "100%", height: Math.round(height * 0.6) }} />
135
+ ) : attachment.kind === "text" ? (
136
+ <ScrollView style={{ maxHeight: Math.round(height * 0.6) }}>
137
+ <Text selectable {...MONO_PROPS} style={{ fontFamily: MONO_FONT, fontSize: code(), lineHeight: codeLine(), color: colors.foreground }}>
138
+ {attachment.text}
139
+ </Text>
140
+ </ScrollView>
141
+ ) : null}
142
+ </Modal.Content>
143
+ </Modal>
144
+ );
145
+ }
@@ -0,0 +1,94 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+ import { Image, View } from "react-native";
3
+ import { pixelAvatar, SPRITE_SIZE } from "../shared/avatar";
4
+ import type { BotAvatar, BotGroup } from "../shared/bot";
5
+ import { teamLogoOf } from "../shared/groups";
6
+ import type { PixelRun } from "../shared/pixel";
7
+ import { LOGO_SIZE, teamLogo } from "../shared/team-logo";
8
+
9
+ interface AvatarProps {
10
+ avatar: Pick<BotAvatar, "seed"> & Partial<BotAvatar>;
11
+ size: number;
12
+ /** Overrides the surrounding AvatarTheme; sheets need it, as Paseo draws them outside the plugin's tree on phones. */
13
+ dark?: boolean;
14
+ }
15
+
16
+ const AvatarThemeContext = createContext(false);
17
+
18
+ /** Tells every Avatar below it whether the theme is dark, so pastel backgrounds are toned down. */
19
+ /** Pictures are shown only from a data: URL, so the browser never fetches an address. */
20
+ function isDataImage(url: string | null | undefined): boolean {
21
+ return !!url && url.startsWith("data:image/");
22
+ }
23
+
24
+ export function AvatarTheme({ dark, children }: { dark: boolean; children: ReactNode }) {
25
+ return <AvatarThemeContext.Provider value={dark}>{children}</AvatarThemeContext.Provider>;
26
+ }
27
+
28
+ function radiusFor(shape: BotAvatar["shape"] | undefined, size: number): number {
29
+ return shape === "square" ? size * 0.12 : shape === "rounded" ? size * 0.28 : size / 2;
30
+ }
31
+
32
+ export function Avatar({ avatar, size, dark: darkProp }: AvatarProps) {
33
+ const themeDark = useContext(AvatarThemeContext);
34
+ const dark = darkProp ?? themeDark;
35
+ const palette = avatar.palette ?? null;
36
+ const sprite = useMemo(() => pixelAvatar(avatar.seed, palette, { dark }), [avatar.seed, palette, dark]);
37
+ const radius = radiusFor(avatar.shape, size);
38
+ if (isDataImage(avatar.imageUrl)) {
39
+ return <Image accessibilityIgnoresInvertColors source={{ uri: avatar.imageUrl! }} style={{ width: size, height: size, borderRadius: radius }} />;
40
+ }
41
+ return <PixelSprite sprite={sprite} size={size} radius={radius} />;
42
+ }
43
+
44
+ /** A team's logo: its picture, or the generated pixel-art motif on a rounded tile. */
45
+ export function TeamLogo({ group, size, dark: darkProp }: { group: Pick<BotGroup, "id" | "logo">; size: number; dark?: boolean }) {
46
+ const themeDark = useContext(AvatarThemeContext);
47
+ const dark = darkProp ?? themeDark;
48
+ const logo = teamLogoOf(group);
49
+ const image = useMemo(() => teamLogo(logo.seed, logo.palette, { dark }), [logo.seed, logo.palette, dark]);
50
+ const radius = size / 4;
51
+ if (isDataImage(logo.imageUrl)) {
52
+ return <Image accessibilityIgnoresInvertColors source={{ uri: logo.imageUrl! }} style={{ width: size, height: size, borderRadius: radius }} />;
53
+ }
54
+ // The motif has its margin drawn in, so it fills the tile.
55
+ return <PixelSprite sprite={image} size={size} radius={radius} grid={LOGO_SIZE} inset={false} />;
56
+ }
57
+
58
+ /**
59
+ * Draws a generated sprite: one View per same-coloured run on a rounded background. Avatars
60
+ * sit inset in their frame except when small; `inset` false fills it (team logos).
61
+ */
62
+ export function PixelSprite({ sprite, size, radius = size / 2, grid = SPRITE_SIZE, inset = true }: { sprite: { background: string; rows: PixelRun[][] }; size: number; radius?: number; grid?: number; inset?: boolean }) {
63
+ // Small avatars (sidebar rows) use the full frame so each sprite pixel stays about one point.
64
+ const inner = !inset || size <= 24 ? size : size * 0.82;
65
+ const pixel = inner / grid;
66
+ return (
67
+ <View
68
+ accessibilityElementsHidden
69
+ importantForAccessibility="no-hide-descendants"
70
+ style={{ width: size, height: size, borderRadius: radius, backgroundColor: sprite.background, alignItems: "center", justifyContent: "center", overflow: "hidden" }}
71
+ >
72
+ <View style={{ width: inner, height: inner }}>
73
+ {sprite.rows.map((runs, y) =>
74
+ runs.map((run) =>
75
+ run.color ? (
76
+ <View
77
+ key={`${y}-${run.x}`}
78
+ style={{
79
+ position: "absolute",
80
+ left: run.x * pixel,
81
+ top: y * pixel,
82
+ // Overlap by a hair so fractional pixel sizes don't leave seams.
83
+ width: run.width * pixel + 0.3,
84
+ height: pixel + 0.3,
85
+ backgroundColor: run.color,
86
+ }}
87
+ />
88
+ ) : null,
89
+ ),
90
+ )}
91
+ </View>
92
+ </View>
93
+ );
94
+ }