Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
135 changes: 123 additions & 12 deletions src/chat/chat-view.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "util/git-info.h"
#include "util/subagent-tool.h"
#include "util/workflow-run.h"
#include "util/workspace-block.h"
#include "util/worktree.h"

/*
Expand Down Expand Up @@ -993,10 +994,16 @@ append_reply (XdChatView *self,
gboolean answerable)
{
g_autoptr (XdAsk) ask = NULL;
g_autofree char *workspace = NULL;
g_autofree char *without_workspace = NULL;
g_autofree char *prose = NULL;
const char *shown = text;
XdMessageRow *row;

ask = xd_ask_parse (text, &prose);
workspace = xd_workspace_block_parse (text, &without_workspace);
if (workspace != NULL)
shown = without_workspace;
ask = xd_ask_parse (shown, &prose);

{
g_autofree char *said = NULL;
Expand All @@ -1007,7 +1014,7 @@ append_reply (XdChatView *self,
said = *prose != '\0' ? g_strdup_printf ("%s\n\n**%s**", prose, ask->question)
: g_strdup_printf ("**%s**", ask->question);

row = append_row (self, XD_MESSAGE_ASSISTANT, said != NULL ? said : text);
row = append_row (self, XD_MESSAGE_ASSISTANT, said != NULL ? said : shown);
xd_message_row_set_source (row, source);
}

Expand Down Expand Up @@ -1584,12 +1591,22 @@ on_remote_messages (GObject *source,
static void
close_remote_segment (XdChatView *self)
{
g_autofree char *workspace = NULL;
g_autofree char *without_workspace = NULL;
const char *text;

if (self->remote_said == NULL || self->remote_said->len == 0)
return;

text = self->remote_said->str;
workspace =
xd_workspace_block_parse (self->remote_said->str, &without_workspace);
if (workspace != NULL)
text = without_workspace;

{
gsize visible = xd_ask_visible_length (self->remote_said->str);
g_autofree char *prose = g_strndup (self->remote_said->str, visible);
gsize visible = xd_ask_visible_length (text);
g_autofree char *prose = g_strndup (text, visible);

g_strchomp (prose);
if (*prose != '\0')
Expand Down Expand Up @@ -1769,8 +1786,38 @@ on_remote_event (XdRemoteClient *client,

if (g_strcmp0 (name, "tool") == 0 && text != NULL)
{
const char *workdir = member_string (event, "workdir", NULL);
const char *context = member_string (event, "context", NULL);

close_remote_segment (self);
if (workdir != NULL)
{
g_autofree char *tooltip = g_strdup_printf (
"Terminal on %s in %s",
xd_remote_client_get_host (self->remote), workdir);

if (context != NULL)
{
gtk_label_set_label (self->context_label, context);
gtk_widget_set_tooltip_text (
GTK_WIDGET (self->context_label), context);
}
xd_terminal_panel_set_workdir (self->terminal, workdir);
xd_file_pane_set_workdir (self->files, workdir);
xd_diff_pane_set_workdir (self->diff, workdir);
gtk_widget_set_sensitive (
GTK_WIDGET (self->terminal_button), TRUE);
gtk_widget_set_sensitive (GTK_WIDGET (self->file_button), TRUE);
gtk_widget_set_sensitive (GTK_WIDGET (self->diff_button), TRUE);
gtk_widget_set_tooltip_text (
GTK_WIDGET (self->terminal_button), tooltip);
gtk_widget_set_tooltip_text (
GTK_WIDGET (self->file_button), "Browse files");
gtk_widget_set_tooltip_text (
GTK_WIDGET (self->diff_button), "Changed files");
}
show_tool_use (self, text);
xd_diff_pane_refresh (self->diff);
queue_scroll_to_bottom (self);
return;
}
Expand Down Expand Up @@ -2011,9 +2058,7 @@ on_remote_options_received (GObject *source,
}
else
{
XdMessageRow *row = append_row (self, XD_MESSAGE_ASSISTANT, text);

xd_message_row_set_source (row, self->remote_label);
append_reply (self, text, self->remote_label, FALSE);
}
}

Expand Down Expand Up @@ -2386,6 +2431,56 @@ on_text_delta (XdChatSession *session,
*/
}

/*
* Moves the chat only to a checkout Git already associates with its current
* repository. The agent can point xd at work it created without gaining a way
* to enlarge the next turn's write sandbox to an unrelated directory.
*/
static gboolean
switch_turn_workspace (Turn *turn,
const char *reported)
{
XdChatView *self = turn->view;
g_autofree char *workdir =
xd_worktree_registered_path (turn->workdir, reported);
g_autoptr (GError) error = NULL;

if (workdir == NULL)
{
g_warning ("ignoring unregistered workspace reported by agent: %s",
reported);
return FALSE;
}

if (xd_worktree_path_equal (turn->workdir, workdir))
return TRUE;

if (!xd_storage_set_workdir (
self->storage, turn->chat_id, workdir, &error))
{
g_warning ("cannot switch to the agent's workspace: %s", error->message);
return FALSE;
}

g_free (turn->workdir);
turn->workdir = g_steal_pointer (&workdir);
g_clear_pointer (&turn->diff_tracker, xd_git_diff_tracker_free);
turn->diff_tracker = xd_git_diff_tracker_new (turn->workdir);

if (turn_is_visible (turn))
{
g_autoptr (XdChat) chat =
xd_storage_get_chat (self->storage, turn->chat_id, NULL);

if (chat != NULL)
update_context_bar (self, chat);
xd_diff_pane_refresh (self->diff);
xd_git_actions_refresh (self->git_actions);
}

return TRUE;
}

