OXYGENHUB

0.1.4: replies show under their message in a channel tab 95a01bce821d

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key eb5a6b74b5a2
When
· 2026-11-03 19:55 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
95a01bce821d280641efe7f85f4691cfbe08a1dc8524b25c475ab2d1283d68d3
Depends on
9a80f3825237 ff39a5e808da
Depended on by
0bfe88762caa 5482605b3353
In bookmarks
main
Receipts
✔ check passed in 6.7 s · signed by earth-1 job 14847 · download
✔ package passed in 5.5 s · signed by earth-1 job 14848 · download
Edits
12 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified CHANGELOG.md +3 −0
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 +
## 0.1.4
6 +
7 +
- **Changed: replies show under their message** in a channel tab, the latest three with a link to any earlier ones, instead of behind a "1 reply" link.
5 8
## 0.1.3
6 9
7 10
- **Fixed: connecting Kimi Code.** Kimi Code has no `kimi mcp` command; the extension now writes Spectra's entry into `~/.kimi-code/mcp.json` (or `$KIMI_CODE_HOME/mcp.json`), once for every folder, keeping the servers already there.
…
13 unchanged lines
modified package.json +1 −1
…
1 unchanged lines
2 2
  "name": "spectra-vscode",
3 3
  "displayName": "Spectra",
4 4
  "description": "Spectra in the editor: set up your AI agents, chat with them, and give them their briefs, without the command line",
5 -
  "version": "0.1.3",
5 +
  "version": "0.1.4",
6 6
  "publisher": "andymccall",
7 7
  "license": "MIT OR Apache-2.0",
8 8
  "repository": {
…
291 unchanged lines
modified src/chat.ts +25 −6
…
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