|
|
… |
45 unchanged lines |
| 46 |
46 |
|
const thread = this.openThread |
| 47 |
47 |
|
? await this.bridge.request<Messages>("GET", `/api/v1/threads/${encodeURIComponent(this.openThread)}`).catch(() => undefined) |
| 48 |
48 |
|
: undefined; |
|
49 |
+ |
// Each message's latest replies show beneath it, so none is hidden. |
|
50 |
+ |
const replies = new Map<string, Message[]>(); |
|
51 |
+ |
await Promise.all( |
|
52 |
+ |
m.items |
|
53 |
+ |
.filter((x) => x.replies > 0) |
|
54 |
+ |
.map(async (x) => { |
|
55 |
+ |
const t = await this.bridge.request<Messages>("GET", `/api/v1/threads/${encodeURIComponent(x.id)}`).catch(() => undefined); |
|
56 |
+ |
if (t) replies.set(x.id, t.items.filter((y) => y.id !== x.id)); |
|
57 |
+ |
}), |
|
58 |
+ |
); |
| 49 |
59 |
|
const brief = this.model.briefs.projects.find((b) => b.project === this.name)?.text; |
| 50 |
60 |
|
const purpose = this.model.channels.find((c) => c.name === this.name)?.purpose ?? ""; |
| 51 |
|
- |
void this.panel.webview.postMessage({ type: "render", html: render(m.items, thread?.items, this.openThread), brief: brief ?? "", purpose }); |
|
61 |
+ |
void this.panel.webview.postMessage({ type: "render", html: render(m.items, thread?.items, this.openThread, replies), brief: brief ?? "", purpose }); |
| 52 |
62 |
|
} catch (e) { |
| 53 |
63 |
|
void this.panel.webview.postMessage({ type: "error", text: (e as Error).message }); |
| 54 |
64 |
|
} |
|
|
… |
31 unchanged lines |
| 86 |
96 |
|
return h.replace(/\n/g, "<br>"); |
| 87 |
97 |
|
} |
| 88 |
98 |
|
|
| 89 |
|
- |
function one(m: Message, inThread: boolean): string { |
|
99 |
+ |
const INLINE_REPLIES = 3; |
|
100 |
+ |
|
|
101 |
+ |
function one(m: Message, inThread: boolean, latest?: Message[]): string { |
| 90 |
102 |
|
const who = m.author === "human" ? "you" : m.author_display || m.author; |
| 91 |
|
- |
const replies = !inThread ? `<a class="replies" data-thread="${esc(m.id)}">${m.replies ? `${m.replies} repl${m.replies === 1 ? "y" : "ies"}` : "reply"}</a>` : ""; |
|
103 |
+ |
const link = !inThread ? `<a class="replies" data-thread="${esc(m.id)}">${m.replies ? "open thread" : "reply"}</a>` : ""; |
| 92 |
104 |
|
const body = m.redacted ? `<div class="redacted">${esc(m.body)}</div>` : `<div class="body">${md(m.body)}</div>`; |
| 93 |
105 |
|
const kind = m.kind && m.kind !== "chat" ? ` <span class="pill">${esc(m.kind)}</span>` : ""; |
| 94 |
|
- |
return `<div class="msg${m.author === "human" ? " mine" : ""}"><div class="meta"><strong>${esc(who)}</strong> · ${esc(when(m.at_ms))}${kind} ${replies}</div>${body}</div>`; |
|
106 |
+ |
let under = ""; |
|
107 |
+ |
if (!inThread && latest && latest.length) { |
|
108 |
+ |
const more = latest.length > INLINE_REPLIES ? `<a class="more" data-thread="${esc(m.id)}">${latest.length - INLINE_REPLIES} earlier repl${latest.length - INLINE_REPLIES === 1 ? "y" : "ies"}</a>` : ""; |
|
109 |
+ |
under = `<div class="inline">${more}${latest.slice(-INLINE_REPLIES).map((x) => one(x, true)).join("")}<a class="replies" data-thread="${esc(m.id)}">Reply in thread</a></div>`; |
|
110 |
+ |
} |
|
111 |
+ |
return `<div class="msg${m.author === "human" ? " mine" : ""}"><div class="meta"><strong>${esc(who)}</strong> · ${esc(when(m.at_ms))}${kind} ${link}</div>${body}${under}</div>`; |
| 95 |
112 |
|
} |
| 96 |
113 |
|
|
| 97 |
|
- |
function render(items: Message[], thread: Message[] | undefined, open: string | undefined): string { |
|
114 |
+ |
function render(items: Message[], thread: Message[] | undefined, open: string | undefined, replies: Map<string, Message[]>): string { |
| 98 |
115 |
|
if (!items.length && !thread) return `<p class="empty">No messages yet. Say hello below.</p>`; |
| 99 |
|
- |
const list = items.map((m) => one(m, false)).join(""); |
|
116 |
+ |
const list = items.map((m) => one(m, false, replies.get(m.id))).join(""); |
| 100 |
117 |
|
if (!thread || !open) return list; |
| 101 |
118 |
|
return `${list}<div class="thread"><div class="thead">Thread <a class="close" data-thread="">close</a></div>${thread.map((m) => one(m, true)).join("")}</div>`; |
| 102 |
119 |
|
} |
|
|
… |
16 unchanged lines |
| 119 |
136 |
|
.redacted { color: var(--vscode-disabledForeground); font-style: italic; } |
| 120 |
137 |
|
.pill { border: 1px solid var(--vscode-panel-border); border-radius: 8px; padding: 0 6px; font-size: 11px; } |
| 121 |
138 |
|
a { color: var(--vscode-textLink-foreground); cursor: pointer; } |
|
139 |
+ |
.inline { margin: 6px 0 2px 10px; padding-left: 10px; border-left: 3px solid var(--vscode-textLink-foreground); } |
|
140 |
+ |
.inline .msg { border-bottom: 0; padding: 3px 0; } .inline .more, .inline > .replies { font-size: 12px; } |
| 122 |
141 |
|
.thread { margin: 10px 0 4px 16px; padding-left: 10px; border-left: 2px solid var(--vscode-textLink-foreground); } |
| 123 |
142 |
|
.thead { font-weight: 600; margin-bottom: 4px; } |
| 124 |
143 |
|
.empty, .error { color: var(--vscode-descriptionForeground); padding: 12px 0; } .error { color: var(--vscode-errorForeground); } |
|
|
… |
31 unchanged lines |