Skip to content

Commit 87bbe6a

Browse files
authored
Merge branch 'main' into fix/codex-default-mode-user-input
2 parents e4a0e10 + 0a7c662 commit 87bbe6a

162 files changed

Lines changed: 34279 additions & 493 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/mobile-eas-production.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ jobs:
195195
run: |
196196
message="$(git log -1 --pretty=%s | head -c 120) ($(git rev-parse --short=9 HEAD))"
197197
for platform in ios android; do
198-
hash="$(eas fingerprint:generate --platform "$platform" --environment production --json --non-interactive | jq -r '.hash')"
198+
# eas-cli prints an environment-loaded notice to stdout before the
199+
# JSON even with --json, so discard everything before the document.
200+
hash="$(eas fingerprint:generate --platform "$platform" --environment production --json --non-interactive | sed -n '/^{/,$p' | jq -er '.hash | select(type == "string" and length > 0)')"
199201
matching="$(eas build:list --platform "$platform" --build-profile production --status finished --fingerprint-hash "$hash" --limit 1 --json --non-interactive | jq 'length')"
200202
if [ "$matching" -gt 0 ]; then
201203
eas update \

‎apps/desktop/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@t3tools/desktop",
3-
"version": "0.0.32",
3+
"version": "0.0.33",
44
"private": true,
55
"type": "module",
66
"main": "dist-electron/main.cjs",

