@gitim-runtime/cli 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.
Files changed (64) hide show
  1. package/dist/client.d.ts +34 -0
  2. package/dist/client.js +99 -0
  3. package/dist/commands/archive-channel.d.ts +1 -0
  4. package/dist/commands/archive-channel.js +19 -0
  5. package/dist/commands/archived-channels.d.ts +1 -0
  6. package/dist/commands/archived-channels.js +26 -0
  7. package/dist/commands/channels.d.ts +1 -0
  8. package/dist/commands/channels.js +18 -0
  9. package/dist/commands/create-channel.d.ts +4 -0
  10. package/dist/commands/create-channel.js +19 -0
  11. package/dist/commands/dm.d.ts +10 -0
  12. package/dist/commands/dm.js +81 -0
  13. package/dist/commands/init.d.ts +1 -0
  14. package/dist/commands/init.js +24 -0
  15. package/dist/commands/join-channel.d.ts +3 -0
  16. package/dist/commands/join-channel.js +20 -0
  17. package/dist/commands/onboard.d.ts +17 -0
  18. package/dist/commands/onboard.js +261 -0
  19. package/dist/commands/read.d.ts +4 -0
  20. package/dist/commands/read.js +20 -0
  21. package/dist/commands/reindex.d.ts +1 -0
  22. package/dist/commands/reindex.js +18 -0
  23. package/dist/commands/search.d.ts +7 -0
  24. package/dist/commands/search.js +41 -0
  25. package/dist/commands/send.d.ts +4 -0
  26. package/dist/commands/send.js +19 -0
  27. package/dist/commands/status.d.ts +1 -0
  28. package/dist/commands/status.js +18 -0
  29. package/dist/commands/stop.d.ts +1 -0
  30. package/dist/commands/stop.js +21 -0
  31. package/dist/commands/tui.d.ts +1 -0
  32. package/dist/commands/tui.js +57 -0
  33. package/dist/commands/users.d.ts +1 -0
  34. package/dist/commands/users.js +18 -0
  35. package/dist/commands/webui.d.ts +5 -0
  36. package/dist/commands/webui.js +41 -0
  37. package/dist/daemon.d.ts +3 -0
  38. package/dist/daemon.js +73 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +141 -0
  41. package/dist/tui/app.d.ts +43 -0
  42. package/dist/tui/app.js +461 -0
  43. package/dist/tui/channel-sidebar.d.ts +14 -0
  44. package/dist/tui/channel-sidebar.js +62 -0
  45. package/dist/tui/daemon-connection.d.ts +38 -0
  46. package/dist/tui/daemon-connection.js +118 -0
  47. package/dist/tui/mention-popup.d.ts +13 -0
  48. package/dist/tui/mention-popup.js +59 -0
  49. package/dist/tui/message-view.d.ts +38 -0
  50. package/dist/tui/message-view.js +166 -0
  51. package/dist/tui/split-layout.d.ts +17 -0
  52. package/dist/tui/split-layout.js +48 -0
  53. package/dist/tui/status-bar.d.ts +12 -0
  54. package/dist/tui/status-bar.js +39 -0
  55. package/dist/tui/themes.d.ts +22 -0
  56. package/dist/tui/themes.js +49 -0
  57. package/dist/tui/thread-view.d.ts +14 -0
  58. package/dist/tui/thread-view.js +72 -0
  59. package/dist/webui/assets/index-YH6ztb9g.css +1 -0
  60. package/dist/webui/assets/index-nFs-lh9F.js +49 -0
  61. package/dist/webui/index.html +13 -0
  62. package/dist/webui/server.d.ts +7 -0
  63. package/dist/webui/server.js +229 -0
  64. package/package.json +28 -0
