allaboard 0.0.0-stage → 0.4.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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -2
  3. package/dist/agent/claude/backend.js +124 -0
  4. package/dist/agent/claude/capabilities.js +30 -0
  5. package/dist/agent/claude/chrome.js +42 -0
  6. package/dist/agent/claude/hooks.js +37 -0
  7. package/dist/agent/claude/info.js +61 -0
  8. package/dist/agent/claude/listing.js +230 -0
  9. package/dist/agent/claude/module.js +82 -0
  10. package/dist/agent/claude/prompt.js +24 -0
  11. package/dist/agent/claude/record.js +9 -0
  12. package/dist/agent/claude/requests.js +122 -0
  13. package/dist/agent/claude/session.js +441 -0
  14. package/dist/agent/claude/translate.js +581 -0
  15. package/dist/agent/claude/trust.js +576 -0
  16. package/dist/agent/claude/usage-probe.js +103 -0
  17. package/dist/agent/claude/usage.js +110 -0
  18. package/dist/agent/claude/wire.js +22 -0
  19. package/dist/agent/seam.js +21 -0
  20. package/dist/chat/seam.js +10 -0
  21. package/dist/chat/slack/app/answers.js +209 -0
  22. package/dist/chat/slack/app/app.js +262 -0
  23. package/dist/chat/slack/app/clip.js +191 -0
  24. package/dist/chat/slack/app/guards.js +162 -0
  25. package/dist/chat/slack/app/home-controls.js +91 -0
  26. package/dist/chat/slack/app/messages.js +181 -0
  27. package/dist/chat/slack/app/open.js +392 -0
  28. package/dist/chat/slack/app/order.js +37 -0
  29. package/dist/chat/slack/app/requests.js +482 -0
  30. package/dist/chat/slack/app/resume-bind.js +71 -0
  31. package/dist/chat/slack/app/wire.js +83 -0
  32. package/dist/chat/slack/attachments.js +348 -0
  33. package/dist/chat/slack/bind.js +67 -0
  34. package/dist/chat/slack/channel.js +67 -0
  35. package/dist/chat/slack/channels.js +43 -0
  36. package/dist/chat/slack/delete.js +278 -0
  37. package/dist/chat/slack/footer.js +43 -0
  38. package/dist/chat/slack/hold.js +37 -0
  39. package/dist/chat/slack/home.js +1069 -0
  40. package/dist/chat/slack/module.js +174 -0
  41. package/dist/chat/slack/notice.js +64 -0
  42. package/dist/chat/slack/openfile/modal.js +362 -0
  43. package/dist/chat/slack/openfile/read.js +29 -0
  44. package/dist/chat/slack/quiet-logger.js +52 -0
  45. package/dist/chat/slack/repair.js +123 -0
  46. package/dist/chat/slack/reply/blocks.js +372 -0
  47. package/dist/chat/slack/reply/chars.js +59 -0
  48. package/dist/chat/slack/reply/clients.js +171 -0
  49. package/dist/chat/slack/reply/errors.js +125 -0
  50. package/dist/chat/slack/reply/escape.js +25 -0
  51. package/dist/chat/slack/reply/limiter.js +86 -0
  52. package/dist/chat/slack/reply/markdown.js +210 -0
  53. package/dist/chat/slack/reply/places.js +262 -0
  54. package/dist/chat/slack/reply/sinks.js +2403 -0
  55. package/dist/chat/slack/reply/status.js +433 -0
  56. package/dist/chat/slack/reply/tasks.js +11 -0
  57. package/dist/chat/slack/requests.js +552 -0
  58. package/dist/chat/slack/resume.js +127 -0
  59. package/dist/chat/slack/setup.js +114 -0
  60. package/dist/chat/slack/texts.js +185 -0
  61. package/dist/chat/slack/thread.js +537 -0
  62. package/dist/chat/slack/update.js +71 -0
  63. package/dist/chat/slack/voice.js +84 -0
  64. package/dist/cli.js +326 -0
  65. package/dist/clock.js +21 -0
  66. package/dist/core/cleanup.js +145 -0
  67. package/dist/core/commands.js +172 -0
  68. package/dist/core/config.js +174 -0
  69. package/dist/core/files/changed.js +125 -0
  70. package/dist/core/files/listing.js +382 -0
  71. package/dist/core/files/paths.js +9 -0
  72. package/dist/core/files/read.js +191 -0
  73. package/dist/core/files/waiting.js +34 -0
  74. package/dist/core/flows/answers.js +133 -0
  75. package/dist/core/flows/ask-owner.js +95 -0
  76. package/dist/core/flows/asking.js +14 -0
  77. package/dist/core/flows/bind.js +95 -0
  78. package/dist/core/flows/contract.js +1 -0
  79. package/dist/core/flows/flows.js +35 -0
  80. package/dist/core/flows/hold.js +22 -0
  81. package/dist/core/flows/inbound.js +24 -0
  82. package/dist/core/flows/lists.js +21 -0
  83. package/dist/core/flows/messages.js +293 -0
  84. package/dist/core/flows/notices.js +51 -0
  85. package/dist/core/flows/repair.js +106 -0
  86. package/dist/core/flows/resume.js +163 -0
  87. package/dist/core/flows/setup.js +130 -0
  88. package/dist/core/flows/status.js +98 -0
  89. package/dist/core/flows/talk.js +207 -0
  90. package/dist/core/flows/update.js +370 -0
  91. package/dist/core/flows/words.js +171 -0
  92. package/dist/core/folders.js +93 -0
  93. package/dist/core/footer.js +313 -0
  94. package/dist/core/guide.js +50 -0
  95. package/dist/core/hold.js +126 -0
  96. package/dist/core/instance.js +46 -0
  97. package/dist/core/lock.js +412 -0
  98. package/dist/core/models.js +10 -0
  99. package/dist/core/paths.js +75 -0
  100. package/dist/core/port/file-flows.js +1 -0
  101. package/dist/core/port/home.js +1 -0
  102. package/dist/core/port/requests.js +1 -0
  103. package/dist/core/port/threads.js +1 -0
  104. package/dist/core/port/updates.js +1 -0
  105. package/dist/core/port.js +17 -0
  106. package/dist/core/reply/fold.js +175 -0
  107. package/dist/core/reply/previews.js +258 -0
  108. package/dist/core/reply/renderer.js +846 -0
  109. package/dist/core/reply/tail.js +24 -0
  110. package/dist/core/reply/words.js +32 -0
  111. package/dist/core/requests.js +158 -0
  112. package/dist/core/sessions/constants.js +61 -0
  113. package/dist/core/sessions/deps.js +1 -0
  114. package/dist/core/sessions/directory.js +86 -0
  115. package/dist/core/sessions/errors.js +79 -0
  116. package/dist/core/sessions/manager.js +378 -0
  117. package/dist/core/sessions/session.js +3297 -0
  118. package/dist/core/sessions/tasks.js +266 -0
  119. package/dist/core/sessions/turn.js +45 -0
  120. package/dist/core/setup.js +28 -0
  121. package/dist/core/state.js +724 -0
  122. package/dist/core/texts.js +451 -0
  123. package/dist/core/updates.js +332 -0
  124. package/dist/daemon.js +566 -0
  125. package/dist/host/npm.js +134 -0
  126. package/dist/host/program.js +49 -0
  127. package/dist/host/run.js +52 -0
  128. package/dist/host/service/launchd.js +385 -0
  129. package/dist/host/service/listing.js +42 -0
  130. package/dist/host/service/seam.js +1 -0
  131. package/dist/log.js +37 -0
  132. package/dist/main.js +29 -0
  133. package/dist/node-version.js +40 -0
  134. package/dist/registry.js +12 -0
  135. package/dist/semver.js +53 -0
  136. package/dist/text.js +89 -0
  137. package/dist/updates-host.js +113 -0
  138. package/dist/values.js +30 -0
  139. package/dist/version.js +54 -0
  140. package/package.json +32 -4
  141. package/slack-app-manifest.json +50 -0
