agent-slack 0.3.0 → 0.4.1
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/README.md +48 -0
- package/dist/index.js +1771 -7
- package/dist/index.js.map +14 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1317,6 +1317,32 @@ async function resolveChannelId(client, input) {
|
|
|
1317
1317
|
}
|
|
1318
1318
|
throw new Error(`Could not resolve channel name: #${name}`);
|
|
1319
1319
|
}
|
|
1320
|
+
async function resolveChannelName(client, channelId) {
|
|
1321
|
+
try {
|
|
1322
|
+
const resp = await client.api("conversations.info", { channel: channelId });
|
|
1323
|
+
const channel = isRecord5(resp) ? resp.channel : null;
|
|
1324
|
+
if (!isRecord5(channel)) {
|
|
1325
|
+
return channelId;
|
|
1326
|
+
}
|
|
1327
|
+
if (channel.is_im) {
|
|
1328
|
+
const userId = getString(channel.user);
|
|
1329
|
+
if (userId) {
|
|
1330
|
+
try {
|
|
1331
|
+
const userResp = await client.api("users.info", { user: userId });
|
|
1332
|
+
const user = isRecord5(userResp) ? userResp.user : null;
|
|
1333
|
+
const profile = isRecord5(user) ? user.profile : null;
|
|
1334
|
+
if (isRecord5(profile)) {
|
|
1335
|
+
return getString(profile.display_name) || getString(profile.real_name) || channelId;
|
|
1336
|
+
}
|
|
1337
|
+
} catch {}
|
|
1338
|
+
}
|
|
1339
|
+
return channelId;
|
|
1340
|
+
}
|
|
1341
|
+
return getString(channel.name) || channelId;
|
|
1342
|
+
} catch {
|
|
1343
|
+
return channelId;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1320
1346
|
|
|
1321
1347
|
// src/slack/client.ts
|
|
1322
1348
|
import { WebClient } from "@slack/web-api";
|
|
@@ -3369,6 +3395,1471 @@ async function reactOnTarget(input) {
|
|
|
3369
3395
|
return { ok: true };
|
|
3370
3396
|
}
|
|
3371
3397
|
|
|
3398
|
+
// src/cli/draft-server.ts
|
|
3399
|
+
import { createServer } from "node:http";
|
|
3400
|
+
import { exec } from "node:child_process";
|
|
3401
|
+
function openDraftEditor(config) {
|
|
3402
|
+
return new Promise((resolve3, reject) => {
|
|
3403
|
+
let settled = false;
|
|
3404
|
+
const server = createServer(async (req, res) => {
|
|
3405
|
+
if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
|
|
3406
|
+
const html = buildEditorHtml(config);
|
|
3407
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3408
|
+
res.end(html);
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
if (req.method === "POST" && req.url === "/send") {
|
|
3412
|
+
try {
|
|
3413
|
+
const body = await readBody(req);
|
|
3414
|
+
const data = JSON.parse(body);
|
|
3415
|
+
if (typeof data.text !== "string" || !data.text.trim()) {
|
|
3416
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3417
|
+
res.end(JSON.stringify({ ok: false, error: "text is required" }));
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
const sendResult = await config.onSend(data.text);
|
|
3421
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3422
|
+
res.end(JSON.stringify({ ok: true, ts: sendResult.ts }));
|
|
3423
|
+
settled = true;
|
|
3424
|
+
resolve3({ sent: true, text: data.text });
|
|
3425
|
+
setTimeout(() => server.close(), 300);
|
|
3426
|
+
} catch (err) {
|
|
3427
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3428
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
3429
|
+
}
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
if (req.method === "POST" && req.url === "/cancel") {
|
|
3433
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3434
|
+
res.end(JSON.stringify({ ok: true }));
|
|
3435
|
+
settled = true;
|
|
3436
|
+
resolve3({ cancelled: true });
|
|
3437
|
+
setTimeout(() => server.close(), 300);
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
res.writeHead(404);
|
|
3441
|
+
res.end("Not found");
|
|
3442
|
+
});
|
|
3443
|
+
server.on("error", (err) => {
|
|
3444
|
+
if (!settled) {
|
|
3445
|
+
reject(err);
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3448
|
+
server.on("close", () => {
|
|
3449
|
+
clearTimeout(idleTimeout);
|
|
3450
|
+
if (!settled) {
|
|
3451
|
+
settled = true;
|
|
3452
|
+
resolve3({ cancelled: true });
|
|
3453
|
+
}
|
|
3454
|
+
});
|
|
3455
|
+
const idleTimeout = setTimeout(() => {
|
|
3456
|
+
if (!settled) {
|
|
3457
|
+
server.close();
|
|
3458
|
+
}
|
|
3459
|
+
}, 30 * 60 * 1000);
|
|
3460
|
+
server.listen(0, "127.0.0.1", () => {
|
|
3461
|
+
const addr = server.address();
|
|
3462
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
3463
|
+
const url = `http://127.0.0.1:${port}`;
|
|
3464
|
+
process.stderr.write(`Draft editor: ${url}
|
|
3465
|
+
`);
|
|
3466
|
+
openBrowser(url);
|
|
3467
|
+
});
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
function readBody(req) {
|
|
3471
|
+
return new Promise((resolve3, reject) => {
|
|
3472
|
+
const chunks = [];
|
|
3473
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
3474
|
+
req.on("end", () => resolve3(Buffer.concat(chunks).toString()));
|
|
3475
|
+
req.on("error", reject);
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
function openBrowser(url) {
|
|
3479
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3480
|
+
exec(`${cmd} "${url}"`, () => {});
|
|
3481
|
+
}
|
|
3482
|
+
function buildSlackThreadUrl(config) {
|
|
3483
|
+
if (!config.workspaceUrl || !config.channelId || !config.threadTs) {
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
3486
|
+
const tsNoDot = config.threadTs.replace(".", "");
|
|
3487
|
+
return `${config.workspaceUrl.replace(/\/$/, "")}/archives/${config.channelId}/p${tsNoDot}`;
|
|
3488
|
+
}
|
|
3489
|
+
function extractWorkspaceName(url) {
|
|
3490
|
+
if (!url) {
|
|
3491
|
+
return null;
|
|
3492
|
+
}
|
|
3493
|
+
try {
|
|
3494
|
+
const host = new URL(url).hostname;
|
|
3495
|
+
const parts = host.split(".");
|
|
3496
|
+
if (parts.length >= 3 && parts.at(-2) === "slack") {
|
|
3497
|
+
return parts.slice(0, -2).join(".");
|
|
3498
|
+
}
|
|
3499
|
+
return host;
|
|
3500
|
+
} catch {
|
|
3501
|
+
return null;
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
function buildEditorHtml(config) {
|
|
3505
|
+
const threadUrl = buildSlackThreadUrl(config);
|
|
3506
|
+
const workspaceName = extractWorkspaceName(config.workspaceUrl);
|
|
3507
|
+
const injectedConfig = JSON.stringify({
|
|
3508
|
+
channelName: config.channelName,
|
|
3509
|
+
channelId: config.channelId || null,
|
|
3510
|
+
workspaceUrl: config.workspaceUrl || null,
|
|
3511
|
+
workspaceName,
|
|
3512
|
+
threadTs: config.threadTs || null,
|
|
3513
|
+
threadUrl,
|
|
3514
|
+
initialText: config.initialText || ""
|
|
3515
|
+
});
|
|
3516
|
+
return EDITOR_HTML.replace("__DRAFT_CONFIG__", injectedConfig.replace(/</g, "\\u003c"));
|
|
3517
|
+
}
|
|
3518
|
+
var EDITOR_HTML = `<!DOCTYPE html>
|
|
3519
|
+
<html lang="en">
|
|
3520
|
+
<head>
|
|
3521
|
+
<meta charset="UTF-8">
|
|
3522
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3523
|
+
<title>Draft Message — Agent Slack</title>
|
|
3524
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
3525
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
3526
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" rel="stylesheet">
|
|
3527
|
+
<style>
|
|
3528
|
+
:root {
|
|
3529
|
+
--bg: #1a1d21;
|
|
3530
|
+
--surface: #222529;
|
|
3531
|
+
--surface-raised: #2c2d31;
|
|
3532
|
+
--border: #393b3f;
|
|
3533
|
+
--border-focus: #1264a3;
|
|
3534
|
+
--text: #d1d2d3;
|
|
3535
|
+
--text-secondary: #ababad;
|
|
3536
|
+
--text-muted: #696a6d;
|
|
3537
|
+
--green: #007a5a;
|
|
3538
|
+
--green-hover: #148567;
|
|
3539
|
+
--red: #e01e5a;
|
|
3540
|
+
--blue: #1264a3;
|
|
3541
|
+
--blue-link: #1d9bd1;
|
|
3542
|
+
--code-bg: #1a1d21;
|
|
3543
|
+
--blockquote-border: #616061;
|
|
3544
|
+
--toolbar-active: rgba(29,155,209,0.15);
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3548
|
+
|
|
3549
|
+
body {
|
|
3550
|
+
font-family: Slack-Lato, Lato, appleLogo, sans-serif;
|
|
3551
|
+
background: var(--bg);
|
|
3552
|
+
color: var(--text);
|
|
3553
|
+
display: flex;
|
|
3554
|
+
align-items: flex-start;
|
|
3555
|
+
justify-content: center;
|
|
3556
|
+
min-height: 100vh;
|
|
3557
|
+
padding: 40px 24px;
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
.page {
|
|
3561
|
+
width: 100%;
|
|
3562
|
+
max-width: 720px;
|
|
3563
|
+
display: flex;
|
|
3564
|
+
flex-direction: column;
|
|
3565
|
+
align-items: center;
|
|
3566
|
+
animation: fadeIn 0.18s ease-out;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
@keyframes fadeIn {
|
|
3570
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
3571
|
+
to { opacity: 1; transform: translateY(0); }
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
/* ── Branding ── */
|
|
3575
|
+
.brand {
|
|
3576
|
+
position: relative;
|
|
3577
|
+
text-align: center;
|
|
3578
|
+
margin-bottom: 20px;
|
|
3579
|
+
user-select: none;
|
|
3580
|
+
}
|
|
3581
|
+
.brand-name {
|
|
3582
|
+
font-size: 17px;
|
|
3583
|
+
font-weight: 900;
|
|
3584
|
+
letter-spacing: -0.3px;
|
|
3585
|
+
color: var(--text);
|
|
3586
|
+
}
|
|
3587
|
+
.brand-byline {
|
|
3588
|
+
display: flex;
|
|
3589
|
+
align-items: center;
|
|
3590
|
+
gap: 6px;
|
|
3591
|
+
margin-top: 2px;
|
|
3592
|
+
justify-content: center;
|
|
3593
|
+
font-family: 'Inter', sans-serif;
|
|
3594
|
+
font-size: 10px;
|
|
3595
|
+
font-weight: 500;
|
|
3596
|
+
color: rgba(255,255,255,0.5);
|
|
3597
|
+
}
|
|
3598
|
+
.brand-byline a {
|
|
3599
|
+
color: rgba(255,255,255,0.6);
|
|
3600
|
+
text-decoration: none;
|
|
3601
|
+
}
|
|
3602
|
+
.brand-byline a:hover {
|
|
3603
|
+
color: rgba(255,255,255,0.8);
|
|
3604
|
+
}
|
|
3605
|
+
.brand-sub {
|
|
3606
|
+
font-size: 11px;
|
|
3607
|
+
font-weight: 600;
|
|
3608
|
+
letter-spacing: 1.5px;
|
|
3609
|
+
text-transform: uppercase;
|
|
3610
|
+
color: var(--text-muted);
|
|
3611
|
+
margin-top: 8px;
|
|
3612
|
+
}
|
|
3613
|
+
.brand-byline img {
|
|
3614
|
+
width: 12px;
|
|
3615
|
+
height: 12px;
|
|
3616
|
+
opacity: 0.6;
|
|
3617
|
+
vertical-align: -1px;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
/* ── Context bar ── */
|
|
3621
|
+
.context-bar {
|
|
3622
|
+
width: 100%;
|
|
3623
|
+
display: flex;
|
|
3624
|
+
align-items: center;
|
|
3625
|
+
gap: 6px;
|
|
3626
|
+
padding: 0 4px;
|
|
3627
|
+
margin-bottom: 10px;
|
|
3628
|
+
font-size: 13px;
|
|
3629
|
+
color: var(--text-secondary);
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
.context-bar .hash-icon {
|
|
3633
|
+
width: 15px;
|
|
3634
|
+
height: 15px;
|
|
3635
|
+
color: var(--text-muted);
|
|
3636
|
+
flex-shrink: 0;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
.context-bar .channel {
|
|
3640
|
+
font-weight: 700;
|
|
3641
|
+
color: var(--text);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
.context-bar .workspace-label {
|
|
3645
|
+
color: var(--text-muted);
|
|
3646
|
+
font-size: 13px;
|
|
3647
|
+
margin-left: 8px;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
.context-bar .thread-link {
|
|
3651
|
+
margin-left: auto;
|
|
3652
|
+
font-size: 12px;
|
|
3653
|
+
color: var(--blue-link);
|
|
3654
|
+
text-decoration: none;
|
|
3655
|
+
display: flex;
|
|
3656
|
+
align-items: center;
|
|
3657
|
+
gap: 4px;
|
|
3658
|
+
}
|
|
3659
|
+
.context-bar .thread-link:hover { text-decoration: underline; }
|
|
3660
|
+
.context-bar .thread-link svg {
|
|
3661
|
+
width: 12px;
|
|
3662
|
+
height: 12px;
|
|
3663
|
+
fill: none;
|
|
3664
|
+
stroke: currentColor;
|
|
3665
|
+
stroke-width: 2;
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
/* ── Composer card ── */
|
|
3669
|
+
.composer {
|
|
3670
|
+
width: 100%;
|
|
3671
|
+
background: var(--surface);
|
|
3672
|
+
border: 1px solid #818385;
|
|
3673
|
+
border-radius: 8px;
|
|
3674
|
+
overflow: hidden;
|
|
3675
|
+
transition: border-color 0.15s;
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
.composer:focus-within {
|
|
3679
|
+
border-color: var(--border-focus);
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
/* ── Toolbar ── */
|
|
3683
|
+
.toolbar {
|
|
3684
|
+
display: flex;
|
|
3685
|
+
align-items: center;
|
|
3686
|
+
gap: 1px;
|
|
3687
|
+
padding: 4px 8px;
|
|
3688
|
+
border-bottom: 1px solid var(--border);
|
|
3689
|
+
background: var(--surface);
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
.toolbar-btn {
|
|
3693
|
+
display: inline-flex;
|
|
3694
|
+
align-items: center;
|
|
3695
|
+
justify-content: center;
|
|
3696
|
+
width: 32px;
|
|
3697
|
+
height: 32px;
|
|
3698
|
+
border: none;
|
|
3699
|
+
border-radius: 4px;
|
|
3700
|
+
background: transparent;
|
|
3701
|
+
color: var(--text-secondary);
|
|
3702
|
+
cursor: pointer;
|
|
3703
|
+
font-size: 13px;
|
|
3704
|
+
font-family: inherit;
|
|
3705
|
+
transition: all 0.08s;
|
|
3706
|
+
position: relative;
|
|
3707
|
+
flex-shrink: 0;
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
.toolbar-btn:hover {
|
|
3711
|
+
background: var(--surface-raised);
|
|
3712
|
+
color: var(--text);
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
.toolbar-btn.active {
|
|
3716
|
+
background: var(--toolbar-active);
|
|
3717
|
+
color: var(--blue-link);
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
.toolbar-btn.disabled {
|
|
3721
|
+
opacity: 0.3;
|
|
3722
|
+
pointer-events: none;
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
.toolbar-btn svg { width: 16px; height: 16px; fill: currentColor; }
|
|
3726
|
+
.toolbar-btn .b { font-weight: 800; font-size: 15px; line-height: 1; }
|
|
3727
|
+
.toolbar-btn .i { font-style: italic; font-weight: 600; font-size: 15px; font-family: Georgia, serif; line-height: 1; }
|
|
3728
|
+
.toolbar-btn .s { text-decoration: line-through; font-weight: 600; font-size: 13px; line-height: 1; }
|
|
3729
|
+
|
|
3730
|
+
.toolbar-sep {
|
|
3731
|
+
width: 1px;
|
|
3732
|
+
height: 20px;
|
|
3733
|
+
background: var(--border);
|
|
3734
|
+
margin: 0 4px;
|
|
3735
|
+
flex-shrink: 0;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
/* Tooltip */
|
|
3739
|
+
.toolbar-btn[data-tip]::after {
|
|
3740
|
+
content: attr(data-tip);
|
|
3741
|
+
position: absolute;
|
|
3742
|
+
bottom: calc(100% + 6px);
|
|
3743
|
+
left: 50%;
|
|
3744
|
+
transform: translateX(-50%);
|
|
3745
|
+
background: #1d1d1d;
|
|
3746
|
+
color: #e0e0e0;
|
|
3747
|
+
font-size: 11px;
|
|
3748
|
+
font-weight: 400;
|
|
3749
|
+
font-style: normal;
|
|
3750
|
+
text-decoration: none;
|
|
3751
|
+
padding: 4px 8px;
|
|
3752
|
+
border-radius: 6px;
|
|
3753
|
+
white-space: nowrap;
|
|
3754
|
+
pointer-events: none;
|
|
3755
|
+
opacity: 0;
|
|
3756
|
+
transition: opacity 0.12s;
|
|
3757
|
+
z-index: 20;
|
|
3758
|
+
}
|
|
3759
|
+
.toolbar-btn:hover[data-tip]::after { opacity: 1; }
|
|
3760
|
+
|
|
3761
|
+
/* ── Editor ── */
|
|
3762
|
+
.editor {
|
|
3763
|
+
min-height: 200px;
|
|
3764
|
+
max-height: 55vh;
|
|
3765
|
+
overflow-y: auto;
|
|
3766
|
+
padding: 12px 16px;
|
|
3767
|
+
font-size: 15px;
|
|
3768
|
+
line-height: 1.46668;
|
|
3769
|
+
color: var(--text);
|
|
3770
|
+
outline: none;
|
|
3771
|
+
word-wrap: break-word;
|
|
3772
|
+
overflow-wrap: break-word;
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
.editor:empty::before {
|
|
3776
|
+
content: attr(data-placeholder);
|
|
3777
|
+
color: var(--text-muted);
|
|
3778
|
+
pointer-events: none;
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
.editor b, .editor strong { font-weight: 700; }
|
|
3782
|
+
.editor i, .editor em { font-style: italic; }
|
|
3783
|
+
.editor s, .editor strike, .editor del { text-decoration: line-through; }
|
|
3784
|
+
|
|
3785
|
+
.editor code {
|
|
3786
|
+
background: var(--code-bg);
|
|
3787
|
+
border: 1px solid var(--border);
|
|
3788
|
+
border-radius: 3px;
|
|
3789
|
+
padding: 2px 4px;
|
|
3790
|
+
font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
|
|
3791
|
+
font-size: 12px;
|
|
3792
|
+
color: #e06c75;
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
.editor pre {
|
|
3796
|
+
background: var(--code-bg);
|
|
3797
|
+
border: 1px solid var(--border);
|
|
3798
|
+
border-radius: 4px;
|
|
3799
|
+
padding: 8px 12px;
|
|
3800
|
+
margin: 4px 0;
|
|
3801
|
+
font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
|
|
3802
|
+
font-size: 12px;
|
|
3803
|
+
line-height: 1.5;
|
|
3804
|
+
overflow-x: auto;
|
|
3805
|
+
white-space: pre-wrap;
|
|
3806
|
+
color: var(--text);
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
.editor pre code {
|
|
3810
|
+
background: none;
|
|
3811
|
+
border: none;
|
|
3812
|
+
padding: 0;
|
|
3813
|
+
font-size: inherit;
|
|
3814
|
+
color: inherit;
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
/* Adjacent <pre> elements look like one code block (browser splits on Enter) */
|
|
3818
|
+
.editor pre + pre,
|
|
3819
|
+
.editor pre + br + pre {
|
|
3820
|
+
border-top: none;
|
|
3821
|
+
margin-top: -4px; /* collapse gap */
|
|
3822
|
+
padding-top: 0;
|
|
3823
|
+
border-top-left-radius: 0;
|
|
3824
|
+
border-top-right-radius: 0;
|
|
3825
|
+
}
|
|
3826
|
+
.editor pre:has(+ pre),
|
|
3827
|
+
.editor pre:has(+ br + pre) {
|
|
3828
|
+
border-bottom: none;
|
|
3829
|
+
margin-bottom: 0;
|
|
3830
|
+
padding-bottom: 0;
|
|
3831
|
+
border-bottom-left-radius: 0;
|
|
3832
|
+
border-bottom-right-radius: 0;
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
.editor blockquote {
|
|
3836
|
+
border-left: 4px solid var(--blockquote-border);
|
|
3837
|
+
padding: 4px 0 4px 16px;
|
|
3838
|
+
margin: 4px 0;
|
|
3839
|
+
color: var(--text-secondary);
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
.editor ul, .editor ol { padding-left: 26px; margin: 4px 0; }
|
|
3843
|
+
.editor li { margin: 2px 0; }
|
|
3844
|
+
.editor a { color: var(--blue-link); text-decoration: none; }
|
|
3845
|
+
.editor a:hover { text-decoration: underline; }
|
|
3846
|
+
|
|
3847
|
+
/* ── Source textarea ── */
|
|
3848
|
+
.source-editor {
|
|
3849
|
+
display: none;
|
|
3850
|
+
min-height: 200px;
|
|
3851
|
+
max-height: 55vh;
|
|
3852
|
+
width: 100%;
|
|
3853
|
+
padding: 12px 16px;
|
|
3854
|
+
font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
|
|
3855
|
+
font-size: 13px;
|
|
3856
|
+
line-height: 1.5;
|
|
3857
|
+
background: var(--surface);
|
|
3858
|
+
color: var(--text);
|
|
3859
|
+
border: none;
|
|
3860
|
+
outline: none;
|
|
3861
|
+
resize: vertical;
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
/* ── Bottom bar ── */
|
|
3865
|
+
.bottom-bar {
|
|
3866
|
+
display: flex;
|
|
3867
|
+
align-items: center;
|
|
3868
|
+
justify-content: space-between;
|
|
3869
|
+
padding: 6px 10px;
|
|
3870
|
+
border-top: 1px solid var(--border);
|
|
3871
|
+
background: var(--surface);
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
.bottom-left {
|
|
3875
|
+
display: flex;
|
|
3876
|
+
align-items: center;
|
|
3877
|
+
gap: 10px;
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
.hint {
|
|
3881
|
+
font-size: 12px;
|
|
3882
|
+
color: var(--text-muted);
|
|
3883
|
+
}
|
|
3884
|
+
.hint kbd {
|
|
3885
|
+
background: var(--surface-raised);
|
|
3886
|
+
border: 1px solid var(--border);
|
|
3887
|
+
border-radius: 3px;
|
|
3888
|
+
padding: 1px 4px;
|
|
3889
|
+
font-family: inherit;
|
|
3890
|
+
font-size: 11px;
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
.btn-send {
|
|
3894
|
+
background: var(--green);
|
|
3895
|
+
color: #fff;
|
|
3896
|
+
display: inline-flex;
|
|
3897
|
+
align-items: center;
|
|
3898
|
+
justify-content: center;
|
|
3899
|
+
width: 32px;
|
|
3900
|
+
height: 32px;
|
|
3901
|
+
border: none;
|
|
3902
|
+
border-radius: 4px;
|
|
3903
|
+
cursor: pointer;
|
|
3904
|
+
transition: all 0.1s;
|
|
3905
|
+
flex-shrink: 0;
|
|
3906
|
+
}
|
|
3907
|
+
.btn-send:hover { background: var(--green-hover); }
|
|
3908
|
+
.btn-send:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
3909
|
+
.btn-send svg { width: 16px; height: 16px; fill: currentColor; }
|
|
3910
|
+
|
|
3911
|
+
.cancel-link {
|
|
3912
|
+
background: none;
|
|
3913
|
+
border: none;
|
|
3914
|
+
color: var(--text-muted);
|
|
3915
|
+
font-size: 12px;
|
|
3916
|
+
font-family: inherit;
|
|
3917
|
+
cursor: pointer;
|
|
3918
|
+
padding: 4px 6px;
|
|
3919
|
+
}
|
|
3920
|
+
.cancel-link:hover { color: var(--text-secondary); text-decoration: underline; }
|
|
3921
|
+
|
|
3922
|
+
.btn-aa {
|
|
3923
|
+
display: inline-flex;
|
|
3924
|
+
align-items: center;
|
|
3925
|
+
justify-content: center;
|
|
3926
|
+
width: 32px;
|
|
3927
|
+
height: 32px;
|
|
3928
|
+
border: none;
|
|
3929
|
+
border-radius: 4px;
|
|
3930
|
+
background: transparent;
|
|
3931
|
+
color: var(--text-secondary);
|
|
3932
|
+
cursor: pointer;
|
|
3933
|
+
font-size: 14px;
|
|
3934
|
+
font-weight: 700;
|
|
3935
|
+
font-family: inherit;
|
|
3936
|
+
transition: all 0.08s;
|
|
3937
|
+
flex-shrink: 0;
|
|
3938
|
+
}
|
|
3939
|
+
.btn-aa:hover { background: var(--surface-raised); color: var(--text); }
|
|
3940
|
+
.btn-aa.active { background: var(--toolbar-active); color: var(--blue-link); }
|
|
3941
|
+
|
|
3942
|
+
.btn-source {
|
|
3943
|
+
background: transparent;
|
|
3944
|
+
color: var(--text-muted);
|
|
3945
|
+
font-size: 11px;
|
|
3946
|
+
font-weight: 600;
|
|
3947
|
+
padding: 3px 6px;
|
|
3948
|
+
border-radius: 4px;
|
|
3949
|
+
border: 1px solid transparent;
|
|
3950
|
+
cursor: pointer;
|
|
3951
|
+
font-family: Monaco, Menlo, Consolas, monospace;
|
|
3952
|
+
}
|
|
3953
|
+
.btn-source:hover { color: var(--text-secondary); border-color: var(--border); }
|
|
3954
|
+
.btn-source.active { color: var(--blue-link); border-color: rgba(29,155,209,0.3); }
|
|
3955
|
+
|
|
3956
|
+
/* ── Inline link popover (Slack-style two-field) ── */
|
|
3957
|
+
.link-popover {
|
|
3958
|
+
display: none;
|
|
3959
|
+
position: absolute;
|
|
3960
|
+
z-index: 30;
|
|
3961
|
+
background: var(--surface-raised);
|
|
3962
|
+
border: 1px solid var(--border);
|
|
3963
|
+
border-radius: 8px;
|
|
3964
|
+
padding: 12px 14px;
|
|
3965
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
|
3966
|
+
flex-direction: column;
|
|
3967
|
+
gap: 8px;
|
|
3968
|
+
width: 340px;
|
|
3969
|
+
}
|
|
3970
|
+
.link-popover.visible { display: flex; }
|
|
3971
|
+
|
|
3972
|
+
.link-popover label {
|
|
3973
|
+
font-size: 12px;
|
|
3974
|
+
font-weight: 600;
|
|
3975
|
+
color: var(--text-secondary);
|
|
3976
|
+
display: block;
|
|
3977
|
+
margin-bottom: 3px;
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
.link-popover input {
|
|
3981
|
+
width: 100%;
|
|
3982
|
+
background: var(--bg);
|
|
3983
|
+
border: 1px solid var(--border);
|
|
3984
|
+
border-radius: 4px;
|
|
3985
|
+
padding: 6px 8px;
|
|
3986
|
+
font-size: 13px;
|
|
3987
|
+
color: var(--text);
|
|
3988
|
+
font-family: inherit;
|
|
3989
|
+
outline: none;
|
|
3990
|
+
}
|
|
3991
|
+
.link-popover input:focus { border-color: var(--border-focus); }
|
|
3992
|
+
.link-popover input::placeholder { color: var(--text-muted); }
|
|
3993
|
+
|
|
3994
|
+
.link-popover .popover-actions {
|
|
3995
|
+
display: flex;
|
|
3996
|
+
justify-content: flex-end;
|
|
3997
|
+
gap: 6px;
|
|
3998
|
+
margin-top: 2px;
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
.link-popover .popover-btn {
|
|
4002
|
+
border: none;
|
|
4003
|
+
border-radius: 4px;
|
|
4004
|
+
padding: 5px 12px;
|
|
4005
|
+
font-size: 12px;
|
|
4006
|
+
font-weight: 700;
|
|
4007
|
+
cursor: pointer;
|
|
4008
|
+
font-family: inherit;
|
|
4009
|
+
}
|
|
4010
|
+
.link-popover .popover-btn-save {
|
|
4011
|
+
background: var(--green);
|
|
4012
|
+
color: #fff;
|
|
4013
|
+
}
|
|
4014
|
+
.link-popover .popover-btn-save:hover { background: var(--green-hover); }
|
|
4015
|
+
.link-popover .popover-btn-cancel {
|
|
4016
|
+
background: transparent;
|
|
4017
|
+
color: var(--text-muted);
|
|
4018
|
+
}
|
|
4019
|
+
.link-popover .popover-btn-cancel:hover { color: var(--text-secondary); }
|
|
4020
|
+
|
|
4021
|
+
/* ── Result overlay ── */
|
|
4022
|
+
.overlay {
|
|
4023
|
+
display: none;
|
|
4024
|
+
position: fixed;
|
|
4025
|
+
inset: 0;
|
|
4026
|
+
background: rgba(0,0,0,0.8);
|
|
4027
|
+
z-index: 100;
|
|
4028
|
+
align-items: center;
|
|
4029
|
+
justify-content: center;
|
|
4030
|
+
}
|
|
4031
|
+
.overlay.visible { display: flex; }
|
|
4032
|
+
|
|
4033
|
+
.overlay-msg {
|
|
4034
|
+
text-align: center;
|
|
4035
|
+
font-size: 18px;
|
|
4036
|
+
font-weight: 600;
|
|
4037
|
+
}
|
|
4038
|
+
.overlay-msg.success { color: #2eb67d; }
|
|
4039
|
+
.overlay-msg.error { color: var(--red); }
|
|
4040
|
+
.overlay-msg small {
|
|
4041
|
+
display: block;
|
|
4042
|
+
margin-top: 8px;
|
|
4043
|
+
font-size: 13px;
|
|
4044
|
+
font-weight: 400;
|
|
4045
|
+
color: var(--text-muted);
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
/* Scrollbar */
|
|
4049
|
+
.editor::-webkit-scrollbar { width: 6px; }
|
|
4050
|
+
.editor::-webkit-scrollbar-track { background: transparent; }
|
|
4051
|
+
.editor::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
4052
|
+
</style>
|
|
4053
|
+
</head>
|
|
4054
|
+
<body>
|
|
4055
|
+
|
|
4056
|
+
<div class="page">
|
|
4057
|
+
<!-- Branding -->
|
|
4058
|
+
<div class="brand">
|
|
4059
|
+
<div class="brand-name">Agent Slack</div>
|
|
4060
|
+
<div class="brand-byline">By <a href="https://stably.ai" target="_blank" rel="noopener"><img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjM3OCIgaGVpZ2h0PSIyMzc4IiB2aWV3Qm94PSIwIDAgMjM3OCAyMzc4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfMTZfNDcpIj4KPHBhdGggZD0iTTExODguOTQgNjE4LjY0NUMxNTAwLjY5IDYxOC42NDUgMTc1My4zNiA4NzEuMzIyIDE3NTMuMzYgMTE4My4wNEMxNzUzLjM2IDE0OTQuNzIgMTUwMC42OSAxNzQ3LjM5IDExODguOTQgMTc0Ny4zOUM4NzcuMjUyIDE3NDcuMzkgNjI0LjU0NyAxNDk0LjcyIDYyNC41NDcgMTE4My4wNEM2MjQuNTQ3IDg3MS4zMTcgODc3LjI1MiA2MTguNjQ1IDExODguOTQgNjE4LjY0NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMTg4Ljk0IDBDMTA0MC44MSAwIDg5OS40NiAyOC40NTgxIDc2OC42MTkgNzguMTc0MkM3OTcuMjAyIDEwNy4yMDMgODIwLjg0IDE0MC45OTIgODM4Ljk5IDE3OC4wNTJDOTQ4Ljg2MSAxMzkuODE4IDEwNjYuMzUgMTE4LjA1NSAxMTg5IDExOC4wNTVDMTc3OS41IDExOC4wNTUgMjI1OS44NSA1OTguNTYxIDIyNTkuODUgMTE4OC45NEMyMjU5Ljg1IDE3NzkuMzEgMTc3OS41IDIyNTkuOTEgMTE4OC45NCAyMjU5LjkxQzU5OC41MDEgMjI1OS45MSAxMTguMTIgMTc3OS4zNyAxMTguMTIgMTE4OUMxMTguMTIgOTkwLjEwNiAxNzMuNjA3IDgwNC40MDggMjY4LjQwOSA2NDQuNjNDMjM1LjkxOSA2MTkuNzU5IDIwNy4yNzEgNTg5LjkzNyAxODQuMzI4IDU1NS45NThDNjguMjEzNiA3MzkuNDkzIDAgOTU2LjE4NiAwIDExODlDMCAxODQ0LjYgNTMzLjMzIDIzNzguMDEgMTE4OC45NCAyMzc4LjAxQzE4NDQuNiAyMzc4LjAxIDIzNzggMTg0NC42MSAyMzc4IDExODlDMjM3OCA1MzMuMzk1IDE4NDQuNiAwIDExODguOTQgMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik00OTguNjI0IDExMS4xMUM2MzMuMjA5IDExMS4xMSA3NDIuMjg3IDIyMC4xODggNzQyLjI4NyAzNTQuNzczQzc0Mi4yODcgNDg5LjM1OCA2MzMuMjA5IDU5OC40MzYgNDk4LjYyNCA1OTguNDM2QzM2NC4wNzEgNTk4LjQzNiAyNTQuOTYxIDQ4OS4zNTggMjU0Ljk2MSAzNTQuNzczQzI1NC45NjEgMjIwLjE4OCAzNjQuMDcxIDExMS4xMSA0OTguNjI0IDExMS4xMVoiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTZfNDciPgo8cmVjdCB3aWR0aD0iMjM3OCIgaGVpZ2h0PSIyMzc4IiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=" alt="" /></a><a href="https://stably.ai" target="_blank" rel="noopener">Stably.ai</a></div>
|
|
4061
|
+
<div class="brand-sub">draft mode</div>
|
|
4062
|
+
</div>
|
|
4063
|
+
|
|
4064
|
+
<!-- Context: channel + thread link -->
|
|
4065
|
+
<div class="context-bar">
|
|
4066
|
+
<svg class="hash-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
4067
|
+
<line x1="7.5" y1="2" x2="6" y2="18"/><line x1="14" y1="2" x2="12.5" y2="18"/>
|
|
4068
|
+
<line x1="3" y1="7" x2="17.5" y2="7"/><line x1="2.5" y1="13" x2="17" y2="13"/>
|
|
4069
|
+
</svg>
|
|
4070
|
+
<span class="channel" id="channelName"></span>
|
|
4071
|
+
<span class="workspace-label" id="workspaceName"></span>
|
|
4072
|
+
<a class="thread-link" id="threadLink" style="display:none" target="_blank">
|
|
4073
|
+
<span>View thread</span>
|
|
4074
|
+
<svg viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
|
|
4075
|
+
<path d="M6 3h7v7"/><path d="M13 3L6 10"/>
|
|
4076
|
+
</svg>
|
|
4077
|
+
</a>
|
|
4078
|
+
</div>
|
|
4079
|
+
|
|
4080
|
+
<!-- Composer -->
|
|
4081
|
+
<div class="composer">
|
|
4082
|
+
<div class="toolbar" id="toolbar">
|
|
4083
|
+
<button class="toolbar-btn" data-cmd="bold" data-tip="Bold (⌘B)"><span class="b">B</span></button>
|
|
4084
|
+
<button class="toolbar-btn" data-cmd="italic" data-tip="Italic (⌘I)"><span class="i">I</span></button>
|
|
4085
|
+
<button class="toolbar-btn" data-cmd="strikethrough" data-tip="Strikethrough (⌘⇧X)"><span class="s">S</span></button>
|
|
4086
|
+
<div class="toolbar-sep"></div>
|
|
4087
|
+
<button class="toolbar-btn" data-cmd="link" data-tip="Link (⌘K)">
|
|
4088
|
+
<svg viewBox="0 0 16 16"><path d="M6.354 5.5H4a3 3 0 0 0 0 6h3a3 3 0 0 0 2.83-4M9.646 10.5H12a3 3 0 0 0 0-6H9a3 3 0 0 0-2.83 4" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
|
4089
|
+
</button>
|
|
4090
|
+
<div class="toolbar-sep"></div>
|
|
4091
|
+
<button class="toolbar-btn" data-cmd="insertOrderedList" data-tip="Numbered list (⌘⇧7)">
|
|
4092
|
+
<svg viewBox="0 0 16 16"><text x="1" y="5" font-size="5" fill="currentColor" font-family="system-ui" font-weight="600">1.</text><line x1="7" y1="3.5" x2="15" y2="3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><text x="1" y="10" font-size="5" fill="currentColor" font-family="system-ui" font-weight="600">2.</text><line x1="7" y1="8.5" x2="15" y2="8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><text x="1" y="15" font-size="5" fill="currentColor" font-family="system-ui" font-weight="600">3.</text><line x1="7" y1="13.5" x2="15" y2="13.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
|
4093
|
+
</button>
|
|
4094
|
+
<button class="toolbar-btn" data-cmd="insertUnorderedList" data-tip="Bulleted list (⌘⇧8)">
|
|
4095
|
+
<svg viewBox="0 0 16 16"><circle cx="3" cy="3.5" r="1.5" fill="currentColor"/><line x1="7" y1="3.5" x2="15" y2="3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="3" cy="8.5" r="1.5" fill="currentColor"/><line x1="7" y1="8.5" x2="15" y2="8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="3" cy="13.5" r="1.5" fill="currentColor"/><line x1="7" y1="13.5" x2="15" y2="13.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
|
4096
|
+
</button>
|
|
4097
|
+
<div class="toolbar-sep"></div>
|
|
4098
|
+
<button class="toolbar-btn" data-cmd="blockquote" data-tip="Quote (⌘⇧9)">
|
|
4099
|
+
<svg viewBox="0 0 16 16"><rect x="1" y="2" width="2.5" height="12" rx="1" fill="currentColor" opacity="0.5"/><line x1="6" y1="4" x2="15" y2="4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><line x1="6" y1="8" x2="13" y2="8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><line x1="6" y1="12" x2="11" y2="12" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
|
4100
|
+
</button>
|
|
4101
|
+
<div class="toolbar-sep"></div>
|
|
4102
|
+
<button class="toolbar-btn" data-cmd="code" data-tip="Code (⌘E)">
|
|
4103
|
+
<svg viewBox="0 0 16 16"><polyline points="5,3 1,8 5,13" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><polyline points="11,3 15,8 11,13" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
4104
|
+
</button>
|
|
4105
|
+
<button class="toolbar-btn" data-cmd="codeblock" data-tip="Code block (⌘⇧C)">
|
|
4106
|
+
<svg viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.2"/><polyline points="5,5 3,8 5,11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/><polyline points="11,5 13,8 11,11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
4107
|
+
</button>
|
|
4108
|
+
</div>
|
|
4109
|
+
|
|
4110
|
+
<div
|
|
4111
|
+
class="editor"
|
|
4112
|
+
id="editor"
|
|
4113
|
+
contenteditable="true"
|
|
4114
|
+
data-placeholder="Message #channel"
|
|
4115
|
+
spellcheck="true"
|
|
4116
|
+
></div>
|
|
4117
|
+
|
|
4118
|
+
<textarea class="source-editor" id="sourceEditor" spellcheck="false"></textarea>
|
|
4119
|
+
|
|
4120
|
+
<div class="bottom-bar">
|
|
4121
|
+
<div class="bottom-left">
|
|
4122
|
+
<button class="btn-aa active" id="aaToggle" onclick="toggleToolbar()" data-tip="Formatting">Aa</button>
|
|
4123
|
+
<button class="btn-source" id="sourceToggle" onclick="toggleSourceMode()">mrkdwn</button>
|
|
4124
|
+
<span class="hint"><kbd id="modKey">⌘</kbd><kbd>Enter</kbd> to send</span>
|
|
4125
|
+
</div>
|
|
4126
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
4127
|
+
<button class="cancel-link" onclick="handleCancel()">Cancel</button>
|
|
4128
|
+
<button class="btn-send" id="sendBtn" onclick="handleSend()" disabled>
|
|
4129
|
+
<svg viewBox="0 0 20 20"><path d="M1.7 9.1l7.3 1 .01 0L1.7 9.1zm0 1.8l7.3-1-7.3 1zM1.5 2.1c-.2-.7.5-1.3 1.1-1L19 8.9c.6.3.6 1.2 0 1.5L2.6 18.9c-.7.3-1.3-.3-1.1-1l1.8-6.9L11 10 3.3 8.9 1.5 2.1z"/></svg>
|
|
4130
|
+
</button>
|
|
4131
|
+
</div>
|
|
4132
|
+
</div>
|
|
4133
|
+
</div>
|
|
4134
|
+
</div>
|
|
4135
|
+
|
|
4136
|
+
<!-- Inline link popover (Slack-style two-field) -->
|
|
4137
|
+
<div class="link-popover" id="linkPopover">
|
|
4138
|
+
<div>
|
|
4139
|
+
<label>Text</label>
|
|
4140
|
+
<input type="text" id="linkTextInput" placeholder="Display text">
|
|
4141
|
+
</div>
|
|
4142
|
+
<div>
|
|
4143
|
+
<label>Link</label>
|
|
4144
|
+
<input type="text" id="linkUrlInput" placeholder="https://example.com">
|
|
4145
|
+
</div>
|
|
4146
|
+
<div class="popover-actions">
|
|
4147
|
+
<button class="popover-btn popover-btn-cancel" onclick="closeLinkPopover()">Cancel</button>
|
|
4148
|
+
<button class="popover-btn popover-btn-save" onclick="applyLink()">Save</button>
|
|
4149
|
+
</div>
|
|
4150
|
+
</div>
|
|
4151
|
+
|
|
4152
|
+
<!-- Result overlay -->
|
|
4153
|
+
<div class="overlay" id="resultOverlay">
|
|
4154
|
+
<div class="overlay-msg" id="resultMsg"></div>
|
|
4155
|
+
</div>
|
|
4156
|
+
|
|
4157
|
+
<script>
|
|
4158
|
+
// ─── Config ───
|
|
4159
|
+
const CONFIG = __DRAFT_CONFIG__;
|
|
4160
|
+
const IS_MAC = /Mac|iPhone/.test(navigator.platform);
|
|
4161
|
+
const MOD = IS_MAC ? 'metaKey' : 'ctrlKey';
|
|
4162
|
+
|
|
4163
|
+
// ─── Init header ───
|
|
4164
|
+
document.getElementById('channelName').textContent = CONFIG.channelName;
|
|
4165
|
+
if (CONFIG.workspaceName) {
|
|
4166
|
+
document.getElementById('workspaceName').textContent = CONFIG.workspaceName;
|
|
4167
|
+
}
|
|
4168
|
+
if (!IS_MAC) {
|
|
4169
|
+
document.getElementById('modKey').textContent = 'Ctrl';
|
|
4170
|
+
document.querySelectorAll('[data-tip]').forEach(el => {
|
|
4171
|
+
el.dataset.tip = el.dataset.tip.replace(/\\u2318/g, 'Ctrl+').replace(/\\u21E7/g, 'Shift+');
|
|
4172
|
+
});
|
|
4173
|
+
}
|
|
4174
|
+
if (CONFIG.threadTs) {
|
|
4175
|
+
const link = document.getElementById('threadLink');
|
|
4176
|
+
link.style.display = '';
|
|
4177
|
+
if (CONFIG.threadUrl) {
|
|
4178
|
+
link.href = CONFIG.threadUrl;
|
|
4179
|
+
} else {
|
|
4180
|
+
link.removeAttribute('href');
|
|
4181
|
+
link.style.cursor = 'default';
|
|
4182
|
+
link.style.color = 'var(--text-muted)';
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
// ─── Editor refs ───
|
|
4187
|
+
const editor = document.getElementById('editor');
|
|
4188
|
+
const sourceEditor = document.getElementById('sourceEditor');
|
|
4189
|
+
const linkPopover = document.getElementById('linkPopover');
|
|
4190
|
+
const linkUrlInput = document.getElementById('linkUrlInput');
|
|
4191
|
+
let sourceMode = false;
|
|
4192
|
+
|
|
4193
|
+
// Set Slack-style placeholder
|
|
4194
|
+
editor.dataset.placeholder = 'Message #' + CONFIG.channelName;
|
|
4195
|
+
|
|
4196
|
+
// ─── Detect if cursor is inside a code element ───
|
|
4197
|
+
function isInsideCode(node) {
|
|
4198
|
+
let el = node;
|
|
4199
|
+
while (el && el !== editor) {
|
|
4200
|
+
if (el.nodeType === 1) {
|
|
4201
|
+
const tag = el.tagName.toLowerCase();
|
|
4202
|
+
if (tag === 'pre' || tag === 'code') { return true; }
|
|
4203
|
+
}
|
|
4204
|
+
el = el.parentNode;
|
|
4205
|
+
}
|
|
4206
|
+
return false;
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
function cursorInCode() {
|
|
4210
|
+
const sel = window.getSelection();
|
|
4211
|
+
if (!sel || sel.rangeCount === 0) { return false; }
|
|
4212
|
+
return isInsideCode(sel.anchorNode);
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
// ─── mrkdwn to HTML (initial content) ───
|
|
4216
|
+
function escapeHtml(t) {
|
|
4217
|
+
return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
function mrkdwnToHtml(text) {
|
|
4221
|
+
if (!text) { return ''; }
|
|
4222
|
+
const codeBlocks = [];
|
|
4223
|
+
text = text.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, (_, code) => {
|
|
4224
|
+
codeBlocks.push('<pre>' + escapeHtml(code.trim()) + '</pre>');
|
|
4225
|
+
return '\\x00CB' + (codeBlocks.length - 1) + '\\x00';
|
|
4226
|
+
});
|
|
4227
|
+
const inlineCodes = [];
|
|
4228
|
+
text = text.replace(/\`([^\`\\n]+)\`/g, (_, code) => {
|
|
4229
|
+
inlineCodes.push('<code>' + escapeHtml(code) + '</code>');
|
|
4230
|
+
return '\\x00IC' + (inlineCodes.length - 1) + '\\x00';
|
|
4231
|
+
});
|
|
4232
|
+
|
|
4233
|
+
const lines = text.split('\\n');
|
|
4234
|
+
let html = '', idx = 0;
|
|
4235
|
+
while (idx < lines.length) {
|
|
4236
|
+
const line = lines[idx];
|
|
4237
|
+
if (line.startsWith('> ')) {
|
|
4238
|
+
const q = [];
|
|
4239
|
+
while (idx < lines.length && lines[idx].startsWith('> ')) { q.push(fmtInline(lines[idx].slice(2))); idx++; }
|
|
4240
|
+
html += '<blockquote>' + q.join('<br>') + '</blockquote>'; continue;
|
|
4241
|
+
}
|
|
4242
|
+
if (/^[\\u2022\\-\\*] /.test(line)) {
|
|
4243
|
+
const items = [];
|
|
4244
|
+
while (idx < lines.length && /^[\\u2022\\-\\*] /.test(lines[idx])) { items.push('<li>' + fmtInline(lines[idx].replace(/^[\\u2022\\-\\*] /, '')) + '</li>'); idx++; }
|
|
4245
|
+
html += '<ul>' + items.join('') + '</ul>'; continue;
|
|
4246
|
+
}
|
|
4247
|
+
if (/^\\d+[\\.\\)] /.test(line)) {
|
|
4248
|
+
const items = [];
|
|
4249
|
+
while (idx < lines.length && /^\\d+[\\.\\)] /.test(lines[idx])) { items.push('<li>' + fmtInline(lines[idx].replace(/^\\d+[\\.\\)] /, '')) + '</li>'); idx++; }
|
|
4250
|
+
html += '<ol>' + items.join('') + '</ol>'; continue;
|
|
4251
|
+
}
|
|
4252
|
+
html += (line ? fmtInline(line) : '') + '<br>';
|
|
4253
|
+
idx++;
|
|
4254
|
+
}
|
|
4255
|
+
codeBlocks.forEach((b, i) => { html = html.replace('\\x00CB' + i + '\\x00', b); });
|
|
4256
|
+
inlineCodes.forEach((c, i) => { html = html.replace('\\x00IC' + i + '\\x00', c); });
|
|
4257
|
+
return html.replace(/(<br>)+$/, '');
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
function fmtInline(t) {
|
|
4261
|
+
t = t.replace(/<(https?:\\/\\/[^|>]+)\\|([^>]+)>/g, '<a href="$1" target="_blank">$2</a>');
|
|
4262
|
+
t = t.replace(/<(https?:\\/\\/[^>]+)>/g, '<a href="$1" target="_blank">$1</a>');
|
|
4263
|
+
t = t.replace(/\\*([^\\*]+)\\*/g, '<b>$1</b>');
|
|
4264
|
+
t = t.replace(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, '<i>$1</i>');
|
|
4265
|
+
t = t.replace(/~([^~]+)~/g, '<s>$1</s>');
|
|
4266
|
+
return t;
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
// ─── HTML to mrkdwn (submission) ───
|
|
4270
|
+
function htmlToMrkdwn(root) {
|
|
4271
|
+
const visited = new Set();
|
|
4272
|
+
function walk(node) {
|
|
4273
|
+
if (visited.has(node)) { return ''; }
|
|
4274
|
+
if (node.nodeType === 3) { return node.textContent || ''; }
|
|
4275
|
+
if (node.nodeType !== 1) { return ''; }
|
|
4276
|
+
const el = node;
|
|
4277
|
+
const tag = el.tagName.toLowerCase();
|
|
4278
|
+
const kids = () => Array.from(el.childNodes).map(walk).join('');
|
|
4279
|
+
switch (tag) {
|
|
4280
|
+
case 'b': case 'strong': { const c = kids(); return c.trim() ? '*' + c + '*' : c; }
|
|
4281
|
+
case 'i': case 'em': { const c = kids(); return c.trim() ? '_' + c + '_' : c; }
|
|
4282
|
+
case 's': case 'strike': case 'del': { const c = kids(); return c.trim() ? '~' + c + '~' : c; }
|
|
4283
|
+
case 'code': return '\`' + (el.textContent || '') + '\`';
|
|
4284
|
+
case 'pre': {
|
|
4285
|
+
// Collect adjacent pres into one code block
|
|
4286
|
+
const lines = [(el.textContent || '').trimEnd()];
|
|
4287
|
+
let next = el.nextElementSibling;
|
|
4288
|
+
while (next && next.tagName === 'PRE') {
|
|
4289
|
+
lines.push((next.textContent || '').trimEnd());
|
|
4290
|
+
visited.add(next);
|
|
4291
|
+
next = next.nextElementSibling;
|
|
4292
|
+
}
|
|
4293
|
+
return '\`\`\`\\n' + lines.filter(l => l).join('\\n') + '\\n\`\`\`\\n';
|
|
4294
|
+
}
|
|
4295
|
+
case 'blockquote': { const c = kids().trim(); return c.split('\\n').map(l => '> ' + l).join('\\n') + '\\n'; }
|
|
4296
|
+
case 'ul': { let r = ''; for (const li of el.querySelectorAll(':scope > li')) { r += '\\u2022 ' + walk(li).trim() + '\\n'; } return r; }
|
|
4297
|
+
case 'ol': { let r = '', n = 1; for (const li of el.querySelectorAll(':scope > li')) { r += n + '. ' + walk(li).trim() + '\\n'; n++; } return r; }
|
|
4298
|
+
case 'li': return kids();
|
|
4299
|
+
case 'a': { const h = el.getAttribute('href'); const t = kids(); return (h && t && h !== t) ? '<' + h + '|' + t + '>' : (h || t); }
|
|
4300
|
+
case 'br': return '\\n';
|
|
4301
|
+
case 'div': case 'p': {
|
|
4302
|
+
const c = kids();
|
|
4303
|
+
const p = el.parentElement;
|
|
4304
|
+
if (p && ['li','blockquote','td'].includes(p.tagName.toLowerCase())) { return c; }
|
|
4305
|
+
return c.endsWith('\\n') ? c : c + '\\n';
|
|
4306
|
+
}
|
|
4307
|
+
case 'span': {
|
|
4308
|
+
const st = el.style;
|
|
4309
|
+
let c = kids();
|
|
4310
|
+
if (st.fontWeight === 'bold' || Number(st.fontWeight) >= 700) { c = c.trim() ? '*' + c + '*' : c; }
|
|
4311
|
+
if (st.fontStyle === 'italic') { c = c.trim() ? '_' + c + '_' : c; }
|
|
4312
|
+
if (st.textDecoration && st.textDecoration.includes('line-through')) { c = c.trim() ? '~' + c + '~' : c; }
|
|
4313
|
+
return c;
|
|
4314
|
+
}
|
|
4315
|
+
default: return kids();
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
return walk(root).replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
// ─── Init editor content ───
|
|
4322
|
+
if (CONFIG.initialText) { editor.innerHTML = mrkdwnToHtml(CONFIG.initialText); }
|
|
4323
|
+
|
|
4324
|
+
// ─── Toolbar execution ───
|
|
4325
|
+
function execCmd(command) {
|
|
4326
|
+
// Block formatting inside code (except toggling code off)
|
|
4327
|
+
const inCode = cursorInCode();
|
|
4328
|
+
if (inCode && command !== 'code' && command !== 'codeblock') { return; }
|
|
4329
|
+
|
|
4330
|
+
switch (command) {
|
|
4331
|
+
case 'bold':
|
|
4332
|
+
case 'italic':
|
|
4333
|
+
case 'strikethrough':
|
|
4334
|
+
case 'insertOrderedList':
|
|
4335
|
+
case 'insertUnorderedList':
|
|
4336
|
+
document.execCommand(command, false, null);
|
|
4337
|
+
break;
|
|
4338
|
+
case 'blockquote':
|
|
4339
|
+
document.execCommand('formatBlock', false, 'blockquote');
|
|
4340
|
+
break;
|
|
4341
|
+
case 'code': {
|
|
4342
|
+
const sel = window.getSelection();
|
|
4343
|
+
if (!sel || sel.rangeCount === 0) { break; }
|
|
4344
|
+
// If already in code, unwrap
|
|
4345
|
+
if (inCode) {
|
|
4346
|
+
const codeEl = sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement;
|
|
4347
|
+
const code = codeEl.closest('code');
|
|
4348
|
+
if (code) {
|
|
4349
|
+
const text = document.createTextNode(code.textContent || '');
|
|
4350
|
+
code.parentNode.replaceChild(text, code);
|
|
4351
|
+
const r = document.createRange(); r.selectNodeContents(text); sel.removeAllRanges(); sel.addRange(r);
|
|
4352
|
+
}
|
|
4353
|
+
break;
|
|
4354
|
+
}
|
|
4355
|
+
const range = sel.getRangeAt(0);
|
|
4356
|
+
if (range.collapsed) { break; }
|
|
4357
|
+
const code = document.createElement('code');
|
|
4358
|
+
try { range.surroundContents(code); } catch (_) {
|
|
4359
|
+
const frag = range.extractContents(); code.appendChild(frag); range.insertNode(code);
|
|
4360
|
+
}
|
|
4361
|
+
sel.removeAllRanges();
|
|
4362
|
+
const nr = document.createRange(); nr.selectNodeContents(code); sel.addRange(nr);
|
|
4363
|
+
break;
|
|
4364
|
+
}
|
|
4365
|
+
case 'codeblock': {
|
|
4366
|
+
if (inCode) { break; } // No nesting
|
|
4367
|
+
const sel = window.getSelection();
|
|
4368
|
+
if (!sel) { break; }
|
|
4369
|
+
const pre = document.createElement('pre');
|
|
4370
|
+
if (!sel.isCollapsed) {
|
|
4371
|
+
const range = sel.getRangeAt(0);
|
|
4372
|
+
pre.textContent = range.extractContents().textContent || '';
|
|
4373
|
+
range.insertNode(pre);
|
|
4374
|
+
} else {
|
|
4375
|
+
pre.innerHTML = '<br>';
|
|
4376
|
+
const range = sel.getRangeAt(0);
|
|
4377
|
+
range.insertNode(pre);
|
|
4378
|
+
const nr = document.createRange(); nr.setStart(pre, 0); nr.collapse(true);
|
|
4379
|
+
sel.removeAllRanges(); sel.addRange(nr);
|
|
4380
|
+
}
|
|
4381
|
+
break;
|
|
4382
|
+
}
|
|
4383
|
+
case 'link':
|
|
4384
|
+
openLinkPopover();
|
|
4385
|
+
return; // Don't refocus — popover takes focus
|
|
4386
|
+
}
|
|
4387
|
+
editor.focus();
|
|
4388
|
+
updateToolbarState();
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
// Toolbar button clicks
|
|
4392
|
+
document.querySelectorAll('.toolbar-btn[data-cmd]').forEach(btn => {
|
|
4393
|
+
btn.addEventListener('mousedown', (e) => {
|
|
4394
|
+
e.preventDefault();
|
|
4395
|
+
execCmd(btn.dataset.cmd);
|
|
4396
|
+
});
|
|
4397
|
+
});
|
|
4398
|
+
|
|
4399
|
+
// ─── Toolbar state ───
|
|
4400
|
+
function updateToolbarState() {
|
|
4401
|
+
const inCode = cursorInCode();
|
|
4402
|
+
document.querySelectorAll('.toolbar-btn[data-cmd]').forEach(btn => {
|
|
4403
|
+
const cmd = btn.dataset.cmd;
|
|
4404
|
+
// Disable non-code buttons when inside code
|
|
4405
|
+
if (inCode && cmd !== 'code' && cmd !== 'codeblock') {
|
|
4406
|
+
btn.classList.add('disabled');
|
|
4407
|
+
btn.classList.remove('active');
|
|
4408
|
+
return;
|
|
4409
|
+
}
|
|
4410
|
+
btn.classList.remove('disabled');
|
|
4411
|
+
let active = false;
|
|
4412
|
+
try {
|
|
4413
|
+
if (['bold','italic','strikethrough','insertOrderedList','insertUnorderedList'].includes(cmd)) {
|
|
4414
|
+
active = document.queryCommandState(cmd);
|
|
4415
|
+
}
|
|
4416
|
+
if (cmd === 'code') { active = inCode; }
|
|
4417
|
+
} catch (_) {}
|
|
4418
|
+
btn.classList.toggle('active', active);
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
editor.addEventListener('keyup', updateToolbarState);
|
|
4423
|
+
editor.addEventListener('mouseup', updateToolbarState);
|
|
4424
|
+
editor.addEventListener('focus', updateToolbarState);
|
|
4425
|
+
|
|
4426
|
+
// ─── Keyboard shortcuts (Slack-identical) ───
|
|
4427
|
+
editor.addEventListener('keydown', (e) => {
|
|
4428
|
+
const mod = e[MOD];
|
|
4429
|
+
|
|
4430
|
+
// Cmd+Enter → Send
|
|
4431
|
+
if (mod && e.key === 'Enter') { e.preventDefault(); handleSend(); return; }
|
|
4432
|
+
|
|
4433
|
+
// Cmd+B → Bold
|
|
4434
|
+
if (mod && !e.shiftKey && e.key === 'b') { e.preventDefault(); execCmd('bold'); return; }
|
|
4435
|
+
|
|
4436
|
+
// Cmd+I → Italic
|
|
4437
|
+
if (mod && !e.shiftKey && e.key === 'i') { e.preventDefault(); execCmd('italic'); return; }
|
|
4438
|
+
|
|
4439
|
+
// Cmd+Shift+X → Strikethrough
|
|
4440
|
+
if (mod && e.shiftKey && (e.key === 'X' || e.key === 'x')) { e.preventDefault(); execCmd('strikethrough'); return; }
|
|
4441
|
+
|
|
4442
|
+
// Cmd+E → Inline code (Slack's actual shortcut)
|
|
4443
|
+
if (mod && !e.shiftKey && e.key === 'e') { e.preventDefault(); execCmd('code'); return; }
|
|
4444
|
+
|
|
4445
|
+
// Cmd+Shift+C → Code block
|
|
4446
|
+
if (mod && e.shiftKey && (e.key === 'C' || e.key === 'c' || e.code === 'KeyC')) { e.preventDefault(); execCmd('codeblock'); return; }
|
|
4447
|
+
|
|
4448
|
+
// Cmd+K → Link
|
|
4449
|
+
if (mod && !e.shiftKey && e.key === 'k') { e.preventDefault(); execCmd('link'); return; }
|
|
4450
|
+
|
|
4451
|
+
// Cmd+Shift+7 → Numbered list
|
|
4452
|
+
if (mod && e.shiftKey && e.key === '7') { e.preventDefault(); execCmd('insertOrderedList'); return; }
|
|
4453
|
+
|
|
4454
|
+
// Cmd+Shift+8 → Bulleted list
|
|
4455
|
+
if (mod && e.shiftKey && e.key === '8') { e.preventDefault(); execCmd('insertUnorderedList'); return; }
|
|
4456
|
+
|
|
4457
|
+
// Cmd+Shift+9 → Blockquote
|
|
4458
|
+
if (mod && e.shiftKey && e.key === '9') { e.preventDefault(); execCmd('blockquote'); return; }
|
|
4459
|
+
});
|
|
4460
|
+
|
|
4461
|
+
// ─── Code block: merge split <pre> elements + escape logic ───
|
|
4462
|
+
function findParentPre(node) {
|
|
4463
|
+
while (node && node !== editor) {
|
|
4464
|
+
if (node.nodeType === 1 && node.tagName === 'PRE') { return node; }
|
|
4465
|
+
node = node.parentNode;
|
|
4466
|
+
}
|
|
4467
|
+
return null;
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
function exitCodeBlock(pre) {
|
|
4471
|
+
const sel = window.getSelection();
|
|
4472
|
+
const p = document.createElement('div');
|
|
4473
|
+
p.innerHTML = '<br>';
|
|
4474
|
+
if (pre.parentNode === editor) {
|
|
4475
|
+
pre.parentNode.insertBefore(p, pre.nextSibling);
|
|
4476
|
+
} else {
|
|
4477
|
+
const wrapper = pre.parentNode;
|
|
4478
|
+
wrapper.parentNode.insertBefore(p, wrapper.nextSibling);
|
|
4479
|
+
}
|
|
4480
|
+
const nr = document.createRange();
|
|
4481
|
+
nr.setStart(p, 0);
|
|
4482
|
+
nr.collapse(true);
|
|
4483
|
+
sel.removeAllRanges();
|
|
4484
|
+
sel.addRange(nr);
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
// Code block management: the browser splits <pre> on Enter, creating adjacent
|
|
4488
|
+
// <pre> elements. We let this happen and handle it via:
|
|
4489
|
+
// 1. CSS: adjacent pres look like one block (no gaps/borders between them)
|
|
4490
|
+
// 2. Serialization: htmlToMrkdwn treats adjacent pres as one code block
|
|
4491
|
+
// 3. Escape: when user creates an empty pre after another empty pre, exit code
|
|
4492
|
+
|
|
4493
|
+
// Track code block escape (double-Enter on empty line)
|
|
4494
|
+
editor.addEventListener('input', () => {
|
|
4495
|
+
const pres = Array.from(editor.querySelectorAll('pre'));
|
|
4496
|
+
if (pres.length < 2) { return; }
|
|
4497
|
+
|
|
4498
|
+
// Check for escape: last two adjacent pres are both empty
|
|
4499
|
+
for (let i = pres.length - 1; i > 0; i--) {
|
|
4500
|
+
const cur = pres[i];
|
|
4501
|
+
const prev = pres[i - 1];
|
|
4502
|
+
const curEmpty = !cur.textContent?.trim();
|
|
4503
|
+
const prevEmpty = !prev.textContent?.trim();
|
|
4504
|
+
|
|
4505
|
+
if (curEmpty && prevEmpty) {
|
|
4506
|
+
// Double-Enter escape: remove both empty pres, place cursor after
|
|
4507
|
+
const lastRealPre = pres[i - 2] || null;
|
|
4508
|
+
// Remove the two empty pres and their wrappers
|
|
4509
|
+
[cur, prev].forEach(p => {
|
|
4510
|
+
const parent = p.parentNode;
|
|
4511
|
+
p.remove();
|
|
4512
|
+
if (parent && parent !== editor && parent.tagName === 'DIV' && !parent.textContent?.trim()) {
|
|
4513
|
+
parent.remove();
|
|
4514
|
+
}
|
|
4515
|
+
});
|
|
4516
|
+
if (lastRealPre) {
|
|
4517
|
+
exitCodeBlock(lastRealPre);
|
|
4518
|
+
} else {
|
|
4519
|
+
// No code left — just place cursor
|
|
4520
|
+
const sel = window.getSelection();
|
|
4521
|
+
if (sel) {
|
|
4522
|
+
const div = document.createElement('div');
|
|
4523
|
+
div.innerHTML = '<br>';
|
|
4524
|
+
editor.appendChild(div);
|
|
4525
|
+
const r = document.createRange();
|
|
4526
|
+
r.setStart(div, 0);
|
|
4527
|
+
r.collapse(true);
|
|
4528
|
+
sel.removeAllRanges();
|
|
4529
|
+
sel.addRange(r);
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
// ArrowDown at end of last code block → exit
|
|
4538
|
+
editor.addEventListener('keydown', (e) => {
|
|
4539
|
+
if (e.key === 'ArrowDown') {
|
|
4540
|
+
const sel = window.getSelection();
|
|
4541
|
+
if (!sel || sel.rangeCount === 0) { return; }
|
|
4542
|
+
const pre = findParentPre(sel.anchorNode);
|
|
4543
|
+
if (pre && !pre.nextElementSibling) {
|
|
4544
|
+
e.preventDefault();
|
|
4545
|
+
exitCodeBlock(pre);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
}, true);
|
|
4549
|
+
|
|
4550
|
+
// ─── Paste handler ───
|
|
4551
|
+
editor.addEventListener('paste', (e) => {
|
|
4552
|
+
const cd = e.clipboardData;
|
|
4553
|
+
if (!cd) { return; }
|
|
4554
|
+
const html = cd.getData('text/html');
|
|
4555
|
+
if (html) {
|
|
4556
|
+
e.preventDefault();
|
|
4557
|
+
const tmp = document.createElement('div');
|
|
4558
|
+
tmp.innerHTML = html;
|
|
4559
|
+
tmp.querySelectorAll('script,style,meta,link').forEach(el => el.remove());
|
|
4560
|
+
tmp.querySelectorAll('[style]').forEach(el => {
|
|
4561
|
+
const s = el.style;
|
|
4562
|
+
const keep = {};
|
|
4563
|
+
if (s.fontWeight === 'bold' || Number(s.fontWeight) >= 700) { keep.fontWeight = s.fontWeight; }
|
|
4564
|
+
if (s.fontStyle === 'italic') { keep.fontStyle = s.fontStyle; }
|
|
4565
|
+
if (s.textDecoration?.includes('line-through')) { keep.textDecoration = 'line-through'; }
|
|
4566
|
+
el.removeAttribute('style');
|
|
4567
|
+
Object.assign(el.style, keep);
|
|
4568
|
+
});
|
|
4569
|
+
document.execCommand('insertHTML', false, tmp.innerHTML);
|
|
4570
|
+
}
|
|
4571
|
+
});
|
|
4572
|
+
|
|
4573
|
+
// ─── Link popover (Slack-style: select text, Cmd+K, two-field form) ───
|
|
4574
|
+
let linkSavedRange = null;
|
|
4575
|
+
let linkPopoverOpen = false;
|
|
4576
|
+
const linkTextInput = document.getElementById('linkTextInput');
|
|
4577
|
+
|
|
4578
|
+
function openLinkPopover() {
|
|
4579
|
+
const sel = window.getSelection();
|
|
4580
|
+
if (!sel || sel.rangeCount === 0) { return; }
|
|
4581
|
+
|
|
4582
|
+
linkSavedRange = sel.getRangeAt(0).cloneRange();
|
|
4583
|
+
const selectedText = sel.toString();
|
|
4584
|
+
|
|
4585
|
+
// Position near selection
|
|
4586
|
+
const rect = linkSavedRange.getBoundingClientRect();
|
|
4587
|
+
const editorRect = editor.getBoundingClientRect();
|
|
4588
|
+
linkPopover.style.top = (rect.bottom + window.scrollY + 6) + 'px';
|
|
4589
|
+
linkPopover.style.left = Math.max(8, Math.min(rect.left + window.scrollX, editorRect.right - 360)) + 'px';
|
|
4590
|
+
|
|
4591
|
+
// Pre-fill text field with selected text
|
|
4592
|
+
linkTextInput.value = selectedText;
|
|
4593
|
+
|
|
4594
|
+
// If selection is already a link, pre-fill URL
|
|
4595
|
+
let existingUrl = '';
|
|
4596
|
+
const anchor = sel.anchorNode?.parentElement?.closest('a');
|
|
4597
|
+
if (anchor) {
|
|
4598
|
+
existingUrl = anchor.getAttribute('href') || '';
|
|
4599
|
+
linkTextInput.value = anchor.textContent || selectedText;
|
|
4600
|
+
}
|
|
4601
|
+
linkUrlInput.value = existingUrl;
|
|
4602
|
+
|
|
4603
|
+
// Show popover (use rAF to avoid same-frame mousedown closing it)
|
|
4604
|
+
requestAnimationFrame(() => {
|
|
4605
|
+
linkPopover.classList.add('visible');
|
|
4606
|
+
linkPopoverOpen = true;
|
|
4607
|
+
linkUrlInput.focus();
|
|
4608
|
+
linkUrlInput.select();
|
|
4609
|
+
});
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4612
|
+
function closeLinkPopover() {
|
|
4613
|
+
linkPopover.classList.remove('visible');
|
|
4614
|
+
linkPopoverOpen = false;
|
|
4615
|
+
editor.focus();
|
|
4616
|
+
if (linkSavedRange) {
|
|
4617
|
+
const sel = window.getSelection();
|
|
4618
|
+
sel.removeAllRanges();
|
|
4619
|
+
sel.addRange(linkSavedRange);
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
function applyLink() {
|
|
4624
|
+
const url = linkUrlInput.value.trim();
|
|
4625
|
+
const text = linkTextInput.value.trim();
|
|
4626
|
+
linkPopover.classList.remove('visible');
|
|
4627
|
+
linkPopoverOpen = false;
|
|
4628
|
+
|
|
4629
|
+
if (!url || !linkSavedRange) { editor.focus(); return; }
|
|
4630
|
+
|
|
4631
|
+
const sel = window.getSelection();
|
|
4632
|
+
sel.removeAllRanges();
|
|
4633
|
+
sel.addRange(linkSavedRange);
|
|
4634
|
+
|
|
4635
|
+
// Check if we're updating an existing link
|
|
4636
|
+
const anchor = sel.anchorNode?.parentElement?.closest('a');
|
|
4637
|
+
if (anchor) {
|
|
4638
|
+
anchor.setAttribute('href', url);
|
|
4639
|
+
if (text) { anchor.textContent = text; }
|
|
4640
|
+
} else {
|
|
4641
|
+
// Create new link
|
|
4642
|
+
const a = document.createElement('a');
|
|
4643
|
+
a.href = url;
|
|
4644
|
+
a.target = '_blank';
|
|
4645
|
+
a.textContent = text || url;
|
|
4646
|
+
|
|
4647
|
+
if (!linkSavedRange.collapsed) {
|
|
4648
|
+
linkSavedRange.deleteContents();
|
|
4649
|
+
}
|
|
4650
|
+
linkSavedRange.insertNode(a);
|
|
4651
|
+
|
|
4652
|
+
const nr = document.createRange();
|
|
4653
|
+
nr.setStartAfter(a);
|
|
4654
|
+
nr.collapse(true);
|
|
4655
|
+
sel.removeAllRanges();
|
|
4656
|
+
sel.addRange(nr);
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
editor.focus();
|
|
4660
|
+
linkSavedRange = null;
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
// Popover key handling — both inputs
|
|
4664
|
+
[linkTextInput, linkUrlInput].forEach(input => {
|
|
4665
|
+
input.addEventListener('keydown', (e) => {
|
|
4666
|
+
if (e.key === 'Enter') { e.preventDefault(); applyLink(); }
|
|
4667
|
+
if (e.key === 'Escape') { e.preventDefault(); closeLinkPopover(); }
|
|
4668
|
+
});
|
|
4669
|
+
});
|
|
4670
|
+
|
|
4671
|
+
// Close popover on outside click (delayed check to avoid same-event close)
|
|
4672
|
+
document.addEventListener('mousedown', (e) => {
|
|
4673
|
+
if (linkPopoverOpen && !linkPopover.contains(e.target) && !e.target.closest('.toolbar-btn')) {
|
|
4674
|
+
closeLinkPopover();
|
|
4675
|
+
}
|
|
4676
|
+
});
|
|
4677
|
+
|
|
4678
|
+
// ─── Toolbar toggle (Aa button) ───
|
|
4679
|
+
let toolbarVisible = true;
|
|
4680
|
+
function toggleToolbar() {
|
|
4681
|
+
toolbarVisible = !toolbarVisible;
|
|
4682
|
+
document.getElementById('toolbar').style.display = toolbarVisible ? '' : 'none';
|
|
4683
|
+
document.getElementById('aaToggle').classList.toggle('active', toolbarVisible);
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4686
|
+
// ─── Send button state ───
|
|
4687
|
+
function updateSendBtn() {
|
|
4688
|
+
const text = sourceMode ? sourceEditor.value.trim() : (editor.textContent || '').trim();
|
|
4689
|
+
document.getElementById('sendBtn').disabled = !text;
|
|
4690
|
+
}
|
|
4691
|
+
editor.addEventListener('input', updateSendBtn);
|
|
4692
|
+
|
|
4693
|
+
// ─── Source mode ───
|
|
4694
|
+
function toggleSourceMode() {
|
|
4695
|
+
sourceMode = !sourceMode;
|
|
4696
|
+
document.getElementById('sourceToggle').classList.toggle('active', sourceMode);
|
|
4697
|
+
if (sourceMode) {
|
|
4698
|
+
sourceEditor.value = htmlToMrkdwn(editor);
|
|
4699
|
+
editor.style.display = 'none';
|
|
4700
|
+
sourceEditor.style.display = 'block';
|
|
4701
|
+
document.getElementById('toolbar').style.display = 'none';
|
|
4702
|
+
document.getElementById('aaToggle').style.display = 'none';
|
|
4703
|
+
sourceEditor.focus();
|
|
4704
|
+
} else {
|
|
4705
|
+
editor.innerHTML = mrkdwnToHtml(sourceEditor.value);
|
|
4706
|
+
sourceEditor.style.display = 'none';
|
|
4707
|
+
editor.style.display = '';
|
|
4708
|
+
document.getElementById('toolbar').style.display = toolbarVisible ? '' : 'none';
|
|
4709
|
+
document.getElementById('aaToggle').style.display = '';
|
|
4710
|
+
editor.focus();
|
|
4711
|
+
}
|
|
4712
|
+
updateSendBtn();
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
sourceEditor.addEventListener('keydown', (e) => {
|
|
4716
|
+
if (e[MOD] && e.key === 'Enter') { e.preventDefault(); handleSend(); }
|
|
4717
|
+
});
|
|
4718
|
+
sourceEditor.addEventListener('input', updateSendBtn);
|
|
4719
|
+
|
|
4720
|
+
// ─── Send / Cancel ───
|
|
4721
|
+
let sending = false;
|
|
4722
|
+
|
|
4723
|
+
async function handleSend() {
|
|
4724
|
+
if (sending) { return; }
|
|
4725
|
+
const text = sourceMode ? sourceEditor.value.trim() : htmlToMrkdwn(editor);
|
|
4726
|
+
if (!text) { editor.focus(); return; }
|
|
4727
|
+
|
|
4728
|
+
sending = true;
|
|
4729
|
+
const btn = document.getElementById('sendBtn');
|
|
4730
|
+
btn.disabled = true;
|
|
4731
|
+
btn.innerHTML = '\\u2026';
|
|
4732
|
+
|
|
4733
|
+
try {
|
|
4734
|
+
const resp = await fetch('/send', {
|
|
4735
|
+
method: 'POST',
|
|
4736
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4737
|
+
body: JSON.stringify({ text }),
|
|
4738
|
+
});
|
|
4739
|
+
const data = await resp.json();
|
|
4740
|
+
if (data.ok) {
|
|
4741
|
+
let sub = 'You can close this tab.';
|
|
4742
|
+
if (data.ts && CONFIG.workspaceUrl && CONFIG.channelId) {
|
|
4743
|
+
const tsNoDot = data.ts.replace('.', '');
|
|
4744
|
+
const baseUrl = CONFIG.workspaceUrl.replace(/\\/$/, '');
|
|
4745
|
+
const msgUrl = baseUrl + '/archives/' + CONFIG.channelId + '/p' + tsNoDot;
|
|
4746
|
+
sub = '<a href="' + msgUrl + '" target="_blank" style="color:var(--accent);text-decoration:none;">View in Slack \\u2197</a><br><span style="opacity:0.6;font-size:12px;">You can close this tab.</span>';
|
|
4747
|
+
}
|
|
4748
|
+
showOverlay('Message sent \\u2705', sub, 'success');
|
|
4749
|
+
} else {
|
|
4750
|
+
throw new Error(data.error || 'Send failed');
|
|
4751
|
+
}
|
|
4752
|
+
} catch (err) {
|
|
4753
|
+
showOverlay('Failed to send', err.message, 'error');
|
|
4754
|
+
sending = false;
|
|
4755
|
+
btn.disabled = false;
|
|
4756
|
+
btn.innerHTML = '<svg viewBox="0 0 20 20" style="width:16px;height:16px;fill:currentColor"><path d="M1.7 9.1l7.3 1 .01 0L1.7 9.1zm0 1.8l7.3-1-7.3 1zM1.5 2.1c-.2-.7.5-1.3 1.1-1L19 8.9c.6.3.6 1.2 0 1.5L2.6 18.9c-.7.3-1.3-.3-1.1-1l1.8-6.9L11 10 3.3 8.9 1.5 2.1z"/></svg>';
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
async function handleCancel() {
|
|
4761
|
+
try { await fetch('/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); } catch (_) {}
|
|
4762
|
+
showOverlay('Draft cancelled', 'You can close this tab.', 'success');
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4765
|
+
function showOverlay(title, sub, type) {
|
|
4766
|
+
const overlay = document.getElementById('resultOverlay');
|
|
4767
|
+
const msg = document.getElementById('resultMsg');
|
|
4768
|
+
msg.className = 'overlay-msg ' + type;
|
|
4769
|
+
msg.innerHTML = title + '<small>' + sub + '</small>';
|
|
4770
|
+
overlay.classList.add('visible');
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
// ─── Focus ───
|
|
4774
|
+
editor.focus();
|
|
4775
|
+
updateSendBtn();
|
|
4776
|
+
</script>
|
|
4777
|
+
</body>
|
|
4778
|
+
</html>`;
|
|
4779
|
+
|
|
4780
|
+
// src/cli/draft-actions.ts
|
|
4781
|
+
async function draftMessage(input) {
|
|
4782
|
+
const target = parseMsgTarget(String(input.targetInput));
|
|
4783
|
+
if (target.kind === "url") {
|
|
4784
|
+
const { ref } = target;
|
|
4785
|
+
warnOnTruncatedSlackUrl(ref);
|
|
4786
|
+
return input.ctx.withAutoRefresh({
|
|
4787
|
+
workspaceUrl: ref.workspace_url,
|
|
4788
|
+
work: async () => {
|
|
4789
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspace_url);
|
|
4790
|
+
const msg = await fetchMessage(client, { ref });
|
|
4791
|
+
const threadTs = input.options.threadTs ?? msg.thread_ts ?? msg.ts;
|
|
4792
|
+
const channelName = await resolveChannelName(client, ref.channel_id);
|
|
4793
|
+
return draftWithEditor({
|
|
4794
|
+
channelName,
|
|
4795
|
+
channelId: ref.channel_id,
|
|
4796
|
+
workspaceUrl: ref.workspace_url,
|
|
4797
|
+
threadTs,
|
|
4798
|
+
initialText: input.initialText,
|
|
4799
|
+
sendFn: async (text) => {
|
|
4800
|
+
const resp = await client.api("chat.postMessage", {
|
|
4801
|
+
channel: ref.channel_id,
|
|
4802
|
+
text,
|
|
4803
|
+
thread_ts: threadTs
|
|
4804
|
+
});
|
|
4805
|
+
return { ts: resp.ts };
|
|
4806
|
+
}
|
|
4807
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
});
|
|
4810
|
+
}
|
|
4811
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
|
|
4812
|
+
await input.ctx.assertWorkspaceSpecifiedForChannelNames({
|
|
4813
|
+
workspaceUrl,
|
|
4814
|
+
channels: [String(target.channel)]
|
|
4815
|
+
});
|
|
4816
|
+
return input.ctx.withAutoRefresh({
|
|
4817
|
+
workspaceUrl,
|
|
4818
|
+
work: async () => {
|
|
4819
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
4820
|
+
const channelId = await resolveChannelId(client, String(target.channel));
|
|
4821
|
+
const normalized = normalizeChannelInput(target.channel);
|
|
4822
|
+
const channelName = normalized.kind === "name" ? normalized.value : await resolveChannelName(client, channelId);
|
|
4823
|
+
return draftWithEditor({
|
|
4824
|
+
channelName,
|
|
4825
|
+
channelId,
|
|
4826
|
+
workspaceUrl,
|
|
4827
|
+
threadTs: input.options.threadTs,
|
|
4828
|
+
initialText: input.initialText,
|
|
4829
|
+
sendFn: async (text) => {
|
|
4830
|
+
const resp = await client.api("chat.postMessage", {
|
|
4831
|
+
channel: channelId,
|
|
4832
|
+
text,
|
|
4833
|
+
thread_ts: input.options.threadTs
|
|
4834
|
+
});
|
|
4835
|
+
return { ts: resp.ts };
|
|
4836
|
+
}
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
});
|
|
4840
|
+
}
|
|
4841
|
+
async function draftWithEditor(input) {
|
|
4842
|
+
if (process.env.CI) {
|
|
4843
|
+
if (!input.initialText) {
|
|
4844
|
+
throw new Error("In CI mode, initial text is required (no editor available)");
|
|
4845
|
+
}
|
|
4846
|
+
const result2 = await input.sendFn(input.initialText);
|
|
4847
|
+
return { ok: true, sent: true, editor: "skipped", ts: result2.ts };
|
|
4848
|
+
}
|
|
4849
|
+
const result = await openDraftEditor({
|
|
4850
|
+
channelName: input.channelName,
|
|
4851
|
+
channelId: input.channelId,
|
|
4852
|
+
workspaceUrl: input.workspaceUrl,
|
|
4853
|
+
threadTs: input.threadTs,
|
|
4854
|
+
initialText: input.initialText,
|
|
4855
|
+
onSend: input.sendFn
|
|
4856
|
+
});
|
|
4857
|
+
if ("cancelled" in result) {
|
|
4858
|
+
return { ok: true, cancelled: true };
|
|
4859
|
+
}
|
|
4860
|
+
return { ok: true, sent: true };
|
|
4861
|
+
}
|
|
4862
|
+
|
|
3372
4863
|
// src/cli/message-command.ts
|
|
3373
4864
|
function collectOptionValue(value, previous = []) {
|
|
3374
4865
|
return [...previous, value];
|
|
@@ -3439,6 +4930,21 @@ function registerMessageCommand(input) {
|
|
|
3439
4930
|
process.exitCode = 1;
|
|
3440
4931
|
}
|
|
3441
4932
|
});
|
|
4933
|
+
messageCmd.command("draft").description("Open a rich Slack-like editor to compose and send a message").argument("<target>", "Slack message URL, #name/name, or channel id").argument("[text]", "Initial draft text (mrkdwn format)").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--thread-ts <ts>", "Thread root ts to post into (optional)").action(async (...args) => {
|
|
4934
|
+
const [targetInput, text, options] = args;
|
|
4935
|
+
try {
|
|
4936
|
+
const payload = await draftMessage({
|
|
4937
|
+
ctx: input.ctx,
|
|
4938
|
+
targetInput,
|
|
4939
|
+
initialText: text,
|
|
4940
|
+
options
|
|
4941
|
+
});
|
|
4942
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4943
|
+
} catch (err) {
|
|
4944
|
+
console.error(input.ctx.errorMessage(err));
|
|
4945
|
+
process.exitCode = 1;
|
|
4946
|
+
}
|
|
4947
|
+
});
|
|
3442
4948
|
const reactCmd = messageCmd.command("react").description("Add or remove reactions");
|
|
3443
4949
|
reactCmd.command("add").description("Add a reaction to a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to react with (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
|
|
3444
4950
|
const [targetInput, emoji2, options] = args;
|
|
@@ -4184,10 +5690,11 @@ function registerSearchCommand(input) {
|
|
|
4184
5690
|
}
|
|
4185
5691
|
|
|
4186
5692
|
// src/lib/update.ts
|
|
5693
|
+
import { execSync as execSync3 } from "node:child_process";
|
|
4187
5694
|
import { createHash } from "node:crypto";
|
|
4188
5695
|
import { chmod, copyFile, mkdir as mkdir5, readFile as readFile5, rename, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
4189
5696
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4190
|
-
import { join as join10 } from "node:path";
|
|
5697
|
+
import { basename as basename2, join as join10 } from "node:path";
|
|
4191
5698
|
var REPO = "stablyai/agent-slack";
|
|
4192
5699
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
4193
5700
|
function getCachePath() {
|
|
@@ -4247,6 +5754,37 @@ async function checkForUpdate(force = false) {
|
|
|
4247
5754
|
update_available: compareSemver(latest, current) > 0
|
|
4248
5755
|
};
|
|
4249
5756
|
}
|
|
5757
|
+
function detectInstallMethod() {
|
|
5758
|
+
if (process.versions.bun && basename2(process.execPath) === "bun") {
|
|
5759
|
+
return "bun";
|
|
5760
|
+
}
|
|
5761
|
+
const execName = basename2(process.execPath);
|
|
5762
|
+
if (["node", "nodejs", "node.exe"].includes(execName) || process.env.npm_execpath) {
|
|
5763
|
+
return "npm";
|
|
5764
|
+
}
|
|
5765
|
+
return "binary";
|
|
5766
|
+
}
|
|
5767
|
+
function getUpdateCommand(method) {
|
|
5768
|
+
const m = method ?? detectInstallMethod();
|
|
5769
|
+
switch (m) {
|
|
5770
|
+
case "npm":
|
|
5771
|
+
return "npm install -g agent-slack@latest";
|
|
5772
|
+
case "bun":
|
|
5773
|
+
return "bun install -g agent-slack@latest";
|
|
5774
|
+
case "binary":
|
|
5775
|
+
return "agent-slack update";
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
function performPackageManagerUpdate(method) {
|
|
5779
|
+
const cmd = getUpdateCommand(method);
|
|
5780
|
+
try {
|
|
5781
|
+
execSync3(cmd, { stdio: ["inherit", "pipe", "inherit"] });
|
|
5782
|
+
return { success: true, message: `Updated agent-slack via: ${cmd}` };
|
|
5783
|
+
} catch (err) {
|
|
5784
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5785
|
+
return { success: false, message: `Failed to run "${cmd}": ${msg}` };
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
4250
5788
|
function detectPlatformAsset() {
|
|
4251
5789
|
const platform6 = process.platform === "win32" ? "windows" : process.platform;
|
|
4252
5790
|
const archMap = { x64: "x64", arm64: "arm64" };
|
|
@@ -4314,8 +5852,9 @@ async function backgroundUpdateCheck() {
|
|
|
4314
5852
|
try {
|
|
4315
5853
|
const result = await checkForUpdate();
|
|
4316
5854
|
if (result?.update_available) {
|
|
5855
|
+
const cmd = getUpdateCommand();
|
|
4317
5856
|
process.stderr.write(`
|
|
4318
|
-
Update available: ${result.current} → ${result.latest}. Run "
|
|
5857
|
+
Update available: ${result.current} → ${result.latest}. Run "${cmd}" to upgrade.
|
|
4319
5858
|
`);
|
|
4320
5859
|
}
|
|
4321
5860
|
} catch {}
|
|
@@ -4325,6 +5864,7 @@ Update available: ${result.current} → ${result.latest}. Run "agent-slack updat
|
|
|
4325
5864
|
function registerUpdateCommand(input) {
|
|
4326
5865
|
input.program.command("update").description("Update agent-slack to the latest version").option("--check", "Only check for updates (don't install)").action(async (...args) => {
|
|
4327
5866
|
const [options] = args;
|
|
5867
|
+
const method = detectInstallMethod();
|
|
4328
5868
|
try {
|
|
4329
5869
|
const result = await checkForUpdate(true);
|
|
4330
5870
|
if (!result) {
|
|
@@ -4333,16 +5873,28 @@ function registerUpdateCommand(input) {
|
|
|
4333
5873
|
return;
|
|
4334
5874
|
}
|
|
4335
5875
|
if (!result.update_available) {
|
|
4336
|
-
console.log(JSON.stringify(pruneEmpty({ ...result, status: "up_to_date" }), null, 2));
|
|
5876
|
+
console.log(JSON.stringify(pruneEmpty({ ...result, install_method: method, status: "up_to_date" }), null, 2));
|
|
4337
5877
|
return;
|
|
4338
5878
|
}
|
|
4339
5879
|
if (options.check) {
|
|
4340
|
-
console.log(JSON.stringify(pruneEmpty({
|
|
5880
|
+
console.log(JSON.stringify(pruneEmpty({
|
|
5881
|
+
...result,
|
|
5882
|
+
install_method: method,
|
|
5883
|
+
update_command: getUpdateCommand(method),
|
|
5884
|
+
status: "update_available"
|
|
5885
|
+
}), null, 2));
|
|
4341
5886
|
return;
|
|
4342
5887
|
}
|
|
4343
5888
|
process.stderr.write(`Updating agent-slack ${result.current} → ${result.latest}...
|
|
4344
5889
|
`);
|
|
4345
|
-
|
|
5890
|
+
let outcome;
|
|
5891
|
+
if (method === "npm" || method === "bun") {
|
|
5892
|
+
process.stderr.write(`Detected install method: ${method}
|
|
5893
|
+
`);
|
|
5894
|
+
outcome = performPackageManagerUpdate(method);
|
|
5895
|
+
} else {
|
|
5896
|
+
outcome = await performUpdate(result.latest);
|
|
5897
|
+
}
|
|
4346
5898
|
if (!outcome.success) {
|
|
4347
5899
|
console.error(outcome.message);
|
|
4348
5900
|
process.exitCode = 1;
|
|
@@ -4350,6 +5902,7 @@ function registerUpdateCommand(input) {
|
|
|
4350
5902
|
}
|
|
4351
5903
|
console.log(JSON.stringify(pruneEmpty({
|
|
4352
5904
|
status: "updated",
|
|
5905
|
+
install_method: method,
|
|
4353
5906
|
previous_version: result.current,
|
|
4354
5907
|
new_version: result.latest,
|
|
4355
5908
|
message: outcome.message
|
|
@@ -4417,6 +5970,17 @@ async function resolveUserId2(client, input) {
|
|
|
4417
5970
|
if (/^U[A-Z0-9]{8,}$/.test(trimmed)) {
|
|
4418
5971
|
return trimmed;
|
|
4419
5972
|
}
|
|
5973
|
+
const looksLikeEmail = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(trimmed) && !trimmed.startsWith("@");
|
|
5974
|
+
if (looksLikeEmail) {
|
|
5975
|
+
try {
|
|
5976
|
+
const byEmail = await client.api("users.lookupByEmail", { email: trimmed });
|
|
5977
|
+
const user = isRecord5(byEmail.user) ? byEmail.user : null;
|
|
5978
|
+
const userId = user ? getString(user.id) : undefined;
|
|
5979
|
+
if (userId) {
|
|
5980
|
+
return userId;
|
|
5981
|
+
}
|
|
5982
|
+
} catch {}
|
|
5983
|
+
}
|
|
4420
5984
|
const handle = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
4421
5985
|
if (!handle) {
|
|
4422
5986
|
return null;
|
|
@@ -4425,7 +5989,17 @@ async function resolveUserId2(client, input) {
|
|
|
4425
5989
|
for (;; ) {
|
|
4426
5990
|
const resp = await client.api("users.list", { limit: 200, cursor });
|
|
4427
5991
|
const members = asArray(resp.members).filter(isRecord5);
|
|
4428
|
-
const found = members.find((m) =>
|
|
5992
|
+
const found = members.find((m) => {
|
|
5993
|
+
if (getString(m.name) === handle) {
|
|
5994
|
+
return true;
|
|
5995
|
+
}
|
|
5996
|
+
if (looksLikeEmail) {
|
|
5997
|
+
const profile = isRecord5(m.profile) ? m.profile : null;
|
|
5998
|
+
const email = profile ? getString(profile.email) : undefined;
|
|
5999
|
+
return Boolean(email) && email?.toLowerCase() === trimmed.toLowerCase();
|
|
6000
|
+
}
|
|
6001
|
+
return false;
|
|
6002
|
+
});
|
|
4429
6003
|
if (found) {
|
|
4430
6004
|
const id = getString(found.id);
|
|
4431
6005
|
if (id) {
|
|
@@ -4500,6 +6074,195 @@ function registerUserCommand(input) {
|
|
|
4500
6074
|
});
|
|
4501
6075
|
}
|
|
4502
6076
|
|
|
6077
|
+
// src/slack/channel-admin.ts
|
|
6078
|
+
async function createChannel(client, input) {
|
|
6079
|
+
const name = input.name.trim();
|
|
6080
|
+
if (!name) {
|
|
6081
|
+
throw new Error("Channel name is empty");
|
|
6082
|
+
}
|
|
6083
|
+
const resp = await client.api("conversations.create", {
|
|
6084
|
+
name,
|
|
6085
|
+
is_private: Boolean(input.isPrivate)
|
|
6086
|
+
});
|
|
6087
|
+
const channel = isRecord5(resp.channel) ? resp.channel : null;
|
|
6088
|
+
const id = channel ? getString(channel.id) : undefined;
|
|
6089
|
+
const channelName = channel ? getString(channel.name) : undefined;
|
|
6090
|
+
const isPrivate = channel && typeof channel.is_private === "boolean" ? channel.is_private : Boolean(input.isPrivate);
|
|
6091
|
+
if (!id || !channelName) {
|
|
6092
|
+
throw new Error("conversations.create returned no channel");
|
|
6093
|
+
}
|
|
6094
|
+
return {
|
|
6095
|
+
channel: {
|
|
6096
|
+
id,
|
|
6097
|
+
name: channelName,
|
|
6098
|
+
is_private: isPrivate
|
|
6099
|
+
}
|
|
6100
|
+
};
|
|
6101
|
+
}
|
|
6102
|
+
async function inviteUsersToChannel(client, input) {
|
|
6103
|
+
const invitedUserIds = [];
|
|
6104
|
+
const alreadyInChannelUserIds = [];
|
|
6105
|
+
for (const userId of input.userIds) {
|
|
6106
|
+
try {
|
|
6107
|
+
await client.api("conversations.invite", {
|
|
6108
|
+
channel: input.channelId,
|
|
6109
|
+
users: userId
|
|
6110
|
+
});
|
|
6111
|
+
invitedUserIds.push(userId);
|
|
6112
|
+
} catch (err) {
|
|
6113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6114
|
+
if (message.includes("already_in_channel")) {
|
|
6115
|
+
alreadyInChannelUserIds.push(userId);
|
|
6116
|
+
continue;
|
|
6117
|
+
}
|
|
6118
|
+
throw err;
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
return {
|
|
6122
|
+
invited_user_ids: invitedUserIds,
|
|
6123
|
+
already_in_channel_user_ids: alreadyInChannelUserIds
|
|
6124
|
+
};
|
|
6125
|
+
}
|
|
6126
|
+
function parseInviteUsersCsv(input) {
|
|
6127
|
+
return Array.from(new Set(asArray(input.split(",")).map((value) => String(value).trim()).filter(Boolean)));
|
|
6128
|
+
}
|
|
6129
|
+
function splitEmailsFromInviteTargets(input) {
|
|
6130
|
+
const emails = [];
|
|
6131
|
+
const nonEmailTargets = [];
|
|
6132
|
+
for (const target of input) {
|
|
6133
|
+
if (isLikelyEmail(target)) {
|
|
6134
|
+
emails.push(target);
|
|
6135
|
+
continue;
|
|
6136
|
+
}
|
|
6137
|
+
nonEmailTargets.push(target);
|
|
6138
|
+
}
|
|
6139
|
+
return {
|
|
6140
|
+
emails: Array.from(new Set(emails)),
|
|
6141
|
+
non_email_targets: nonEmailTargets
|
|
6142
|
+
};
|
|
6143
|
+
}
|
|
6144
|
+
async function inviteExternalUsersToChannel(client, input) {
|
|
6145
|
+
const invitedEmails = [];
|
|
6146
|
+
const alreadyInvitedEmails = [];
|
|
6147
|
+
for (const email of input.emails) {
|
|
6148
|
+
try {
|
|
6149
|
+
await client.api("conversations.inviteShared", {
|
|
6150
|
+
channel: input.channelId,
|
|
6151
|
+
emails: [email],
|
|
6152
|
+
external_limited: input.externalLimited ?? true
|
|
6153
|
+
});
|
|
6154
|
+
invitedEmails.push(email);
|
|
6155
|
+
} catch (err) {
|
|
6156
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6157
|
+
if (message.includes("already_in_channel") || message.includes("already_invited")) {
|
|
6158
|
+
alreadyInvitedEmails.push(email);
|
|
6159
|
+
continue;
|
|
6160
|
+
}
|
|
6161
|
+
throw err;
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6164
|
+
return {
|
|
6165
|
+
invited_emails: invitedEmails,
|
|
6166
|
+
already_invited_emails: alreadyInvitedEmails
|
|
6167
|
+
};
|
|
6168
|
+
}
|
|
6169
|
+
function isLikelyEmail(input) {
|
|
6170
|
+
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(input.trim());
|
|
6171
|
+
}
|
|
6172
|
+
|
|
6173
|
+
// src/cli/channel-command.ts
|
|
6174
|
+
function registerChannelCommand(input) {
|
|
6175
|
+
const channelCmd = input.program.command("channel").description("Create channels and invite users");
|
|
6176
|
+
channelCmd.command("new").description("Create a new channel").requiredOption("--name <name>", "Channel name").option("--private", "Create as a private channel").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
6177
|
+
const [options] = args;
|
|
6178
|
+
try {
|
|
6179
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
6180
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
6181
|
+
workspaceUrl,
|
|
6182
|
+
work: async () => {
|
|
6183
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
6184
|
+
return await createChannel(client, {
|
|
6185
|
+
name: options.name,
|
|
6186
|
+
isPrivate: Boolean(options.private)
|
|
6187
|
+
});
|
|
6188
|
+
}
|
|
6189
|
+
});
|
|
6190
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
6191
|
+
} catch (err) {
|
|
6192
|
+
console.error(input.ctx.errorMessage(err));
|
|
6193
|
+
process.exitCode = 1;
|
|
6194
|
+
}
|
|
6195
|
+
});
|
|
6196
|
+
channelCmd.command("invite").description("Invite users to a channel").requiredOption("--channel <id-or-name>", "Channel id/name (#general, general, C...)").requiredOption("--users <users>", "Comma-separated users (U..., @handle, handle, email)").option("--external", "Send Slack Connect external invites (email targets only)").option("--allow-external-user-invites", "For --external invites, allow invitees to invite additional users").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
6197
|
+
const [options] = args;
|
|
6198
|
+
try {
|
|
6199
|
+
if (options.allowExternalUserInvites && !options.external) {
|
|
6200
|
+
throw new Error("--allow-external-user-invites requires --external");
|
|
6201
|
+
}
|
|
6202
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
6203
|
+
await input.ctx.assertWorkspaceSpecifiedForChannelNames({
|
|
6204
|
+
workspaceUrl,
|
|
6205
|
+
channels: [options.channel]
|
|
6206
|
+
});
|
|
6207
|
+
const userInputs = parseInviteUsersCsv(options.users);
|
|
6208
|
+
if (userInputs.length === 0) {
|
|
6209
|
+
throw new Error('No users provided. Pass --users "U01...,@alice,bob@example.com"');
|
|
6210
|
+
}
|
|
6211
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
6212
|
+
workspaceUrl,
|
|
6213
|
+
work: async () => {
|
|
6214
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
6215
|
+
const channelId = await resolveChannelId(client, options.channel);
|
|
6216
|
+
if (options.external) {
|
|
6217
|
+
const split = splitEmailsFromInviteTargets(userInputs);
|
|
6218
|
+
if (split.emails.length === 0) {
|
|
6219
|
+
throw new Error('External invites require email targets in --users, e.g. --users "alice@example.com,bob@example.com"');
|
|
6220
|
+
}
|
|
6221
|
+
const externalLimited = !options.allowExternalUserInvites;
|
|
6222
|
+
const inviteResult2 = await inviteExternalUsersToChannel(client, {
|
|
6223
|
+
channelId,
|
|
6224
|
+
emails: split.emails,
|
|
6225
|
+
externalLimited
|
|
6226
|
+
});
|
|
6227
|
+
return {
|
|
6228
|
+
channel_id: channelId,
|
|
6229
|
+
external: true,
|
|
6230
|
+
external_limited: externalLimited,
|
|
6231
|
+
invited_emails: inviteResult2.invited_emails,
|
|
6232
|
+
already_invited_emails: inviteResult2.already_invited_emails,
|
|
6233
|
+
invalid_external_targets: split.non_email_targets
|
|
6234
|
+
};
|
|
6235
|
+
}
|
|
6236
|
+
const resolvedUserIds = [];
|
|
6237
|
+
const unresolvedUsers = [];
|
|
6238
|
+
for (const userInput of userInputs) {
|
|
6239
|
+
const userId = await resolveUserId2(client, userInput);
|
|
6240
|
+
if (!userId) {
|
|
6241
|
+
unresolvedUsers.push(userInput);
|
|
6242
|
+
continue;
|
|
6243
|
+
}
|
|
6244
|
+
resolvedUserIds.push(userId);
|
|
6245
|
+
}
|
|
6246
|
+
const inviteResult = await inviteUsersToChannel(client, {
|
|
6247
|
+
channelId,
|
|
6248
|
+
userIds: resolvedUserIds
|
|
6249
|
+
});
|
|
6250
|
+
return {
|
|
6251
|
+
channel_id: channelId,
|
|
6252
|
+
invited_user_ids: inviteResult.invited_user_ids,
|
|
6253
|
+
already_in_channel_user_ids: inviteResult.already_in_channel_user_ids,
|
|
6254
|
+
unresolved_users: unresolvedUsers
|
|
6255
|
+
};
|
|
6256
|
+
}
|
|
6257
|
+
});
|
|
6258
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
6259
|
+
} catch (err) {
|
|
6260
|
+
console.error(input.ctx.errorMessage(err));
|
|
6261
|
+
process.exitCode = 1;
|
|
6262
|
+
}
|
|
6263
|
+
});
|
|
6264
|
+
}
|
|
6265
|
+
|
|
4503
6266
|
// src/index.ts
|
|
4504
6267
|
var program = new Command;
|
|
4505
6268
|
program.name("agent-slack").description("Slack automation CLI for AI agents").version(getPackageVersion());
|
|
@@ -4510,6 +6273,7 @@ registerCanvasCommand({ program, ctx });
|
|
|
4510
6273
|
registerSearchCommand({ program, ctx });
|
|
4511
6274
|
registerUpdateCommand({ program });
|
|
4512
6275
|
registerUserCommand({ program, ctx });
|
|
6276
|
+
registerChannelCommand({ program, ctx });
|
|
4513
6277
|
program.parse(process.argv);
|
|
4514
6278
|
if (!process.argv.slice(2).length) {
|
|
4515
6279
|
program.outputHelp();
|
|
@@ -4519,5 +6283,5 @@ if (subcommand && subcommand !== "update") {
|
|
|
4519
6283
|
backgroundUpdateCheck();
|
|
4520
6284
|
}
|
|
4521
6285
|
|
|
4522
|
-
//# debugId=
|
|
6286
|
+
//# debugId=5576104C616416B664756E2164756E21
|
|
4523
6287
|
//# sourceMappingURL=index.js.map
|