Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
f851ef1
Integrated copilotkit to integrate with chatui.
srtab Feb 26, 2026
e51bdb7
Cleaned changelog.
srtab Feb 26, 2026
4f32126
Merge branch 'main' into feat/copilotkit-chat
srtab Mar 14, 2026
cdf2531
Merge branch 'main' into feat/copilotkit-chat
srtab Apr 8, 2026
7bbc8f8
Merge branch 'main' into feat/copilotkit-chat
srtab Apr 23, 2026
80e6689
build(deps): bump ag-ui-langgraph from 0.0.31 to 0.0.34
srtab Apr 23, 2026
8174ceb
test(chat): Rewrite chat API tests for AG-UI endpoint
srtab Apr 23, 2026
7fa1a82
feat(activity): add Activity.thread_id for LangGraph checkpoint persi…
srtab Apr 23, 2026
a66d76c
feat(chat): scaffold chat app and shared checkpointer factory
srtab Apr 23, 2026
39a317a
feat(jobs): persist agent-run state via AsyncRedisSaver; accept threa…
srtab Apr 23, 2026
81ea1a2
feat(activity): mint thread_id per run and persist on Activity
srtab Apr 23, 2026
8489aa4
feat(chat): ChatThread model with per-user ownership
srtab Apr 23, 2026
82e0af5
feat(chat): dual-auth endpoint with thread ownership and concurrency …
srtab Apr 23, 2026
9d63391
feat(chat): dashboard views (list, detail, from-activity bridge)
srtab Apr 23, 2026
f4131eb
feat(chat): list/detail/message/composer templates and Alpine streami…
srtab Apr 23, 2026
576c94c
feat(chat): sidebar entry, chat icon, and 'Continue as chat' button o…
srtab Apr 23, 2026
7db71a5
style(chat): silence ty diagnostics introduced by new code
srtab Apr 23, 2026
83919e0
refactor(chat): simplify views and reuse open_checkpointer in addressors
srtab Apr 24, 2026
dde32f9
fix(chat): surface stream errors and close TOCTOU race on active_run_id
srtab Apr 24, 2026
30abbc6
feat(chat): scaffold build_turns helper module
srtab Apr 24, 2026
f867015
feat(chat): build_turns handles HumanMessage content
srtab Apr 24, 2026
1ed2873
feat(chat): build_turns handles AIMessage with string content
srtab Apr 24, 2026
e5ad7ae
feat(chat): build_turns preserves Anthropic block interleaving
srtab Apr 24, 2026
585a78b
feat(chat): build_turns pairs ToolMessage results, drops orphans
srtab Apr 24, 2026
26213b5
feat(chat): view emits turns via build_turns
srtab Apr 24, 2026
5db9f00
feat(chat): renderMarkdown wrapper around marked + highlight.js
srtab Apr 24, 2026
9394329
feat(chat): per-tool signature + expanded-body renderers
srtab Apr 24, 2026
94f06bd
feat(chat): diff/grep/bash/todos stylesheet for expanded tool views
srtab Apr 24, 2026
298cd39
feat(chat): component styles for shell, turn, tool cards, composer, rail
srtab Apr 24, 2026
63845dd
feat(chat): right-rail partial with repo, status, todos, files touched
srtab Apr 24, 2026
dae0008
feat(chat): sticky composer with kbd hint, send/stop states, jump pill
srtab Apr 24, 2026
d380ad7
feat(chat): rewrite stream controller around turns/segments
srtab Apr 24, 2026
0245080
feat(chat): rewrite detail template around turns/segments with rail +…
srtab Apr 24, 2026
bdb1bb8
chore(chat): remove legacy _message and _tool_call_card partials
srtab Apr 24, 2026
7f0e2b6
fix(chat): bash tool renderer key, icon templates, user bubble sizing
srtab Apr 24, 2026
ef8bf26
fix(chat): right-align user bubble, persistent status bar, unblocked …
srtab Apr 24, 2026
afb96c7
fix(chat): drop turn markers, keep streaming signal as left thread line
srtab Apr 24, 2026
d3ea097
feat(chat): task renderer + suppress nested frames while task runs
srtab Apr 24, 2026
6aba2a4
feat(chat): hero repo picker, reasoning segments, file-op rail, run r…
srtab Apr 25, 2026
2289806
style(chat): markdown hierarchy + grep/glob signatures
srtab Apr 25, 2026
b9287d1
feat(nav): promote chat to sidebar CTA, move run button to activity
srtab Apr 25, 2026
4f6580e
feat(chat): surface pre-existing MR pill on branches with open MRs
srtab Apr 25, 2026
967a6c0
feat(chat): publish-phase chips, MR pill, empty-state polish
srtab Apr 25, 2026
a36d8c4
feat(chat): step counter on running task, surface sibling tasks
srtab Apr 26, 2026
d9ab514
refactor(chat): drive MR pill via STATE_SNAPSHOT, split api/views
srtab Apr 27, 2026
62f5ef8
Merge branch 'main' into feat/copilotkit-chat
srtab Apr 27, 2026
9de9e2c
Review improvements.
srtab Apr 27, 2026
030262c
fix(chat): Handle parallel tool_calls and misrouted arg deltas
srtab Apr 28, 2026
0aafa31
fix(chat): Defer textarea autosize until after DOM update
srtab Apr 28, 2026
f46d4db
feat(chat): Add new chat link to expired conversation banner
srtab Apr 28, 2026
db82fc6
docs(mcp): Reflect MR reuse on existing branches in submit_job
srtab Apr 28, 2026
166e628
feat(titling): Add LLM-generated titles for activities and chat threads
srtab Apr 29, 2026
410414f
Potential fix for pull request finding 'CodeQL / Double escaping or u…
srtab Apr 29, 2026
fd3bd3c
test(mcp): Update _FakeActivity mock for title-task enqueue
srtab Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions daiv/accounts/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