/*
* Ends the message being written, if there is one.
*
Expand All @@ -2399,28 +2494,44 @@ static void
close_segment (Turn *turn,
gboolean answerable)
{
g_autofree char *reported = NULL;
g_autofree char *without_workspace = NULL;
const char *text;

if (turn->segment->len == 0)
return;

text = turn->segment->str;
reported =
xd_workspace_block_parse (turn->segment->str, &without_workspace);
if (reported != NULL && switch_turn_workspace (turn, reported))
text = without_workspace;
Comment on lines +2507 to +2508

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Switch workspaces before capturing a tool diff

When the agent reports <workspace> immediately before a file-changing tool, on_tool_use() captures and formats the tool using the old tracker and workdir before this call processes the report. For backends such as Claude, whose file_change event is deliberately deferred until after the edit completes, the old checkout contains no change; this switch then initializes the new tracker from the already-edited checkout, permanently omitting that edit from the inline diff. The daemon implementation has the same ordering, so both paths should close the segment and switch workspaces before capturing the tool diff.

Useful? React with 👍 / 👎.

Comment on lines +2507 to +2508

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hide rejected workspace control blocks

When a syntactically valid workspace report names a missing, stale, or unregistered checkout, switch_turn_workspace() returns false and this condition leaves text pointing at the original segment. The local live transcript therefore renders and stores the internal <workspace>…</workspace> markup, even though append_reply() strips the same block after a reload and the remote streaming path always hides it. Removing the control block should be independent of whether its path is accepted for retargeting.

Useful? React with 👍 / 👎.


if (*text == '\0')
{
g_string_truncate (turn->segment, 0);
return;
}

/* Held rather than written until the turn ends, alongside its tool calls,
* so interruption can store the exact order that happened. */
remember_turn_item (turn, FALSE, turn->segment->str);
remember_turn_item (turn, FALSE, text);

if (turn_is_visible (turn))
{
g_autoptr (XdAsk) ask = answerable
? xd_ask_parse (turn->segment->str, NULL) : NULL;
? xd_ask_parse (text, NULL) : NULL;

if (ask != NULL)
{
append_reply (turn->view, turn->segment->str, turn->label, TRUE);
append_reply (turn->view, text, turn->label, TRUE);
}
else
{
/* Hide a question block until the turn finishes and it can become
* buttons, rather than briefly showing its machine-facing markup. */
gsize visible = xd_ask_visible_length (turn->segment->str);
g_autofree char *prose = g_strndup (turn->segment->str, visible);
gsize visible = xd_ask_visible_length (text);
g_autofree char *prose = g_strndup (text, visible);

g_strchomp (prose);
if (*prose != '\0')
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ xd_core_sources = files(
'util/unified-diff.c',
'util/update-channel.c',
'util/workflow-run.c',
'util/workspace-block.c',
'util/worktree.c',
'util/host-launch.c',
'util/app-paths.c',
Expand Down
49 changes: 48 additions & 1 deletion src/remote/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -1729,14 +1729,61 @@ on_turn_text (XdDaemonTurn *turn,
broadcast_event (running->server, "text", running->chat_id, "text", delta);
}

static char *
describe_turn_context (const char *workdir)
{
g_autoptr (XdGitInfo) git = xd_git_info_for_path (workdir);
g_autoptr (GString) text = g_string_new (NULL);
g_autofree char *shown = NULL;

if (workdir == NULL)
return g_strdup ("No working directory");

shown = g_str_has_prefix (workdir, g_get_home_dir ())
? g_strconcat ("~", workdir + strlen (g_get_home_dir ()), NULL)
: g_strdup (workdir);

if (git == NULL)
return g_strdup_printf ("%s — not a repository", shown);

if (git->branch != NULL)
g_string_append_printf (text, "%s %s", git->detached ? "detached at" : "⎇",
git->branch);
g_string_append_printf (text, "%s%s", text->len > 0 ? " · " : "", git->name);
if (git->linked_worktree)
g_string_append (text, " (worktree)");
g_string_append_printf (text, " · %s", shown);

return g_string_free (g_steal_pointer (&text), FALSE);
}

static void
on_turn_tool (XdDaemonTurn *turn,
const char *name,
gpointer user_data)
{
Running *running = user_data;
const char *workdir = xd_daemon_turn_get_workdir (turn);
g_autofree char *context = describe_turn_context (workdir);
g_autoptr (JsonBuilder) builder = json_builder_new ();

broadcast_event (running->server, "tool", running->chat_id, "text", name);
json_builder_begin_object (builder);
json_builder_set_member_name (builder, "event");
json_builder_add_string_value (builder, "tool");
json_builder_set_member_name (builder, "chat");
json_builder_add_string_value (builder, running->chat_id);
json_builder_set_member_name (builder, "text");
json_builder_add_string_value (builder, name);
if (workdir != NULL)
{
json_builder_set_member_name (builder, "workdir");
json_builder_add_string_value (builder, workdir);
}
json_builder_set_member_name (builder, "context");
json_builder_add_string_value (builder, context);
json_builder_end_object (builder);

broadcast (running->server, builder);
}

static gboolean forget_turn (gpointer user_data);
Expand Down
Loading
Loading