Skip to content
Open
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
8 changes: 8 additions & 0 deletions apps/web/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ When to add or change one:

- Every route is under `/api/v1/` (plus `/auth/*`, `/healthz`). It must pass the auth cookie and Origin checks in
`server.ts`; the server listens on `127.0.0.1` only. Never log the launch token or the cookie secret.
- `/api/v1/session-imports` adapts the coding-agent offline migration service.
Scan the Host machine, exclude archives by default, and require previewed
fingerprints plus explicit destination directories before importing. Close
the importer before the session index. Imported tools never create native
child-agent links, revertable file changes or running-state indicators.
- Adding or changing a route or payload: update `openapi.yaml` in the same change, update `client/src/state/types.ts`,
and add a unit test.
- Session snapshots and every SSE event carry `revision`, monotonic per bound session. `renderSnapshot` ignores a
Expand All @@ -209,6 +214,9 @@ When to add or change one:
separate. Preserve original
timeline indices for turn footers, file deliveries and search reveal. Group
completion counts tool results, never canonical Tasks or child-agent success.
Imported ordinary calls with unknown outcomes may join folded groups; a group
containing unconfirmed results uses a neutral recorded-call summary, never a
completion claim. Historical tool calls cannot make a group running.
- A subagent record is context the main agent produced, so its inspector has no prompt surface:
the child's input is a collapsed "task message received" receipt, never a bubble or box, and the parent
composer is hidden while the record is open (closing it restores the composer). Do not add a composer,
Expand Down
10 changes: 10 additions & 0 deletions apps/web/client/public/session-import-logos/claudecode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/web/client/public/session-import-logos/codex.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/web/client/public/session-import-logos/qoder-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/web/client/public/session-import-logos/workbuddy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { setUi, useAppStore } from "../../state/store";
import type { MessageTimelineItem, SteeringMessage, TimelineItem } from "../../state/types";
import { Icon } from "../../ui";
import { Composer } from "../composer";
import { openSettings } from "../settings/actions";
import { ClarificationPanel } from "./ClarificationPanel";
import { CommandRow } from "./CommandRow";
import { CompactionRow, CompactionRunningRow } from "./CompactionRow";
Expand Down Expand Up @@ -35,12 +36,26 @@ import { collectTurnFooters } from "./turn-footers";
*/
function EmptyConversation() {
const t = useT();
const draft = useAppStore((state) => state.current === null);
return (
<div className="empty-state">
<span className="empty-mark" aria-hidden="true">
<Icon name="stepcode" />
</span>
<h1 className="empty-title">{t("emptyTitle")}</h1>
<div className="empty-conversation">
<div className="empty-state">
<span className="empty-mark" aria-hidden="true">
<Icon name="stepcode" />
</span>
<h1 className="empty-title">{t("emptyTitle")}</h1>
</div>
{draft ? (
<button
type="button"
id="emptySessionImport"
className="empty-session-import"
onClick={() => openSettings("migration")}
>
<Icon name="folder" size="sm" />
<span>{t("emptySessionImport")}</span>
</button>
) : null}
</div>
);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/client/src/features/conversation/ToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export const ToolRow = memo(function ToolRow({
const call = callPath(item.args, cwd);
const shown = call.shown;
// Only a file opens in the preview; a search or list path is usually a folder.
const openable = FILE_KINDS.has(toolKind(item)) ? call.openable : null;
const openable = !item.imported && FILE_KINDS.has(toolKind(item)) ? call.openable : null;
const copyValue = toolKind(item) === "bash" ? (item.args?.command ?? "") : output;
const card = toolKind(item) === "edit" || toolKind(item) === "write";
const card = !item.imported && (toolKind(item) === "edit" || toolKind(item) === "write");
if (!readOnly && item.toolName === "subagent" && item.subagentTasks?.length && item.state !== "failed") {
return <SubagentLane item={item} />;
}
Expand Down
11 changes: 9 additions & 2 deletions apps/web/client/src/features/conversation/process-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export function groupProcessRows(
rows: pending,
running: pending.some(
({ item, index }) =>
active && index > lastUser && (item.kind === "thinking" ? item.running : item.state === "pending"),
active &&
index > lastUser &&
(item.kind === "thinking" ? item.running : !item.imported && item.state === "pending"),
),
});
pending = [];
Expand All @@ -69,7 +71,9 @@ export function groupProcessRows(
!item.children?.length &&
!item.workflowRun &&
item.state !== "failed" &&
(item.state === "completed" || (active && index > lastUser && !awaiting.has(item.toolName))))
(item.state === "completed" ||
(item.imported && item.state === "unknown") ||
(active && index > lastUser && !awaiting.has(item.toolName))))
) {
pending.push({ ...row, item });
} else {
Expand Down Expand Up @@ -137,6 +141,9 @@ export function processGroupSummary(
};
}
if (!tools.length) return { label: t("processThinkingRecorded"), detail: "", icon: "brain" };
if (tools.some((tool) => tool.state !== "completed")) {
return { label: t("processRecordedTools", { count: tools.length }), detail: "", icon: "wrench" };
}
const counts = new Map<string, number>();
for (const tool of tools) {
const kind = toolKind(tool);
Expand Down
29 changes: 26 additions & 3 deletions apps/web/client/src/features/conversation/stream.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,22 @@
.main:has(.empty-state):not(:has(.stream[data-opening])) > .composer-zone {
flex: 1 1 auto;
}
/* The mark leads the question on one line, centred on the column, just above the composer. */
.empty-conversation {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
flex: 1;
padding: 0 2px 16px;
}
/* The mark and question stay centred on one line, independently of the migration shortcut. */
.empty-state {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 14px;
flex: 1;
padding: 0 2px 16px;
flex: 0 0 auto;
width: 100%;
color: var(--ink-2);
text-align: center;
}
Expand Down Expand Up @@ -197,6 +205,21 @@
}
}

