mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
Left at /api/, because these are not ours to rename: - the vendor endpoint catalogues under products/warehouse_sources - the ingress route table in docs/internal/feature-flags, which really does send /api/* to Django - ActiveCampaign's own /api/3 path segment - the billing service's routes in the Vercel sequence diagrams - the Slack event and callback URLs, which are registered in the Slack app config and have to byte-match what the code still builds
6.7 KiB
6.7 KiB
Channels & Threads (Insights Desktop / Bluebird)
Spec for the Slack-style channel revamp. A channel is a shared feed where each member message kicks off a task; the task renders as a card everyone in the channel can see. Every task is owned by a channel. Each task has one thread — a side-chain of human messages that never reach the agent unless the task author explicitly forwards one.
Django models
class Channel(models.Model):
class ChannelType(models.TextChoices):
PUBLIC = "public", "Public" # visible to the whole team
PERSONAL = "personal", "Personal" # the user's private "#me" channel
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
team = models.ForeignKey("insights.Team", on_delete=models.CASCADE, related_name="+")
name = models.CharField(max_length=128) # rendered as "#<name>"; personal channels are named "me"
channel_type = models.CharField(max_length=16, choices=ChannelType, default=ChannelType.PUBLIC)
created_by = models.ForeignKey("insights.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
deleted = models.BooleanField(default=False)
created_at = models.DateTimeField(default=django_timezone.now)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "insights_task_channel"
constraints = [
# public channel names are unique per team (soft-deleted names are reusable)
models.UniqueConstraint(
fields=["team", "name"],
condition=Q(channel_type="public", deleted=False),
name="task_channel_team_name_public_unique",
),
# exactly one live "#me" channel per user per team
models.UniqueConstraint(
fields=["team", "created_by"],
condition=Q(channel_type="personal", deleted=False),
name="task_channel_team_user_personal_unique",
),
]
class Task(...):
# every new task is owned by the channel it was kicked off in; legacy tasks stay NULL
channel = models.ForeignKey(
"tasks.Channel", on_delete=models.SET_NULL, null=True, blank=True, related_name="tasks", db_index=False
)
# + Index(fields=["channel", "-created_at"], name="insights_task_channel_feed_idx") for the feed
class TaskThreadMessage(models.Model):
"""One human message in a task's thread. Threads are human-only side
conversations; a message reaches the agent only when the task author
forwards it (send_to_agent), which stamps the forwarded_* fields."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
team = models.ForeignKey("insights.Team", on_delete=models.CASCADE, related_name="+")
task = models.ForeignKey("tasks.Task", on_delete=models.CASCADE, related_name="thread_messages")
author = models.ForeignKey("insights.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
content = models.TextField()
forwarded_to_agent_at = models.DateTimeField(null=True, blank=True)
forwarded_by = models.ForeignKey("insights.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
forwarded_run = models.ForeignKey("tasks.TaskRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", db_index=False)
created_at = models.DateTimeField(default=django_timezone.now)
class Meta:
db_table = "insights_task_thread_message"
indexes = [models.Index(fields=["task", "created_at"], name="task_thread_msg_task_created")]
Visibility
task_visibility_q gains | Q(channel__channel_type=PUBLIC): a task filed to a
public channel is visible to every team member (multiplayer feed). Tasks in a
personal channel remain visible only via the existing created_by rule.
Thread messages inherit the task's visibility.
API
/v1/projects/{id}/task_channels/
GET /— list channels: all live public channels plus the requester's personal channel. Listing lazilyget_or_creates the personal#mechannel, so every user always has one.POST / {name}— resolve-or-create a public channel by name (get_or_create, so concurrent creates and name-bridging are race-safe).PATCH /{id}/ {name}— rename a public channel. Personal channels cannot be renamed.DELETE /{id}/— soft-delete a public channel. Personal channels cannot be deleted.
Task endpoints
TaskWriteSerializeracceptschannel(UUID). Must belong to the team; a personal channel is only accepted from its owner.TaskSerializer/TaskDetailDTOemitchannel.GET /tasks/?channel=<uuid>filters the list to a channel's feed.
/v1/projects/{id}/tasks/{task_id}/thread_messages/
GET /— thread messages, ascendingcreated_at(paginated).POST / {content}— add a message as the requester. Anyone who can see the task can post.DELETE /{id}/— author-only.POST /{id}/send_to_agent/— task author only. Signals the latest run's workflow with[Thread comment from <author>] <content>viasignal_task_run_user_message, then stampsforwarded_to_agent_at,forwarded_by,forwarded_run. 400 when the task has no signalable run.
Client (Insights Desktop, bluebird mode)
- Channel feed — the channel view becomes a Slack-like feed: each item is the kickoff message (author avatar + name + prompt) with a task card (title, status badge, repo, replies count) underneath. The composer at the bottom kicks off a task owned by the channel; the author stays in the feed and the card updates live (poll). The existing tabs (Inbox / Artifacts / Recents / CONTEXT.md) stay above the feed.
- Threads — a collapsible right-side panel shows a task's thread: message list plus reply composer. Each message row has a hover menu; the task author gets "Send to agent" there. Forwarded messages show a "Sent to agent" badge. Opening a thread from a feed card shows the panel next to the feed; opening a task shows the same panel next to the task detail (collapsible).
- #me — the sidebar pins the personal channel (
#me) above the channel list; it is each user's private feed. - One identity — the backend
ChannelUUID keys everything: task ownership, feeds, threads, canvases (Canvas.channel), CONTEXT.md instructions, and per-user stars. (The former desktop-file-system folder bridge — folders mapped to channels by name — was retired when canvases became first-class rows; seeproducts/canvas/.)
Out of scope (v1)
- Channel membership / invited private channels (only team-public + personal).
- Message editing and emoji reactions.
- Real-time push for feed/thread updates (clients poll; SSE can come later).