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.
- package/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/agent/claude/backend.js +124 -0
- package/dist/agent/claude/capabilities.js +30 -0
- package/dist/agent/claude/chrome.js +42 -0
- package/dist/agent/claude/hooks.js +37 -0
- package/dist/agent/claude/info.js +61 -0
- package/dist/agent/claude/listing.js +230 -0
- package/dist/agent/claude/module.js +82 -0
- package/dist/agent/claude/prompt.js +24 -0
- package/dist/agent/claude/record.js +9 -0
- package/dist/agent/claude/requests.js +122 -0
- package/dist/agent/claude/session.js +441 -0
- package/dist/agent/claude/translate.js +581 -0
- package/dist/agent/claude/trust.js +576 -0
- package/dist/agent/claude/usage-probe.js +103 -0
- package/dist/agent/claude/usage.js +110 -0
- package/dist/agent/claude/wire.js +22 -0
- package/dist/agent/seam.js +21 -0
- package/dist/chat/seam.js +10 -0
- package/dist/chat/slack/app/answers.js +209 -0
- package/dist/chat/slack/app/app.js +262 -0
- package/dist/chat/slack/app/clip.js +191 -0
- package/dist/chat/slack/app/guards.js +162 -0
- package/dist/chat/slack/app/home-controls.js +91 -0
- package/dist/chat/slack/app/messages.js +181 -0
- package/dist/chat/slack/app/open.js +392 -0
- package/dist/chat/slack/app/order.js +37 -0
- package/dist/chat/slack/app/requests.js +482 -0
- package/dist/chat/slack/app/resume-bind.js +71 -0
- package/dist/chat/slack/app/wire.js +83 -0
- package/dist/chat/slack/attachments.js +348 -0
- package/dist/chat/slack/bind.js +67 -0
- package/dist/chat/slack/channel.js +67 -0
- package/dist/chat/slack/channels.js +43 -0
- package/dist/chat/slack/delete.js +278 -0
- package/dist/chat/slack/footer.js +43 -0
- package/dist/chat/slack/hold.js +37 -0
- package/dist/chat/slack/home.js +1069 -0
- package/dist/chat/slack/module.js +174 -0
- package/dist/chat/slack/notice.js +64 -0
- package/dist/chat/slack/openfile/modal.js +362 -0
- package/dist/chat/slack/openfile/read.js +29 -0
- package/dist/chat/slack/quiet-logger.js +52 -0
- package/dist/chat/slack/repair.js +123 -0
- package/dist/chat/slack/reply/blocks.js +372 -0
- package/dist/chat/slack/reply/chars.js +59 -0
- package/dist/chat/slack/reply/clients.js +171 -0
- package/dist/chat/slack/reply/errors.js +125 -0
- package/dist/chat/slack/reply/escape.js +25 -0
- package/dist/chat/slack/reply/limiter.js +86 -0
- package/dist/chat/slack/reply/markdown.js +210 -0
- package/dist/chat/slack/reply/places.js +262 -0
- package/dist/chat/slack/reply/sinks.js +2403 -0
- package/dist/chat/slack/reply/status.js +433 -0
- package/dist/chat/slack/reply/tasks.js +11 -0
- package/dist/chat/slack/requests.js +552 -0
- package/dist/chat/slack/resume.js +127 -0
- package/dist/chat/slack/setup.js +114 -0
- package/dist/chat/slack/texts.js +185 -0
- package/dist/chat/slack/thread.js +537 -0
- package/dist/chat/slack/update.js +71 -0
- package/dist/chat/slack/voice.js +84 -0
- package/dist/cli.js +326 -0
- package/dist/clock.js +21 -0
- package/dist/core/cleanup.js +145 -0
- package/dist/core/commands.js +172 -0
- package/dist/core/config.js +174 -0
- package/dist/core/files/changed.js +125 -0
- package/dist/core/files/listing.js +382 -0
- package/dist/core/files/paths.js +9 -0
- package/dist/core/files/read.js +191 -0
- package/dist/core/files/waiting.js +34 -0
- package/dist/core/flows/answers.js +133 -0
- package/dist/core/flows/ask-owner.js +95 -0
- package/dist/core/flows/asking.js +14 -0
- package/dist/core/flows/bind.js +95 -0
- package/dist/core/flows/contract.js +1 -0
- package/dist/core/flows/flows.js +35 -0
- package/dist/core/flows/hold.js +22 -0
- package/dist/core/flows/inbound.js +24 -0
- package/dist/core/flows/lists.js +21 -0
- package/dist/core/flows/messages.js +293 -0
- package/dist/core/flows/notices.js +51 -0
- package/dist/core/flows/repair.js +106 -0
- package/dist/core/flows/resume.js +163 -0
- package/dist/core/flows/setup.js +130 -0
- package/dist/core/flows/status.js +98 -0
- package/dist/core/flows/talk.js +207 -0
- package/dist/core/flows/update.js +370 -0
- package/dist/core/flows/words.js +171 -0
- package/dist/core/folders.js +93 -0
- package/dist/core/footer.js +313 -0
- package/dist/core/guide.js +50 -0
- package/dist/core/hold.js +126 -0
- package/dist/core/instance.js +46 -0
- package/dist/core/lock.js +412 -0
- package/dist/core/models.js +10 -0
- package/dist/core/paths.js +75 -0
- package/dist/core/port/file-flows.js +1 -0
- package/dist/core/port/home.js +1 -0
- package/dist/core/port/requests.js +1 -0
- package/dist/core/port/threads.js +1 -0
- package/dist/core/port/updates.js +1 -0
- package/dist/core/port.js +17 -0
- package/dist/core/reply/fold.js +175 -0
- package/dist/core/reply/previews.js +258 -0
- package/dist/core/reply/renderer.js +846 -0
- package/dist/core/reply/tail.js +24 -0
- package/dist/core/reply/words.js +32 -0
- package/dist/core/requests.js +158 -0
- package/dist/core/sessions/constants.js +61 -0
- package/dist/core/sessions/deps.js +1 -0
- package/dist/core/sessions/directory.js +86 -0
- package/dist/core/sessions/errors.js +79 -0
- package/dist/core/sessions/manager.js +378 -0
- package/dist/core/sessions/session.js +3297 -0
- package/dist/core/sessions/tasks.js +266 -0
- package/dist/core/sessions/turn.js +45 -0
- package/dist/core/setup.js +28 -0
- package/dist/core/state.js +724 -0
- package/dist/core/texts.js +451 -0
- package/dist/core/updates.js +332 -0
- package/dist/daemon.js +566 -0
- package/dist/host/npm.js +134 -0
- package/dist/host/program.js +49 -0
- package/dist/host/run.js +52 -0
- package/dist/host/service/launchd.js +385 -0
- package/dist/host/service/listing.js +42 -0
- package/dist/host/service/seam.js +1 -0
- package/dist/log.js +37 -0
- package/dist/main.js +29 -0
- package/dist/node-version.js +40 -0
- package/dist/registry.js +12 -0
- package/dist/semver.js +53 -0
- package/dist/text.js +89 -0
- package/dist/updates-host.js +113 -0
- package/dist/values.js +30 -0
- package/dist/version.js +54 -0
- package/package.json +32 -4
- package/slack-app-manifest.json +50 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deleting from Slack what the Home tab's edit mode names: a session's whole thread, or what
|
|
3
|
+
* sits in a channel outside every thread (the commands typed there and the bot's own messages).
|
|
4
|
+
*
|
|
5
|
+
* `chat.delete` with a bot token "may delete only messages posted by that bot", and with a user
|
|
6
|
+
* token the messages that user can delete (docs.slack.dev/reference/methods/chat.delete, read
|
|
7
|
+
* 2026-10-05). A session's thread is started by the owner, so its root and the owner's replies
|
|
8
|
+
* need the owner's own user token (`SLACK_USER_TOKEN`, user scope `chat:write`): the bot's
|
|
9
|
+
* messages are deleted with the bot token, every other one with the owner's. Without that token
|
|
10
|
+
* there is no deleter, and the page offers no delete.
|
|
11
|
+
*
|
|
12
|
+
* The replies go first and the root last: the reference does not say what a root deleted under
|
|
13
|
+
* its replies leaves behind. `chat.delete` is Tier 3 (50+ per minute); both clients retry a rate
|
|
14
|
+
* limit after the time Slack asks for, which is what a delete's time is made of (measured
|
|
15
|
+
* 2026-10-05 on a free workspace: 20 to 40 seconds a thread, with a rate limit met every few
|
|
16
|
+
* calls), so threads are deleted one at a time. A delete that stops half way leaves the thread
|
|
17
|
+
* in `state.json`, so asking again continues with the messages that are left. The Claude Code
|
|
18
|
+
* session is not touched: its transcript stays, and `!resume` lists it again once no thread
|
|
19
|
+
* holds it.
|
|
20
|
+
*
|
|
21
|
+
* Cleaning up a channel reads its history (`conversations.history`, Tier 3, cursor pagination,
|
|
22
|
+
* read 2026-10-05) and deletes the messages that are no thread and belong to no thread: "Detect
|
|
23
|
+
* a threaded message by looking for a `thread_ts` value in the message object", and a parent
|
|
24
|
+
* keeps it "even if all its replies have been deleted" (docs.slack.dev/messaging/retrieving-
|
|
25
|
+
* messages, read 2026-10-05). In a channel bound to a folder every message the owner sends
|
|
26
|
+
* outside a thread is a prompt or a word for the daemon, so one with no reply is a leftover
|
|
27
|
+
* whoever wrote it: the owner's and the bot's both go. What stays: a thread that has a reply, a
|
|
28
|
+
* message with a `subtype` (Slack's own lines), anyone else's message, and a message
|
|
29
|
+
* `state.json` holds as a thread whose first reply has not come yet. A message that carries
|
|
30
|
+
* `thread_ts` is deleted only after `conversations.replies` on it, with `limit=1`, returned its
|
|
31
|
+
* root with no `reply_count` (a root nobody replied to has none, measured 2026-10-01): the
|
|
32
|
+
* reference does not say what a parent's count reads in the channel's history once its replies
|
|
33
|
+
* are gone, and a thread taken for a leftover would lose its root.
|
|
34
|
+
*/
|
|
35
|
+
import { fill, Mutex, named } from "../../core/port.js";
|
|
36
|
+
import { getLogger } from "../../log.js";
|
|
37
|
+
import { isRecord } from "../../values.js";
|
|
38
|
+
import { describe } from "./reply/errors.js";
|
|
39
|
+
import * as slackTexts from "./texts.js";
|
|
40
|
+
export const logger = getLogger("allaboard.chat.slack.delete");
|
|
41
|
+
/** How many messages one `conversations.replies` page is asked for. */
|
|
42
|
+
export const PAGE = 200;
|
|
43
|
+
export const MESSAGE_NOT_FOUND = "message_not_found";
|
|
44
|
+
export const CANT_DELETE = "cant_delete_message";
|
|
45
|
+
export const THREAD_NOT_FOUND = "thread_not_found";
|
|
46
|
+
/** The messages of a `conversations.*` answer, `[]` for an answer with none. */
|
|
47
|
+
function messagesOf(answer) {
|
|
48
|
+
const messages = isRecord(answer) ? answer.messages : undefined;
|
|
49
|
+
return Array.isArray(messages) ? messages.filter(isRecord) : [];
|
|
50
|
+
}
|
|
51
|
+
/** The cursor of the next page; null on the last one. */
|
|
52
|
+
function nextCursor(answer) {
|
|
53
|
+
const metadata = isRecord(answer) ? answer.response_metadata : undefined;
|
|
54
|
+
const cursor = isRecord(metadata) ? metadata.next_cursor : undefined;
|
|
55
|
+
return typeof cursor === "string" && cursor !== "" ? cursor : null;
|
|
56
|
+
}
|
|
57
|
+
/** A message's `ts`; a message without one is a failure. */
|
|
58
|
+
function tsOf(message) {
|
|
59
|
+
if (message.ts === undefined)
|
|
60
|
+
throw new Error("a message without a ts");
|
|
61
|
+
return String(message.ts);
|
|
62
|
+
}
|
|
63
|
+
/** Deletes the threads the Home tab's Delete names, and cleans the channels it names. */
|
|
64
|
+
export class ThreadDeleter {
|
|
65
|
+
#bot;
|
|
66
|
+
#owner;
|
|
67
|
+
#botUserId;
|
|
68
|
+
#ownerUserId;
|
|
69
|
+
#state;
|
|
70
|
+
#names;
|
|
71
|
+
#release;
|
|
72
|
+
#free;
|
|
73
|
+
// One thread at a time: two deletes at once would share Slack's rate limit, and each would
|
|
74
|
+
// use up its retries on the other's calls.
|
|
75
|
+
#oneAtATime = new Mutex();
|
|
76
|
+
constructor(bot, owner, options) {
|
|
77
|
+
this.#bot = bot;
|
|
78
|
+
this.#owner = owner;
|
|
79
|
+
this.#botUserId = options.botUserId;
|
|
80
|
+
this.#ownerUserId = options.ownerUserId;
|
|
81
|
+
this.#state = options.state;
|
|
82
|
+
this.#names = options.names;
|
|
83
|
+
this.#release = options.release;
|
|
84
|
+
this.#free = options.free;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Delete every message of a thread that holds a session and forget the thread. Returns null
|
|
88
|
+
* once the thread is gone (or was not one of the daemon's), else the line that says why it is
|
|
89
|
+
* still there. Never throws.
|
|
90
|
+
*/
|
|
91
|
+
async delete(channelId, threadTs) {
|
|
92
|
+
if (this.#state.thread(channelId, threadTs) === null) {
|
|
93
|
+
return null; // not a thread of the daemon's: nothing a click may delete
|
|
94
|
+
}
|
|
95
|
+
return this.#oneAtATime.run(() => this.#delete(channelId, threadTs));
|
|
96
|
+
}
|
|
97
|
+
async #delete(channelId, threadTs) {
|
|
98
|
+
if (this.#state.thread(channelId, threadTs) === null) {
|
|
99
|
+
return null; // deleted while this call waited its turn
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
// From here until `free`, the thread is held: a message sent in it while its messages go
|
|
103
|
+
// finds no session, and none is rebuilt from the entry `state.json` still has
|
|
104
|
+
// (`SessionManager.release`).
|
|
105
|
+
if (!(await this.#release(channelId, threadTs)))
|
|
106
|
+
return slackTexts.HOME_DELETE_BUSY;
|
|
107
|
+
const refused = await this.#empty(channelId, threadTs);
|
|
108
|
+
if (refused > 0) {
|
|
109
|
+
// The root stays while a reply does, so the thread can still be opened.
|
|
110
|
+
return fill(named(slackTexts.HOME_DELETE_REFUSED, this.#names()), { count: refused });
|
|
111
|
+
}
|
|
112
|
+
this.#state.removeThread(channelId, threadTs);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.warning(`could not delete ${channelId}/${threadTs}: ${describe(error)}`);
|
|
116
|
+
return fill(slackTexts.HOME_DELETE_FAILED, { error: describe(error) });
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
this.#free(channelId, threadTs);
|
|
120
|
+
}
|
|
121
|
+
logger.info(`deleted thread ${channelId}/${threadTs}`);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Delete the thread's replies, then its root. The thread is read again after each pass, so a
|
|
126
|
+
* message that arrived meanwhile goes too, and the root goes only once a read shows no reply
|
|
127
|
+
* left: a delete never leaves replies under a deleted root. Returns how many messages Slack
|
|
128
|
+
* refused to delete; the root stays when it is not zero.
|
|
129
|
+
*/
|
|
130
|
+
async #empty(channelId, threadTs) {
|
|
131
|
+
const handled = new Set();
|
|
132
|
+
const refused = new Set();
|
|
133
|
+
let messages;
|
|
134
|
+
for (;;) {
|
|
135
|
+
messages = await this.#messages(channelId, threadTs);
|
|
136
|
+
const replies = [...messages].filter(([ts]) => ts !== threadTs && !handled.has(ts));
|
|
137
|
+
if (replies.length === 0)
|
|
138
|
+
break;
|
|
139
|
+
for (const [ts, author] of replies) {
|
|
140
|
+
handled.add(ts);
|
|
141
|
+
if (!(await this.#remove(channelId, ts, author)))
|
|
142
|
+
refused.add(ts);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const root = refused.size > 0 ? undefined : messages.get(threadTs);
|
|
146
|
+
if (root !== undefined && !(await this.#remove(channelId, threadTs, root))) {
|
|
147
|
+
refused.add(threadTs);
|
|
148
|
+
}
|
|
149
|
+
return refused.size;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Delete one message with its author's token; anyone else's is tried with the owner's, which
|
|
153
|
+
* deletes what the owner may delete in Slack. False when Slack refuses this very message
|
|
154
|
+
* (`cant_delete_message`): the caller goes on and counts it. A message already gone is
|
|
155
|
+
* deleted; any other failure throws.
|
|
156
|
+
*/
|
|
157
|
+
async #remove(channelId, ts, author) {
|
|
158
|
+
const client = author === this.#botUserId ? this.#bot : this.#owner;
|
|
159
|
+
try {
|
|
160
|
+
await client.chat.delete({ channel: channelId, ts });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (describe(error) === CANT_DELETE)
|
|
164
|
+
return false;
|
|
165
|
+
if (describe(error) !== MESSAGE_NOT_FOUND)
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Delete what sits in a bound channel outside every thread: the owner's messages and the
|
|
172
|
+
* bot's that have no reply. Returns null when done (or for a channel that is not bound), else
|
|
173
|
+
* the line that says why it stopped. Never throws.
|
|
174
|
+
*/
|
|
175
|
+
async clean(channelId) {
|
|
176
|
+
if (!this.#state.channels().includes(channelId)) {
|
|
177
|
+
return null; // not a channel of the daemon's: nothing a click may clean
|
|
178
|
+
}
|
|
179
|
+
return this.#oneAtATime.run(async () => {
|
|
180
|
+
let loose = new Map();
|
|
181
|
+
let refused = 0;
|
|
182
|
+
try {
|
|
183
|
+
loose = await this.#loose(channelId);
|
|
184
|
+
for (const [ts, author] of loose) {
|
|
185
|
+
if (!(await this.#remove(channelId, ts, author)))
|
|
186
|
+
refused += 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
logger.warning(`could not clean up ${channelId}: ${describe(error)}`);
|
|
191
|
+
return fill(slackTexts.HOME_CLEAN_FAILED, { error: describe(error) });
|
|
192
|
+
}
|
|
193
|
+
logger.info(`cleaned up ${channelId}: ${loose.size} messages, ${refused} refused`);
|
|
194
|
+
return refused > 0
|
|
195
|
+
? fill(named(slackTexts.HOME_CLEAN_REFUSED, this.#names()), { count: refused })
|
|
196
|
+
: null;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** The channel's messages a clean-up deletes, `ts` to author, read page by page. */
|
|
200
|
+
async #loose(channelId) {
|
|
201
|
+
const found = new Map();
|
|
202
|
+
let cursor = null;
|
|
203
|
+
for (;;) {
|
|
204
|
+
const page = await this.#bot.conversations.history({
|
|
205
|
+
channel: channelId,
|
|
206
|
+
limit: PAGE,
|
|
207
|
+
...(cursor === null ? {} : { cursor }),
|
|
208
|
+
});
|
|
209
|
+
for (const message of messagesOf(page)) {
|
|
210
|
+
const ts = tsOf(message);
|
|
211
|
+
const author = String(message.user || "");
|
|
212
|
+
const threaded = message.thread_ts;
|
|
213
|
+
if (message.subtype || (threaded && String(threaded) !== ts)) {
|
|
214
|
+
continue; // Slack's own line; or a thread's reply shown in the channel
|
|
215
|
+
}
|
|
216
|
+
if (author !== this.#botUserId && author !== this.#ownerUserId)
|
|
217
|
+
continue;
|
|
218
|
+
if (this.#state.thread(channelId, ts) !== null) {
|
|
219
|
+
continue; // a thread of the daemon's with no reply yet
|
|
220
|
+
}
|
|
221
|
+
if (threaded && (await this.#hasReplies(channelId, ts)))
|
|
222
|
+
continue; // a thread
|
|
223
|
+
found.set(ts, author);
|
|
224
|
+
}
|
|
225
|
+
cursor = nextCursor(page);
|
|
226
|
+
if (cursor === null)
|
|
227
|
+
return found;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Whether a message that carries `thread_ts` still has a reply, asked of the thread itself.
|
|
232
|
+
* True as well for one that is gone meanwhile: there is nothing to delete.
|
|
233
|
+
*/
|
|
234
|
+
async #hasReplies(channelId, ts) {
|
|
235
|
+
let answer;
|
|
236
|
+
try {
|
|
237
|
+
answer = await this.#bot.conversations.replies({ channel: channelId, ts, limit: 1 });
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (describe(error) === THREAD_NOT_FOUND)
|
|
241
|
+
return true;
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
const messages = messagesOf(answer);
|
|
245
|
+
const root = messages.find((message) => String(message.ts) === ts);
|
|
246
|
+
// No root in the answer is no proof of an empty thread: keep the message.
|
|
247
|
+
return root === undefined || Boolean(root.reply_count) || messages.length > 1;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Every message of the thread, its `ts` to its author's user id, read page by page (cursor
|
|
251
|
+
* pagination, `response_metadata.next_cursor`). Empty when the root is gone.
|
|
252
|
+
*/
|
|
253
|
+
async #messages(channelId, threadTs) {
|
|
254
|
+
const found = new Map();
|
|
255
|
+
let cursor = null;
|
|
256
|
+
for (;;) {
|
|
257
|
+
let page;
|
|
258
|
+
try {
|
|
259
|
+
page = await this.#bot.conversations.replies({
|
|
260
|
+
channel: channelId,
|
|
261
|
+
ts: threadTs,
|
|
262
|
+
limit: PAGE,
|
|
263
|
+
...(cursor === null ? {} : { cursor }),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (describe(error) === THREAD_NOT_FOUND)
|
|
268
|
+
return found;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
for (const message of messagesOf(page))
|
|
272
|
+
found.set(tsOf(message), String(message.user || ""));
|
|
273
|
+
cursor = nextCursor(page);
|
|
274
|
+
if (cursor === null)
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The footer as one line of Slack mrkdwn. */
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { footerFields, valueWithDetail, } from "../../core/port.js";
|
|
4
|
+
import { mrkdwnEscape } from "./reply/escape.js";
|
|
5
|
+
// The footer's fields that come before the folder.
|
|
6
|
+
const SESSION = ["model", "effort"];
|
|
7
|
+
/** A field as the footer writes it: the label in bold (`*ctx* 15%`, the owner's choice). */
|
|
8
|
+
function short(field) {
|
|
9
|
+
const { key, value } = field;
|
|
10
|
+
switch (key) {
|
|
11
|
+
case "model":
|
|
12
|
+
case "changes":
|
|
13
|
+
return value;
|
|
14
|
+
case "effort":
|
|
15
|
+
return `*effort* ${valueWithDetail(field)}`;
|
|
16
|
+
case "branch":
|
|
17
|
+
return mrkdwnEscape(value);
|
|
18
|
+
case "tokens":
|
|
19
|
+
return `${value} *tok*`;
|
|
20
|
+
case "context":
|
|
21
|
+
return `*ctx* ${value}`;
|
|
22
|
+
case "session_limit":
|
|
23
|
+
return `*5h* ${value}`;
|
|
24
|
+
case "week_limit":
|
|
25
|
+
return `*7d* ${value}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One line: bypass, model and effort, the channel's folder, then branch, changes, tokens,
|
|
30
|
+
* context and limits (the owner's order).
|
|
31
|
+
*/
|
|
32
|
+
export function formatFooter(data, now) {
|
|
33
|
+
const fields = footerFields(data, now);
|
|
34
|
+
const parts = data.bypass ? ["⚡ bypass"] : [];
|
|
35
|
+
parts.push(...fields.filter((f) => SESSION.includes(f.key)).map((f) => short(f)));
|
|
36
|
+
// Its name alone, the project's: the whole path is on `!status`'s Directory line.
|
|
37
|
+
const name = data.folder === null ? "" : basename(data.folder);
|
|
38
|
+
if (name) {
|
|
39
|
+
parts.push(mrkdwnEscape(name));
|
|
40
|
+
}
|
|
41
|
+
parts.push(...fields.filter((f) => !SESSION.includes(f.key)).map((f) => short(f)));
|
|
42
|
+
return parts.join(" · ");
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { texts } from "../../core/port.js";
|
|
2
|
+
import * as slackTexts from "./texts.js";
|
|
3
|
+
/** The same-folder hold's question as Slack blocks. */
|
|
4
|
+
export const HOLD_CONTINUE = "hold_continue";
|
|
5
|
+
export const HOLD_CANCEL = "hold_cancel";
|
|
6
|
+
/**
|
|
7
|
+
* The question in the thread: a `mrkdwn` section (`link` in mrkdwn's own `<url|label>` form, same
|
|
8
|
+
* as approvals and the resume picker use for their own buttons) plus Continue and Cancel. Only
|
|
9
|
+
* Continue is `primary`: one button of a set, and never `danger` for an answer that destroys
|
|
10
|
+
* nothing (button element reference, docs.slack.dev, read 2026-10-07).
|
|
11
|
+
*/
|
|
12
|
+
export function holdBlocks(holdId, link) {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
type: "section",
|
|
16
|
+
text: { type: "mrkdwn", text: texts.fill(texts.HOLD_QUESTION, { link }) },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: "actions",
|
|
20
|
+
elements: [
|
|
21
|
+
{
|
|
22
|
+
type: "button",
|
|
23
|
+
action_id: HOLD_CONTINUE,
|
|
24
|
+
value: holdId,
|
|
25
|
+
style: "primary",
|
|
26
|
+
text: { type: "plain_text", text: slackTexts.HOLD_CONTINUE_BUTTON },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "button",
|
|
30
|
+
action_id: HOLD_CANCEL,
|
|
31
|
+
value: holdId,
|
|
32
|
+
text: { type: "plain_text", text: slackTexts.HOLD_CANCEL_BUTTON },
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
}
|