‎apps/desktop/src/shell/DesktopShellEnvironment.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,10 +379,18 @@ const installWindowsEnvironment = Effect.fn("desktop.shellEnvironment.installWin
379379
function* (
380380
config: ShellEnvironmentConfig,
381381
): Effect.fn.Return<void, never, ChildProcessSpawner.ChildProcessSpawner> {
382-
const noProfile = yield* readWindowsEnvironment(["PATH"], { loadProfile: false });
383-
const profile = yield* readWindowsEnvironment(WINDOWS_PROFILE_ENV_NAMES, {
384-
loadProfile: true,
385-
});
382+
// Concurrent, not sequential: these two probes are independent (only their
383+
// results are combined below) and each spawns its own PowerShell. Run in
384+
// series they sit at offset 0 of desktop.startup, before anything else, and
385+
// launch traces measured them at 2718ms then 2066ms — the entire 4.8s
386+
// startup span, of which desktop.bootstrap is ~30ms.
387+
const [noProfile, profile] = yield* Effect.all(
388+
[
389+
readWindowsEnvironment(["PATH"], { loadProfile: false }),
390+
readWindowsEnvironment(WINDOWS_PROFILE_ENV_NAMES, { loadProfile: true }),
391+
],
392+
{ concurrency: 2 },
393+
);
386394
const mergedPath = mergePaths("win32", [
387395
trimNonEmpty(profile.PATH),
388396
trimNonEmpty(knownWindowsCliDirs(config.env).join(";")),

‎apps/mobile/src/features/threads/ThreadFeed.tsx‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reani
4949
import { useThemeColor } from "../../lib/useThemeColor";
5050
import { useFontFamily } from "../../lib/useFontFamily";
5151
import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
52+
import { hasWideMarkdownBlock } from "../../lib/wideMarkdownBlocks";
5253
import {
5354
hasNativeSelectableMarkdownText,
5455
SelectableMarkdownText,
@@ -876,6 +877,12 @@ function renderFeedEntry(
876877
const timestampLabel = formatMessageTime(isUser ? message.createdAt : message.updatedAt);
877878
const attachments = message.attachments ?? [];
878879
const hasReviewCommentContext = message.text.includes("<review_comment");
880+
// A bubble that sizes itself from its content cannot lay out a block whose
881+
// intrinsic width overflows `maxWidth`: Android positions the bubble's
882+
// children during the unclamped pass and never moves them once the width
883+
// is clamped, so the paragraphs around the block end up drawn on top of
884+
// each other. Pinning the width removes that pass.
885+
const hasWideBlock = hasWideMarkdownBlock(message.text);
879886
const assistantTurnStillInProgress =
880887
message.role === "assistant" &&
881888
props.unsettledTurnId !== null &&
@@ -898,7 +905,11 @@ function renderFeedEntry(
898905
style={{
899906
backgroundColor: userBubbleColor,
900907
maxWidth: props.userBubbleMaxWidth,
901-
...(hasReviewCommentContext ? { width: props.reviewCommentBubbleWidth } : null),
908+
...(hasReviewCommentContext
909+
? { width: props.reviewCommentBubbleWidth }
910+
: hasWideBlock
911+
? { width: props.userBubbleMaxWidth }
912+
: null),
902913
}}
903914
>
904915
{message.text.trim().length > 0 ? (

‎apps/mobile/src/lib/threadActivity.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,8 @@ function toolDetailTextLooksLikeFailure(text: string): boolean {
554554
normalized.includes("command not found") ||
555555
(normalized.includes("cannot find path") && normalized.includes("because it does not exist")) ||
556556
(normalized.includes("is not recognized") && normalized.includes("the term '")) ||
557+
normalized.includes("is not recognized as the name of a cmdlet") ||
558+
normalized.includes("a parameter cannot be found that matches parameter name") ||
557559
/<exited with exit code\s+[1-9]\d*\s*>/i.test(text) ||
558560
/exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text) ||
559561
/exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import { hasWideMarkdownBlock } from "./wideMarkdownBlocks";
4+
5+
describe("hasWideMarkdownBlock", () => {
6+
it("ignores prose, inline code, and emphasis", () => {
7+
expect(hasWideMarkdownBlock("just a message")).toBe(false);
8+
expect(hasWideMarkdownBlock("I found it in `secteurs_intervention` earlier")).toBe(false);
9+
expect(hasWideMarkdownBlock("a | b in a sentence")).toBe(false);
10+
expect(hasWideMarkdownBlock("an em dash — and a rule\n\n---\n")).toBe(false);
11+
});
12+
13+
it("detects fenced code blocks", () => {
14+
expect(hasWideMarkdownBlock("before\n```\ncode\n```\nafter")).toBe(true);
15+
expect(hasWideMarkdownBlock("before\n```ts\ncode\n```")).toBe(true);
16+
expect(hasWideMarkdownBlock("before\n~~~\ncode\n~~~")).toBe(true);
17+
expect(hasWideMarkdownBlock(" ```\ncode\n```")).toBe(true);
18+
});
19+
20+
it("detects GFM tables", () => {
21+
expect(hasWideMarkdownBlock("| a | b |\n| --- | --- |\n| 1 | 2 |")).toBe(true);
22+
expect(hasWideMarkdownBlock("a | b\n:-- | --:\n1 | 2")).toBe(true);
23+
});
24+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Detects markdown that the JS renderer draws as a standalone block View
3+
* wrapping a horizontal ScrollView — fenced code blocks and GFM tables.
4+
*
5+
* Those blocks report an intrinsic width equal to their widest line, which is
6+
* effectively unbounded. A user bubble sizes itself from its content
7+
* (`maxWidth` with no `width`), so Android lays the bubble's children out
8+
* during the unclamped intrinsic pass — where the surrounding paragraphs
9+
* collapse to a single line — and never repositions them once the width is
10+
* clamped back to `maxWidth`. The result is siblings drawn on top of each
11+
* other inside an over-tall bubble. Pinning the bubble's width removes the
12+
* intrinsic pass entirely, which is the same reason review-comment bubbles
13+
* already carry an explicit width.
14+
*
15+
* Indented (four-space) code blocks are deliberately not detected: they are
16+
* vanishingly rare in chat input and the check would fire on ordinary nested
17+
* list continuations.
18+
*/
19+
20+
const FENCED_CODE_BLOCK = /^ {0,3}(?:```|~~~)/m;
21+
22+
function isTableDelimiterRow(line: string): boolean {
23+
const trimmed = line.trim();
24+
if (!trimmed.includes("|") || !trimmed.includes("-")) {
25+
return false;
26+
}
27+
return /^[|\-: \t]+$/.test(trimmed);
28+
}
29+
30+
export function hasWideMarkdownBlock(text: string): boolean {
31+
if (FENCED_CODE_BLOCK.test(text)) {
32+
return true;
33+
}
34+
if (!text.includes("|")) {
35+
return false;
36+
}
37+
return text.split("\n").some(isTableDelimiterRow);
38+
}

‎apps/server/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "t3",
3-
"version": "0.0.32",
3+
"version": "0.0.33",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

‎apps/server/src/auth/RpcAuthorization.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ describe("RPC authorization scopes", () => {
3737
expect(requiredScopeForRpcMethod(WS_METHODS.cloudInstallRelayClient)).toBe(AuthRelayWriteScope);
3838
});
3939

40+
it("reads the reviewer menu under the same scope as the pull request it belongs to", () => {
41+
// The candidate list is a read like the detail beside it, and asking somebody for a review is
42+
// a write like every other pull request operation.
43+
expect(requiredScopeForRpcMethod(WS_METHODS.pullRequestsReviewerCandidates)).toBe(
44+
requiredScopeForRpcMethod(WS_METHODS.pullRequestsDetail),
45+
);
46+
expect(requiredScopeForRpcMethod(WS_METHODS.pullRequestsRequestReviewers)).toBe(
47+
requiredScopeForRpcMethod(WS_METHODS.pullRequestsComment),
48+
);
49+
});
50+
4051
it("rejects unknown RPC method names", () => {
4152
for (const method of ["server.notRegistered", "toString", "constructor"]) {
4253
expect(() => requiredScopeForRpcMethod(method)).toThrow(

‎apps/server/src/auth/RpcAuthorization.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,23 @@ export const RPC_REQUIRED_SCOPES = {
5252
[WS_METHODS.serverGetBackgroundPolicy]: AuthOrchestrationReadScope,
5353
[WS_METHODS.cloudGetRelayClientStatus]: AuthRelayReadScope,
5454
[WS_METHODS.cloudInstallRelayClient]: AuthRelayWriteScope,
55+
[WS_METHODS.pullRequestsList]: AuthOrchestrationReadScope,
56+
[WS_METHODS.pullRequestsListStats]: AuthOrchestrationReadScope,
57+
[WS_METHODS.pullRequestsDetail]: AuthOrchestrationReadScope,
58+
[WS_METHODS.pullRequestsActivity]: AuthOrchestrationReadScope,
59+
[WS_METHODS.pullRequestsDiffFileContents]: AuthOrchestrationReadScope,
60+
[WS_METHODS.pullRequestsRunAction]: AuthOrchestrationOperateScope,
61+
[WS_METHODS.pullRequestsComment]: AuthOrchestrationOperateScope,
62+
[WS_METHODS.pullRequestsSubmitReview]: AuthOrchestrationOperateScope,
63+
[WS_METHODS.pullRequestsReplyToThread]: AuthOrchestrationOperateScope,
64+
[WS_METHODS.pullRequestsSetThreadResolution]: AuthOrchestrationOperateScope,
65+
// Read scope like the reads it un-caches: refreshing is part of reading, and a read-only
66+
// client pressing refresh must not be told it may not look again.
67+
[WS_METHODS.pullRequestsInvalidate]: AuthOrchestrationReadScope,
68+
// The candidate list is a read like the detail beside it; asking somebody for a review is a
69+
// write like every other one.
70+
[WS_METHODS.pullRequestsReviewerCandidates]: AuthOrchestrationReadScope,
71+
[WS_METHODS.pullRequestsRequestReviewers]: AuthOrchestrationOperateScope,
5572
[WS_METHODS.sourceControlLookupRepository]: AuthOrchestrationReadScope,
5673
[WS_METHODS.sourceControlCloneRepository]: AuthOrchestrationOperateScope,
5774
[WS_METHODS.sourceControlPublishRepository]: AuthOrchestrationOperateScope,

0 commit comments

Comments
 (0)