.empty-session-import {
display: flex;
align-items: center;
gap: 8px;
margin-top: 20px;
padding: 8px 12px;
border-radius: var(--radius-control);
color: var(--ink-2);
font-size: 13px;
text-align: left;
transition: background-color var(--fast), color var(--fast);
}
.empty-session-import:hover { background: var(--hover); color: var(--ink); }
.empty-session-import:focus-visible { outline: 2px solid var(--focus-border); outline-offset: 2px; }

.message {
display: flex;
flex-direction: column;
Expand Down
36 changes: 34 additions & 2 deletions apps/web/client/src/features/conversation/tool-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,44 @@ const KIND_BY_TOOL: Readonly<Record<string, ToolKind>> = {
ls: "list",
};

export function toolKind(item: Pick<ToolTimelineItem, "toolName">): ToolKind {
/** Presentation-only grouping of historical tool names; execution names are never rewritten. */
const IMPORTED_KIND_BY_TOOL: Readonly<Record<string, ToolKind>> = {
exec_command: "bash",
shell: "bash",
bash: "bash",
run_terminal_cmd: "bash",
read: "read",
read_file: "read",
cat: "read",
write: "write",
write_file: "write",
apply_patch: "write",
create_file: "write",
edit: "edit",
edit_file: "edit",
str_replace_editor: "edit",
search_replace: "edit",
grep: "search",
glob: "search",
search_files: "search",
codebase_search: "search",
web_search: "search",
ls: "list",
list_dir: "list",
list_directory: "list",
};

export function toolKind(item: Pick<ToolTimelineItem, "toolName" | "imported">): ToolKind {
if (item.imported) {
const name = item.toolName.toLowerCase();
return Object.hasOwn(IMPORTED_KIND_BY_TOOL, name) ? IMPORTED_KIND_BY_TOOL[name] : "other";
}
return Object.hasOwn(KIND_BY_TOOL, item.toolName) ? KIND_BY_TOOL[item.toolName] : "other";
}

/** State comes from the host, never from output text or the whole turn's activity. */
export function toolLabel(item: Pick<ToolTimelineItem, "toolName" | "state">, t: TranslateFn): string {
export function toolLabel(item: Pick<ToolTimelineItem, "toolName" | "state" | "imported">, t: TranslateFn): string {
if (item.state === "unknown") return item.toolName || t("toolStatus.unknown");
const kind = toolKind(item);
const action = item.toolName === "find" ? "find" : kind;
return t(`toolAction.${action}.${item.state}`, { name: item.toolName || t("approvalTool") });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function turnArtifacts(
const keyOf = (path: string) => (windows ? path.toLowerCase() : path);
for (let index = Math.max(0, start); index <= footer.lastIndex; index++) {
const item = items[index];
if (item.kind !== "tool" || item.state !== "completed") continue;
if (item.kind !== "tool" || item.state !== "completed" || item.imported) continue;
if (!["edit", "write", "edit_file", "write_file", "apply_patch"].includes(item.toolName)) continue;
const raw = item.change?.path ?? item.args?.path;
const path = raw ? workspaceFilePath(cwd, raw) : undefined;
Expand Down
Loading
Loading