discord-agent-bridge 0.1.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 +195 -0
- package/dist/app.js +210 -0
- package/dist/app.js.map +1 -0
- package/dist/app.test.js +117 -0
- package/dist/app.test.js.map +1 -0
- package/dist/cli.js +63 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.js +91 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/core/auditLog.js +65 -0
- package/dist/core/auditLog.js.map +1 -0
- package/dist/core/auditLog.test.js +124 -0
- package/dist/core/auditLog.test.js.map +1 -0
- package/dist/core/auth.js +106 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/auth.test.js +216 -0
- package/dist/core/auth.test.js.map +1 -0
- package/dist/core/channelRegistry.js +119 -0
- package/dist/core/channelRegistry.js.map +1 -0
- package/dist/core/channelRegistry.test.js +114 -0
- package/dist/core/channelRegistry.test.js.map +1 -0
- package/dist/core/commandPolicy.js +6 -0
- package/dist/core/commandPolicy.js.map +1 -0
- package/dist/core/commandRouter.js +9 -0
- package/dist/core/commandRouter.js.map +1 -0
- package/dist/core/config.js +141 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.js +149 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/configResolver.js +90 -0
- package/dist/core/configResolver.js.map +1 -0
- package/dist/core/configResolver.test.js +138 -0
- package/dist/core/configResolver.test.js.map +1 -0
- package/dist/core/configSchema.js +164 -0
- package/dist/core/configSchema.js.map +1 -0
- package/dist/core/contracts.js +5 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/eventBus.js +42 -0
- package/dist/core/eventBus.js.map +1 -0
- package/dist/core/eventBus.test.js +72 -0
- package/dist/core/eventBus.test.js.map +1 -0
- package/dist/core/hookBridge.js +11 -0
- package/dist/core/hookBridge.js.map +1 -0
- package/dist/core/logger.js +116 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/logger.test.js +88 -0
- package/dist/core/logger.test.js.map +1 -0
- package/dist/core/modeRegistry.js +33 -0
- package/dist/core/modeRegistry.js.map +1 -0
- package/dist/core/permissionResolver.js +43 -0
- package/dist/core/permissionResolver.js.map +1 -0
- package/dist/core/permissionResolver.test.js +136 -0
- package/dist/core/permissionResolver.test.js.map +1 -0
- package/dist/core/providerCatalog.js +240 -0
- package/dist/core/providerCatalog.js.map +1 -0
- package/dist/core/providerCatalog.test.js +230 -0
- package/dist/core/providerCatalog.test.js.map +1 -0
- package/dist/core/sessionOrchestrator.js +463 -0
- package/dist/core/sessionOrchestrator.js.map +1 -0
- package/dist/core/sessionOrchestrator.test.js +344 -0
- package/dist/core/sessionOrchestrator.test.js.map +1 -0
- package/dist/core/state/schema.js +49 -0
- package/dist/core/state/schema.js.map +1 -0
- package/dist/core/state/store.js +86 -0
- package/dist/core/state/store.js.map +1 -0
- package/dist/core/state/store.test.js +99 -0
- package/dist/core/state/store.test.js.map +1 -0
- package/dist/core/usageService.js +310 -0
- package/dist/core/usageService.js.map +1 -0
- package/dist/core/usageService.test.js +234 -0
- package/dist/core/usageService.test.js.map +1 -0
- package/dist/discord/alwaysAllow.test.js +181 -0
- package/dist/discord/alwaysAllow.test.js.map +1 -0
- package/dist/discord/client.js +666 -0
- package/dist/discord/client.js.map +1 -0
- package/dist/discord/client.test.js +256 -0
- package/dist/discord/client.test.js.map +1 -0
- package/dist/discord/configPanel.js +270 -0
- package/dist/discord/configPanel.js.map +1 -0
- package/dist/discord/configPanel.test.js +332 -0
- package/dist/discord/configPanel.test.js.map +1 -0
- package/dist/discord/directoryBrowser.js +151 -0
- package/dist/discord/directoryBrowser.js.map +1 -0
- package/dist/discord/directoryBrowser.test.js +163 -0
- package/dist/discord/directoryBrowser.test.js.map +1 -0
- package/dist/discord/favorites.js +10 -0
- package/dist/discord/favorites.js.map +1 -0
- package/dist/discord/fileDownload.js +94 -0
- package/dist/discord/fileDownload.js.map +1 -0
- package/dist/discord/fileDownload.test.js +59 -0
- package/dist/discord/fileDownload.test.js.map +1 -0
- package/dist/discord/format.js +104 -0
- package/dist/discord/format.js.map +1 -0
- package/dist/discord/format.test.js +40 -0
- package/dist/discord/format.test.js.map +1 -0
- package/dist/discord/guildChannels.js +97 -0
- package/dist/discord/guildChannels.js.map +1 -0
- package/dist/discord/guildChannels.test.js +231 -0
- package/dist/discord/guildChannels.test.js.map +1 -0
- package/dist/discord/i18n.js +207 -0
- package/dist/discord/i18n.js.map +1 -0
- package/dist/discord/i18n.test.js +31 -0
- package/dist/discord/i18n.test.js.map +1 -0
- package/dist/discord/interactionRouter.js +954 -0
- package/dist/discord/interactionRouter.js.map +1 -0
- package/dist/discord/interactionRouter.test.js +1245 -0
- package/dist/discord/interactionRouter.test.js.map +1 -0
- package/dist/discord/messageRouter.js +221 -0
- package/dist/discord/messageRouter.js.map +1 -0
- package/dist/discord/messageRouter.test.js +360 -0
- package/dist/discord/messageRouter.test.js.map +1 -0
- package/dist/discord/ports.js +12 -0
- package/dist/discord/ports.js.map +1 -0
- package/dist/discord/renderers/defaultSet.test.js +97 -0
- package/dist/discord/renderers/defaultSet.test.js.map +1 -0
- package/dist/discord/renderers/diffView.js +73 -0
- package/dist/discord/renderers/diffView.js.map +1 -0
- package/dist/discord/renderers/diffView.test.js +54 -0
- package/dist/discord/renderers/diffView.test.js.map +1 -0
- package/dist/discord/renderers/index.js +143 -0
- package/dist/discord/renderers/index.js.map +1 -0
- package/dist/discord/renderers/index.test.js +124 -0
- package/dist/discord/renderers/index.test.js.map +1 -0
- package/dist/discord/renderers/mentionOnComplete.js +19 -0
- package/dist/discord/renderers/mentionOnComplete.js.map +1 -0
- package/dist/discord/renderers/mentionOnComplete.test.js +35 -0
- package/dist/discord/renderers/mentionOnComplete.test.js.map +1 -0
- package/dist/discord/renderers/permissionButtons.js +150 -0
- package/dist/discord/renderers/permissionButtons.js.map +1 -0
- package/dist/discord/renderers/permissionButtons.test.js +108 -0
- package/dist/discord/renderers/permissionButtons.test.js.map +1 -0
- package/dist/discord/renderers/resultLine.js +25 -0
- package/dist/discord/renderers/resultLine.js.map +1 -0
- package/dist/discord/renderers/statusEmbed.js +20 -0
- package/dist/discord/renderers/statusEmbed.js.map +1 -0
- package/dist/discord/renderers/statusEmbed.test.js +32 -0
- package/dist/discord/renderers/statusEmbed.test.js.map +1 -0
- package/dist/discord/renderers/streamEmbed.js +134 -0
- package/dist/discord/renderers/streamEmbed.js.map +1 -0
- package/dist/discord/renderers/streamEmbed.test.js +97 -0
- package/dist/discord/renderers/streamEmbed.test.js.map +1 -0
- package/dist/discord/renderers/toolThread.js +65 -0
- package/dist/discord/renderers/toolThread.js.map +1 -0
- package/dist/discord/renderers/toolThread.test.js +54 -0
- package/dist/discord/renderers/toolThread.test.js.map +1 -0
- package/dist/discord/renderers/transcriptFeed.js +33 -0
- package/dist/discord/renderers/transcriptFeed.js.map +1 -0
- package/dist/discord/renderers/usageEmbed.js +78 -0
- package/dist/discord/renderers/usageEmbed.js.map +1 -0
- package/dist/discord/renderers/usageEmbed.test.js +42 -0
- package/dist/discord/renderers/usageEmbed.test.js.map +1 -0
- package/dist/discord/wiring.js +214 -0
- package/dist/discord/wiring.js.map +1 -0
- package/dist/discord/wiring.test.js +252 -0
- package/dist/discord/wiring.test.js.map +1 -0
- package/dist/discord/wizard/channelWizard.js +305 -0
- package/dist/discord/wizard/channelWizard.js.map +1 -0
- package/dist/discord/wizard/channelWizard.test.js +298 -0
- package/dist/discord/wizard/channelWizard.test.js.map +1 -0
- package/dist/discord/wizard/resumeWizard.js +123 -0
- package/dist/discord/wizard/resumeWizard.js.map +1 -0
- package/dist/modes/claude/index.js +80 -0
- package/dist/modes/claude/index.js.map +1 -0
- package/dist/modes/claude/listResumable.test.js +72 -0
- package/dist/modes/claude/listResumable.test.js.map +1 -0
- package/dist/modes/claude/mcpFileTool.js +82 -0
- package/dist/modes/claude/mcpFileTool.js.map +1 -0
- package/dist/modes/claude/permissions.js +35 -0
- package/dist/modes/claude/permissions.js.map +1 -0
- package/dist/modes/claude/plugins.js +56 -0
- package/dist/modes/claude/plugins.js.map +1 -0
- package/dist/modes/claude/session.js +324 -0
- package/dist/modes/claude/session.js.map +1 -0
- package/dist/modes/claude/session.test.js +415 -0
- package/dist/modes/claude/session.test.js.map +1 -0
- package/dist/modes/codex/discovery.js +221 -0
- package/dist/modes/codex/discovery.js.map +1 -0
- package/dist/modes/codex/discovery.test.js +213 -0
- package/dist/modes/codex/discovery.test.js.map +1 -0
- package/dist/modes/codex/eventMapper.js +262 -0
- package/dist/modes/codex/eventMapper.js.map +1 -0
- package/dist/modes/codex/index.js +132 -0
- package/dist/modes/codex/index.js.map +1 -0
- package/dist/modes/codex/index.test.js +170 -0
- package/dist/modes/codex/index.test.js.map +1 -0
- package/dist/modes/codex/runner.js +207 -0
- package/dist/modes/codex/runner.js.map +1 -0
- package/dist/modes/codex/runner.test.js +587 -0
- package/dist/modes/codex/runner.test.js.map +1 -0
- package/dist/modes/codex/sqliteReader.js +171 -0
- package/dist/modes/codex/sqliteReader.js.map +1 -0
- package/dist/modes/codex/sqliteReader.test.js +137 -0
- package/dist/modes/codex/sqliteReader.test.js.map +1 -0
- package/dist/setup/wizard.js +115 -0
- package/dist/setup/wizard.js.map +1 -0
- package/dist/setup/wizard.test.js +216 -0
- package/dist/setup/wizard.test.js.map +1 -0
- package/dist/startBot.test.js +78 -0
- package/dist/startBot.test.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PermissionButtonsHandler, buildCustomId, parseCustomId, } from './permissionButtons.js';
|
|
3
|
+
// A fake channel that records what was sent and hands back an editable message.
|
|
4
|
+
function fakeChannel() {
|
|
5
|
+
const sent = [];
|
|
6
|
+
const edits = [];
|
|
7
|
+
const channel = {
|
|
8
|
+
async send(message) {
|
|
9
|
+
sent.push(message);
|
|
10
|
+
const msg = {
|
|
11
|
+
id: `m${sent.length}`,
|
|
12
|
+
async edit(m) {
|
|
13
|
+
edits.push(m);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
return msg;
|
|
17
|
+
},
|
|
18
|
+
async startThread() {
|
|
19
|
+
throw new Error('not used');
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return { channel, sent, edits };
|
|
23
|
+
}
|
|
24
|
+
const req = {
|
|
25
|
+
kind: 'permission_request',
|
|
26
|
+
id: 'abc123',
|
|
27
|
+
toolName: 'Bash',
|
|
28
|
+
input: { command: 'ls' },
|
|
29
|
+
};
|
|
30
|
+
describe('permissionButtons custom_id', () => {
|
|
31
|
+
it('builds and parses perm:<id>:<action>', () => {
|
|
32
|
+
expect(buildCustomId('abc123', 'allow')).toBe('perm:abc123:allow');
|
|
33
|
+
expect(parseCustomId('perm:abc123:deny')).toEqual({ reqId: 'abc123', action: 'deny' });
|
|
34
|
+
});
|
|
35
|
+
it('rejects foreign or malformed ids', () => {
|
|
36
|
+
expect(parseCustomId('dir:into:foo')).toBeNull();
|
|
37
|
+
expect(parseCustomId('perm:abc123:bogus')).toBeNull();
|
|
38
|
+
expect(parseCustomId('perm:abc123')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('PermissionButtonsHandler', () => {
|
|
42
|
+
it('resolves the pending decision to allow on perm:<id>:allow', async () => {
|
|
43
|
+
const { channel } = fakeChannel();
|
|
44
|
+
const handler = new PermissionButtonsHandler({ channel });
|
|
45
|
+
const pending = handler.request(req);
|
|
46
|
+
const applied = await handler.resolve(buildCustomId('abc123', 'allow'));
|
|
47
|
+
expect(applied).toEqual({ behavior: 'allow' });
|
|
48
|
+
await expect(pending).resolves.toEqual({ behavior: 'allow' });
|
|
49
|
+
});
|
|
50
|
+
it('resolves to deny on perm:<id>:deny', async () => {
|
|
51
|
+
const { channel } = fakeChannel();
|
|
52
|
+
const handler = new PermissionButtonsHandler({ channel });
|
|
53
|
+
const pending = handler.request(req);
|
|
54
|
+
const applied = await handler.resolve(buildCustomId('abc123', 'deny'));
|
|
55
|
+
expect(applied?.behavior).toBe('deny');
|
|
56
|
+
await expect(pending).resolves.toMatchObject({ behavior: 'deny' });
|
|
57
|
+
});
|
|
58
|
+
it('treats always-allow as allow for the immediate decision', async () => {
|
|
59
|
+
const { channel } = fakeChannel();
|
|
60
|
+
const handler = new PermissionButtonsHandler({ channel });
|
|
61
|
+
const pending = handler.request(req);
|
|
62
|
+
await handler.resolve(buildCustomId('abc123', 'always'));
|
|
63
|
+
await expect(pending).resolves.toEqual({ behavior: 'allow' });
|
|
64
|
+
});
|
|
65
|
+
it('is idempotent / safe for unknown or already-resolved ids', async () => {
|
|
66
|
+
const { channel } = fakeChannel();
|
|
67
|
+
const handler = new PermissionButtonsHandler({ channel });
|
|
68
|
+
handler.request(req);
|
|
69
|
+
await handler.resolve(buildCustomId('abc123', 'allow'));
|
|
70
|
+
// A second resolve of the same id is a no-op.
|
|
71
|
+
expect(await handler.resolve(buildCustomId('abc123', 'allow'))).toBeNull();
|
|
72
|
+
// An unknown id is a no-op.
|
|
73
|
+
expect(await handler.resolve(buildCustomId('zzz', 'deny'))).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
it('ignores a resolve from the WRONG actor; the prompt stays pending', async () => {
|
|
76
|
+
const { channel } = fakeChannel();
|
|
77
|
+
const handler = new PermissionButtonsHandler({ channel });
|
|
78
|
+
const pending = handler.request(req, 'owner-1');
|
|
79
|
+
// A bystander (not the approver) clicks Allow → ignored, no decision, prompt open.
|
|
80
|
+
const wrong = await handler.resolve(buildCustomId('abc123', 'allow'), 'bystander-2');
|
|
81
|
+
expect(wrong).toBeNull();
|
|
82
|
+
// The pending is still resolvable by the correct approver afterwards.
|
|
83
|
+
const right = await handler.resolve(buildCustomId('abc123', 'allow'), 'owner-1');
|
|
84
|
+
expect(right).toEqual({ behavior: 'allow' });
|
|
85
|
+
await expect(pending).resolves.toEqual({ behavior: 'allow' });
|
|
86
|
+
});
|
|
87
|
+
it('an unbound prompt (no approver) resolves for any actor', async () => {
|
|
88
|
+
const { channel } = fakeChannel();
|
|
89
|
+
const handler = new PermissionButtonsHandler({ channel });
|
|
90
|
+
const pending = handler.request(req); // no approverId
|
|
91
|
+
const applied = await handler.resolve(buildCustomId('abc123', 'allow'), 'anyone');
|
|
92
|
+
expect(applied).toEqual({ behavior: 'allow' });
|
|
93
|
+
await expect(pending).resolves.toEqual({ behavior: 'allow' });
|
|
94
|
+
});
|
|
95
|
+
it('posts buttons then disables them on decision', async () => {
|
|
96
|
+
const { channel, sent, edits } = fakeChannel();
|
|
97
|
+
const handler = new PermissionButtonsHandler({ channel });
|
|
98
|
+
handler.request(req);
|
|
99
|
+
// Let the buttons message finish posting so the disabling edit can target it.
|
|
100
|
+
await new Promise((r) => setImmediate(r));
|
|
101
|
+
expect(sent).toHaveLength(1);
|
|
102
|
+
expect(sent[0].components?.[0].components).toHaveLength(3); // allow/always/deny
|
|
103
|
+
await handler.resolve(buildCustomId('abc123', 'allow'));
|
|
104
|
+
expect(edits).toHaveLength(1);
|
|
105
|
+
expect(edits[0].components).toEqual([]); // buttons removed
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=permissionButtons.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionButtons.test.js","sourceRoot":"","sources":["../../../src/discord/renderers/permissionButtons.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { formatDuration, formatTokens } from '../format.js';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
// Done-line (§6, §5c): a compact cost/tokens/duration line built from a `result`
|
|
4
|
+
// event. Cap-aware by construction — only fields actually present on the event are
|
|
5
|
+
// rendered (Codex, e.g., has no cost). Returns the line string (or null when the
|
|
6
|
+
// result carries no metric fields at all); 7b posts it to the channel.
|
|
7
|
+
export function buildResultLine(ev) {
|
|
8
|
+
const parts = [];
|
|
9
|
+
if (ev.costUsd !== undefined) {
|
|
10
|
+
parts.push(`${t('result.cost')} $${ev.costUsd.toFixed(4)}`);
|
|
11
|
+
}
|
|
12
|
+
const hasTokens = ev.tokensIn !== undefined || ev.tokensOut !== undefined;
|
|
13
|
+
if (hasTokens) {
|
|
14
|
+
const inTok = ev.tokensIn !== undefined ? `${formatTokens(ev.tokensIn)}↓` : '';
|
|
15
|
+
const outTok = ev.tokensOut !== undefined ? `${formatTokens(ev.tokensOut)}↑` : '';
|
|
16
|
+
parts.push(`${t('result.tokens')} ${[inTok, outTok].filter(Boolean).join(' ')}`.trim());
|
|
17
|
+
}
|
|
18
|
+
if (ev.durationMs !== undefined) {
|
|
19
|
+
parts.push(`${t('result.duration')} ${formatDuration(ev.durationMs)}`);
|
|
20
|
+
}
|
|
21
|
+
if (parts.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
return `${t('result.done')} · ${parts.join(' · ')}`;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=resultLine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resultLine.js","sourceRoot":"","sources":["../../../src/discord/renderers/resultLine.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAE/B,iFAAiF;AACjF,mFAAmF;AACnF,iFAAiF;AACjF,uEAAuE;AAEvE,MAAM,UAAU,eAAe,CAAC,EAA2C;IACzE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,EAAE,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,SAAS,GAAG,EAAE,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,CAAC,SAAS,KAAK,SAAS,CAAC;IAC1E,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,MAAM,MAAM,GAAG,EAAE,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,IAAI,EAAE,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAC,IAAI,cAAc,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,GAAG,CAAC,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { COLORS } from '../format.js';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
export function buildStatusEmbed(status) {
|
|
4
|
+
const fields = [
|
|
5
|
+
{ name: t('status.mode'), value: status.mode, inline: true },
|
|
6
|
+
{ name: t('status.permMode'), value: t(`perm.${status.permMode}`), inline: true },
|
|
7
|
+
{ name: t('status.cwd'), value: '`' + status.cwd + '`' },
|
|
8
|
+
{ name: t('status.session'), value: '`' + (status.sessionId ?? '—') + '`' },
|
|
9
|
+
];
|
|
10
|
+
const embed = {
|
|
11
|
+
title: t('status.title'),
|
|
12
|
+
color: COLORS.idle,
|
|
13
|
+
fields,
|
|
14
|
+
};
|
|
15
|
+
if (!status.usagePanel) {
|
|
16
|
+
embed.footer = t('status.usage.codex');
|
|
17
|
+
}
|
|
18
|
+
return embed;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=statusEmbed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statusEmbed.js","sourceRoot":"","sources":["../../../src/discord/renderers/statusEmbed.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAiB/B,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,MAAM,MAAM,GAAwD;QAClE,EAAE,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAC5D,EAAE,IAAI,EAAE,CAAC,CAAC,iBAAiB,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE;QACjF,EAAE,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,GAAG,GAAG,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE;QACxD,EAAE,IAAI,EAAE,CAAC,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC,GAAG,GAAG,EAAE;KAC5E,CAAC;IACF,MAAM,KAAK,GAAc;QACvB,KAAK,EAAE,CAAC,CAAC,cAAc,CAAC;QACxB,KAAK,EAAE,MAAM,CAAC,IAAI;QAClB,MAAM;KACP,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACvB,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,oBAAoB,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildStatusEmbed } from './statusEmbed.js';
|
|
3
|
+
describe('buildStatusEmbed', () => {
|
|
4
|
+
it('renders mode, permission mode, cwd, and session id', () => {
|
|
5
|
+
const embed = buildStatusEmbed({
|
|
6
|
+
mode: 'claude',
|
|
7
|
+
cwd: '/ws',
|
|
8
|
+
sessionId: 'sess-1',
|
|
9
|
+
permMode: 'default',
|
|
10
|
+
usagePanel: true,
|
|
11
|
+
});
|
|
12
|
+
const values = (embed.fields ?? []).map((f) => f.value);
|
|
13
|
+
expect(values).toContain('claude');
|
|
14
|
+
expect(values).toContain('`/ws`');
|
|
15
|
+
expect(values).toContain('`sess-1`');
|
|
16
|
+
// Claude (usagePanel true) → no "unavailable" footer.
|
|
17
|
+
expect(embed.footer).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
it('shows the Codex "usage/limits unavailable" line when usagePanel is false', () => {
|
|
20
|
+
const embed = buildStatusEmbed({
|
|
21
|
+
mode: 'codex',
|
|
22
|
+
cwd: '/ws',
|
|
23
|
+
sessionId: null,
|
|
24
|
+
permMode: 'plan',
|
|
25
|
+
usagePanel: false,
|
|
26
|
+
});
|
|
27
|
+
expect(embed.footer).toBe('사용량/한도 정보 없음 (Codex CLI 제한)');
|
|
28
|
+
// A null session id renders as a dash, not "null".
|
|
29
|
+
expect((embed.fields ?? []).some((f) => f.value === '`—`')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=statusEmbed.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statusEmbed.test.js","sourceRoot":"","sources":["../../../src/discord/renderers/statusEmbed.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,KAAK,GAAG,gBAAgB,CAAC;YAC7B,IAAI,EAAE,QAAQ;YACd,GAAG,EAAE,KAAK;YACV,SAAS,EAAE,QAAQ;YACnB,QAAQ,EAAE,SAAS;YACnB,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACxD,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACrC,sDAAsD;QACtD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,aAAa,EAAE,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,KAAK,GAAG,gBAAgB,CAAC;YAC7B,IAAI,EAAE,OAAO;YACb,GAAG,EAAE,KAAK;YACV,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,MAAM;YAChB,UAAU,EAAE,KAAK;SAClB,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QACzD,mDAAmD;QACnD,MAAM,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { COLORS, EMBED_DESC_LIMIT, chunkMessage, truncate } from '../format.js';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
// Live text/thinking embeds, debounced edit then finalize to chunked text (§6).
|
|
4
|
+
// Ports A4D StreamHandler behavior: accumulate deltas into a buffer, edit a single
|
|
5
|
+
// "Responding…"/"Thinking…" embed on a debounce, and on finalize replace it with
|
|
6
|
+
// the plain chunked text (text) or a collapsed "Thought for Ns" embed (thinking).
|
|
7
|
+
//
|
|
8
|
+
// Injectable timer + clock so tests advance time deterministically without real
|
|
9
|
+
// setTimeout. No discord.js: the sink is the MessageChannel port. One handler
|
|
10
|
+
// instance per (channel, kind); the dispatcher owns their lifecycle.
|
|
11
|
+
// Debounce defaults (A4D: 1s text, 2s thinking).
|
|
12
|
+
const TEXT_DEBOUNCE_MS = 1000;
|
|
13
|
+
const THINKING_DEBOUNCE_MS = 2000;
|
|
14
|
+
export class StreamEmbedHandler {
|
|
15
|
+
channel;
|
|
16
|
+
kind;
|
|
17
|
+
debounceMs;
|
|
18
|
+
setTimer;
|
|
19
|
+
clearTimer;
|
|
20
|
+
now;
|
|
21
|
+
buffer = '';
|
|
22
|
+
deltaCount = 0;
|
|
23
|
+
startedAt = null;
|
|
24
|
+
timer = null;
|
|
25
|
+
message = null;
|
|
26
|
+
// Serializes edits so a flush never overtakes a still-in-flight previous edit.
|
|
27
|
+
inflight = Promise.resolve();
|
|
28
|
+
finalized = false;
|
|
29
|
+
constructor(deps) {
|
|
30
|
+
this.channel = deps.channel;
|
|
31
|
+
this.kind = deps.kind;
|
|
32
|
+
this.debounceMs =
|
|
33
|
+
deps.debounceMs ?? (deps.kind === 'thinking' ? THINKING_DEBOUNCE_MS : TEXT_DEBOUNCE_MS);
|
|
34
|
+
this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
35
|
+
this.clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
|
|
36
|
+
this.now = deps.now ?? Date.now;
|
|
37
|
+
}
|
|
38
|
+
// Accumulate one delta and (re)arm the debounce timer. delta:false is treated as
|
|
39
|
+
// a full-text push (still buffered, then finalized by the dispatcher on result).
|
|
40
|
+
push(ev) {
|
|
41
|
+
if (this.finalized)
|
|
42
|
+
return;
|
|
43
|
+
if (this.startedAt === null)
|
|
44
|
+
this.startedAt = this.now();
|
|
45
|
+
this.buffer += ev.text;
|
|
46
|
+
this.deltaCount += 1;
|
|
47
|
+
this.arm();
|
|
48
|
+
}
|
|
49
|
+
arm() {
|
|
50
|
+
if (this.timer !== null)
|
|
51
|
+
this.clearTimer(this.timer);
|
|
52
|
+
this.timer = this.setTimer(() => {
|
|
53
|
+
this.timer = null;
|
|
54
|
+
void this.flush();
|
|
55
|
+
}, this.debounceMs);
|
|
56
|
+
}
|
|
57
|
+
// Upsert the live embed with the current buffer preview.
|
|
58
|
+
flush() {
|
|
59
|
+
this.inflight = this.inflight.then(async () => {
|
|
60
|
+
if (this.finalized || this.buffer.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
const desc = truncate(this.buffer, EMBED_DESC_LIMIT);
|
|
63
|
+
const title = this.kind === 'thinking' ? t('stream.thinking') : t('stream.responding');
|
|
64
|
+
const color = this.kind === 'thinking' ? COLORS.thinking : COLORS.streaming;
|
|
65
|
+
const embed = { title, description: desc, color, footer: this.footer() };
|
|
66
|
+
if (this.message) {
|
|
67
|
+
await this.message.edit({ embeds: [embed] });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.message = await this.channel.send({ embeds: [embed] });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return this.inflight;
|
|
74
|
+
}
|
|
75
|
+
// Finalize the stream: cancel the timer, then for text replace the live embed
|
|
76
|
+
// with the plain chunked message(s); for thinking collapse to a "Thought for Ns"
|
|
77
|
+
// embed. Idempotent — a second call is a no-op.
|
|
78
|
+
async finalize() {
|
|
79
|
+
if (this.finalized)
|
|
80
|
+
return;
|
|
81
|
+
this.finalized = true;
|
|
82
|
+
if (this.timer !== null) {
|
|
83
|
+
this.clearTimer(this.timer);
|
|
84
|
+
this.timer = null;
|
|
85
|
+
}
|
|
86
|
+
await this.inflight;
|
|
87
|
+
if (this.buffer.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
if (this.kind === 'thinking') {
|
|
90
|
+
const sec = this.elapsedSec();
|
|
91
|
+
const embed = { title: t('stream.thought', { sec }), color: COLORS.thinking };
|
|
92
|
+
if (this.message)
|
|
93
|
+
await this.message.edit({ embeds: [embed] });
|
|
94
|
+
else
|
|
95
|
+
await this.channel.send({ embeds: [embed] });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Text: post the full answer as plain chunked message(s). Edit the first chunk
|
|
99
|
+
// into the live embed's message (replacing the streaming embed) and send the
|
|
100
|
+
// remainder as follow-ups.
|
|
101
|
+
const chunks = chunkMessage(this.buffer);
|
|
102
|
+
const [first, ...rest] = chunks;
|
|
103
|
+
if (this.message) {
|
|
104
|
+
await this.message.edit({ content: first, embeds: [] });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
await this.channel.send({ content: first });
|
|
108
|
+
}
|
|
109
|
+
for (const chunk of rest) {
|
|
110
|
+
await this.channel.send({ content: chunk });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Cancel the debounce timer WITHOUT flushing/finalizing and mark the handler
|
|
114
|
+
// finalized so no further push/flush can fire a late send/edit. Used when a
|
|
115
|
+
// channel is detached mid-stream (e.g. /stop): the live "Responding…" embed must
|
|
116
|
+
// not receive a further edit and no orphan message must be posted. Idempotent.
|
|
117
|
+
cancel() {
|
|
118
|
+
if (this.timer !== null) {
|
|
119
|
+
this.clearTimer(this.timer);
|
|
120
|
+
this.timer = null;
|
|
121
|
+
}
|
|
122
|
+
this.finalized = true;
|
|
123
|
+
}
|
|
124
|
+
footer() {
|
|
125
|
+
const sec = this.elapsedSec();
|
|
126
|
+
return `${sec}s · ${this.deltaCount}`;
|
|
127
|
+
}
|
|
128
|
+
elapsedSec() {
|
|
129
|
+
if (this.startedAt === null)
|
|
130
|
+
return '0.0';
|
|
131
|
+
return ((this.now() - this.startedAt) / 1000).toFixed(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=streamEmbed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamEmbed.js","sourceRoot":"","sources":["../../../src/discord/renderers/streamEmbed.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { StreamEmbedHandler } from './streamEmbed.js';
|
|
3
|
+
// A fake channel + a manual timer: the handler's debounce is driven by firing the
|
|
4
|
+
// captured timer callback explicitly, so no real time passes.
|
|
5
|
+
function harness() {
|
|
6
|
+
const sent = [];
|
|
7
|
+
const edits = [];
|
|
8
|
+
let seq = 0;
|
|
9
|
+
const channel = {
|
|
10
|
+
async send(message) {
|
|
11
|
+
sent.push(message);
|
|
12
|
+
const id = `m${++seq}`;
|
|
13
|
+
const em = {
|
|
14
|
+
id,
|
|
15
|
+
async edit(m) {
|
|
16
|
+
edits.push({ id, msg: m });
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
return em;
|
|
20
|
+
},
|
|
21
|
+
async startThread() {
|
|
22
|
+
throw new Error('not used');
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
let pending = null;
|
|
26
|
+
const setTimer = (fn) => {
|
|
27
|
+
pending = fn;
|
|
28
|
+
return 1;
|
|
29
|
+
};
|
|
30
|
+
const clearTimer = () => {
|
|
31
|
+
pending = null;
|
|
32
|
+
};
|
|
33
|
+
const fire = async () => {
|
|
34
|
+
const fn = pending;
|
|
35
|
+
pending = null;
|
|
36
|
+
fn?.();
|
|
37
|
+
await new Promise((r) => setImmediate(r));
|
|
38
|
+
};
|
|
39
|
+
return { channel, sent, edits, setTimer, clearTimer, fire };
|
|
40
|
+
}
|
|
41
|
+
describe('StreamEmbedHandler (text)', () => {
|
|
42
|
+
it('debounces: one embed sent on flush, edited on subsequent flush', async () => {
|
|
43
|
+
const h = harness();
|
|
44
|
+
const s = new StreamEmbedHandler({ channel: h.channel, kind: 'text', setTimer: h.setTimer, clearTimer: h.clearTimer });
|
|
45
|
+
s.push({ kind: 'text', text: 'Hel', delta: true });
|
|
46
|
+
s.push({ kind: 'text', text: 'lo', delta: true });
|
|
47
|
+
await h.fire();
|
|
48
|
+
expect(h.sent).toHaveLength(1);
|
|
49
|
+
expect(h.sent[0].embeds?.[0].description).toBe('Hello');
|
|
50
|
+
s.push({ kind: 'text', text: ' world', delta: true });
|
|
51
|
+
await h.fire();
|
|
52
|
+
expect(h.sent).toHaveLength(1); // still one message
|
|
53
|
+
expect(h.edits.at(-1)?.msg.embeds?.[0].description).toBe('Hello world');
|
|
54
|
+
});
|
|
55
|
+
it('finalize replaces the streaming embed with plain text content', async () => {
|
|
56
|
+
const h = harness();
|
|
57
|
+
const s = new StreamEmbedHandler({ channel: h.channel, kind: 'text', setTimer: h.setTimer, clearTimer: h.clearTimer });
|
|
58
|
+
s.push({ kind: 'text', text: 'answer', delta: true });
|
|
59
|
+
await h.fire();
|
|
60
|
+
await s.finalize();
|
|
61
|
+
const finalEdit = h.edits.at(-1);
|
|
62
|
+
expect(finalEdit?.msg.content).toBe('answer');
|
|
63
|
+
expect(finalEdit?.msg.embeds).toEqual([]); // embed cleared
|
|
64
|
+
});
|
|
65
|
+
it('finalize with no prior flush sends plain content directly', async () => {
|
|
66
|
+
const h = harness();
|
|
67
|
+
const s = new StreamEmbedHandler({ channel: h.channel, kind: 'text', setTimer: h.setTimer, clearTimer: h.clearTimer });
|
|
68
|
+
s.push({ kind: 'text', text: 'quick', delta: true });
|
|
69
|
+
await s.finalize();
|
|
70
|
+
expect(h.sent).toHaveLength(1);
|
|
71
|
+
expect(h.sent[0].content).toBe('quick');
|
|
72
|
+
});
|
|
73
|
+
it('is a no-op with no buffered text', async () => {
|
|
74
|
+
const h = harness();
|
|
75
|
+
const s = new StreamEmbedHandler({ channel: h.channel, kind: 'text', setTimer: h.setTimer, clearTimer: h.clearTimer });
|
|
76
|
+
await s.finalize();
|
|
77
|
+
expect(h.sent).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('StreamEmbedHandler (thinking)', () => {
|
|
81
|
+
it('finalize collapses to a "thought for" embed', async () => {
|
|
82
|
+
const h = harness();
|
|
83
|
+
const s = new StreamEmbedHandler({
|
|
84
|
+
channel: h.channel,
|
|
85
|
+
kind: 'thinking',
|
|
86
|
+
setTimer: h.setTimer,
|
|
87
|
+
clearTimer: h.clearTimer,
|
|
88
|
+
now: () => 1000,
|
|
89
|
+
});
|
|
90
|
+
s.push({ kind: 'thinking', text: 'hmm', delta: true });
|
|
91
|
+
await h.fire();
|
|
92
|
+
await s.finalize();
|
|
93
|
+
const finalEdit = h.edits.at(-1);
|
|
94
|
+
expect(finalEdit?.msg.embeds?.[0].title).toContain('생각함');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=streamEmbed.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamEmbed.test.js","sourceRoot":"","sources":["../../../src/discord/renderers/streamEmbed.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGtD,kFAAkF;AAClF,8DAA8D;AAC9D,SAAS,OAAO;IACd,MAAM,IAAI,GAAsB,EAAE,CAAC;IACnC,MAAM,KAAK,GAA2C,EAAE,CAAC;IACzD,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,MAAM,OAAO,GAAmB;QAC9B,KAAK,CAAC,IAAI,CAAC,OAAO;YAChB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACnB,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,CAAC;YACvB,MAAM,EAAE,GAAoB;gBAC1B,EAAE;gBACF,KAAK,CAAC,IAAI,CAAC,CAAC;oBACV,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;gBAC7B,CAAC;aACF,CAAC;YACF,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,KAAK,CAAC,WAAW;YACf,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC;QAC9B,CAAC;KACF,CAAC;IACF,IAAI,OAAO,GAAwB,IAAI,CAAC;IACxC,MAAM,QAAQ,GAAG,CAAC,EAAc,EAAE,EAAE;QAClC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,MAAM,UAAU,GAAG,GAAG,EAAE;QACtB,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC,CAAC;IACF,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;QACtB,MAAM,EAAE,GAAG,OAAO,CAAC;QACnB,OAAO,GAAG,IAAI,CAAC;QACf,EAAE,EAAE,EAAE,CAAC;QACP,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC;IACF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AAC9D,CAAC;AAED,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;QACvH,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAExD,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,oBAAoB;QACpD,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;QAC7E,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;QACvH,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;QACnB,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,gBAAgB;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;QACvH,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;QACnB,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;QAChD,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,IAAI,kBAAkB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;QACvH,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;QACnB,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC7C,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,IAAI,kBAAkB,CAAC;YAC/B,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,UAAU,EAAE,CAAC,CAAC,UAAU;YACxB,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI;SAChB,CAAC,CAAC;QACH,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;QACnB,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { chunkMessage, toolThreadName } from '../format.js';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
export class ToolThreadHandler {
|
|
4
|
+
channel;
|
|
5
|
+
// toolUseId → its thread, once opened.
|
|
6
|
+
threads = new Map();
|
|
7
|
+
// toolUseId → results that arrived before the thread was registered.
|
|
8
|
+
pending = new Map();
|
|
9
|
+
constructor(deps) {
|
|
10
|
+
this.channel = deps.channel;
|
|
11
|
+
}
|
|
12
|
+
async handle(ev) {
|
|
13
|
+
if (ev.kind === 'tool_use')
|
|
14
|
+
return this.onToolUse(ev);
|
|
15
|
+
return this.onToolResult(ev);
|
|
16
|
+
}
|
|
17
|
+
async onToolUse(ev) {
|
|
18
|
+
const thread = await this.channel.startThread(toolThreadName(ev.name, ev.input));
|
|
19
|
+
this.threads.set(ev.id, thread);
|
|
20
|
+
// Post the input as the thread's opening message.
|
|
21
|
+
for (const chunk of chunkMessage(formatInput(ev.input))) {
|
|
22
|
+
await thread.send({ content: chunk });
|
|
23
|
+
}
|
|
24
|
+
// Flush any results that raced ahead of the thread.
|
|
25
|
+
const buffered = this.pending.get(ev.id);
|
|
26
|
+
if (buffered) {
|
|
27
|
+
this.pending.delete(ev.id);
|
|
28
|
+
for (const r of buffered)
|
|
29
|
+
await this.postResult(thread, r);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async onToolResult(ev) {
|
|
33
|
+
const thread = this.threads.get(ev.id);
|
|
34
|
+
const result = { ok: ev.ok, content: ev.content };
|
|
35
|
+
if (thread) {
|
|
36
|
+
await this.postResult(thread, result);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Thread not open yet — buffer until onToolUse registers it.
|
|
40
|
+
const list = this.pending.get(ev.id) ?? [];
|
|
41
|
+
list.push(result);
|
|
42
|
+
this.pending.set(ev.id, list);
|
|
43
|
+
}
|
|
44
|
+
async postResult(thread, r) {
|
|
45
|
+
const header = r.ok ? `**${t('tool.result')}**` : `**${t('tool.error')}**`;
|
|
46
|
+
const body = `${header}\n${r.content}`;
|
|
47
|
+
for (const chunk of chunkMessage(body)) {
|
|
48
|
+
await thread.send({ content: chunk });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Format tool input for the thread's opening message. Plain JSON pretty-print keeps
|
|
53
|
+
// this mode-agnostic; per-tool prettifying (diff blocks, code fences) is the
|
|
54
|
+
// diffView renderer's job, not this one.
|
|
55
|
+
function formatInput(input) {
|
|
56
|
+
if (typeof input === 'string')
|
|
57
|
+
return input;
|
|
58
|
+
try {
|
|
59
|
+
return '```json\n' + JSON.stringify(input, null, 2) + '\n```';
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return String(input);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=toolThread.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolThread.js","sourceRoot":"","sources":["../../../src/discord/renderers/toolThread.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ToolThreadHandler } from './toolThread.js';
|
|
3
|
+
const ev = (e) => e;
|
|
4
|
+
function fakeChannel() {
|
|
5
|
+
const threadPosts = {};
|
|
6
|
+
const threadNames = [];
|
|
7
|
+
let seq = 0;
|
|
8
|
+
const channel = {
|
|
9
|
+
async send() {
|
|
10
|
+
return { id: `m${++seq}`, async edit() { } };
|
|
11
|
+
},
|
|
12
|
+
async startThread(name) {
|
|
13
|
+
threadNames.push(name);
|
|
14
|
+
const id = `t${++seq}`;
|
|
15
|
+
threadPosts[id] = [];
|
|
16
|
+
const thread = {
|
|
17
|
+
id,
|
|
18
|
+
async send(message) {
|
|
19
|
+
threadPosts[id].push(message);
|
|
20
|
+
return { id: `tm${++seq}`, async edit() { } };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
return thread;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return { channel, threadPosts, threadNames };
|
|
27
|
+
}
|
|
28
|
+
const flush = () => new Promise((r) => setImmediate(r));
|
|
29
|
+
describe('ToolThreadHandler', () => {
|
|
30
|
+
it('opens a thread named from tool+input and posts the result back', async () => {
|
|
31
|
+
const { channel, threadPosts, threadNames } = fakeChannel();
|
|
32
|
+
const h = new ToolThreadHandler({ channel });
|
|
33
|
+
await h.handle(ev({ kind: 'tool_use', id: 't1', name: 'Bash', input: { command: 'ls -la' } }));
|
|
34
|
+
await h.handle(ev({ kind: 'tool_result', id: 't1', ok: true, content: 'output' }));
|
|
35
|
+
expect(threadNames[0]).toBe('Bash: ls -la');
|
|
36
|
+
const posts = Object.values(threadPosts)[0].map((m) => m.content ?? '');
|
|
37
|
+
expect(posts.some((c) => c.includes('결과'))).toBe(true);
|
|
38
|
+
expect(posts.some((c) => c.includes('output'))).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it('buffers a result that arrives before its thread and flushes on open', async () => {
|
|
41
|
+
const { channel, threadPosts } = fakeChannel();
|
|
42
|
+
const h = new ToolThreadHandler({ channel });
|
|
43
|
+
// Result first (out of order).
|
|
44
|
+
await h.handle(ev({ kind: 'tool_result', id: 't9', ok: true, content: 'early' }));
|
|
45
|
+
// No thread yet → no posts.
|
|
46
|
+
expect(Object.keys(threadPosts)).toHaveLength(0);
|
|
47
|
+
// Now the tool_use opens the thread and the buffered result is flushed.
|
|
48
|
+
await h.handle(ev({ kind: 'tool_use', id: 't9', name: 'Read', input: { file_path: '/ws/x' } }));
|
|
49
|
+
await flush();
|
|
50
|
+
const posts = Object.values(threadPosts)[0].map((m) => m.content ?? '');
|
|
51
|
+
expect(posts.some((c) => c.includes('early'))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=toolThread.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolThread.test.js","sourceRoot":"","sources":["../../../src/discord/renderers/toolThread.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { chunkMessage } from '../format.js';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
export class TranscriptFeedHandler {
|
|
4
|
+
channel;
|
|
5
|
+
// The single status message edited in place as progress advances.
|
|
6
|
+
statusMessage = null;
|
|
7
|
+
constructor(deps) {
|
|
8
|
+
this.channel = deps.channel;
|
|
9
|
+
}
|
|
10
|
+
async handle(ev) {
|
|
11
|
+
if (ev.kind === 'progress')
|
|
12
|
+
return this.onProgress(ev);
|
|
13
|
+
return this.onResult(ev);
|
|
14
|
+
}
|
|
15
|
+
async onProgress(ev) {
|
|
16
|
+
const label = ev.label || t('transcript.working');
|
|
17
|
+
const line = ev.detail ? `${label} — ${ev.detail}` : label;
|
|
18
|
+
if (this.statusMessage) {
|
|
19
|
+
await this.statusMessage.edit({ content: line });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
this.statusMessage = await this.channel.send({ content: line });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async onResult(ev) {
|
|
26
|
+
if (!ev.text)
|
|
27
|
+
return;
|
|
28
|
+
for (const chunk of chunkMessage(ev.text)) {
|
|
29
|
+
await this.channel.send({ content: chunk });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=transcriptFeed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcriptFeed.js","sourceRoot":"","sources":["../../../src/discord/renderers/transcriptFeed.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAa/B,MAAM,OAAO,qBAAqB;IACf,OAAO,CAAiB;IACzC,kEAAkE;IAC1D,aAAa,GAA2B,IAAI,CAAC;IAErD,YAAY,IAAwB;QAClC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAwD;QACnE,IAAI,EAAE,CAAC,IAAI,KAAK,UAAU;YAAE,OAAO,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QACvD,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,EAA6C;QACpE,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC,oBAAoB,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAC3D,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,aAAa,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,QAAQ,CAAC,EAA2C;QAChE,IAAI,CAAC,EAAE,CAAC,IAAI;YAAE,OAAO;QACrB,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;CACF"}
|