From d7c9d4ee01df8c9ea9a00c4ec3d1931f6a00f93e Mon Sep 17 00:00:00 2001 From: Abhiram Date: Wed, 20 May 2026 11:35:47 -0500 Subject: [PATCH] fix: restore message + phone IDs in list output, clarify direction --- src/commands/messages/list.ts | 17 +++++++---- src/commands/phonenumbers.ts | 4 +-- src/lib/format.ts | 47 ++++++++++++++++++++++++----- test/commands/messages/list.test.ts | 8 +++-- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/commands/messages/list.ts b/src/commands/messages/list.ts index 771d210..4d7eeb3 100644 --- a/src/commands/messages/list.ts +++ b/src/commands/messages/list.ts @@ -49,15 +49,20 @@ export default class MessagesList extends BaseCommand { const client = createApiClient(token); try { - const data = await client.chats.messages.list(args.chatId, { - limit: flags.limit, - cursor: flags.cursor, - }); + // Fetch the chat alongside the messages so we can render + // "you → counterparty" / "counterparty → you" in the list. + const [data, chat] = await Promise.all([ + client.chats.messages.list(args.chatId, { + limit: flags.limit, + cursor: flags.cursor, + }), + client.chats.retrieve(args.chatId).catch(() => undefined), + ]); if (flags.json) { - this.log(JSON.stringify(data, null, 2)); + this.log(JSON.stringify({ messages: data.messages, next_cursor: data.next_cursor }, null, 2)); } else { - this.log(formatMessagesList(data)); + this.log(formatMessagesList(data, chat)); } } catch (e) { this.error(`Failed to list messages: ${e instanceof Error ? e.message : String(e)}`); diff --git a/src/commands/phonenumbers.ts b/src/commands/phonenumbers.ts index ba8d265..4345e38 100644 --- a/src/commands/phonenumbers.ts +++ b/src/commands/phonenumbers.ts @@ -44,7 +44,7 @@ export default class PhoneNumbers extends BaseCommand { const token = requireToken(flags.token, config); const client = createApiClient(token); - let phones: { phone_number: string }[]; + let phones: { id: string; phone_number: string }[]; try { const data = await client.phoneNumbers.list(); phones = (data as any).phone_numbers || []; @@ -70,7 +70,7 @@ export default class PhoneNumbers extends BaseCommand { this.log(`\n ${chalk.bold('Your phone numbers')}\n`); for (const p of phones) { const isDefault = p.phone_number === config.fromPhone; - this.log(` ${this.formatPhone(p.phone_number)}${isDefault ? chalk.green(' ← default') : ''}`); + this.log(` ${chalk.cyan(p.id)} ${this.formatPhone(p.phone_number)}${isDefault ? chalk.green(' ← default') : ''}`); } if (!config.fromPhone && phones.length > 1) { this.log(`\n ${chalk.dim('Tip: run')} ${chalk.cyan('linq phonenumbers set')} ${chalk.dim('to pick a default.')}`); diff --git a/src/lib/format.ts b/src/lib/format.ts index 430581c..4227983 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -33,6 +33,20 @@ function fmtDateAbsolute(iso: string | null | undefined): string { return new Date(iso).toLocaleString(); } +function fmtDateShort(iso: string | null | undefined): string { + if (!iso) return '–'; + const d = new Date(iso); + const now = new Date(); + const sameYear = d.getFullYear() === now.getFullYear(); + return d.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + ...(sameYear ? {} : { year: 'numeric' }), + hour: 'numeric', + minute: '2-digit', + }); +} + function fmtPhone(phone: string): string { // Format US numbers as +1 (XXX) XXX-XXXX const match = phone.match(/^\+1(\d{3})(\d{3})(\d{4})$/); @@ -77,7 +91,7 @@ export function formatPhoneNumbers(data: { phone_numbers: PhoneNumberInfo[] | nu const lines = ['\n ' + chalk.bold('Your phone numbers') + '\n']; for (const p of phones) { - lines.push(` ${fmtPhone(p.phone_number)}`); + lines.push(` ${chalk.cyan(p.id)} ${fmtPhone(p.phone_number)}`); } lines.push(''); return lines.join('\n'); @@ -190,22 +204,41 @@ interface Message { delivery_status?: string | null; } -export function formatMessagesList(data: { messages: Message[]; next_cursor?: string | null }): string { +export function formatMessagesList( + data: { messages: Message[]; next_cursor?: string | null }, + chat?: Chat, +): string { const msgs = data.messages; if (msgs.length === 0) return 'No messages found.'; - const rows = msgs.map((m) => { + // Counterparty = everyone in the chat who isn't me. Used when I sent the + // message ("you → them"). If we don't have the chat, fall back to "you →". + const counterparty = (chat?.handles || []) + .filter((h) => !h.is_me) + .map((h) => fmtPhone(h.handle)) + .join(', '); + + const direction = (m: Message): string => { + if (m.is_from_me) { + const right = counterparty ? ` ${chalk.bold(counterparty)}` : ''; + return `${chalk.dim('you')} ${chalk.dim('→')}${right}`; + } const sender = m.from_handle?.handle ? fmtPhone(m.from_handle.handle) - : m.from ? fmtPhone(m.from) : (m.is_from_me ? chalk.dim('you') : '?'); + : m.from ? fmtPhone(m.from) : '?'; + return `${chalk.bold(sender)} ${chalk.dim('→')} ${chalk.dim('you')}`; + }; + + const rows = msgs.map((m) => { + const id = chalk.cyan(m.id); + const when = chalk.dim(fmtDateShort(m.created_at)); const body = (m.parts || []) .map((p) => (p?.type === 'text' ? p.value : `[${p?.type || 'media'}]`)) .join(' ') || ''; - const status = m.delivery_status || (m.is_read ? 'read' : m.is_delivered ? 'delivered' : 'sent'); - const direction = m.is_from_me ? chalk.dim('→') : chalk.green('←'); - return `${chalk.dim(fmtDate(m.created_at))} ${direction} ${chalk.bold(sender)} ${truncate(body, 50)} ${statusColor(status)}`; + return `${id} ${when} ${direction(m)} ${truncate(body, 60)}`; }); + const lines = [...rows]; if (data.next_cursor) { lines.push(chalk.dim(`\nMore results available. Use --cursor ${data.next_cursor}`)); diff --git a/test/commands/messages/list.test.ts b/test/commands/messages/list.test.ts index ff93025..484b1f3 100644 --- a/test/commands/messages/list.test.ts +++ b/test/commands/messages/list.test.ts @@ -74,9 +74,11 @@ describe('messages list', () => { const cmd = new MessagesList(['chat-123'], config); await cmd.run(); - expect(mockFetch).toHaveBeenCalledOnce(); - const [url] = mockFetch.mock.calls[0]; - expect(url).toContain('/v3/chats/chat-123/messages'); + // messages list now also fetches the chat (to render "you → counterparty") + expect(mockFetch).toHaveBeenCalledTimes(2); + const urls = mockFetch.mock.calls.map(([u]) => u.toString()); + expect(urls.some((u) => u.includes('/v3/chats/chat-123/messages'))).toBe(true); + expect(urls.some((u) => u.includes('/v3/chats/chat-123') && !u.includes('/messages'))).toBe(true); }); it('handles pagination parameters', async () => {