@orthacms/copilot-server 0.0.0-reserve.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 +7 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/lib/chat/application/appliers-registrar.d.ts +16 -0
- package/dist/lib/chat/application/appliers-registrar.d.ts.map +1 -0
- package/dist/lib/chat/application/appliers-registrar.js +50 -0
- package/dist/lib/chat/application/capability-profile.service.d.ts +43 -0
- package/dist/lib/chat/application/capability-profile.service.d.ts.map +1 -0
- package/dist/lib/chat/application/capability-profile.service.js +75 -0
- package/dist/lib/chat/application/content-type-summary.service.d.ts +32 -0
- package/dist/lib/chat/application/content-type-summary.service.d.ts.map +1 -0
- package/dist/lib/chat/application/content-type-summary.service.js +64 -0
- package/dist/lib/chat/application/decide-proposal.service.d.ts +80 -0
- package/dist/lib/chat/application/decide-proposal.service.d.ts.map +1 -0
- package/dist/lib/chat/application/decide-proposal.service.js +112 -0
- package/dist/lib/chat/application/dto/create-run.dto.d.ts +138 -0
- package/dist/lib/chat/application/dto/create-run.dto.d.ts.map +1 -0
- package/dist/lib/chat/application/dto/create-run.dto.js +253 -0
- package/dist/lib/chat/application/dto/decide-tool-permission.dto.d.ts +20 -0
- package/dist/lib/chat/application/dto/decide-tool-permission.dto.d.ts.map +1 -0
- package/dist/lib/chat/application/dto/decide-tool-permission.dto.js +44 -0
- package/dist/lib/chat/application/dto/extend-tool-permission.dto.d.ts +18 -0
- package/dist/lib/chat/application/dto/extend-tool-permission.dto.d.ts.map +1 -0
- package/dist/lib/chat/application/dto/extend-tool-permission.dto.js +33 -0
- package/dist/lib/chat/application/dto/list-conversations-query.dto.d.ts +18 -0
- package/dist/lib/chat/application/dto/list-conversations-query.dto.d.ts.map +1 -0
- package/dist/lib/chat/application/dto/list-conversations-query.dto.js +39 -0
- package/dist/lib/chat/application/dto/list-proposals-query.dto.d.ts +15 -0
- package/dist/lib/chat/application/dto/list-proposals-query.dto.d.ts.map +1 -0
- package/dist/lib/chat/application/dto/list-proposals-query.dto.js +41 -0
- package/dist/lib/chat/application/dto/update-conversation.dto.d.ts +68 -0
- package/dist/lib/chat/application/dto/update-conversation.dto.d.ts.map +1 -0
- package/dist/lib/chat/application/dto/update-conversation.dto.js +115 -0
- package/dist/lib/chat/application/proposal-applier.registry.d.ts +27 -0
- package/dist/lib/chat/application/proposal-applier.registry.d.ts.map +1 -0
- package/dist/lib/chat/application/proposal-applier.registry.js +62 -0
- package/dist/lib/chat/application/run-engine.service.d.ts +243 -0
- package/dist/lib/chat/application/run-engine.service.d.ts.map +1 -0
- package/dist/lib/chat/application/run-engine.service.js +1058 -0
- package/dist/lib/chat/application/summarize-tool-output.d.ts +15 -0
- package/dist/lib/chat/application/summarize-tool-output.d.ts.map +1 -0
- package/dist/lib/chat/application/summarize-tool-output.js +61 -0
- package/dist/lib/chat/application/system-prompt.d.ts +81 -0
- package/dist/lib/chat/application/system-prompt.d.ts.map +1 -0
- package/dist/lib/chat/application/system-prompt.js +477 -0
- package/dist/lib/chat/application/tool-permission.broker.d.ts +118 -0
- package/dist/lib/chat/application/tool-permission.broker.d.ts.map +1 -0
- package/dist/lib/chat/application/tool-permission.broker.js +221 -0
- package/dist/lib/chat/http/controllers/create-run.controller.d.ts +30 -0
- package/dist/lib/chat/http/controllers/create-run.controller.d.ts.map +1 -0
- package/dist/lib/chat/http/controllers/create-run.controller.js +159 -0
- package/dist/lib/chat/http/controllers/get-conversation.controller.d.ts +22 -0
- package/dist/lib/chat/http/controllers/get-conversation.controller.d.ts.map +1 -0
- package/dist/lib/chat/http/controllers/get-conversation.controller.js +50 -0
- package/dist/lib/chat/http/controllers/list-conversations.controller.d.ts +20 -0
- package/dist/lib/chat/http/controllers/list-conversations.controller.d.ts.map +1 -0
- package/dist/lib/chat/http/controllers/list-conversations.controller.js +47 -0
- package/dist/lib/chat/http/controllers/list-models.controller.d.ts +35 -0
- package/dist/lib/chat/http/controllers/list-models.controller.d.ts.map +1 -0
- package/dist/lib/chat/http/controllers/list-models.controller.js +48 -0
- package/dist/lib/chat/http/controllers/proposals.controller.d.ts +43 -0
- package/dist/lib/chat/http/controllers/proposals.controller.d.ts.map +1 -0
- package/dist/lib/chat/http/controllers/proposals.controller.js +98 -0
- package/dist/lib/chat/http/controllers/tool-permission.controller.d.ts +57 -0
- package/dist/lib/chat/http/controllers/tool-permission.controller.d.ts.map +1 -0
- package/dist/lib/chat/http/controllers/tool-permission.controller.js +116 -0
- package/dist/lib/chat/http/controllers/update-conversation.controller.d.ts +37 -0
- package/dist/lib/chat/http/controllers/update-conversation.controller.d.ts.map +1 -0
- package/dist/lib/chat/http/controllers/update-conversation.controller.js +108 -0
- package/dist/lib/chat/http/sse-stream.d.ts +41 -0
- package/dist/lib/chat/http/sse-stream.d.ts.map +1 -0
- package/dist/lib/chat/http/sse-stream.js +95 -0
- package/dist/lib/chat/infrastructure/persistence/conversation.repository.d.ts +159 -0
- package/dist/lib/chat/infrastructure/persistence/conversation.repository.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/persistence/conversation.repository.js +249 -0
- package/dist/lib/chat/infrastructure/persistence/derive-title.d.ts +13 -0
- package/dist/lib/chat/infrastructure/persistence/derive-title.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/persistence/derive-title.js +35 -0
- package/dist/lib/chat/infrastructure/persistence/proposal.repository.d.ts +86 -0
- package/dist/lib/chat/infrastructure/persistence/proposal.repository.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/persistence/proposal.repository.js +152 -0
- package/dist/lib/chat/infrastructure/schema/conversations.d.ts +189 -0
- package/dist/lib/chat/infrastructure/schema/conversations.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/schema/conversations.js +100 -0
- package/dist/lib/chat/infrastructure/schema/external-refs.d.ts +67 -0
- package/dist/lib/chat/infrastructure/schema/external-refs.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/schema/external-refs.js +28 -0
- package/dist/lib/chat/infrastructure/schema/index.d.ts +5 -0
- package/dist/lib/chat/infrastructure/schema/index.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/schema/index.js +12 -0
- package/dist/lib/chat/infrastructure/schema/messages.d.ts +264 -0
- package/dist/lib/chat/infrastructure/schema/messages.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/schema/messages.js +96 -0
- package/dist/lib/chat/infrastructure/schema/proposals.d.ts +328 -0
- package/dist/lib/chat/infrastructure/schema/proposals.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/schema/proposals.js +87 -0
- package/dist/lib/chat/infrastructure/schema/tool-calls.d.ts +203 -0
- package/dist/lib/chat/infrastructure/schema/tool-calls.d.ts.map +1 -0
- package/dist/lib/chat/infrastructure/schema/tool-calls.js +50 -0
- package/dist/lib/copilot.module.d.ts +30 -0
- package/dist/lib/copilot.module.d.ts.map +1 -0
- package/dist/lib/copilot.module.js +146 -0
- package/dist/lib/copilot.tokens.d.ts +34 -0
- package/dist/lib/copilot.tokens.d.ts.map +1 -0
- package/dist/lib/copilot.tokens.js +38 -0
- package/dist/lib/infrastructure/model-registry.d.ts +22 -0
- package/dist/lib/infrastructure/model-registry.d.ts.map +1 -0
- package/dist/lib/infrastructure/model-registry.js +54 -0
- package/dist/lib/skills/application/dto/create-skill.dto.d.ts +36 -0
- package/dist/lib/skills/application/dto/create-skill.dto.d.ts.map +1 -0
- package/dist/lib/skills/application/dto/create-skill.dto.js +106 -0
- package/dist/lib/skills/application/dto/update-skill.dto.d.ts +28 -0
- package/dist/lib/skills/application/dto/update-skill.dto.d.ts.map +1 -0
- package/dist/lib/skills/application/dto/update-skill.dto.js +96 -0
- package/dist/lib/skills/application/skill-catalog.service.d.ts +97 -0
- package/dist/lib/skills/application/skill-catalog.service.d.ts.map +1 -0
- package/dist/lib/skills/application/skill-catalog.service.js +157 -0
- package/dist/lib/skills/http/controllers/list-skills.controller.d.ts +19 -0
- package/dist/lib/skills/http/controllers/list-skills.controller.d.ts.map +1 -0
- package/dist/lib/skills/http/controllers/list-skills.controller.js +50 -0
- package/dist/lib/skills/http/controllers/manage-skills.controller.d.ts +64 -0
- package/dist/lib/skills/http/controllers/manage-skills.controller.d.ts.map +1 -0
- package/dist/lib/skills/http/controllers/manage-skills.controller.js +182 -0
- package/dist/lib/skills/infrastructure/persistence/skill.repository.d.ts +78 -0
- package/dist/lib/skills/infrastructure/persistence/skill.repository.d.ts.map +1 -0
- package/dist/lib/skills/infrastructure/persistence/skill.repository.js +116 -0
- package/dist/lib/skills/infrastructure/schema/index.d.ts +2 -0
- package/dist/lib/skills/infrastructure/schema/index.d.ts.map +1 -0
- package/dist/lib/skills/infrastructure/schema/index.js +6 -0
- package/dist/lib/skills/infrastructure/schema/skills.d.ts +212 -0
- package/dist/lib/skills/infrastructure/schema/skills.d.ts.map +1 -0
- package/dist/lib/skills/infrastructure/schema/skills.js +78 -0
- package/dist/lib/types/copilot-config.d.ts +55 -0
- package/dist/lib/types/copilot-config.d.ts.map +1 -0
- package/dist/lib/types/copilot-config.js +2 -0
- package/dist/lib/utils/copilot-plugin.d.ts +66 -0
- package/dist/lib/utils/copilot-plugin.d.ts.map +1 -0
- package/dist/lib/utils/copilot-plugin.js +87 -0
- package/migrations/0000_copilot_chat.sql +48 -0
- package/migrations/0001_proposals.sql +34 -0
- package/migrations/0002_drop_workspace_policies.sql +1 -0
- package/migrations/0003_conversation_allowed_tools.sql +1 -0
- package/migrations/0004_message_attachments.sql +1 -0
- package/migrations/0005_skills.sql +19 -0
- package/migrations/0006_conversation_model_choice.sql +1 -0
- package/migrations/meta/0000_snapshot.json +390 -0
- package/migrations/meta/0001_snapshot.json +657 -0
- package/migrations/meta/0002_snapshot.json +610 -0
- package/migrations/meta/0003_snapshot.json +617 -0
- package/migrations/meta/0004_snapshot.json +623 -0
- package/migrations/meta/0005_snapshot.json +771 -0
- package/migrations/meta/0006_snapshot.json +777 -0
- package/migrations/meta/_journal.json +55 -0
- package/package.json +46 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { CopilotRunEvent } from '@orthacms/copilot-domain';
|
|
3
|
+
/**
|
|
4
|
+
* Writes a run's events to an Express response as Server-Sent Events.
|
|
5
|
+
*
|
|
6
|
+
* **Verified through the admin's Vite dev proxy** (`^/api/` → `:3000`), for
|
|
7
|
+
* both GET and POST: frames arrive one at a time with the same timing as a
|
|
8
|
+
* direct connection, so nothing here is working around a buffering proxy. The
|
|
9
|
+
* headers below still matter for whatever fronts production — `no-transform`
|
|
10
|
+
* stops a compressing proxy from buffering to gzip, and `X-Accel-Buffering: no`
|
|
11
|
+
* is nginx's opt-out.
|
|
12
|
+
*/
|
|
13
|
+
export declare class SseStream {
|
|
14
|
+
private readonly res;
|
|
15
|
+
private heartbeat;
|
|
16
|
+
private closed;
|
|
17
|
+
constructor(res: Response);
|
|
18
|
+
/** Sends the headers and starts the heartbeat. */
|
|
19
|
+
open(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Sends one event. The event's `type` becomes the SSE event name, so a
|
|
22
|
+
* client can listen per kind, and the whole event is the JSON payload.
|
|
23
|
+
*/
|
|
24
|
+
send(event: CopilotRunEvent): void;
|
|
25
|
+
/** Stops the heartbeat and ends the response. Safe to call twice. */
|
|
26
|
+
close(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Aborts `controller` when the client goes away.
|
|
29
|
+
*
|
|
30
|
+
* **This hangs off the *response*, not the request** — a distinction that
|
|
31
|
+
* costs a whole afternoon if you get it wrong. Express has already consumed
|
|
32
|
+
* the request body by the time a handler runs, and a fully-consumed
|
|
33
|
+
* `IncomingMessage` emits `'close'` immediately, while the client is still
|
|
34
|
+
* connected and waiting. Wiring the abort to `req.on('close')` therefore
|
|
35
|
+
* cancels every run the instant it starts — and because `writeHead` hasn't
|
|
36
|
+
* flushed yet, it presents as a request that hangs with no response at all
|
|
37
|
+
* rather than as an error. Verified directly on Node 22.
|
|
38
|
+
*/
|
|
39
|
+
onClientDisconnect(controller: AbortController): void;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=sse-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-stream.d.ts","sourceRoot":"","sources":["../../../../src/lib/chat/http/sse-stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAShE;;;;;;;;;GASG;AACH,qBAAa,SAAS;IAIN,OAAO,CAAC,QAAQ,CAAC,GAAG;IAHhC,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,MAAM,CAAS;gBAEM,GAAG,EAAE,QAAQ;IAE1C,kDAAkD;IAClD,IAAI,IAAI,IAAI;IAsBZ;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAWlC,qEAAqE;IACrE,KAAK,IAAI,IAAI;IAYb;;;;;;;;;;;OAWG;IACH,kBAAkB,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI;CAUxD"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SseStream = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* How often to send a comment frame while nothing else is happening. A model
|
|
6
|
+
* can think for a while before its first token, and an idle connection is
|
|
7
|
+
* exactly what an intermediary decides to reap.
|
|
8
|
+
*/
|
|
9
|
+
const HEARTBEAT_MS = 15_000;
|
|
10
|
+
/**
|
|
11
|
+
* Writes a run's events to an Express response as Server-Sent Events.
|
|
12
|
+
*
|
|
13
|
+
* **Verified through the admin's Vite dev proxy** (`^/api/` → `:3000`), for
|
|
14
|
+
* both GET and POST: frames arrive one at a time with the same timing as a
|
|
15
|
+
* direct connection, so nothing here is working around a buffering proxy. The
|
|
16
|
+
* headers below still matter for whatever fronts production — `no-transform`
|
|
17
|
+
* stops a compressing proxy from buffering to gzip, and `X-Accel-Buffering: no`
|
|
18
|
+
* is nginx's opt-out.
|
|
19
|
+
*/
|
|
20
|
+
class SseStream {
|
|
21
|
+
res;
|
|
22
|
+
heartbeat = null;
|
|
23
|
+
closed = false;
|
|
24
|
+
constructor(res) {
|
|
25
|
+
this.res = res;
|
|
26
|
+
}
|
|
27
|
+
/** Sends the headers and starts the heartbeat. */
|
|
28
|
+
open() {
|
|
29
|
+
this.res.writeHead(200, {
|
|
30
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
31
|
+
// `no-transform` is the load-bearing half: without it a
|
|
32
|
+
// compressing intermediary may buffer the whole body to compress it.
|
|
33
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
34
|
+
Connection: 'keep-alive',
|
|
35
|
+
'X-Accel-Buffering': 'no'
|
|
36
|
+
});
|
|
37
|
+
// `writeHead` alone sends nothing — Node buffers until the first write.
|
|
38
|
+
// Flush a comment now so the client's `fetch` resolves and the UI can
|
|
39
|
+
// render a pending state instead of waiting on the first token.
|
|
40
|
+
this.res.write(': open\n\n');
|
|
41
|
+
this.res.flushHeaders?.();
|
|
42
|
+
this.heartbeat = setInterval(() => {
|
|
43
|
+
if (!this.closed) {
|
|
44
|
+
this.res.write(': ping\n\n');
|
|
45
|
+
}
|
|
46
|
+
}, HEARTBEAT_MS);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Sends one event. The event's `type` becomes the SSE event name, so a
|
|
50
|
+
* client can listen per kind, and the whole event is the JSON payload.
|
|
51
|
+
*/
|
|
52
|
+
send(event) {
|
|
53
|
+
if (this.closed) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// JSON.stringify cannot emit a raw newline inside a string, so a single
|
|
57
|
+
// `data:` line is always well-formed — no need to split the payload.
|
|
58
|
+
this.res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
59
|
+
}
|
|
60
|
+
/** Stops the heartbeat and ends the response. Safe to call twice. */
|
|
61
|
+
close() {
|
|
62
|
+
if (this.closed) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.closed = true;
|
|
66
|
+
if (this.heartbeat) {
|
|
67
|
+
clearInterval(this.heartbeat);
|
|
68
|
+
this.heartbeat = null;
|
|
69
|
+
}
|
|
70
|
+
this.res.end();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Aborts `controller` when the client goes away.
|
|
74
|
+
*
|
|
75
|
+
* **This hangs off the *response*, not the request** — a distinction that
|
|
76
|
+
* costs a whole afternoon if you get it wrong. Express has already consumed
|
|
77
|
+
* the request body by the time a handler runs, and a fully-consumed
|
|
78
|
+
* `IncomingMessage` emits `'close'` immediately, while the client is still
|
|
79
|
+
* connected and waiting. Wiring the abort to `req.on('close')` therefore
|
|
80
|
+
* cancels every run the instant it starts — and because `writeHead` hasn't
|
|
81
|
+
* flushed yet, it presents as a request that hangs with no response at all
|
|
82
|
+
* rather than as an error. Verified directly on Node 22.
|
|
83
|
+
*/
|
|
84
|
+
onClientDisconnect(controller) {
|
|
85
|
+
this.res.on('close', () => {
|
|
86
|
+
this.closed = true;
|
|
87
|
+
if (this.heartbeat) {
|
|
88
|
+
clearInterval(this.heartbeat);
|
|
89
|
+
this.heartbeat = null;
|
|
90
|
+
}
|
|
91
|
+
controller.abort();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.SseStream = SseStream;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { type Database } from '@orthacms/database';
|
|
2
|
+
import type { AttachmentRef, ModelContentBlock, SkillRef } from '@orthacms/copilot-domain';
|
|
3
|
+
/** A thread as the list and the transcript render it. */
|
|
4
|
+
export interface ConversationView {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string | null;
|
|
7
|
+
surface: string;
|
|
8
|
+
archived: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The model last picked for this thread — `null` when nobody has,
|
|
11
|
+
* `'default'` for the host's resolver, else `'<provider>:<model>'`. See the
|
|
12
|
+
* column's own note for why the three states are one nullable text field.
|
|
13
|
+
*/
|
|
14
|
+
modelChoice: string | null;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
}
|
|
18
|
+
/** One persisted turn. */
|
|
19
|
+
export interface MessageView {
|
|
20
|
+
id: string;
|
|
21
|
+
runId: string;
|
|
22
|
+
role: 'user' | 'assistant';
|
|
23
|
+
content: ModelContentBlock[];
|
|
24
|
+
/** Files attached to a user turn; null on an assistant turn and when none. */
|
|
25
|
+
attachments: AttachmentRef[] | null;
|
|
26
|
+
/** Skills in force for a user turn; null on an assistant turn and when none. */
|
|
27
|
+
skills: SkillRef[] | null;
|
|
28
|
+
model: string | null;
|
|
29
|
+
provider: string | null;
|
|
30
|
+
stopReason: string | null;
|
|
31
|
+
position: number;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
}
|
|
34
|
+
/** What {@link ConversationRepository.recordToolCall} writes. */
|
|
35
|
+
export interface ToolCallRecord {
|
|
36
|
+
conversationId: string;
|
|
37
|
+
runId: string;
|
|
38
|
+
callId: string;
|
|
39
|
+
name: string;
|
|
40
|
+
input: unknown;
|
|
41
|
+
outputSummary: string | null;
|
|
42
|
+
ok: boolean;
|
|
43
|
+
error: string | null;
|
|
44
|
+
durationMs: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reads and writes the copilot's transcript.
|
|
48
|
+
*
|
|
49
|
+
* **Every method takes the owning `userId` and `workspaceId` and filters on
|
|
50
|
+
* both.** Not defence in depth for its own sake: `WorkspaceGuard` proves the
|
|
51
|
+
* caller belongs to the workspace they named, but nothing upstream proves a
|
|
52
|
+
* *conversation id* belongs to that user or that workspace — that check has to
|
|
53
|
+
* live here, or a valid session could pull another member's thread by id.
|
|
54
|
+
*/
|
|
55
|
+
export declare class ConversationRepository {
|
|
56
|
+
private readonly db;
|
|
57
|
+
constructor(db: Database);
|
|
58
|
+
/**
|
|
59
|
+
* This user's threads in this workspace, most recently used first.
|
|
60
|
+
*
|
|
61
|
+
* `archived` selects **one of two disjoint sets** rather than widening the
|
|
62
|
+
* result: archiving means "hide this from the list", so an archived thread
|
|
63
|
+
* appearing beside its active siblings would defeat the flag entirely.
|
|
64
|
+
*/
|
|
65
|
+
list(userId: string, workspaceId: string, archived?: boolean): Promise<ConversationView[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Renames a thread, files it away, and/or records the model picked for it,
|
|
68
|
+
* returning the updated row — or `null` when the id is not this user's in
|
|
69
|
+
* this workspace.
|
|
70
|
+
*
|
|
71
|
+
* **`updatedAt` is deliberately not touched.** It means "last used", and the
|
|
72
|
+
* list sorts by it: bumping it here would send a thread you merely renamed
|
|
73
|
+
* to the top of the rail, above conversations you actually had since. That
|
|
74
|
+
* goes double for `modelChoice`, which the client writes as a side effect of
|
|
75
|
+
* touching a picker — a thread must not climb the rail because someone
|
|
76
|
+
* looked at its model.
|
|
77
|
+
*
|
|
78
|
+
* The ownership predicate is part of the `UPDATE` rather than a read
|
|
79
|
+
* beforehand, so there is no window between checking and writing — and a
|
|
80
|
+
* miss is reported as `null` for "not yours" and "no such id" alike, so an
|
|
81
|
+
* id cannot be probed for existence.
|
|
82
|
+
*/
|
|
83
|
+
update(conversationId: string, userId: string, workspaceId: string, patch: {
|
|
84
|
+
title?: string;
|
|
85
|
+
archived?: boolean;
|
|
86
|
+
modelChoice?: string;
|
|
87
|
+
}): Promise<ConversationView | null>;
|
|
88
|
+
/**
|
|
89
|
+
* A thread the caller owns, or `null`. Returns null rather than throwing
|
|
90
|
+
* for "not yours" as well as "no such id" — the two are indistinguishable
|
|
91
|
+
* to a caller, so an id can't be probed for existence.
|
|
92
|
+
*/
|
|
93
|
+
find(conversationId: string, userId: string, workspaceId: string): Promise<ConversationView | null>;
|
|
94
|
+
/** Like {@link find}, but 404s — for routes that need the thread present. */
|
|
95
|
+
findOrFail(conversationId: string, userId: string, workspaceId: string): Promise<ConversationView>;
|
|
96
|
+
/**
|
|
97
|
+
* Tool names this thread has been told to stop asking about.
|
|
98
|
+
*
|
|
99
|
+
* Read **per run**, never cached, for the same reason the capability
|
|
100
|
+
* profile is: the list can grow mid-thread (the user answers "allow for
|
|
101
|
+
* this chat" on turn three), and a run holding a snapshot from turn one
|
|
102
|
+
* would ask again for something already answered.
|
|
103
|
+
*/
|
|
104
|
+
allowedTools(conversationId: string): Promise<string[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Adds a tool to this thread's allow list.
|
|
107
|
+
*
|
|
108
|
+
* The append happens **in the database**, not by reading and writing back:
|
|
109
|
+
* two calls in one turn can both be answered "allow for this chat" while
|
|
110
|
+
* the run is parked, and a read-modify-write would lose one of them.
|
|
111
|
+
*/
|
|
112
|
+
allowTool(conversationId: string, toolName: string): Promise<void>;
|
|
113
|
+
/** Starts a thread owned by `userId` in `workspaceId`. */
|
|
114
|
+
create(userId: string, workspaceId: string, surface: string, firstMessage: string): Promise<ConversationView>;
|
|
115
|
+
/** One thread's turns, in transcript order. */
|
|
116
|
+
messages(conversationId: string): Promise<MessageView[]>;
|
|
117
|
+
/**
|
|
118
|
+
* Appends a turn and bumps the thread's `updatedAt`.
|
|
119
|
+
*
|
|
120
|
+
* The position comes from `max(position) + 1` **computed in SQL**, inside
|
|
121
|
+
* the same statement as the insert, rather than read into JS and written
|
|
122
|
+
* back. Two runs in one thread can overlap (a second question asked while
|
|
123
|
+
* the first is still streaming), and a read-then-write would hand both the
|
|
124
|
+
* same position.
|
|
125
|
+
*/
|
|
126
|
+
appendMessage(input: {
|
|
127
|
+
conversationId: string;
|
|
128
|
+
runId: string;
|
|
129
|
+
role: 'user' | 'assistant';
|
|
130
|
+
content: ModelContentBlock[];
|
|
131
|
+
attachments?: AttachmentRef[] | null;
|
|
132
|
+
skills?: SkillRef[] | null;
|
|
133
|
+
model?: string | null;
|
|
134
|
+
provider?: string | null;
|
|
135
|
+
stopReason?: string | null;
|
|
136
|
+
inputTokens?: number | null;
|
|
137
|
+
outputTokens?: number | null;
|
|
138
|
+
}): Promise<{
|
|
139
|
+
id: string;
|
|
140
|
+
position: number;
|
|
141
|
+
}>;
|
|
142
|
+
/** Writes the audit row for one attempted tool call. */
|
|
143
|
+
recordToolCall(record: ToolCallRecord): Promise<void>;
|
|
144
|
+
/** One run's tool calls, oldest first — the UI's step list. */
|
|
145
|
+
toolCalls(runId: string): Promise<{
|
|
146
|
+
id: string;
|
|
147
|
+
conversationId: string;
|
|
148
|
+
runId: string;
|
|
149
|
+
callId: string;
|
|
150
|
+
name: string;
|
|
151
|
+
input: unknown;
|
|
152
|
+
outputSummary: string | null;
|
|
153
|
+
ok: boolean;
|
|
154
|
+
error: string | null;
|
|
155
|
+
durationMs: number;
|
|
156
|
+
createdAt: Date;
|
|
157
|
+
}[]>;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=conversation.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation.repository.d.ts","sourceRoot":"","sources":["../../../../../src/lib/chat/infrastructure/persistence/conversation.repository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConversationRepository = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
7
|
+
const database_1 = require("@orthacms/database");
|
|
8
|
+
const conversations_1 = require("../schema/conversations");
|
|
9
|
+
const messages_1 = require("../schema/messages");
|
|
10
|
+
const tool_calls_1 = require("../schema/tool-calls");
|
|
11
|
+
const derive_title_1 = require("./derive-title");
|
|
12
|
+
/**
|
|
13
|
+
* The columns every conversation read serves.
|
|
14
|
+
*
|
|
15
|
+
* One object rather than four copies: the read shape is `ConversationView`, and
|
|
16
|
+
* a column added to the table but forgotten in one of the selects is a field
|
|
17
|
+
* that is present on the list and missing on the detail — which typechecks.
|
|
18
|
+
*/
|
|
19
|
+
const CONVERSATION_COLUMNS = {
|
|
20
|
+
id: conversations_1.copilotConversations.id,
|
|
21
|
+
title: conversations_1.copilotConversations.title,
|
|
22
|
+
surface: conversations_1.copilotConversations.surface,
|
|
23
|
+
archived: conversations_1.copilotConversations.archived,
|
|
24
|
+
modelChoice: conversations_1.copilotConversations.modelChoice,
|
|
25
|
+
createdAt: conversations_1.copilotConversations.createdAt,
|
|
26
|
+
updatedAt: conversations_1.copilotConversations.updatedAt
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Reads and writes the copilot's transcript.
|
|
30
|
+
*
|
|
31
|
+
* **Every method takes the owning `userId` and `workspaceId` and filters on
|
|
32
|
+
* both.** Not defence in depth for its own sake: `WorkspaceGuard` proves the
|
|
33
|
+
* caller belongs to the workspace they named, but nothing upstream proves a
|
|
34
|
+
* *conversation id* belongs to that user or that workspace — that check has to
|
|
35
|
+
* live here, or a valid session could pull another member's thread by id.
|
|
36
|
+
*/
|
|
37
|
+
let ConversationRepository = class ConversationRepository {
|
|
38
|
+
db;
|
|
39
|
+
constructor(db) {
|
|
40
|
+
this.db = db;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* This user's threads in this workspace, most recently used first.
|
|
44
|
+
*
|
|
45
|
+
* `archived` selects **one of two disjoint sets** rather than widening the
|
|
46
|
+
* result: archiving means "hide this from the list", so an archived thread
|
|
47
|
+
* appearing beside its active siblings would defeat the flag entirely.
|
|
48
|
+
*/
|
|
49
|
+
async list(userId, workspaceId, archived = false) {
|
|
50
|
+
return this.db
|
|
51
|
+
.select(CONVERSATION_COLUMNS)
|
|
52
|
+
.from(conversations_1.copilotConversations)
|
|
53
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(conversations_1.copilotConversations.userId, userId), (0, drizzle_orm_1.eq)(conversations_1.copilotConversations.workspaceId, workspaceId), (0, drizzle_orm_1.eq)(conversations_1.copilotConversations.archived, archived)))
|
|
54
|
+
.orderBy((0, drizzle_orm_1.desc)(conversations_1.copilotConversations.updatedAt));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Renames a thread, files it away, and/or records the model picked for it,
|
|
58
|
+
* returning the updated row — or `null` when the id is not this user's in
|
|
59
|
+
* this workspace.
|
|
60
|
+
*
|
|
61
|
+
* **`updatedAt` is deliberately not touched.** It means "last used", and the
|
|
62
|
+
* list sorts by it: bumping it here would send a thread you merely renamed
|
|
63
|
+
* to the top of the rail, above conversations you actually had since. That
|
|
64
|
+
* goes double for `modelChoice`, which the client writes as a side effect of
|
|
65
|
+
* touching a picker — a thread must not climb the rail because someone
|
|
66
|
+
* looked at its model.
|
|
67
|
+
*
|
|
68
|
+
* The ownership predicate is part of the `UPDATE` rather than a read
|
|
69
|
+
* beforehand, so there is no window between checking and writing — and a
|
|
70
|
+
* miss is reported as `null` for "not yours" and "no such id" alike, so an
|
|
71
|
+
* id cannot be probed for existence.
|
|
72
|
+
*/
|
|
73
|
+
async update(conversationId, userId, workspaceId, patch) {
|
|
74
|
+
const [row] = await this.db
|
|
75
|
+
.update(conversations_1.copilotConversations)
|
|
76
|
+
.set({
|
|
77
|
+
...(patch.title !== undefined ? { title: patch.title } : {}),
|
|
78
|
+
...(patch.archived !== undefined
|
|
79
|
+
? { archived: patch.archived }
|
|
80
|
+
: {}),
|
|
81
|
+
...(patch.modelChoice !== undefined
|
|
82
|
+
? { modelChoice: patch.modelChoice }
|
|
83
|
+
: {})
|
|
84
|
+
})
|
|
85
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(conversations_1.copilotConversations.id, conversationId), (0, drizzle_orm_1.eq)(conversations_1.copilotConversations.userId, userId), (0, drizzle_orm_1.eq)(conversations_1.copilotConversations.workspaceId, workspaceId)))
|
|
86
|
+
.returning(CONVERSATION_COLUMNS);
|
|
87
|
+
return row ?? null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A thread the caller owns, or `null`. Returns null rather than throwing
|
|
91
|
+
* for "not yours" as well as "no such id" — the two are indistinguishable
|
|
92
|
+
* to a caller, so an id can't be probed for existence.
|
|
93
|
+
*/
|
|
94
|
+
async find(conversationId, userId, workspaceId) {
|
|
95
|
+
const [row] = await this.db
|
|
96
|
+
.select(CONVERSATION_COLUMNS)
|
|
97
|
+
.from(conversations_1.copilotConversations)
|
|
98
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(conversations_1.copilotConversations.id, conversationId), (0, drizzle_orm_1.eq)(conversations_1.copilotConversations.userId, userId), (0, drizzle_orm_1.eq)(conversations_1.copilotConversations.workspaceId, workspaceId)))
|
|
99
|
+
.limit(1);
|
|
100
|
+
return row ?? null;
|
|
101
|
+
}
|
|
102
|
+
/** Like {@link find}, but 404s — for routes that need the thread present. */
|
|
103
|
+
async findOrFail(conversationId, userId, workspaceId) {
|
|
104
|
+
const found = await this.find(conversationId, userId, workspaceId);
|
|
105
|
+
if (!found) {
|
|
106
|
+
throw new common_1.NotFoundException('Conversation not found.');
|
|
107
|
+
}
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Tool names this thread has been told to stop asking about.
|
|
112
|
+
*
|
|
113
|
+
* Read **per run**, never cached, for the same reason the capability
|
|
114
|
+
* profile is: the list can grow mid-thread (the user answers "allow for
|
|
115
|
+
* this chat" on turn three), and a run holding a snapshot from turn one
|
|
116
|
+
* would ask again for something already answered.
|
|
117
|
+
*/
|
|
118
|
+
async allowedTools(conversationId) {
|
|
119
|
+
const [row] = await this.db
|
|
120
|
+
.select({ allowedTools: conversations_1.copilotConversations.allowedTools })
|
|
121
|
+
.from(conversations_1.copilotConversations)
|
|
122
|
+
.where((0, drizzle_orm_1.eq)(conversations_1.copilotConversations.id, conversationId))
|
|
123
|
+
.limit(1);
|
|
124
|
+
return row?.allowedTools ?? [];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Adds a tool to this thread's allow list.
|
|
128
|
+
*
|
|
129
|
+
* The append happens **in the database**, not by reading and writing back:
|
|
130
|
+
* two calls in one turn can both be answered "allow for this chat" while
|
|
131
|
+
* the run is parked, and a read-modify-write would lose one of them.
|
|
132
|
+
*/
|
|
133
|
+
async allowTool(conversationId, toolName) {
|
|
134
|
+
await this.db
|
|
135
|
+
.update(conversations_1.copilotConversations)
|
|
136
|
+
.set({
|
|
137
|
+
allowedTools: (0, drizzle_orm_1.sql) `(
|
|
138
|
+
select coalesce(jsonb_agg(distinct value), '[]'::jsonb)
|
|
139
|
+
from jsonb_array_elements(
|
|
140
|
+
${conversations_1.copilotConversations.allowedTools} || ${JSON.stringify([toolName])}::jsonb
|
|
141
|
+
) as value
|
|
142
|
+
)`
|
|
143
|
+
})
|
|
144
|
+
.where((0, drizzle_orm_1.eq)(conversations_1.copilotConversations.id, conversationId));
|
|
145
|
+
}
|
|
146
|
+
/** Starts a thread owned by `userId` in `workspaceId`. */
|
|
147
|
+
async create(userId, workspaceId, surface, firstMessage) {
|
|
148
|
+
const [row] = await this.db
|
|
149
|
+
.insert(conversations_1.copilotConversations)
|
|
150
|
+
.values({
|
|
151
|
+
userId,
|
|
152
|
+
workspaceId,
|
|
153
|
+
surface,
|
|
154
|
+
title: (0, derive_title_1.deriveTitle)(firstMessage)
|
|
155
|
+
})
|
|
156
|
+
// The view's columns, not `returning()`: a bare one hands back the
|
|
157
|
+
// owner ids and the allow-list too, which is more than any caller of
|
|
158
|
+
// a `ConversationView` should be holding.
|
|
159
|
+
.returning(CONVERSATION_COLUMNS);
|
|
160
|
+
return row;
|
|
161
|
+
}
|
|
162
|
+
/** One thread's turns, in transcript order. */
|
|
163
|
+
async messages(conversationId) {
|
|
164
|
+
return this.db
|
|
165
|
+
.select({
|
|
166
|
+
id: messages_1.copilotMessages.id,
|
|
167
|
+
runId: messages_1.copilotMessages.runId,
|
|
168
|
+
role: messages_1.copilotMessages.role,
|
|
169
|
+
content: messages_1.copilotMessages.content,
|
|
170
|
+
attachments: messages_1.copilotMessages.attachments,
|
|
171
|
+
skills: messages_1.copilotMessages.skills,
|
|
172
|
+
model: messages_1.copilotMessages.model,
|
|
173
|
+
provider: messages_1.copilotMessages.provider,
|
|
174
|
+
stopReason: messages_1.copilotMessages.stopReason,
|
|
175
|
+
position: messages_1.copilotMessages.position,
|
|
176
|
+
createdAt: messages_1.copilotMessages.createdAt
|
|
177
|
+
})
|
|
178
|
+
.from(messages_1.copilotMessages)
|
|
179
|
+
.where((0, drizzle_orm_1.eq)(messages_1.copilotMessages.conversationId, conversationId))
|
|
180
|
+
.orderBy((0, drizzle_orm_1.asc)(messages_1.copilotMessages.position));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Appends a turn and bumps the thread's `updatedAt`.
|
|
184
|
+
*
|
|
185
|
+
* The position comes from `max(position) + 1` **computed in SQL**, inside
|
|
186
|
+
* the same statement as the insert, rather than read into JS and written
|
|
187
|
+
* back. Two runs in one thread can overlap (a second question asked while
|
|
188
|
+
* the first is still streaming), and a read-then-write would hand both the
|
|
189
|
+
* same position.
|
|
190
|
+
*/
|
|
191
|
+
async appendMessage(input) {
|
|
192
|
+
const nextPosition = (0, drizzle_orm_1.sql) `(
|
|
193
|
+
select coalesce(max(${messages_1.copilotMessages.position}), 0) + 1
|
|
194
|
+
from ${messages_1.copilotMessages}
|
|
195
|
+
where ${messages_1.copilotMessages.conversationId} = ${input.conversationId}
|
|
196
|
+
)`;
|
|
197
|
+
const [row] = await this.db
|
|
198
|
+
.insert(messages_1.copilotMessages)
|
|
199
|
+
.values({
|
|
200
|
+
conversationId: input.conversationId,
|
|
201
|
+
runId: input.runId,
|
|
202
|
+
role: input.role,
|
|
203
|
+
content: input.content,
|
|
204
|
+
// Null rather than `[]` for "none": the column is read as
|
|
205
|
+
// "did this turn carry files", and an empty array answers that
|
|
206
|
+
// question with a value that has to be length-checked.
|
|
207
|
+
attachments: input.attachments?.length
|
|
208
|
+
? input.attachments
|
|
209
|
+
: null,
|
|
210
|
+
// Null for "none", for the same reason as `attachments`: the
|
|
211
|
+
// column answers "did this turn run with skills", and an empty
|
|
212
|
+
// array answers it with a value every reader has to length-check.
|
|
213
|
+
skills: input.skills?.length ? input.skills : null,
|
|
214
|
+
model: input.model ?? null,
|
|
215
|
+
provider: input.provider ?? null,
|
|
216
|
+
stopReason: input.stopReason ?? null,
|
|
217
|
+
inputTokens: input.inputTokens ?? null,
|
|
218
|
+
outputTokens: input.outputTokens ?? null,
|
|
219
|
+
position: nextPosition
|
|
220
|
+
})
|
|
221
|
+
.returning({
|
|
222
|
+
id: messages_1.copilotMessages.id,
|
|
223
|
+
position: messages_1.copilotMessages.position
|
|
224
|
+
});
|
|
225
|
+
await this.db
|
|
226
|
+
.update(conversations_1.copilotConversations)
|
|
227
|
+
.set({ updatedAt: new Date() })
|
|
228
|
+
.where((0, drizzle_orm_1.eq)(conversations_1.copilotConversations.id, input.conversationId));
|
|
229
|
+
return row;
|
|
230
|
+
}
|
|
231
|
+
/** Writes the audit row for one attempted tool call. */
|
|
232
|
+
async recordToolCall(record) {
|
|
233
|
+
await this.db.insert(tool_calls_1.copilotToolCalls).values(record);
|
|
234
|
+
}
|
|
235
|
+
/** One run's tool calls, oldest first — the UI's step list. */
|
|
236
|
+
async toolCalls(runId) {
|
|
237
|
+
return this.db
|
|
238
|
+
.select()
|
|
239
|
+
.from(tool_calls_1.copilotToolCalls)
|
|
240
|
+
.where((0, drizzle_orm_1.eq)(tool_calls_1.copilotToolCalls.runId, runId))
|
|
241
|
+
.orderBy((0, drizzle_orm_1.asc)(tool_calls_1.copilotToolCalls.createdAt));
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
exports.ConversationRepository = ConversationRepository;
|
|
245
|
+
exports.ConversationRepository = ConversationRepository = tslib_1.__decorate([
|
|
246
|
+
(0, common_1.Injectable)(),
|
|
247
|
+
tslib_1.__param(0, (0, database_1.InjectDatabase)()),
|
|
248
|
+
tslib_1.__metadata("design:paramtypes", [Object])
|
|
249
|
+
], ConversationRepository);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names a thread from its opening message.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately **not** a model call: titling is the kind of thing that quietly
|
|
5
|
+
* doubles the cost and latency of starting a chat, and the first line of what
|
|
6
|
+
* someone typed is a better title than a summary anyway. Phase 4 can revisit it
|
|
7
|
+
* when there is a settings surface to make it optional.
|
|
8
|
+
*
|
|
9
|
+
* Returns `null` for an effectively empty message, leaving the thread untitled
|
|
10
|
+
* rather than titled with whitespace.
|
|
11
|
+
*/
|
|
12
|
+
export declare function deriveTitle(message: string): string | null;
|
|
13
|
+
//# sourceMappingURL=derive-title.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive-title.d.ts","sourceRoot":"","sources":["../../../../../src/lib/chat/infrastructure/persistence/derive-title.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB1D"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveTitle = deriveTitle;
|
|
4
|
+
/** Longest title we store. Long enough to be recognisable in a list, no more. */
|
|
5
|
+
const MAX_TITLE_LENGTH = 60;
|
|
6
|
+
/**
|
|
7
|
+
* Names a thread from its opening message.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately **not** a model call: titling is the kind of thing that quietly
|
|
10
|
+
* doubles the cost and latency of starting a chat, and the first line of what
|
|
11
|
+
* someone typed is a better title than a summary anyway. Phase 4 can revisit it
|
|
12
|
+
* when there is a settings surface to make it optional.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` for an effectively empty message, leaving the thread untitled
|
|
15
|
+
* rather than titled with whitespace.
|
|
16
|
+
*/
|
|
17
|
+
function deriveTitle(message) {
|
|
18
|
+
// Collapse all whitespace first: a pasted multi-line message would
|
|
19
|
+
// otherwise produce a title with a newline in it, which renders as a
|
|
20
|
+
// broken row rather than a truncated one.
|
|
21
|
+
const collapsed = message.replace(/\s+/g, ' ').trim();
|
|
22
|
+
if (collapsed.length === 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (collapsed.length <= MAX_TITLE_LENGTH) {
|
|
26
|
+
return collapsed;
|
|
27
|
+
}
|
|
28
|
+
// Prefer cutting at a word boundary, but only if that leaves most of the
|
|
29
|
+
// budget used — otherwise a single very long word would truncate to almost
|
|
30
|
+
// nothing.
|
|
31
|
+
const clipped = collapsed.slice(0, MAX_TITLE_LENGTH);
|
|
32
|
+
const lastSpace = clipped.lastIndexOf(' ');
|
|
33
|
+
const cut = lastSpace > MAX_TITLE_LENGTH * 0.6 ? lastSpace : clipped.length;
|
|
34
|
+
return `${clipped.slice(0, cut).trimEnd()}…`;
|
|
35
|
+
}
|