SECTION_URL_NAMES: dict[str, set[str]] = {
"dashboard": {"dashboard"},
"runs": {"agent_run_new"},
"activity": {"activity_list", "activity_detail", "activity_stream", "activity_download_md"},
"activity": {"activity_list", "activity_detail", "activity_stream", "activity_download_md", "agent_run_new"},
"chat": {"chat_list", "chat_new", "chat_detail"},
"schedules": {
"schedule_list",
"schedule_create",
Expand Down
17 changes: 12 additions & 5 deletions daiv/accounts/templates/accounts/_sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
</div>

{# Primary action — promoted out of the nav list #}
<a href="{% url 'runs:agent_run_new' %}"
data-testid="nav-start-run-cta"
class="group relative mx-3 mb-5 flex items-center gap-2.5 overflow-hidden rounded-lg bg-gradient-to-br from-violet-500 to-indigo-500 px-3 py-2.5 text-sm font-semibold tracking-tight text-white shadow-lg shadow-violet-500/20 ring-1 ring-inset ring-white/15 transition-all duration-200 hover:shadow-violet-500/40 hover:ring-white/25 {% if nav_active_section == 'runs' %}ring-2 ring-white/30 shadow-violet-500/40{% endif %}">
<a href="{% url 'chat_new' %}"
data-testid="nav-chat-cta"
class="group relative mx-3 mb-5 flex items-center gap-2.5 overflow-hidden rounded-lg bg-gradient-to-br from-violet-500 to-indigo-500 px-3 py-2.5 text-sm font-semibold tracking-tight text-white shadow-lg shadow-violet-500/20 ring-1 ring-inset ring-white/15 transition-all duration-200 hover:shadow-violet-500/40 hover:ring-white/25">
<span aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 h-1/2 bg-gradient-to-b from-white/15 to-transparent"></span>
<span class="relative flex h-6 w-6 items-center justify-center rounded-md bg-white/15 ring-1 ring-inset ring-white/20">
{% icon "bolt" "h-3.5 w-3.5" %}
{% icon "squares-plus" "h-3.5 w-3.5" %}
</span>
<span class="relative flex-1">{% translate "Start a run" %}</span>
<span class="relative flex-1">{% translate "New chat" %}</span>
<span aria-hidden="true" class="relative text-white/70 transition-transform duration-200 group-hover:translate-x-0.5">&rarr;</span>
</a>

Expand All @@ -42,6 +42,13 @@
{% endif %}
</a>

<a href="{% url 'chat_list' %}"
class="group relative mb-0.5 flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-gray-400 transition-colors hover:text-gray-200 {% nav_active 'chat' %}">
<span class="absolute left-0 top-1.5 bottom-1.5 w-[3px] rounded-r bg-violet-500 {% if nav_active_section != 'chat' %}opacity-0{% endif %}"></span>
{% icon "chat-bubble" "h-4 w-4" %}
<span>{% translate "Chat" %}</span>
</a>

<a href="{% url 'schedule_list' %}"
class="group relative mb-0.5 flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-gray-400 transition-colors hover:text-gray-200 {% nav_active 'schedules' %}">
<span class="absolute left-0 top-1.5 bottom-1.5 w-[3px] rounded-r bg-violet-500 {% if nav_active_section != 'schedules' %}opacity-0{% endif %}"></span>
Expand Down
30 changes: 30 additions & 0 deletions daiv/activity/migrations/0009_activity_thread_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 6.0.4 on 2026-04-23 23:18

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [("activity", "0008_activity_batch_id")]

operations = [
migrations.AddField(
model_name="activity",
name="thread_id",
field=models.CharField(
blank=True,
db_index=True,
help_text="LangGraph checkpoint key. Lets chat resume this run.",
max_length=64,
null=True,
unique=True,
verbose_name="thread ID",
),
),
migrations.AddConstraint(
model_name="activity",
constraint=models.CheckConstraint(
condition=models.Q(("thread_id__isnull", True)) | models.Q(("thread_id", ""), _negated=True),
name="activity_thread_id_nonempty",
),
),
]
15 changes: 15 additions & 0 deletions daiv/activity/migrations/0010_title.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Generated by Django 6.0.4 on 2026-04-28 20:57

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [("activity", "0009_activity_thread_id")]

operations = [
migrations.AddField(
model_name="activity",
name="title",
field=models.CharField(blank=True, default="", max_length=120, verbose_name="title"),
)
]
20 changes: 20 additions & 0 deletions daiv/activity/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ class Activity(models.Model):

status = models.CharField(_("status"), max_length=10, choices=ActivityStatus.choices, default=ActivityStatus.READY)

title = models.CharField(_("title"), max_length=120, blank=True, default="")

batch_id = models.UUIDField(
_("batch ID"),
null=True,
Expand All @@ -96,6 +98,16 @@ class Activity(models.Model):
help_text=_("Shared identifier for activities from the same submission."),
)

thread_id = models.CharField(
_("thread ID"),
max_length=64,
null=True,
blank=True,
unique=True,
db_index=True,
help_text=_("LangGraph checkpoint key. Lets chat resume this run."),
)

external_username = models.CharField(
_("external username"),
max_length=255,
Expand Down Expand Up @@ -175,6 +187,14 @@ class Meta:
condition=models.Q(external_username__gt=""),
),
]
constraints = [
# ``thread_id`` is unique=True; "" would collide on the second insert
# under Postgres (which treats NULL as not-equal but "" as a real
# value). Forbid the empty-string sentinel so callers must use NULL.
models.CheckConstraint(
condition=models.Q(thread_id__isnull=True) | ~models.Q(thread_id=""), name="activity_thread_id_nonempty"
)
]

def __str__(self) -> str:
return f"{self.get_trigger_type_display()} on {self.repo_id} ({self.status})"
Expand Down
47 changes: 42 additions & 5 deletions daiv/activity/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
from asgiref.sync import async_to_sync
from jobs.tasks import run_job_task

from activity.models import Activity
from activity.models import Activity, TriggerType
from automation.titling.tasks import generate_title_task

_PROMPT_DRIVEN = {TriggerType.API_JOB, TriggerType.MCP_JOB, TriggerType.UI_JOB}

if TYPE_CHECKING:
from notifications.choices import NotifyOn
Expand Down Expand Up @@ -96,6 +99,8 @@ def create_activity(
external_username: str = "",
notify_on: NotifyOn | None = None,
batch_id: uuid.UUID | None = None,
thread_id: str | None = None,
title: str = "",
) -> Activity:
"""Create an Activity record linked to a DBTaskResult.

Expand All @@ -116,6 +121,8 @@ def create_activity(
external_username=external_username,
notify_on=notify_on,
batch_id=batch_id,
thread_id=thread_id,
title=title[: Activity._meta.get_field("title").max_length],
)


Expand All @@ -135,6 +142,8 @@ async def acreate_activity(
external_username: str = "",
notify_on: NotifyOn | None = None,
batch_id: uuid.UUID | None = None,
thread_id: str | None = None,
title: str = "",
) -> Activity:
"""Async variant of create_activity."""
return await Activity.objects.acreate(
Expand All @@ -152,6 +161,8 @@ async def acreate_activity(
external_username=external_username,
notify_on=notify_on,
batch_id=batch_id,
thread_id=thread_id,
title=title[: Activity._meta.get_field("title").max_length],
)


Expand All @@ -175,16 +186,27 @@ async def asubmit_batch_runs(
_validate(repos)
batch_id = uuid.uuid4()

async def _submit_one(target: RepoTarget) -> Activity | BatchSubmitFailure:
schedule_run_base = 0
if trigger_type == TriggerType.SCHEDULE and scheduled_job is not None:
schedule_run_base = await Activity.objects.filter(scheduled_job=scheduled_job).acount()

async def _submit_one(idx: int, target: RepoTarget) -> Activity | BatchSubmitFailure:
ref_for_task = target.ref or None
thread_id = str(uuid.uuid4())
try:
task = await run_job_task.aenqueue(repo_id=target.repo_id, prompt=prompt, ref=ref_for_task, use_max=use_max)
task = await run_job_task.aenqueue(
repo_id=target.repo_id, prompt=prompt, ref=ref_for_task, use_max=use_max, thread_id=thread_id
)
except Exception as err: # noqa: BLE001
logger.exception("submit_batch_runs: enqueue failed for repo_id=%s batch_id=%s", target.repo_id, batch_id)
return BatchSubmitFailure(repo_id=target.repo_id, ref=target.ref, error=f"{type(err).__name__}: {err}")

activity_title = ""
if trigger_type == TriggerType.SCHEDULE and scheduled_job is not None:
activity_title = f"{scheduled_job.name} · run #{schedule_run_base + idx + 1}"

try:
return await acreate_activity(
activity = await acreate_activity(
trigger_type=trigger_type,
task_result_id=task.id,
repo_id=target.repo_id,
Expand All @@ -196,6 +218,8 @@ async def _submit_one(target: RepoTarget) -> Activity | BatchSubmitFailure:
external_username=external_username,
notify_on=notify_on,
batch_id=batch_id,
thread_id=thread_id,
title=activity_title,
)
except Exception:
logger.exception(
Expand All @@ -205,9 +229,22 @@ async def _submit_one(target: RepoTarget) -> Activity | BatchSubmitFailure:
)
return BatchSubmitFailure(repo_id=target.repo_id, ref=target.ref, error="ActivityCreationFailed")

if trigger_type in _PROMPT_DRIVEN and prompt:
try:
await generate_title_task.aenqueue(
entity_type="activity",
pk=str(activity.pk),
prompt=prompt,
repo_id=target.repo_id,
ref=target.ref or "",
)
except Exception: # noqa: BLE001
logger.exception("Failed to enqueue title task for activity %s", activity.pk)
return activity

# return_exceptions=True guards against BaseException (CancelledError, etc.) aborting the
# whole batch; _submit_one already catches Exception itself.
outcomes = await asyncio.gather(*[_submit_one(t) for t in repos], return_exceptions=True)
outcomes = await asyncio.gather(*[_submit_one(i, t) for i, t in enumerate(repos)], return_exceptions=True)

activities: list[Activity] = []
failed: list[BatchSubmitFailure] = []
Expand Down
19 changes: 15 additions & 4 deletions daiv/activity/static/activity/js/activity-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@
* Alpine.js components for real-time activity status updates via SSE.
*
* activityStream (list page) — tracks multiple activities in place:
* dotClass(id, fallback) → "status-dot-{variant}" CSS class
* statusClass(id, fallback) → "status-badge-{variant}" CSS class
* dotClass(id, fallback) → object toggling status-dot-{variant} classes
* statusClass(id, fallback) → object toggling status-badge-{variant} classes
* statusLabel(id, fallback) → human-readable label
*
* Object class maps (rather than a single string) are required so Alpine
* removes the previously rendered variant class when the status transitions —
* otherwise the static server-rendered class lingers alongside the new one
* and the later CSS rule wins.
*
* activityDetail (detail page) — subscribes to one activity and reloads the
* page on any state change so server-rendered fields (started_at, finished_at,
* elapsed counter, duration, timeline dots) reflect the new state.
*/
document.addEventListener("alpine:init", () => {
const VARIANTS = ["success", "failed", "running", "pending"];

function statusVariantFor(status) {
if (status === "SUCCESSFUL") return "success";
if (status === "FAILED") return "failed";
Expand All @@ -25,6 +32,10 @@ document.addEventListener("alpine:init", () => {
return "Pending";
}

function variantClassMap(prefix, active) {
return Object.fromEntries(VARIANTS.map((v) => [prefix + v, v === active]));
}

Alpine.data("activityStream", (streamUrl, inFlightIds) => ({
updates: {},
init() {
Expand All @@ -42,10 +53,10 @@ document.addEventListener("alpine:init", () => {
source.onerror = () => source.close();
},
statusClass(id, fallback) {
return "status-badge-" + statusVariantFor(this.updates[id]?.status || fallback);
return variantClassMap("status-badge-", statusVariantFor(this.updates[id]?.status || fallback));
},
dotClass(id, fallback) {
return "status-dot-" + statusVariantFor(this.updates[id]?.status || fallback);
return variantClassMap("status-dot-", statusVariantFor(this.updates[id]?.status || fallback));
},
statusLabel(id, fallback) {
const update = this.updates[id];
Expand Down
14 changes: 14 additions & 0 deletions daiv/activity/static/activity/js/prompt-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ document.addEventListener("alpine:init", () => {
repoPickerUrl = "",
branchPickerTemplate = "",
conflictMessageTemplate = "Repository already in the list: __LABEL__.",
onChangeEvent = "",
}) => ({
repos: (initialRepos || []).map(r => ({ slug: r.repo_id, ref: r.ref || "" })),
useMax: initialUseMax,
maxRepos,
repoPickerUrl,
branchPickerTemplate,
conflictMessageTemplate,
onChangeEvent,

popover: null,
editingIndex: null,
Expand All @@ -45,6 +47,15 @@ document.addEventListener("alpine:init", () => {
});
},

_emitChange() {
if (!this.onChangeEvent) return;
window.dispatchEvent(
new CustomEvent(this.onChangeEvent, {
detail: { repos: this.repos.map((r) => ({ repo_id: r.slug, ref: r.ref || "" })) },
}),
);
},

destroy() {
if (this._conflictTimer) clearTimeout(this._conflictTimer);
},
Expand Down Expand Up @@ -109,6 +120,7 @@ document.addEventListener("alpine:init", () => {
if (this.editingIndex === null) this.repos.push(entry);
else this.repos.splice(this.editingIndex, 1, entry);
this.closePopover();
this._emitChange();
},

setBranch(ref) {
Expand All @@ -122,6 +134,7 @@ document.addEventListener("alpine:init", () => {
}
this.repos[this.editingIndex].ref = ref;
this.closePopover();
this._emitChange();
},

remove(index) {
Expand All @@ -134,6 +147,7 @@ document.addEventListener("alpine:init", () => {
if (this.editingIndex === index) this.closePopover();
else if (index < this.editingIndex) this.editingIndex -= 1;
}
this._emitChange();
},

_findConflict(slug, ref, skipIndex) {
Expand Down
Loading
Loading