@@ -0,0 +1,451 @@
1
+ import { SETUP_SECTIONS, setupGuide } from "../version.js";
2
+ /**
3
+ * Fills a template's `{name}` fields: every template below keeps its `{name}` fields as written,
4
+ * and a caller fills one with this. A field without a value throws.
5
+ */
6
+ export function fill(template, values) {
7
+ return template.replace(/\{([a-z_]+)\}/g, (_field, name) => {
8
+ const value = values[name];
9
+ if (value === undefined)
10
+ throw new Error(`no value for the field {${name}}`);
11
+ return String(value);
12
+ });
13
+ }
14
+ /**
15
+ * A template that names the agent or the chat, with the fields `{agent}`, `{author}`, `{cli}`,
16
+ * `{chat}`, `{examples}`, `{login}` and `{logout}`. It is no string, and it throws where one is
17
+ * made of it: nothing can show it before `named` filled the names in.
18
+ */
19
+ export class Named {
20
+ template;
21
+ constructor(template) {
22
+ this.template = template;
23
+ Object.freeze(this);
24
+ }
25
+ toString() {
26
+ throw new Error("a text that names the agent or the chat was shown before `named` filled it");
27
+ }
28
+ }
29
+ function naming(template) {
30
+ return new Named(template);
31
+ }
32
+ /**
33
+ * `text` with the names filled in; its other `{name}` fields are left for `fill`. A text that
34
+ * takes `{login}` or `{logout}` is never shown for an agent that has no such command: it throws.
35
+ */
36
+ export function named(text, names) {
37
+ return text.template.replace(/\{(agent|author|cli|chat|examples|login|logout)\}/g, (_field, name) => {
38
+ const value = names[name];
39
+ if (value === null)
40
+ throw new Error(`no name for the field {${name}}`);
41
+ return value;
42
+ });
43
+ }
44
+ /** The names of an agent and a chat, as the two modules give them. */
45
+ export function namesOf(agent, chat) {
46
+ return {
47
+ agent: agent.name,
48
+ author: agent.words.author,
49
+ cli: agent.words.command,
50
+ chat: chat.name,
51
+ examples: agent.words.examples,
52
+ newSession: agent.words.newSession,
53
+ login: agent.words.login,
54
+ logout: agent.words.logout,
55
+ };
56
+ }
57
+ export const UNBOUND = naming("This channel is not bound to a folder yet. Send `!bind` to choose one of the folders " +
58
+ "{agent} trusts, or `!bind <folder>` with the folder's path relative to `{root}`, for " +
59
+ "example `!bind my-project`. `!guide` explains how allaboard works.");
60
+ // The same notice for an agent that keeps no record of trust: every folder under the root is offered.
61
+ export const UNBOUND_ANY = "This channel is not bound to a folder yet. Send `!bind` to choose one of the folders " +
62
+ "under `{root}`, or `!bind <folder>` with the folder's path relative to `{root}`, for " +
63
+ "example `!bind my-project`. `!guide` explains how allaboard works.";
64
+ export const AUTH_FAILED = naming("{agent} is not logged in on the host. On that machine, run `{cli}`, then `/{login}`. " +
65
+ "The login is never done from {chat}.");
66
+ // The same for an agent with no command of its own that logs in.
67
+ export const AUTH_FAILED_NO_LOGIN = naming("{agent} is not logged in on the host. Log in on that machine. " +
68
+ "The login is never done from {chat}.");
69
+ export const LOGIN_ON_HOST = naming("Log in on the host: on that machine, run `{cli}`, then `/{login}`. " +
70
+ "The login is never done from {chat}.");
71
+ export const LOGOUT_ON_HOST = naming("Log out on the host: on that machine, run `{cli}`, then `/{logout}`. " +
72
+ "It is never done from {chat}.");
73
+ export const DIRECTORY_MISSING = "The directory `{directory}` no longer exists. Restore it to keep using this thread, or, in " +
74
+ "the channel, bind another folder with `!bind <path>` and send a new message to start a " +
75
+ "session there.";
76
+ export const DIRECTORY_UNTRUSTED = naming("{agent} has not been trusted in `{directory}`, so its hooks and settings would run " +
77
+ "without asking. Open `{cli}` there in the terminal once and accept the trust dialog, then " +
78
+ "send your message again.");
79
+ export const DIRECTORY_UNREADABLE = "macOS does not let allaboard read `{directory}`. Grant access in System Settings, " +
80
+ `Privacy & Security, Full Disk Access (see ${setupGuide(SETUP_SECTIONS.protectedFolders)}).`;
81
+ export const SESSION_GONE = naming("This thread's session no longer exists: {agent} deleted it or cannot find it. Send a " +
82
+ "new message in the channel to start one.");
83
+ // Names no cause: the daemon keeps no record of a thread whose entry it dropped, so it cannot
84
+ // tell a thread that never held a session from one whose session may still be resumable.
85
+ export const NOT_A_SESSION = "This thread holds no session. In the channel, send a new message to start one, or " +
86
+ "`!resume` to continue an earlier one.";
87
+ export const OPEN_TOP_LEVEL = "Opening a file belongs to one session: send `!open` inside its thread.";
88
+ // Shown only by a chat that cannot share a file into a thread.
89
+ export const OPEN_UNAVAILABLE = naming("{chat} cannot show a file here, so `!open` does nothing.");
90
+ export const WORD_IN_THREAD = "`!{word}` works in the channel, not inside a thread.";
91
+ // `!stop` typed in a thread that holds no session: it would have stopped the whole channel.
92
+ export const STOP_OUTSIDE_SESSION = "This thread holds no session. Send `!stop` in the channel to stop every session, or inside " +
93
+ "a session's thread to stop that one.";
94
+ export const CLEAR_IN_THREAD = "One thread is one session: send a new message in the channel to start a new one.";
95
+ export const UPGRADE_NOTICE = naming("allaboard now runs one {agent} session per thread. Send a new message in the " +
96
+ "channel to start a session; reply in its thread to continue it. The session this channel " +
97
+ "had is still in the folder: !resume brings it into a thread. Bypass is now set per " +
98
+ "session: send !bypass on inside a thread.");
99
+ export const ERROR_REPLY = naming("{agent} reported an error: `{error}`");
100
+ export const ENDED = "_This reply ended before an answer: {reason}._";
101
+ export const ENDED_SHUTDOWN = "allaboard stopped";
102
+ // Crash repair (issue #19): what a reply the daemon died in the middle of ends with.
103
+ export const STOPPED_BEFORE_ANSWER = "allaboard stopped before this answer.";
104
+ // Messages a restart or a failure dropped from the queue (S3): they get no reply of their own,
105
+ // one note names them. `{because}` is one of the two phrases below.
106
+ export const NOT_SENT_ONE = "1 message was not sent because {because}: send it again.";
107
+ export const NOT_SENT_MANY = "{count} messages were not sent because {because}: send them again.";
108
+ // Prompts Claude Code took into a running turn of its own (a report of a background task): no
109
+ // result of their own follows, so the reply of that turn says so at its end.
110
+ export const TAKEN_INTO_REPLY_ONE = naming("{agent} took your message into this reply: send it again if it is not answered here.");
111
+ export const TAKEN_INTO_REPLY_MANY = naming("{agent} took {count} messages into this reply: " +
112
+ "send them again if they are not answered here.");
113
+ export const BECAUSE_RESTARTED = "allaboard restarted";
114
+ export const BECAUSE_SHUTDOWN = "allaboard stopped";
115
+ export const BECAUSE_STOPPED = naming("{agent} stopped");
116
+ export const ENDED_RESTARTING = "allaboard is restarting; send your message again in a moment";
117
+ // Never shown: an idle close always finds nothing running, sent or queued to end with it.
118
+ export const ENDED_IDLE = "allaboard closed this idle session";
119
+ export const RESTARTING = "allaboard is restarting; send this again in a moment.";
120
+ // `!allaboard` and the update from the chat (issue #361). A text that names the chat is `naming`;
121
+ // the link to the changelog is drawn by the chat (its markup), beside `UPDATE_WHAT_CHANGED`.
122
+ export const UPDATE_STATUS_NEWER = "allaboard {installed} · latest {latest}, checked {time} · `!allaboard update` installs it";
123
+ export const UPDATE_STATUS_CURRENT = "allaboard {installed} · the latest version, checked {time}";
124
+ export const UPDATE_STATUS_UNKNOWN = "allaboard {installed} · latest version unknown: {reason} at {time}";
125
+ // Why npm could not say, as the terminal's `allaboard update` words it.
126
+ export const UPDATE_WHY_NETWORK = "npm could not reach the registry";
127
+ export const UPDATE_WHY_PERMISSION = "npm may not read what it needs";
128
+ export const UPDATE_WHY_STATUS = "npm ended with status {status}";
129
+ export const UPDATE_OUTSIDE = naming("This copy updates outside {chat}: {reason}.");
130
+ export const UPDATE_REASON_NOT_GLOBAL = "it is not the copy `npm install -g` replaces ({path})";
131
+ export const UPDATE_REASON_LINK = "it is a link, not an install";
132
+ export const UPDATE_REASON_UNPUBLISHED = "its version {version} is not published on npm";
133
+ export const UPDATE_NOT_A_SERVICE = naming("allaboard is not running as a service. `allaboard service install` in a terminal makes it one; " +
134
+ "then it can update and restart from {chat}.");
135
+ export const UPDATE_OFFER = "Update allaboard {installed} → {latest}";
136
+ export const UPDATE_WHAT_CHANGED = "What changed";
137
+ export const UPDATE_DRAIN = "Running turns finish first, for up to 29 minutes, then allaboard restarts.";
138
+ export const UPDATE_SHARED_ONE = "Instance `{name}` runs the same copy and restarts too.";
139
+ export const UPDATE_SHARED_DEFAULT = "The default instance runs the same copy and restarts too.";
140
+ export const UPDATE_SHARED_MANY = "Instances {names} run the same copy and restart too.";
141
+ export const UPDATE_DEFAULT_INSTANCE = "the default instance";
142
+ export const UPDATE_BUTTON = "Update to {latest}";
143
+ export const UPDATE_LATEST = "allaboard {installed} is already the latest version.";
144
+ export const UPDATE_UNKNOWN = "The latest version is unknown: {reason}. `!allaboard update` tries again.";
145
+ export const UPDATE_NOTICE = "allaboard {latest} is available (this copy runs {installed}) · `!allaboard update` installs it";
146
+ // The line of the chat's own page: `UPDATE_HOME_AVAILABLE` with the link and the button, the
147
+ // dialog the button asks through, then the states of a click.
148
+ export const UPDATE_HOME_AVAILABLE = "allaboard {latest} is available, this copy runs {installed}";
149
+ export const UPDATE_CONFIRM_TITLE = "Update allaboard?";
150
+ export const UPDATE_CONFIRM_TEXT = "{installed} → {latest}. Running turns finish first, for up to 29 minutes, then allaboard " +
151
+ "restarts.";
152
+ export const UPDATE_CONFIRM_YES = "Update";
153
+ export const UPDATE_CONFIRM_NO = "Cancel";
154
+ export const UPDATE_INSTALLING = "Installing allaboard {latest}…";
155
+ export const UPDATE_INSTALLED = "Installed allaboard {latest}. Restarting once running turns finish.";
156
+ export const UPDATE_UNDER_WAY = "An update to {version} is already under way.";
157
+ export const UPDATE_NEWER_SINCE = "Installing allaboard {latest}… {newer} came out since this message: `!allaboard update` " +
158
+ "installs it after the restart.";
159
+ export const UPDATE_ALREADY_RUNS = "allaboard already runs {latest}.";
160
+ export const UPDATE_COPY_HOLDS = "The copy already holds {latest}; allaboard runs {installed} until it restarts: " +
161
+ "`!allaboard restart`.";
162
+ // Another process holds the machine's update lock (another instance's click, or the terminal's
163
+ // `allaboard update`): whose it is and which version are not known.
164
+ export const UPDATE_ELSEWHERE = "An update of allaboard is already under way.";
165
+ export const UPDATE_DRAINING = "allaboard is restarting: send `!allaboard update` again once it is back.";
166
+ export const UPDATE_FAILED_PERMISSION = "The update failed: npm may not write to {folder}. allaboard keeps running {installed}. npm's " +
167
+ "message is in the log, {log}.";
168
+ export const UPDATE_FAILED_NETWORK = "The update failed: npm could not reach the registry. allaboard keeps running {installed}; " +
169
+ "`!allaboard update` tries again.";
170
+ export const UPDATE_FAILED_OTHER = "The update failed: npm ended with status {status}. allaboard keeps running {installed}. npm's " +
171
+ "message is in the log, {log}.";
172
+ export const UPDATE_MISMATCH = "npm ended, but the copy now holds {found} instead of {latest}, so allaboard does not restart " +
173
+ "and keeps running {installed}. In a terminal, `npm install -g allaboard@{installed}` puts back " +
174
+ "the version that runs.";
175
+ export const UPDATE_UNREADABLE = "npm ended, but the copy's version cannot be read, so allaboard does not restart and keeps " +
176
+ "running {installed}. In a terminal, `npm install -g allaboard@{installed}` repairs it.";
177
+ // The install went through and an instance on the same copy did not restart; the reason says
178
+ // what the service manager answered.
179
+ export const UPDATE_OTHER_FAILED = "Instance `{name}` did not restart: {reason}. `allaboard service restart --instance {name}` in " +
180
+ "a terminal restarts it.";
181
+ export const UPDATE_OTHER_FAILED_DEFAULT = "The default instance did not restart: {reason}. `allaboard service restart` in a terminal " +
182
+ "restarts it.";
183
+ export const UPDATE_OTHER_STATUS = "the service manager ended with status {status}";
184
+ export const UPDATE_OTHER_NO_MANAGER = "it has no service manager";
185
+ export const UPDATE_RESTART = "Restarting allaboard once running turns finish.";
186
+ // What the line above, or an update's "Installed", is rewritten into once the daemon is back
187
+ // (the next start reads the message from `state.json`).
188
+ export const UPDATE_RESTARTED = "allaboard restarted at {time}.";
189
+ export const UPDATE_DONE = "Updated to allaboard {version}.";
190
+ export const UPDATE_NOT_TAKEN = "allaboard restarted on {installed}, not on {asked}: the update did not take.";
191
+ // Under RESTARTING, and after a channel's `!status`, while a stop waits: one row per thread that
192
+ // holds it (issue #119).
193
+ export const RESTART_WAITS_FOR = "It is waiting for:";
194
+ export const RESTART_WAITS_HEADER = `allaboard is restarting. ${RESTART_WAITS_FOR}`;
195
+ export const RESTART_WAIT_ROW = "• {channel}, {link}: {hold}";
196
+ export const RESTART_WAIT_SESSION = "Session"; // the link's label for a session with no title yet
197
+ export const RESTART_WAIT_STOP = "`!stop` in a thread ends the wait there.";
198
+ export const RESTART_WAITS_MORE = "…and {count} more.";
199
+ // In a channel's `!status`, a post every member reads: the threads of other channels are counted
200
+ // and not named.
201
+ export const RESTART_WAITS_ELSEWHERE = "{count} more in other channels.";
202
+ export const RESTART_HOLD_OWNER = "an approval or a question is waiting for you";
203
+ export const RESTART_HOLD_TURN = "a turn is running";
204
+ export const RESTART_HOLD_TASKS = "{counts} running";
205
+ // Ends by itself (`INJECTED_TURN_WAIT`): `!stop` has nothing to stop there.
206
+ export const RESTART_HOLD_REPORT = "a background task is about to report, within 30 seconds";
207
+ // The one form of a task started in the background, whatever its kind (issue #311): the
208
+ // terminal's two lines for an agent it starts in the background, `**type**(description)` and
209
+ // `⎿ Backgrounded agent` without its key hints (captured on Claude Code 2.1.296, 2026-10-13),
210
+ // with words that fit every task under the name. `{model}` is the model an agent ran on when it
211
+ // is not the session's, a space and its name after the description as the terminal writes it
212
+ // (captured on Claude Code 2.1.300, 2026-10-15), and empty otherwise.
213
+ export const TASK_LAUNCHED = "**{name}**({description}){model}\n⎿ Running in background";
214
+ // The terminal's one line for several agents started in one message, with what each does under
215
+ // it as a tree (captured the same day: `2 background agents launched`), for every task.
216
+ export const TASKS_LAUNCHED = "**{n} background tasks launched**";
217
+ export const TASKS_LAUNCHED_BRANCH = "├ {description}";
218
+ export const TASKS_LAUNCHED_LAST = "└ {description}";
219
+ export const PROMPT_IMAGE = "an image";
220
+ export const COMPACTED = "Compacted the conversation: {before} → {after} tokens.";
221
+ export const COMPACTED_PLAIN = "Compacted the conversation.";
222
+ export const NO_OUTPUT = naming("_Done. {agent} returned no text._");
223
+ export const BIND_OK = "Bound this channel to `{directory}`. The next message starts a new session there.";
224
+ // An old thread's session keeps the folder it was created in; `{old}` names each of them
225
+ // (comma-separated, backticked).
226
+ export const BIND_OK_ELSEWHERE = "Bound this channel to `{directory}`. New messages start sessions there; existing threads " +
227
+ "keep working in {old}, where their sessions live.";
228
+ export const BIND_UNAVAILABLE = "Bound this channel to `{directory}`, but no session can start there yet. {reason}";
229
+ // Shown, ephemeral, before every prompt in a thread whose folder differs from the channel's.
230
+ export const OLD_THREAD_FOLDER = naming("This session works in `{old}`, the folder it was created in: {agent} resumes a session " +
231
+ "only there. New messages in the channel use `{new}`.");
232
+ export const BIND_OUTSIDE = "`{path}` is not a folder under `{root}`. Give its path relative to that folder, for " +
233
+ "example `!bind my-project`.";
234
+ export const SESSION_CLOSED = "Nothing was done: this session closed while it ran (idle for a while, or the daemon " +
235
+ "restarted). Send it again if it is still meant.";
236
+ export const BIND_LIST = naming("Folders under `{root}` that {agent} trusts:");
237
+ // The title of the list for an agent that keeps no record of trust: every folder is offered.
238
+ export const BIND_LIST_ANY = "Folders under `{root}`:";
239
+ export const BIND_EMPTY = naming("No folder under `{root}` is trusted by {agent} yet. Open one in the terminal with " +
240
+ "`{cli}` and accept the trust dialog, then send `!bind` again.");
241
+ export const BIND_ALREADY = "This channel is already bound to `{directory}`.";
242
+ export const BIND_BUSY = "Sessions are running in this channel: binding another folder now would cut their work. " +
243
+ "Let them finish or send `!stop`, then bind again.";
244
+ export const BYPASS_TOP_LEVEL = "Bypass belongs to one session: send `!bypass on` inside its thread.";
245
+ // True with the setup on screen and without it (after a stop, a restart or a Cancel).
246
+ // Shown only by an agent with no mode that skips permission requests.
247
+ export const BYPASS_UNAVAILABLE = naming("{agent} has no mode that runs tools without asking, so there is no bypass to switch.");
248
+ export const BYPASS_BEFORE_START = "This session has not started yet: tick Bypass in its setup and press Start. " +
249
+ "If no setup is shown, send a message here first.";
250
+ export const BYPASS_ON_THREAD = "Bypass is on in this session: every tool runs without asking, until `!bypass off`. " +
251
+ "It survives a restart.";
252
+ // Says nothing about what is asked: that is the mode the owner's own settings give.
253
+ export const BYPASS_OFF_THREAD = naming("Bypass is off in this session: {agent} follows your permission settings again.");
254
+ export const STOPPED = "Stopped the current turn.";
255
+ // An answered question, as the terminal keeps it in the transcript.
256
+ export const ANSWERED = naming("User answered {author}'s questions:");
257
+ // Slack drops plain spaces at the start of a line; no-break spaces stay and make the indent.
258
+ export const NESTED = `${"\u00a0".repeat(4)}⎿ `;
259
+ export const STOPPED_CHANNEL = "Stopped what was running in this channel.";
260
+ // The thread's status line while a restart waits for background tasks only, and the same after
261
+ // the app's name. Never a message: it would notify and stay in the thread after the restart.
262
+ export const RESTART_WAITS = "Restart waits for {counts} · !stop ends {them} now";
263
+ export const RESTART_WAITS_STATUS = "is waiting to restart: {counts} still running";
264
+ // The same as a message, only where Slack refuses the app a thread status.
265
+ export const RESTART_WAITS_MESSAGE = "allaboard is restarting once these background tasks end: {counts}. `!stop` ends them now.";
266
+ export const NOTHING_TO_STOP = "Nothing is running in this channel.";
267
+ // Both answers to `!stop` in a thread are posts that stay: an ephemeral line is gone on reload.
268
+ export const STOPPED_THREAD = "Stopped.";
269
+ export const NOTHING_TO_STOP_THREAD = "Nothing is running in this session.";
270
+ // The same-folder hold: two sessions in one folder at once.
271
+ export const HOLD_QUESTION = "Another session is working in this folder: {link}. Send anyway?";
272
+ export const HOLD_UNPOSTED = naming("allaboard could not show this question in {chat}, so the message was not sent. Send it again.");
273
+ export const HOLD_GONE = "This question is no longer open: it was already answered, or allaboard restarted.";
274
+ export const NOT_SENT = "Not sent.";
275
+ // Session setup: asked once per top-level message, before the first prompt of a new session.
276
+ export const SETUP_FALLBACK = "Set up this session";
277
+ // The line that replaces the setup's controls once the owner started the session.
278
+ export const SETUP_SUMMARY = "Model: {model} · Effort: {effort} · Bypass: {bypass}";
279
+ export const SETUP_EFFORT_DEFAULT = "Default";
280
+ /** The effort the footer shows for a model a turn reported takes none. */
281
+ export const EFFORT_NOT_SUPPORTED = "not supported";
282
+ export const STATUS = "Directory: `{directory}`\nSession: `{session}`";
283
+ export const STATUS_STATE = naming("Mode: `{mode}`\n{agent}: `{version}`\nNow: {activity}");
284
+ // The line between the two once the thread has a session id (issue #12). Claude Code leaves a
285
+ // session created through the Agent SDK out of the terminal's picker; a fork made in the
286
+ // terminal gets an id of its own and a row there (sessions reference, read 2026-10-04; the
287
+ // picker listing such a fork measured on CLI 2.1.282). `claude --resume <id>` finds a session
288
+ // from any folder since 2.1.223 (same reference): the `cd` chooses where the fork works and
289
+ // whose picker lists it. `directory` arrives quoted for the shell, and `command` as a code
290
+ // span, or escaped as plain text when it holds a backtick.
291
+ export const STATUS_TERMINAL = "Terminal: {command}";
292
+ export const ACTIVITY_IDLE = "idle";
293
+ export const VERSION_PENDING = "started, version shown after the first turn";
294
+ export const STATUS_BACKGROUND = "Background: `{counts}`";
295
+ export const STATUS_WORKING = "Working in: `{directory}`";
296
+ // The count `!status` adds to a session's row.
297
+ export const RUNNING = "⏳ {counts}";
298
+ // Slack's status line under a thread's last message (issues #83 and #295). A client shows the
299
+ // loading message: `THREAD_WORKING` while a prompt of the owner's is owed its answer and while
300
+ // the agent says it works (issue #301).
301
+ // `STILL_RUNNING` is the terminal's words for a task that outlives its turn (`· 1 shell still
302
+ // running`, Claude Code 2.1.287, read 2026-10-02), and the title of the list of what still runs,
303
+ // with no hourglass: an emoji draws large and grey in a status line (seen on desktop,
304
+ // 2026-10-02). `THREAD_WORKING_STATUS` says the same
305
+ // after the app's name, which is what a client that draws `<app name> <status>` shows
306
+ // (measured 2026-10-02, desktop and iOS).
307
+ export const THREAD_WORKING = "Working…";
308
+ // The terminal's words while it compacts (`Compacting conversation`, read in the CLI 2.1.292).
309
+ export const THREAD_COMPACTING = "Compacting conversation…";
310
+ export const THREAD_COMPACTING_STATUS = "is compacting the conversation…";
311
+ export const STILL_RUNNING = "{counts} still running";
312
+ export const THREAD_WORKING_STATUS = "is working…";
313
+ export const ACTIVITY_BUSY = "running a turn, {queued} queued";
314
+ // `!status` sent to the channel (top-level, or a thread that holds no session): the channel's
315
+ // folder, then one line per live session, each with a link to its thread.
316
+ export const STATUS_CHANNEL_HEADER = "Directory: `{directory}`";
317
+ export const STATUS_CHANNEL_EMPTY = "No live session in this channel.";
318
+ export const STATUS_CHANNEL_ROW = "{link}: {activity}";
319
+ export const STATUS_CHANNEL_WAITING = "waiting for you";
320
+ export const STATUS_CHANNEL_BUSY = "busy";
321
+ export const STATUS_CHANNEL_IDLE = "idle";
322
+ // After `idle`, how long ago the session's last message was written, when that is known.
323
+ export const STATUS_CHANNEL_SINCE = " · {ago}";
324
+ export const STATUS_CHANNEL_SESSION = "Session"; // the link's label for a session with no title yet
325
+ export const STATUS_CHANNEL_BYPASS = " · ⚡ bypass";
326
+ export const STATUS_CHANNEL_FOLDER = " · folder `{directory}`";
327
+ export const STATUS_CHANNEL_LINK_FALLBACK = "thread `{thread_ts}`";
328
+ export const HELP_OWN = "**allaboard**";
329
+ export const HELP_RULE = "A message that starts with `!` is a command, in code or bold too. To send it as text, put " +
330
+ "anything before the `!`, as in `\\!goal`.";
331
+ export const HELP_WORDS = [
332
+ "`!guide` how allaboard works, in a few lines; in the channel or inside a thread",
333
+ "`!help [text]` this list, or only the lines that contain the text; in the channel or " +
334
+ "inside a thread",
335
+ "`!status` in the channel: every session's state; inside a thread: that session's " +
336
+ "directory, mode and the footer's values",
337
+ "`!stop` in the channel: every running session, its background tasks and its pending " +
338
+ "approvals; inside a thread: only that session",
339
+ "`!bind [folder]` the folders {agent} trusts, or bind this channel to one, its path " +
340
+ "relative to the allowed root; in the channel, refused inside a thread",
341
+ "`!bypass on|off` run every tool without asking, until `!bypass off`; it survives a " +
342
+ "restart; inside a thread, refused in the channel",
343
+ "`!resume [session]` this directory's sessions, or resume one by id or name in the " +
344
+ "thread of your `!resume` message; in the channel, refused inside a thread",
345
+ "`!open [file]` share a file of the session's folder into the thread, where {chat} opens it in " +
346
+ "its file viewer: a picker alone, or a path or part of a name; inside a thread, refused in " +
347
+ "the channel",
348
+ "`!allaboard [update|restart]` the version that runs and the latest one; `update` offers the " +
349
+ "latest and installs it on your click, `restart` restarts allaboard, both once running turns " +
350
+ "finish; `update` and `restart` in the channel, refused inside a thread",
351
+ ].map(naming);
352
+ // The words for an agent that keeps no record of trust: only the `!bind` line differs.
353
+ const HELP_BIND_ANY = naming("`!bind [folder]` the folders under the allowed root, or bind this channel to one, its " +
354
+ "path relative to that root; in the channel, refused inside a thread");
355
+ export const HELP_WORDS_ANY = HELP_WORDS.map((word) => word.template.startsWith("`!bind ") ? HELP_BIND_ANY : word);
356
+ export const HELP_NO_MATCH = "No command matches `{query}`.";
357
+ export const HELP_AGENT = naming("\n**{agent}** (this session, now). Any other `!name args` runs that command; " +
358
+ "these words above come first.");
359
+ export const HELP_UNBOUND = naming("\n{agent}'s own commands are listed inside a session's thread.");
360
+ export const DENY_MESSAGE = naming("The owner denied this from {chat}.");
361
+ export const SKIP_MESSAGE = "The owner dismissed the question without answering.";
362
+ export const APPROVAL_UNPOSTED = naming("allaboard could not show this request in {chat}, so nobody approved it.");
363
+ export const APPROVAL_GONE = "This request is no longer pending: the turn ended or allaboard restarted.";
364
+ export const RESUME_LIST = "Sessions in `{directory}`, newest first:";
365
+ // Shown only by an agent that keeps no list of its sessions.
366
+ export const RESUME_UNLISTED = naming("{agent} keeps no list of its sessions, so `!resume` has nothing to pick from.");
367
+ // Two processes on one session share its file and not its conversation (measured 2026-10-03,
368
+ // issue #41, SDK 0.2.163 beside a host CLI 2.1.288 process): each keeps the session as it loaded
369
+ // it plus its own turns, and a later resume continues one of the two branches.
370
+ export const RESUME_OK = "Resumed **{title}**: your next message continues it. If it is open in a terminal, close it " +
371
+ "there first: while it is open in both, neither sees the other's messages, and a later " +
372
+ "resume keeps only one side's.";
373
+ export const RESUME_NONE = "No session in `{directory}` has the id or name `{target}`: `!resume` lists them.";
374
+ export const RESUME_AMBIGUOUS = "More than one session in `{directory}` is named or starts with `{target}`: pick one from " +
375
+ "`!resume`.";
376
+ export const RESUME_STALE = "This list is out of date: send `!resume` again for a current one.";
377
+ export const RESUME_GONE = "That session is not in this channel's directory any more: `!resume` lists them.";
378
+ export const RESUME_HELD = "This thread already holds a session: send `!resume` again in the channel to pick another.";
379
+ // What the list is rewritten to after a Resume click, only when it cannot be deleted.
380
+ export const RESUME_LISTED = "Resumed {title} in {link}.";
381
+ // A session held by any thread of any channel is never resumed a second time; `{link}` is the
382
+ // holding thread's permalink (a plain fallback when Slack would not give one).
383
+ export const RESUME_ELSEWHERE = "This session is already open in another thread: {link}.";
384
+ // `!guide`: how to use the bot, in the owner's words, as parts that `src/core/guide.ts` chooses
385
+ // among and joins: a part appears only when the agent and the chat serve what it describes.
386
+ // `test/core/commands.test.ts` fails when a word of the daemon is missing from the whole; keep
387
+ // the tone plain and every line true of the current behaviour.
388
+ export const GUIDE_INTRO = naming("**allaboard**\n" +
389
+ "This channel runs {agent} on your Mac, in one folder, and answers you alone. One {chat} " +
390
+ "thread is one {agent} session: a top-level message starts a new one, and a reply inside its " +
391
+ "thread continues it, even days later.");
392
+ export const GUIDE_START = "**Get started**";
393
+ export const GUIDE_BIND_TRUSTED = naming("1. `!bind` lists the folders {agent} trusts, with a Bind button each; `!bind <folder>` " +
394
+ "binds one by its path relative to your allowed root: `!bind my-project`. {agent} must trust " +
395
+ "that folder first: open `{cli}` there once in the terminal and accept. `!bind` works only as a " +
396
+ "top-level message in the channel.");
397
+ export const GUIDE_BIND_ANY = "1. `!bind` lists the folders under your allowed root, with a Bind button each; " +
398
+ "`!bind <folder>` binds one by its path relative to your allowed root: `!bind my-project`. " +
399
+ "`!bind` works only as a top-level message in the channel.";
400
+ export const GUIDE_FIRST_MESSAGE = naming("2. Write a message in the channel: it opens a session, and its reply appears in a thread of its " +
401
+ "own, growing as {author} works, with a line for each tool it uses. Reply inside that thread to " +
402
+ "continue the same session. Attach images or files to a message: {author} sees a JPEG, PNG, GIF or " +
403
+ "WebP image directly (other image types are refused) and reads a text, code, PDF, JSON, XML, YAML " +
404
+ "or notebook file from a copy saved on this Mac; other files are refused.");
405
+ // Step 3 holds what the setup asks for; `{choices}` is the items on offer, joined as a list.
406
+ export const GUIDE_SETUP = "3. Before that first message is sent, the thread asks for {choices}; **Start** sends your " +
407
+ "message with those choices, and `!stop` cancels it instead.";
408
+ export const GUIDE_SETUP_MODEL = "the **Model**";
409
+ export const GUIDE_SETUP_EFFORT = naming("the **Effort** (`Default` leaves {agent}'s own choice)");
410
+ export const GUIDE_SETUP_BYPASS = "**Bypass permissions**";
411
+ export const GUIDE_COMMANDS = naming("**Commands**\n" +
412
+ "{agent}'s commands start with `!` instead of `/`: {examples}. They run inside " +
413
+ "a session's thread, where `!help` lists every command that session offers, and `!help <text>` " +
414
+ "filters the list; typed in the channel, `!help` lists allaboard's own words instead.");
415
+ // Joined to the guide's commands section, with `{names}` the agent's new-session commands.
416
+ export const GUIDE_NEW_SESSION = "{names} is refused inside a thread (one thread is one session): start a " +
417
+ "fresh session with a new top-level message instead.";
418
+ export const GUIDE_APPROVALS = "**Approvals**";
419
+ export const GUIDE_APPROVALS_AND_QUESTIONS = "**Approvals and questions**";
420
+ export const GUIDE_PERMISSION = naming("When {agent} asks permission, the request shows what will run, with **Approve** and " +
421
+ "**Deny**; a very long one shows its start and its end and says how much it leaves out.");
422
+ export const GUIDE_QUESTION = naming("A question from {author} comes with **Answer**, which opens a short form, and **Skip**.");
423
+ // The Sessions section, sentence by sentence: `guide.ts` leaves out the tasks and the resume ones.
424
+ export const GUIDE_SESSIONS_STATUS = "**Sessions**\n" +
425
+ "`!status` typed in the channel lists the folder and every session still live, each linked to " +
426
+ "its thread; inside a thread it shows that session's directory, mode and the footer's values. " +
427
+ "`!stop` typed in the channel stops every running session";
428
+ export const GUIDE_SESSIONS_TASKS = " and its background tasks";
429
+ export const GUIDE_SESSIONS_STOP = "; inside a thread it stops only that one.";
430
+ export const GUIDE_SESSIONS_RESUME = "`!resume`, typed in the channel only, lists this folder's twenty " +
431
+ "newest sessions, from the terminal too, each with the start of its id and a **Resume** button; " +
432
+ "`!resume <id>` (that start is enough), or `!resume <title>` for a session that has one, resumes " +
433
+ "it in the thread of your `!resume` message.";
434
+ export const GUIDE_SESSIONS_TERMINAL = "To continue a session in the terminal, run the `Terminal:` command `!status` shows in its thread.";
435
+ export const GUIDE_FILES = naming("**Files**\n" +
436
+ "`!open`, sent inside a session's thread, offers the files changed in that session and a search " +
437
+ "over the folder's files; choosing one shares it into the thread, where {chat} opens it in its " +
438
+ "file viewer, with Markdown rendered. `!open <path>` opens that file, and `!open <words>` the file " +
439
+ "whose path contains them, or lists the files that do. The search reads the folder from disk and " +
440
+ "leaves out what `.gitignore` excludes inside a git repository. A file over 1 MB is not opened.");
441
+ export const GUIDE_BYPASS = naming("**Bypass**\n" +
442
+ "`!bypass on`, sent inside a session's thread, lets {agent} run every tool without asking in " +
443
+ "that session, until `!bypass off`; a restart of allaboard keeps it. The footer shows ⚡ " +
444
+ "bypass while it is on.");
445
+ // Joined to the guide only where allaboard can update itself (`GuideOffer.updates`).
446
+ export const GUIDE_UPDATES = "**Updates**\n" +
447
+ "`!allaboard` shows the version that runs and the latest one. `!allaboard update`, typed in the " +
448
+ "channel, offers the latest version with a button that installs it: running turns finish " +
449
+ "first, then allaboard restarts on the new version. `!allaboard restart` restarts it without " +
450
+ "installing anything. Both work only while allaboard runs as a service.";
451
+ export const GUIDE_END = "`!guide` shows this text again.";