Skip to content

feat: add list-conversations tool#184

Open
jgalea wants to merge 2 commits into
Doist:mainfrom
jgalea:feat/list-conversations
Open

feat: add list-conversations tool#184
jgalea wants to merge 2 commits into
Doist:mainfrom
jgalea:feat/list-conversations

Conversation

@jgalea

@jgalea jgalea commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a list-conversations tool that mirrors list-channels, letting agents enumerate the direct-message conversations in a workspace.

Today the server exposes load-conversation (by ID) and channel listing, but there is no way to discover which DMs exist in a workspace. The Twist inbox view only surfaces unread/active conversations, so agents reviewing recent activity miss group DMs that did not bubble up. With this tool an agent can fetch the full list, resolve participant names, and link directly to each conversation.

Behavior

  • Args: workspaceId (required), includeArchived (optional, defaults to false).
  • When includeArchived is false: a single client.conversations.getConversations({ workspaceId }) call.
  • When includeArchived is true: batches the active and archived requests in parallel and merges the results.
  • Resolves participant names via a single batched workspaceUsers.getUserById call across all unique user IDs in the result set.
  • Returns both Markdown textContent (one section per conversation, with title or Conversation <id> fallback, archived status, last-active timestamp, participants, snippet) and a typed structuredContent payload (type: 'list_conversations').
  • Annotations: readOnlyHint: true, destructiveHint: false, idempotentHint: true (matches list-channels).

Test plan

  • npm run build — clean
  • npm test — 178 pass (16 new)
  • npm run format:check — clean (oxlint + oxfmt)
  • npm run type-check — clean
  • Tool registered in mcp-server.ts and covered by tool-annotations.test.ts
  • Output schema added to StructuredOutputSchema union and exported as ListConversationsOutput
  • Tests cover: happy path, empty result, single conversation, title fallback, snippet present/absent, archived status, SDK-supplied vs generated URL, participant deduplication, missing-name fallback, includeArchived active-only and merged paths, error propagation

Comment thread src/tools/list-conversations.ts Outdated
Comment on lines +76 to +79
const userRequests = Array.from(participantIds).map((userId) =>
client.workspaceUsers.getUserById({ workspaceId, userId }, { batch: true }),
)
const userResponses = await client.batch(...userRequests)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to be split up, there's a limit of 10 batch requests at a time (this probably needs documenting in the SDK to prevent it from happening again).

You will need to batch the list of requests into groupings of 10 and do it that way.

Comment thread src/tools/list-conversations.ts Outdated
const participantNames = conversation.userIds.map(
(id) => participantLookup[id] ?? String(id),
)
lines.push(`**Participants:** ${participantNames.join(', ')}`)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could become a very long string if it's a big group (yes, they exist 😄), we should probably consider cutting this down to maybe 5 participants and do something like "Alan, Ellie, Ian, John, Donald, and x more)".

If we do this, we could probably re-work how we fetch the participants as well, take the first five ids, add them to a set (to remove duplication), then fetch the IDs for those. You'd still need to factor in the limit of 10 requests per batch, but at least it would drastically reduce how much data is required.

If a user wishes to see all participants of a conversation, that should require a different tool call, we don't need to display them all here.

Comment thread src/tools/list-conversations.ts Outdated
const listConversations = {
name: ToolNames.LIST_CONVERSATIONS,
description:
'List conversations (direct messages) in a workspace. By default returns only active conversations; set includeArchived to true to also include archived conversations. Returns conversation IDs, titles, participant user IDs and names, archive status, last-active timestamps, snippets, and URLs.',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'List conversations (direct messages) in a workspace. By default returns only active conversations; set includeArchived to true to also include archived conversations. Returns conversation IDs, titles, participant user IDs and names, archive status, last-active timestamps, snippets, and URLs.',
'List conversations (direct messages) in a workspace. By default returns only active conversations; set includeArchived to true to also include archived conversations. Returns conversation IDs, titles, partial list of participant user IDs and names, archive status, last-active timestamps, snippets, and URLs.',

jgalea added 2 commits June 16, 2026 10:23
Mirrors list-channels so agents can enumerate direct messages in a
workspace. Returns active conversations by default, with includeArchived
to also fetch archived ones; resolves participant names via a single
batched user lookup.
…okups

Addresses review feedback:
- Resolve and display only the first 5 participants per conversation, with an
  "and N more" summary for larger groups. Callers needing the full participant
  list should load the conversation directly.
- Split participant name lookups into batches of at most 10 to respect the
  Twist API's per-batch request limit, instead of issuing one unbounded batch.
- Reflect the partial participant list in the tool description.

Structured output now returns the first 5 participant IDs and names to match
what is displayed; added tests for the truncation summary and batch chunking.
@jgalea jgalea force-pushed the feat/list-conversations branch from 435ea43 to 03334f4 Compare June 16, 2026 08:28
@jgalea

jgalea commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Pushed an update addressing the three notes (rebased onto main first):

  • Participant name lookups are now split into batches of at most 10, respecting the API's per-batch request limit, instead of one unbounded batch.
  • Display is capped at the first 5 participants per conversation with an and N more summary, and only those IDs are resolved. The structured output returns the same first-5 IDs and names so it matches what's shown, and the description now says "partial list of participant user IDs and names". Anyone needing the full participant list can load the conversation directly.
  • Applied your description wording.

Added tests for the truncation summary and the batch chunking (15 unique participants across three conversations splits into 10 + 5). Full suite green (225 tests).

@scottlovegrove scottlovegrove changed the title Add list-conversations tool feat: Add list-conversations tool Jun 16, 2026
@jgalea jgalea changed the title feat: Add list-conversations tool feat: add list-conversations tool Jun 18, 2026
@jgalea

jgalea commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @scottlovegrove — the review points are addressed in the latest commit:

  • Participant display is capped at 5 (MAX_DISPLAYED_PARTICIPANTS), rendered as A, B, C, D, E, and N more. IDs are deduped into a Set before resolving, so a big group DM no longer produces an unbounded string or a flood of lookups.
  • Name resolution batches lookups in chunks of 10 (BATCH_REQUEST_LIMIT) to respect the per-batch request cap.
  • Description updated to your suggested wording ("partial list of participant user IDs and names").

Also lowercased the PR title so the Validate Title check passes. Tests green (20 suites / 225). Ready for another look.

@scottlovegrove scottlovegrove left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jgalea — the three points from my last review are all sorted: lookups now batch in chunks of 10, participants are capped at 5 with a deduplicated Set and the "and N more" suffix, and the description wording is in. The global dedup across conversations is a nice touch — a user shared across several DMs only gets resolved once.

Before this can merge, though, the new tool isn't fully registered. AGENTS.md lists seven required locations and three are missing:

  1. src/index.tslistConversations isn't imported, isn't in the tools object, and isn't in the named exports. Consumers importing from the package can't reach it.
  2. scripts/run-tool.ts — not in the tools record, so npx tsx scripts/run-tool.ts list-conversations won't find it.
  3. src/mcp-server.ts instructions string — the tool is registered with registerTool(), but there's no - **list-conversations**: … bullet in the LLM instructions string like every other tool has, so agents get no usage guidance.

The reason CI stayed green is that only tool-annotations.test.ts self-guards registration, and that entry is present — the other three spots have no test covering them.

One non-blocking note for the structured output: userIds now holds only the first five IDs and participantNames drops any that didn't resolve, but unlike the text render there's no total-participant count, so a consumer reading the structured payload can't tell the list is partial. The description says "partial", which covers intent, but a totalParticipants field would make it machine-legible. Happy to leave that for a follow-up.

Once the three registration spots are in I'm happy to approve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants