Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6a2d246
fix(pr): update labels and reviewers without redundant reloads (#11117)
maria-rcks Sep 11, 2026
27eb79d
fix(chat): fold question answers into tool activity (#11014)
maria-rcks Sep 11, 2026
48654c1
fix(usage): flag unpriced model activity instead of showing $0.00 (#1…
maria-rcks Sep 11, 2026
5735693
fix(server): let Claude launch args override the derived permission m…
maria-rcks Sep 11, 2026
20ef250
fix(editors): accept root paths and Windows servers in Zed remote lin…
maria-rcks Sep 11, 2026
4d06156
fix(web): center pull request unavailable states (#11110)
maria-rcks Sep 11, 2026
0a37240
fix(web): remove sidebar pull request link icon (#11179)
maria-rcks Sep 11, 2026
02297e3
fix(ui): color linked pr counts by aggregate status (#11180)
maria-rcks Sep 11, 2026
6c69534
fix(preview): render website favicons for browser tool activity (#11032)
maria-rcks Sep 11, 2026
18c5a1d
fix(web): simplify pull request summary sections (#10612)
maria-rcks Sep 11, 2026
ef6fa11
fix(web): preserve drafts when compacting context (#11103)
maria-rcks Sep 11, 2026
57aee3e
fix(server): queue messages during context compaction (#11107)
maria-rcks Sep 11, 2026
8fc2536
perf(web): format minimap previews only when opened (#11181)
juliusmarminge Sep 11, 2026
a9dabbf
perf(web): reuse completed Markdown prefixes while streaming (#11193)
juliusmarminge Sep 11, 2026
d7d7f8f
perf(web): resume syntax highlighting from completed lines (#11196)
juliusmarminge Sep 11, 2026
8078c53
perf(web): preserve completed code-line DOM while streaming (#11198)
juliusmarminge Sep 11, 2026
211618f
perf(web): huge-thread switch no longer blanks the chat pane (#11169)
juliusmarminge Sep 11, 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
12 changes: 9 additions & 3 deletions apps/desktop/src/electron/ElectronShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,16 @@ describe("ElectronShell", () => {
openExternalMock.mockResolvedValue(undefined);

const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal("zed://ssh/example.com/home/user/project");
const results = yield* Effect.all([
electronShell.openExternal("zed://ssh/example.com/home/user/project"),
electronShell.openExternal("zed://ssh/example.com/"),
]);

assert.equal(result, true);
assert.deepEqual(openExternalMock.mock.calls, [["zed://ssh/example.com/home/user/project"]]);
assert.deepEqual(results, [true, true]);
assert.deepEqual(openExternalMock.mock.calls, [
["zed://ssh/example.com/home/user/project"],
["zed://ssh/example.com/"],
]);
}).pipe(Effect.provide(ElectronShell.layer)),
);

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/electron/ElectronShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const REMOTE_EDITOR_PROTOCOLS = new Set(
);

// Zed's host sits in the first path segment, so it needs its own userinfo ban.
const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.+$/;
const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.*$/;

const isRemoteEditorUrl = (url: URL) =>
REMOTE_EDITOR_PROTOCOLS.has(url.protocol) &&
Expand Down
13 changes: 7 additions & 6 deletions apps/mobile/src/features/threads/QuestionAnswerHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
UserInputAttachments,
} from "@t3tools/contracts";
import { Image, Linking, Pressable, View } from "react-native";
import { getQuestionAnswerText } from "@t3tools/client-runtime/work-log/user-input";
import { AppText as Text } from "../../components/AppText";
import { useAssetUrl } from "../../state/assets";

Expand Down Expand Up @@ -41,6 +42,7 @@ export function QuestionAnswerHistory(props: {
<View className="gap-2">
{[
...new Set([
...Object.keys(props.answer.questionTextById ?? {}),
...Object.keys(props.answer.answers),
...Object.keys(props.answer.attachmentsByQuestionId),
]),
Expand All @@ -51,12 +53,11 @@ export function QuestionAnswerHistory(props: {
{props.answer.questionTextById[questionId]}
</Text>
) : null}
<Text className="text-sm text-foreground">
{[props.answer.answers[questionId]]
.flat()
.filter((value): value is string => typeof value === "string")
.join(", ")}
</Text>
{getQuestionAnswerText(props.answer.answers[questionId]) ? (
<Text className="ml-3 text-sm text-foreground-muted">
{getQuestionAnswerText(props.answer.answers[questionId])}
</Text>
) : null}
{(props.answer.attachmentsByQuestionId[questionId] ?? []).map((attachment) => (
<AnswerFile
key={attachment.id}
Expand Down
10 changes: 7 additions & 3 deletions apps/mobile/src/features/threads/thread-list-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,24 @@ export const THREAD_LIST_COMPACT_INSET = HOME_HORIZONTAL_INSET;
const SIDEBAR_ROW_RADIUS = 12;

function pullRequestTintColor(
pr: Pick<ThreadPrPresentation, "state" | "isDraft" | "others">,
pr: Pick<ThreadPrPresentation, "state" | "isDraft" | "others" | "kind">,
colorScheme: "light" | "dark",
) {
const dark = colorScheme === "dark";
if (pr.others > 0 || (pr.state === "open" && pr.isDraft === true)) {
if (pr.state === "open" && pr.isDraft === true) {
return dark ? "#a1a1aa" : "#71717a";
}
switch (pr.state) {
case "open":
return dark ? "#34d399" : "#059669";
case "merged":
return dark ? "#a78bfa" : "#7c3aed";
case null:
case "closed":
if (pr.kind === "stack" || pr.others > 0) {
return dark ? "#fb7185" : "#e11d48";
}
return dark ? "#a1a1aa" : "#71717a";
case null:
return dark ? "#a1a1aa" : "#71717a";
}
}
Expand Down
8 changes: 7 additions & 1 deletion apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,13 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
? materialYouStyleLayoutActive
? "accent-thread-selected-foreground"
: "accent-user-bubble-foreground"
: "accent-foreground-muted"
: pr.state === null || pr.isDraft
? "accent-foreground-muted"
: pr.state === "open"
? "accent-adaptive-emerald-600-400"
: pr.state === "closed"
? "accent-adaptive-rose-600-400"
: "accent-adaptive-violet-600-400"
}
/>
) : null}
Expand Down
21 changes: 20 additions & 1 deletion apps/mobile/src/features/threads/thread-work-log.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { QuestionAnswerHistory } from "./QuestionAnswerHistory";
import {
getQuestionAnswerPreview,
hasQuestionAnswer,
} from "@t3tools/client-runtime/work-log/user-input";
import * as Haptics from "expo-haptics";
import { Image } from "expo-image";
import { type AppSymbolName, SymbolView } from "../../components/AppSymbol";
Expand Down Expand Up @@ -745,6 +749,10 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
const viewedImagePath = workEntryViewedImagePath(row.workEntry);
const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry);
const previewText = workEntryRowLabel(row.workEntry);
const answerPreview = row.workEntry.questionAnswer
? getQuestionAnswerPreview(row.workEntry.questionAnswer)
: null;
const accessiblePreview = [previewText, answerPreview].filter(Boolean).join(": ");
const displayText = workEntryRowLabel(row.workEntry, expanded);
const iconIsDestructive = row.icon === "alert" || row.icon === "warning";
const failed = row.status === "failure";
Expand All @@ -759,7 +767,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
>
<Pressable
accessibilityRole={canExpand ? "button" : undefined}
accessibilityLabel={failed ? `${previewText}, tool call failed` : previewText}
accessibilityLabel={failed ? `${accessiblePreview}, tool call failed` : accessiblePreview}
accessibilityHint={
canExpand
? `Double tap to ${expanded ? "hide" : "show"} full details. Long press to copy.`
Expand Down Expand Up @@ -820,6 +828,17 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
numberOfLines={expanded ? undefined : 1}
>
{displayText}
{answerPreview ? (
<Text
className={
!expanded &&
row.workEntry.questionAnswer &&
hasQuestionAnswer(row.workEntry.questionAnswer)
? "text-foreground"
: "text-foreground-subtle"
}
>{` ${answerPreview}`}</Text>
) : null}
</Text>
</>
)}
Expand Down
9 changes: 7 additions & 2 deletions apps/mobile/src/features/usage/UsageRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EnvironmentId, USAGE_CONTRACT_VERSION } from "@t3tools/contracts";
import { useNavigation } from "@react-navigation/native";
import {
isCompatibleUsageContractVersion,
isModelCostUnknown,
type DailyTotals,
type MergedUsage,
} from "@t3tools/shared/usageMerge";
Expand Down Expand Up @@ -607,10 +608,14 @@ function ModelsSection(props: { readonly merged: MergedUsage }) {
{model.model}
</Text>
<Text className="text-sm text-foreground-muted">
{formatPercent(model.costShare)} of cost · {formatTokens(model.totalTokens)} tokens
{isModelCostUnknown(model)
? `no known rates · ${formatTokens(model.totalTokens)} tokens`
: `${formatPercent(model.costShare)} of cost · ${formatTokens(model.totalTokens)} tokens`}
</Text>
</View>
<Text className="text-base tabular-nums text-foreground">{formatUsd(model.costUsd)}</Text>
<Text className="text-base tabular-nums text-foreground">
{isModelCostUnknown(model) ? "Unpriced" : formatUsd(model.costUsd)}
</Text>
</View>
))}
</SettingsSection>
Expand Down
39 changes: 25 additions & 14 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Option from "effect/Option";
import { foldUserInputActivities } from "@t3tools/client-runtime/work-log/user-input";
import * as Schema from "effect/Schema";
import {
requestKindFromRequestType,
Expand Down Expand Up @@ -405,7 +406,7 @@ function deriveWorkLogEntries(
): DerivedWorkLogEntry[] {
const ordered = Arr.sort(activities, activityOrder);
const entries: DerivedWorkLogEntry[] = [];
for (const activity of ordered) {
for (const activity of foldUserInputActivities(ordered)) {
if (activity.tone !== "error" && isWorktreeSetupActivity(activity.kind)) continue;
if (activity.kind === "tool.started") continue;
// Like web: an agent's task.started row anchors its batch. It has a fixed
Expand Down Expand Up @@ -936,6 +937,7 @@ function workEntryStatus(entry: WorkLogEntry): ThreadFeedActivity["status"] {
function workEntryIcon(entry: DerivedWorkLogEntry): ThreadFeedActivity["icon"] {
if (entry.agentSpawn) return "agent";
if (
entry.questionAnswer ||
entry.sourceActivityKind === "user-input.requested" ||
entry.sourceActivityKind === "user-input.resolved"
) {
Expand Down Expand Up @@ -2184,21 +2186,30 @@ export function buildThreadFeed(
: loadedMessages;
const oldestLoadedMessageCreatedAt =
options?.loadedMessages !== undefined ? (loadedMessages[0]?.createdAt ?? null) : null;
const activityEntries = getThreadFeedActivityEntries(thread.activities);
const activityEntries = getThreadFeedActivityEntries(thread.activities).filter(
(entry) =>
oldestLoadedMessageCreatedAt === null || entry.createdAt >= oldestLoadedMessageCreatedAt,
);
const foldedAnswerMessageIds = new Set(
activityEntries.flatMap((entry) =>
entry.activity.workEntry.questionAnswer
? [`async-answer:${entry.activity.workEntry.questionAnswer.requestId}`]
: [],
),
);
const entries = Arr.sortWith(
[
...messages.map((message) => {
let entry = messageEntriesCache.get(message);
if (!entry) {
entry = { type: "message", id: message.id, createdAt: message.createdAt, message };
messageEntriesCache.set(message, entry);
}
return entry;
}),
...activityEntries.filter(
(entry) =>
oldestLoadedMessageCreatedAt === null || entry.createdAt >= oldestLoadedMessageCreatedAt,
),
...messages
.filter((message) => message.role !== "user" || !foldedAnswerMessageIds.has(message.id))
.map((message) => {
let entry = messageEntriesCache.get(message);
if (!entry) {
entry = { type: "message", id: message.id, createdAt: message.createdAt, message };
messageEntriesCache.set(message, entry);
}
return entry;
}),
...activityEntries,
],
(s) => new Date(s.createdAt),
Order.Date,
Expand Down
23 changes: 17 additions & 6 deletions apps/mobile/src/state/thread-pr-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,16 @@ export function presentThreadLinkedPullRequests(
const badge = resolveThreadPullRequestBadge(links);
if (link === null || badge === null) return null;
const snapshot = link.snapshot;
const state = badge.kind === "stack" ? badge.state : (snapshot?.state ?? null);
const isDraft = snapshot?.isDraft === true && state === "open";
const linkedCount = badge.kind === "pull-request" && badge.others > 0 ? badge.others + 1 : null;
const isMultiple = badge.kind === "stack" || linkedCount !== null;
const state = isMultiple
? badge.state === "draft"
? "open"
: badge.state
: (snapshot?.state ?? null);
const isDraft = isMultiple
? badge.state === "draft"
: snapshot?.isDraft === true && state === "open";
const label =
badge.kind === "stack"
? String(badge.layers)
Expand All @@ -83,12 +90,16 @@ export function presentThreadLinkedPullRequests(
label,
accessibilityLabel:
badge.kind === "stack"
? `${badge.layers} pull requests in stack, ${state ?? "status pending"}`
: `#${link.number} pull request ${state === null ? "status pending" : isDraft ? "draft" : state}${badge.others > 0 ? `, ${badge.others} more linked` : ""}`,
? `${badge.layers} pull requests in stack, ${isDraft ? "draft" : (state ?? "status pending")}`
: linkedCount !== null
? `${linkedCount} linked pull requests, overall ${badge.state}`
: `#${link.number} pull request ${state === null ? "status pending" : isDraft ? "draft" : state}`,
textClassName:
linkedCount !== null || state === null || isDraft
state === null || isDraft
? "text-foreground-muted"
: PR_STATE_TEXT_CLASS[state],
: isMultiple && state === "closed"
? "text-adaptive-rose-600-400"
: PR_STATE_TEXT_CLASS[state],
};
}

Expand Down
34 changes: 33 additions & 1 deletion apps/mobile/src/state/use-thread-pr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,42 @@ describe("presentThreadLinkedPullRequests", () => {
kind: "pull-request",
label: "+2",
others: 1,
textClassName: "text-foreground-muted",
state: "open",
isDraft: false,
textClassName: "text-adaptive-emerald-600-400",
});
});

it.each([
["closed", false, "closed", false, "closed", false, "text-adaptive-rose-600-400"],
["open", true, "open", true, "open", true, "text-foreground-muted"],
["open", true, "open", false, "open", false, "text-adaptive-emerald-600-400"],
["closed", false, "open", false, "open", false, "text-adaptive-emerald-600-400"],
["merged", false, "merged", false, "merged", false, "text-adaptive-violet-600-400"],
["closed", false, "merged", false, "closed", false, "text-adaptive-rose-600-400"],
] as const)(
"colors linked %s (draft %s) and %s (draft %s) by their aggregate state",
(firstState, firstDraft, secondState, secondDraft, state, isDraft, textClassName) => {
const first = linkedPr(1);
const second = linkedPr(2);
expect(
presentThreadLinkedPullRequests([
{ ...first, snapshot: { ...first.snapshot!, state: firstState, isDraft: firstDraft } },
{
...second,
snapshot: { ...second.snapshot!, state: secondState, isDraft: secondDraft },
},
]),
).toMatchObject({
label: "+2",
state,
isDraft,
textClassName,
accessibilityLabel: `2 linked pull requests, overall ${isDraft ? "draft" : state}`,
});
},
);

it("uses the top of a derived stack even when its bottom was linked later", () => {
const bottom = linkedPr(1, { linkedAt: "2026-09-09T00:00:00.000Z" });
const top = linkedPr(2);
Expand Down
Loading
Loading