@@ -0,0 +1,13 @@
1
+ import { type Component } from '@mariozechner/pi-tui';
2
+ export declare class MentionPopup implements Component {
3
+ allUsers: string[];
4
+ private filtered;
5
+ private filter;
6
+ selectedIndex: number;
7
+ onSelect?: (user: string) => void;
8
+ onCancel?: () => void;
9
+ setFilter(text: string): void;
10
+ invalidate(): void;
11
+ handleInput(data: string): void;
12
+ render(width: number): string[];
13
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @提及弹窗组件(在 Overlay 中显示)
3
+ */
4
+ import chalk from 'chalk';
5
+ import { truncateToWidth } from '@mariozechner/pi-tui';
6
+ import { Key, matchesKey } from '@mariozechner/pi-tui';
7
+ export class MentionPopup {
8
+ allUsers = [];
9
+ filtered = [];
10
+ filter = '';
11
+ selectedIndex = 0;
12
+ onSelect;
13
+ onCancel;
14
+ setFilter(text) {
15
+ this.filter = text.toLowerCase();
16
+ this.filtered = this.allUsers.filter(u => u.toLowerCase().includes(this.filter));
17
+ this.selectedIndex = 0;
18
+ }
19
+ invalidate() { }
20
+ handleInput(data) {
21
+ if (matchesKey(data, Key.up)) {
22
+ if (this.selectedIndex > 0)
23
+ this.selectedIndex--;
24
+ }
25
+ else if (matchesKey(data, Key.down)) {
26
+ if (this.selectedIndex < this.filtered.length - 1)
27
+ this.selectedIndex++;
28
+ }
29
+ else if (matchesKey(data, Key.enter)) {
30
+ if (this.filtered[this.selectedIndex]) {
31
+ this.onSelect?.(this.filtered[this.selectedIndex]);
32
+ }
33
+ }
34
+ else if (matchesKey(data, Key.escape)) {
35
+ this.onCancel?.();
36
+ }
37
+ }
38
+ render(width) {
39
+ const lines = [];
40
+ const items = this.filtered.length > 0 ? this.filtered : this.allUsers;
41
+ const title = chalk.bold(' @提及用户 ');
42
+ lines.push(truncateToWidth(title, width));
43
+ lines.push(truncateToWidth(chalk.gray('─'.repeat(width)), width));
44
+ const maxShow = Math.min(items.length, 8);
45
+ for (let i = 0; i < maxShow; i++) {
46
+ const user = items[i];
47
+ if (i === this.selectedIndex) {
48
+ lines.push(truncateToWidth(chalk.bgCyan.black(` ▸ @${user} `.padEnd(width)), width));
49
+ }
50
+ else {
51
+ lines.push(truncateToWidth(` @${user}`.padEnd(width), width));
52
+ }
53
+ }
54
+ if (items.length === 0) {
55
+ lines.push(truncateToWidth(chalk.dim(' 无匹配用户'), width));
56
+ }
57
+ return lines;
58
+ }
59
+ }
@@ -0,0 +1,38 @@
1
+ import { type Component } from '@mariozechner/pi-tui';
2
+ import type { Message } from './daemon-connection.js';
3
+ export declare class MessageView implements Component {
4
+ messages: Message[];
5
+ /** 浏览模式中选中的索引(messages 数组中的索引) */
6
+ selectedIndex: number;
7
+ /** 视口偏移(从底部算起的行数偏移) */
8
+ private scrollOffset;
9
+ /** 是否处于浏览模式 */
10
+ browsing: boolean;
11
+ /** 回复选中消息的回调 */
12
+ onReply?: (msg: Message) => void;
13
+ /** 展开线程链的回调 */
14
+ onThread?: (msg: Message) => void;
15
+ invalidate(): void;
16
+ /**
17
+ * 查找消息(用于显示回复摘要)
18
+ */
19
+ private findMessage;
20
+ /**
21
+ * 进入浏览模式
22
+ */
23
+ enterBrowse(): void;
24
+ /**
25
+ * 退出浏览模式
26
+ */
27
+ exitBrowse(): void;
28
+ moveUp(): void;
29
+ moveDown(): void;
30
+ jumpToBottom(): void;
31
+ handleInput(data: string): void;
32
+ render(width: number): string[];
33
+ /**
34
+ * 渲染带视口的消息(需要知道可用高度)
35
+ */
36
+ renderWithHeight(width: number, height: number): string[];
37
+ private renderMessage;
38
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 消息视图组件
3
+ */
4
+ import chalk from 'chalk';
5
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@mariozechner/pi-tui';
6
+ import { authorColor, colors } from './themes.js';
7
+ /**
8
+ * 格式化时间戳 20260317T120000Z → 12:00
9
+ */
10
+ function formatTime(ts) {
11
+ const m = ts.match(/T(\d{2})(\d{2})/);
12
+ if (!m)
13
+ return '??:??';
14
+ return `${m[1]}:${m[2]}`;
15
+ }
16
+ export class MessageView {
17
+ messages = [];
18
+ /** 浏览模式中选中的索引(messages 数组中的索引) */
19
+ selectedIndex = -1;
20
+ /** 视口偏移(从底部算起的行数偏移) */
21
+ scrollOffset = 0;
22
+ /** 是否处于浏览模式 */
23
+ browsing = false;
24
+ /** 回复选中消息的回调 */
25
+ onReply;
26
+ /** 展开线程链的回调 */
27
+ onThread;
28
+ invalidate() { }
29
+ /**
30
+ * 查找消息(用于显示回复摘要)
31
+ */
32
+ findMessage(lineNumber) {
33
+ return this.messages.find(m => m.line_number === lineNumber);
34
+ }
35
+ /**
36
+ * 进入浏览模式
37
+ */
38
+ enterBrowse() {
39
+ this.browsing = true;
40
+ if (this.messages.length > 0) {
41
+ this.selectedIndex = this.messages.length - 1;
42
+ }
43
+ this.scrollOffset = 0;
44
+ }
45
+ /**
46
+ * 退出浏览模式
47
+ */
48
+ exitBrowse() {
49
+ this.browsing = false;
50
+ this.selectedIndex = -1;
51
+ this.scrollOffset = 0;
52
+ }
53
+ moveUp() {
54
+ if (this.selectedIndex > 0)
55
+ this.selectedIndex--;
56
+ }
57
+ moveDown() {
58
+ if (this.selectedIndex < this.messages.length - 1)
59
+ this.selectedIndex++;
60
+ }
61
+ jumpToBottom() {
62
+ if (this.messages.length > 0) {
63
+ this.selectedIndex = this.messages.length - 1;
64
+ this.scrollOffset = 0;
65
+ }
66
+ }
67
+ handleInput(data) {
68
+ // 由 app.ts 处理
69
+ }
70
+ render(width) {
71
+ if (this.messages.length === 0) {
72
+ return [chalk.dim(' 暂无消息')];
73
+ }
74
+ // 渲染所有消息为行
75
+ const allLines = [];
76
+ for (let i = 0; i < this.messages.length; i++) {
77
+ const msg = this.messages[i];
78
+ const isSelected = this.browsing && i === this.selectedIndex;
79
+ const rendered = this.renderMessage(msg, width, isSelected);
80
+ allLines.push(...rendered);
81
+ }
82
+ return allLines;
83
+ }
84
+ /**
85
+ * 渲染带视口的消息(需要知道可用高度)
86
+ */
87
+ renderWithHeight(width, height) {
88
+ if (this.messages.length === 0) {
89
+ const lines = Array(height).fill('');
90
+ lines[Math.floor(height / 2)] = chalk.dim(' 暂无消息');
91
+ return lines;
92
+ }
93
+ // 渲染所有消息
94
+ const blocks = [];
95
+ for (let i = 0; i < this.messages.length; i++) {
96
+ const msg = this.messages[i];
97
+ const isSelected = this.browsing && i === this.selectedIndex;
98
+ const rendered = this.renderMessage(msg, width, isSelected);
99
+ blocks.push({ lines: rendered, msgIndex: i });
100
+ }
101
+ const allLines = blocks.flatMap(b => b.lines);
102
+ // 确保选中的消息在视口中可见
103
+ if (this.browsing && this.selectedIndex >= 0) {
104
+ // 计算选中消息在 allLines 中的起始行
105
+ let selectedStart = 0;
106
+ for (let i = 0; i < blocks.length && blocks[i].msgIndex < this.selectedIndex; i++) {
107
+ selectedStart += blocks[i].lines.length;
108
+ }
109
+ const selectedBlock = blocks.find(b => b.msgIndex === this.selectedIndex);
110
+ const selectedEnd = selectedStart + (selectedBlock?.lines.length ?? 1);
111
+ // 调整 scrollOffset 使选中消息可见
112
+ if (selectedEnd > allLines.length - this.scrollOffset) {
113
+ this.scrollOffset = allLines.length - selectedEnd;
114
+ }
115
+ if (selectedStart < allLines.length - this.scrollOffset - height) {
116
+ this.scrollOffset = allLines.length - selectedStart - height;
117
+ }
118
+ if (this.scrollOffset < 0)
119
+ this.scrollOffset = 0;
120
+ }
121
+ // 从底部截取 height 行
122
+ const endIdx = allLines.length - this.scrollOffset;
123
+ const startIdx = Math.max(0, endIdx - height);
124
+ const visible = allLines.slice(startIdx, endIdx);
125
+ // 如果行数不够,在顶部填空行
126
+ while (visible.length < height) {
127
+ visible.unshift('');
128
+ }
129
+ return visible;
130
+ }
131
+ renderMessage(msg, width, selected) {
132
+ const lines = [];
133
+ const colorFn = authorColor(msg.author);
134
+ const time = colors.timestamp(formatTime(msg.timestamp));
135
+ const author = colorFn(`@${msg.author}`);
136
+ // 回复提示行
137
+ if (msg.point_to > 0) {
138
+ const parent = this.findMessage(msg.point_to);
139
+ const parentSummary = parent
140
+ ? `@${parent.author}: ${parent.body.slice(0, 30)}${parent.body.length > 30 ? '...' : ''}`
141
+ : `L${msg.point_to}`;
142
+ const replyLine = colors.reply(` ↩ ${parentSummary}`);
143
+ lines.push(truncateToWidth(replyLine, width));
144
+ }
145
+ // 主消息行
146
+ const header = `${author} ${time} `;
147
+ const headerW = visibleWidth(header);
148
+ const bodyW = Math.max(10, width - headerW);
149
+ // 消息正文可能需要换行
150
+ const bodyLines = wrapTextWithAnsi(msg.body, bodyW);
151
+ if (bodyLines.length === 0) {
152
+ lines.push(truncateToWidth(header, width));
153
+ }
154
+ else {
155
+ lines.push(truncateToWidth(header + bodyLines[0], width));
156
+ for (let i = 1; i < bodyLines.length; i++) {
157
+ lines.push(truncateToWidth(' '.repeat(headerW) + bodyLines[i], width));
158
+ }
159
+ }
160
+ // 选中高亮
161
+ if (selected) {
162
+ return lines.map(l => colors.highlight(l));
163
+ }
164
+ return lines;
165
+ }
166
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 分栏布局组件 — 左侧(侧边栏)和右侧(主区域)横向拼接
3
+ */
4
+ import { type Component } from '@mariozechner/pi-tui';
5
+ export interface RightPanel {
6
+ renderWithHeight(width: number, height: number): string[];
7
+ }
8
+ export declare class SplitLayout implements Component {
9
+ left: Component;
10
+ right: RightPanel;
11
+ sidebarWidth: number;
12
+ /** 可用高度(不含状态栏) */
13
+ height: number;
14
+ constructor(left: Component, right: RightPanel, sidebarWidth: number);
15
+ invalidate(): void;
16
+ render(width: number): string[];
17
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 分栏布局组件 — 左侧(侧边栏)和右侧(主区域)横向拼接
3
+ */
4
+ import { truncateToWidth, visibleWidth } from '@mariozechner/pi-tui';
5
+ import chalk from 'chalk';
6
+ export class SplitLayout {
7
+ left;
8
+ right;
9
+ sidebarWidth;
10
+ /** 可用高度(不含状态栏) */
11
+ height = 24;
12
+ constructor(left, right, sidebarWidth) {
13
+ this.left = left;
14
+ this.right = right;
15
+ this.sidebarWidth = sidebarWidth;
16
+ }
17
+ invalidate() {
18
+ this.left.invalidate();
19
+ }
20
+ render(width) {
21
+ const leftW = this.sidebarWidth;
22
+ const sepW = 1; // │
23
+ const rightW = Math.max(1, width - leftW - sepW);
24
+ const leftLines = this.left.render(leftW);
25
+ const rightLines = this.right.renderWithHeight(rightW, this.height);
26
+ const maxLines = Math.max(leftLines.length, rightLines.length, this.height);
27
+ const result = [];
28
+ for (let i = 0; i < maxLines; i++) {
29
+ const l = leftLines[i] ?? '';
30
+ const r = rightLines[i] ?? '';
31
+ // 左侧填充到固定宽度
32
+ const lPadded = padToWidth(l, leftW);
33
+ const sep = chalk.gray('│');
34
+ const line = lPadded + sep + r;
35
+ result.push(truncateToWidth(line, width));
36
+ }
37
+ return result;
38
+ }
39
+ }
40
+ /**
41
+ * 将一行补齐到指定可见宽度
42
+ */
43
+ function padToWidth(line, targetWidth) {
44
+ const w = visibleWidth(line);
45
+ if (w >= targetWidth)
46
+ return truncateToWidth(line, targetWidth);
47
+ return line + ' '.repeat(targetWidth - w);
48
+ }
@@ -0,0 +1,12 @@
1
+ import { type Component } from '@mariozechner/pi-tui';
2
+ export type AppMode = 'normal' | 'browse' | 'chain';
3
+ export declare class StatusBar implements Component {
4
+ user: string;
5
+ connected: boolean;
6
+ mode: AppMode;
7
+ channel: string;
8
+ replyTarget: number | null;
9
+ error: string | null;
10
+ invalidate(): void;
11
+ render(width: number): string[];
12
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 状态栏组件
3
+ */
4
+ import chalk from 'chalk';
5
+ import { truncateToWidth, visibleWidth } from '@mariozechner/pi-tui';
6
+ import { colors } from './themes.js';
7
+ export class StatusBar {
8
+ user = '';
9
+ connected = false;
10
+ mode = 'normal';
11
+ channel = '';
12
+ replyTarget = null;
13
+ error = null;
14
+ invalidate() { }
15
+ render(width) {
16
+ const modeLabel = this.mode === 'normal' ? colors.modeNormal(' 普通 ')
17
+ : this.mode === 'browse' ? colors.modeBrowse(' 浏览 ')
18
+ : colors.modeChain(' 线程 ');
19
+ const connLabel = this.connected ? chalk.green('●') : chalk.red('●');
20
+ let left = ` ${connLabel} @${this.user} | #${this.channel} ${modeLabel}`;
21
+ if (this.error) {
22
+ left += chalk.red(` ✗ ${this.error}`);
23
+ }
24
+ else if (this.replyTarget) {
25
+ left += chalk.dim(` 回复 L${this.replyTarget}`);
26
+ }
27
+ const hints = this.mode === 'normal'
28
+ ? 'Ctrl+B:浏览 | Alt+↑↓:切频道 | Ctrl+Q:退出'
29
+ : this.mode === 'browse'
30
+ ? 'j/k:选择 | r:回复 | Enter:线程 | Esc:返回 | G:跳底'
31
+ : 'j/k:选择 | r:回复 | Esc:关闭';
32
+ const right = ` ${hints} `;
33
+ const leftW = visibleWidth(left);
34
+ const rightW = visibleWidth(right);
35
+ const pad = Math.max(0, width - leftW - rightW);
36
+ const line = chalk.bgGray.white(truncateToWidth(left + ' '.repeat(pad) + right, width));
37
+ return [line];
38
+ }
39
+ }
@@ -0,0 +1,22 @@
1
+ import type { EditorTheme, SelectListTheme } from '@mariozechner/pi-tui';
2
+ export declare const selectListTheme: SelectListTheme;
3
+ export declare const editorTheme: EditorTheme;
4
+ export declare const colors: {
5
+ authorColors: import("chalk").ChalkInstance[];
6
+ timestamp: import("chalk").ChalkInstance;
7
+ highlight: import("chalk").ChalkInstance;
8
+ reply: import("chalk").ChalkInstance;
9
+ separator: import("chalk").ChalkInstance;
10
+ channelActive: import("chalk").ChalkInstance;
11
+ channelNormal: import("chalk").ChalkInstance;
12
+ unread: import("chalk").ChalkInstance;
13
+ statusBar: import("chalk").ChalkInstance;
14
+ modeNormal: import("chalk").ChalkInstance;
15
+ modeBrowse: import("chalk").ChalkInstance;
16
+ modeChain: import("chalk").ChalkInstance;
17
+ threadBranch: import("chalk").ChalkInstance;
18
+ };
19
+ /**
20
+ * 根据作者名分配固定颜色
21
+ */
22
+ export declare function authorColor(author: string): (text: string) => string;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * TUI 主题定义
3
+ */
4
+ import chalk from 'chalk';
5
+ export const selectListTheme = {
6
+ selectedPrefix: (text) => chalk.cyan(text),
7
+ selectedText: (text) => chalk.cyan.bold(text),
8
+ description: (text) => chalk.dim(text),
9
+ scrollInfo: (text) => chalk.dim(text),
10
+ noMatch: (text) => chalk.dim(text),
11
+ };
12
+ export const editorTheme = {
13
+ borderColor: (str) => chalk.gray(str),
14
+ selectList: selectListTheme,
15
+ };
16
+ // 颜色方案
17
+ export const colors = {
18
+ // 消息作者颜色池
19
+ authorColors: [
20
+ chalk.cyan,
21
+ chalk.green,
22
+ chalk.yellow,
23
+ chalk.magenta,
24
+ chalk.blue,
25
+ chalk.red,
26
+ ],
27
+ timestamp: chalk.dim,
28
+ highlight: chalk.bgCyan.black,
29
+ reply: chalk.dim,
30
+ separator: chalk.gray,
31
+ channelActive: chalk.bgCyan.black,
32
+ channelNormal: chalk.white,
33
+ unread: chalk.yellow,
34
+ statusBar: chalk.bgGray.white,
35
+ modeNormal: chalk.green,
36
+ modeBrowse: chalk.yellow,
37
+ modeChain: chalk.magenta,
38
+ threadBranch: chalk.gray,
39
+ };
40
+ /**
41
+ * 根据作者名分配固定颜色
42
+ */
43
+ export function authorColor(author) {
44
+ let hash = 0;
45
+ for (let i = 0; i < author.length; i++) {
46
+ hash = ((hash << 5) - hash + author.charCodeAt(i)) | 0;
47
+ }
48
+ return colors.authorColors[Math.abs(hash) % colors.authorColors.length];
49
+ }
@@ -0,0 +1,14 @@
1
+ import { type Component } from '@mariozechner/pi-tui';
2
+ import type { Message } from './daemon-connection.js';
3
+ export declare class ThreadView implements Component {
4
+ messages: Message[];
5
+ rootLine: number;
6
+ selectedIndex: number;
7
+ onReply?: (msg: Message) => void;
8
+ onClose?: () => void;
9
+ invalidate(): void;
10
+ moveUp(): void;
11
+ moveDown(): void;
12
+ handleInput(data: string): void;
13
+ render(width: number): string[];
14
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 线程链视图组件(在 Overlay 中显示)
3
+ */
4
+ import chalk from 'chalk';
5
+ import { truncateToWidth, visibleWidth } from '@mariozechner/pi-tui';
6
+ import { authorColor, colors } from './themes.js';
7
+ function formatTime(ts) {
8
+ const m = ts.match(/T(\d{2})(\d{2})/);
9
+ if (!m)
10
+ return '??:??';
11
+ return `${m[1]}:${m[2]}`;
12
+ }
13
+ export class ThreadView {
14
+ messages = [];
15
+ rootLine = 0;
16
+ selectedIndex = 0;
17
+ onReply;
18
+ onClose;
19
+ invalidate() { }
20
+ moveUp() {
21
+ if (this.selectedIndex > 0)
22
+ this.selectedIndex--;
23
+ }
24
+ moveDown() {
25
+ if (this.selectedIndex < this.messages.length - 1)
26
+ this.selectedIndex++;
27
+ }
28
+ handleInput(data) {
29
+ // 由 app.ts 处理
30
+ }
31
+ render(width) {
32
+ const lines = [];
33
+ const innerW = width - 2; // 边框
34
+ // 标题
35
+ const title = ` 引用链: L${this.rootLine} `;
36
+ const titleLine = '┌' + chalk.bold(title) + '─'.repeat(Math.max(0, width - 2 - visibleWidth(title))) + '┐';
37
+ lines.push(truncateToWidth(titleLine, width));
38
+ if (this.messages.length === 0) {
39
+ const emptyText = chalk.dim(' 无消息');
40
+ const emptyPad = Math.max(0, innerW - visibleWidth(emptyText));
41
+ lines.push(truncateToWidth('│' + emptyText + ' '.repeat(emptyPad) + '│', width));
42
+ }
43
+ else {
44
+ for (let i = 0; i < this.messages.length; i++) {
45
+ const msg = this.messages[i];
46
+ const isLast = i === this.messages.length - 1;
47
+ const isSelected = i === this.selectedIndex;
48
+ const colorFn = authorColor(msg.author);
49
+ // 树形连接符
50
+ const branch = i === 0 ? '┌' : isLast ? '└' : '├';
51
+ const branchStr = colors.threadBranch(branch + '─ ');
52
+ const time = colors.timestamp(formatTime(msg.timestamp));
53
+ const author = colorFn(`@${msg.author}`);
54
+ const bodyPreview = msg.body.slice(0, Math.max(10, innerW - 25));
55
+ let content = `${branchStr}${author} ${time} ${bodyPreview}`;
56
+ if (isSelected) {
57
+ content = colors.highlight(content);
58
+ }
59
+ lines.push(truncateToWidth('│' + truncateToWidth(content, innerW) + '│', width));
60
+ // 非最后元素之间的竖线
61
+ if (!isLast) {
62
+ lines.push(truncateToWidth('│' + colors.threadBranch('│') + ' '.repeat(Math.max(0, innerW - 1)) + '│', width));
63
+ }
64
+ }
65
+ }
66
+ // 底部边框 + 操作提示
67
+ const hint = chalk.dim(' j/k:选择 r:回复 Esc:关闭 ');
68
+ const bottomLine = '└' + hint + '─'.repeat(Math.max(0, width - 2 - visibleWidth(hint))) + '┘';
69
+ lines.push(truncateToWidth(bottomLine, width));
70
+ return lines;
71
+ }
72
+ }
@@ -0,0 +1 @@
1
+ :root{--bg-primary: #1a1a2e;--bg-secondary: #16213e;--bg-tertiary: #0f3460;--text-primary: #e0e0e0;--text-secondary: #a0a0a0;--accent: #4fc3f7;--border: #2a2a4a;--reply-bg: #252545;--success: #66bb6a;--error: #ef5350;--hover: rgba(255, 255, 255, .05)}*{margin:0;padding:0;box-sizing:border-box}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--bg-primary);color:var(--text-primary);font-size:14px;line-height:1.5}.app-layout{display:flex;flex-direction:column;height:100%}.app-body{display:flex;flex:1;overflow:hidden}.header{display:flex;align-items:center;justify-content:space-between;padding:0 16px;height:48px;background:var(--bg-secondary);border-bottom:1px solid var(--border);flex-shrink:0}.header-left{display:flex;align-items:center;gap:12px}.header-logo{font-weight:700;font-size:16px;color:var(--accent)}.header-channel{font-size:15px;color:var(--text-primary)}.header-right{display:flex;align-items:center;gap:10px}.connection-dot{width:8px;height:8px;border-radius:50%;display:inline-block}.connection-dot.online{background:var(--success)}.connection-dot.offline{background:var(--error)}.header-user{color:var(--text-secondary);font-size:13px}.members-btn-wrapper{position:relative}.members-btn{background:none;border:1px solid var(--border);border-radius:4px;color:var(--text-secondary);padding:3px 8px;font-size:13px;cursor:pointer;transition:background .15s}.members-btn:hover{background:var(--hover);color:var(--text-primary)}.members-dropdown{position:absolute;right:0;top:calc(100% + 6px);background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;width:200px;max-height:300px;overflow-y:auto;box-shadow:0 4px 12px #0000004d;z-index:200}.members-dropdown-title{padding:8px 12px;font-size:12px;font-weight:600;color:var(--text-secondary);border-bottom:1px solid var(--border)}.members-dropdown-item{padding:6px 12px;font-size:13px;color:var(--text-primary);display:flex;align-items:center;gap:6px}.members-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.members-dm-btn{background:none;border:none;cursor:pointer;font-size:14px;padding:0 2px;opacity:0;transition:opacity .15s}.members-dropdown-item:hover .members-dm-btn{opacity:1}.members-dot{width:6px;height:6px;border-radius:50%;background:var(--success);flex-shrink:0}.sidebar{width:220px;min-width:220px;background:var(--bg-secondary);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto}.sidebar-section-title{padding:12px 12px 4px;font-size:11px;text-transform:uppercase;color:var(--text-secondary);font-weight:600;letter-spacing:.5px}.sidebar-item{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;cursor:pointer;color:var(--text-secondary);font-size:14px;border-left:3px solid transparent;transition:background .15s}.sidebar-item:hover{background:var(--hover)}.sidebar-item.active{background:var(--hover);color:var(--text-primary);border-left-color:var(--accent)}.sidebar-item-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.unread-badge{background:var(--accent);color:#000;font-size:11px;font-weight:600;padding:1px 6px;border-radius:10px;min-width:18px;text-align:center}.dm-new-btn{color:var(--accent)!important;font-size:13px}.dm-search{padding:4px 8px}.dm-search-input{width:100%;padding:6px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:13px;outline:none}.dm-search-input:focus{border-color:var(--accent)}.dm-search-list{max-height:200px;overflow-y:auto;margin-top:4px}.dm-search-item{padding:6px 8px;cursor:pointer;font-size:13px;color:var(--text-secondary);border-radius:4px}.dm-search-item:hover{background:var(--hover);color:var(--text-primary)}.dm-search-empty{padding:8px;font-size:12px;color:var(--text-secondary);text-align:center}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}.message-list{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:2px}.message-list-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);font-size:15px}.message-event{padding:4px 8px;font-size:13px;color:var(--text-secondary)}.message-event .message-author{font-size:13px}.message-item{padding:6px 8px;border-radius:4px;position:relative;transition:background .1s}.message-item:hover{background:var(--hover)}.message-header{display:flex;align-items:baseline;gap:8px;margin-bottom:2px}.message-author{font-weight:600;color:var(--accent);font-size:14px}.message-time{font-size:12px;color:var(--text-secondary)}.message-reply-ref{padding:4px 8px;margin-bottom:4px;border-left:2px solid var(--text-secondary);background:var(--reply-bg);border-radius:0 4px 4px 0;font-size:13px;color:var(--text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:500px}.message-reply-ref .reply-author{font-weight:600;margin-right:4px}.message-body{font-size:14px;white-space:pre-wrap;word-break:break-word}.message-pending{opacity:.6}.message-pending .message-body{font-style:italic}.message-sent{opacity:.85}.message-failed{opacity:.8;border-left:3px solid var(--error)}.message-status{font-size:11px;margin-left:8px}.message-status-sending{color:var(--text-secondary);animation:pulse 1.5s ease-in-out infinite}.message-status-sent{color:var(--success)}.message-status-failed{color:var(--error)}.message-actions{display:none;position:absolute;right:8px;top:-12px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:4px;overflow:hidden}.message-item:hover .message-actions{display:flex}.message-action-btn{background:none;border:none;color:var(--text-secondary);padding:4px 8px;font-size:12px;cursor:pointer}.message-action-btn:hover{background:var(--hover);color:var(--text-primary)}.input-area{padding:0 16px 16px;flex-shrink:0}.input-reply-bar{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background:var(--reply-bg);border-radius:4px 4px 0 0;font-size:13px;color:var(--text-secondary)}.input-reply-bar span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.input-reply-close{background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:16px;padding:0 4px;line-height:1}.input-reply-close:hover{color:var(--text-primary)}.input-wrapper{position:relative}.input-textarea{width:100%;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:14px;font-family:inherit;line-height:1.5;resize:none;outline:none;min-height:42px;max-height:160px}.input-textarea:focus{border-color:var(--accent)}.input-textarea::placeholder{color:var(--text-secondary)}.input-sending{color:var(--text-secondary);font-size:12px;margin-top:4px;animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,to{opacity:.5}50%{opacity:1}}.input-error{color:var(--error);font-size:12px;margin-top:4px}.thread-panel{width:340px;min-width:340px;background:var(--bg-secondary);border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}.thread-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:14px}.thread-close-btn{background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:18px}.thread-close-btn:hover{color:var(--text-primary)}.thread-messages{flex:1;overflow-y:auto;padding:12px}.thread-msg{padding:6px 8px;border-radius:4px;margin-bottom:4px}.thread-msg:hover{background:var(--hover)}.thread-msg-root{border-left:2px solid var(--accent);padding-left:8px}.mention-popup{position:absolute;bottom:100%;left:0;background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;max-height:200px;overflow-y:auto;width:220px;box-shadow:0 4px 12px #0000004d;z-index:100;margin-bottom:4px}.mention-item{padding:8px 12px;cursor:pointer;font-size:14px;color:var(--text-primary)}.mention-item:hover,.mention-item.active{background:var(--bg-tertiary)}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-secondary)}.message-highlight{animation:highlight-fade 1.5s ease-out}@keyframes highlight-fade{0%{background:#4fc3f74d}to{background:transparent}}.message-replying{background:#4fc3f714;border-left:2px solid var(--accent)}.reply-ref-clickable{cursor:pointer;transition:background .15s}.reply-ref-clickable:hover{background:#4fc3f71a;border-left-color:var(--accent)}.message-list .message-body{cursor:pointer}