Skip to content

Commit 052d8e1

Browse files
authored
Merge branch 'main' into t3code/worktree-cleanup-disposable-files
2 parents bc95ac1 + 37de6cb commit 052d8e1

341 files changed

Lines changed: 17494 additions & 3196 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/release.yml‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,24 @@ jobs:
13471347
args:
13481348
- --filter=@t3tools/scripts...
13491349
1350+
- name: Read published release notes
1351+
if: needs.preflight.outputs.is_prerelease == 'true'
1352+
continue-on-error: true
1353+
env:
1354+
GH_TOKEN: ${{ github.token }}
1355+
GH_REPO: ${{ github.repository }}
1356+
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
1357+
RELEASE_CHANNEL: ${{ needs.preflight.outputs.release_channel }}
1358+
run: |
1359+
if [ "$RELEASE_CHANNEL" = 'nightly' ]; then
1360+
if ! gh release view "$RELEASE_TAG" --json body --jq '.body // ""' > "$RUNNER_TEMP/discord-release-notes.md"; then
1361+
: > "$RUNNER_TEMP/discord-release-notes.md"
1362+
echo '::warning::Could not read published release notes; announcing without a changelog.'
1363+
fi
1364+
else
1365+
: > "$RUNNER_TEMP/discord-release-notes.md"
1366+
fi
1367+
13501368
- name: Announce prerelease on Discord
13511369
if: needs.preflight.outputs.is_prerelease == 'true'
13521370
continue-on-error: true
@@ -1359,7 +1377,8 @@ jobs:
13591377
--release-name "${{ needs.preflight.outputs.release_name }}" \
13601378
--release-version "${{ needs.preflight.outputs.version }}" \
13611379
--tag "${{ needs.preflight.outputs.tag }}" \
1362-
--release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}"
1380+
--release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" \
1381+
--release-notes-file "$RUNNER_TEMP/discord-release-notes.md"
13631382
13641383
- name: Announce latest release on Discord
13651384
if: needs.preflight.outputs.make_latest == 'true'

‎apps/desktop/src/main.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ for (const stream of [process.stdout, process.stderr]) {
88
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
99
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
1010
import * as NodeServices from "@effect/platform-node/NodeServices";
11+
// @effect-diagnostics-next-line nodeBuiltinImport:off - Version output must flush before Electron exits, without acquiring the runtime.
12+
import * as NodeFS from "node:fs";
1113
import * as NodeOS from "node:os";
1214
import * as Effect from "effect/Effect";
1315
import * as Layer from "effect/Layer";
@@ -69,6 +71,15 @@ import * as DesktopWslBackend from "./wsl/DesktopWslBackend.ts";
6971
import * as DesktopWslEnvironment from "./wsl/DesktopWslEnvironment.ts";
7072
import * as DesktopWslServerTree from "./wsl/DesktopWslServerTree.ts";
7173

74+
if (process.argv.includes("--version")) {
75+
try {
76+
NodeFS.writeSync(process.stdout.fd, `${Electron.app.getVersion()}\n`);
77+
} catch (error) {
78+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EPIPE") throw error;
79+
}
80+
Electron.app.exit(0);
81+
}
82+
7283
const desktopEnvironmentLayer = Layer.unwrap(
7384
Effect.gen(function* () {
7485
const metadata = yield* Effect.service(ElectronApp.ElectronApp).pipe(

‎apps/mobile/app.config.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ const config: ExpoConfig = {
306306
// JS back handling survives it via react-native's Android 16 shim plus
307307
// withAndroidPredictiveBackCompat on Android 13-15.
308308
predictiveBackGestureEnabled: true,
309+
// expo-sensors declares this for its pedometer, which the app does not use.
310+
blockedPermissions: ["android.permission.ACTIVITY_RECOGNITION"],
309311
},
310312
web: {
311313
favicon: variant.assets.appIcon,
@@ -440,6 +442,9 @@ const config: ExpoConfig = {
440442
},
441443
],
442444
"./plugins/withIosCocoaPodsUuidCache.cjs",
445+
// Only the accelerometer is used (device viewer shake). Compile out the
446+
// pedometer so iOS needs no motion purpose string.
447+
["expo-sensors", { motionPermission: false }],
443448
// Must be listed BEFORE expo-widgets: same-type mods run last-registered-
444449
// first, so registering earlier makes this plugin's mods run AFTER
445450
// expo-widgets' — its dangerous mod wipes ios/ExpoWidgetsTarget/ (which

‎apps/mobile/modules/t3-markdown-text/ios/T3MarkdownText.mm‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,12 @@ - (instancetype)initWithFrame:(CGRect)frame
311311
_textView.editable = false;
312312
_textView.textContainerInset = UIEdgeInsetsZero;
313313
_textView.textContainer.lineFragmentPadding = 0;
314+
// The shadow node already sized this view to fit every line. A container
315+
// that tracks the frame can come out a fraction of a point shorter after
316+
// pixel rounding, and TextKit then drops the last line. Size it by hand
317+
// with unbounded height and let the view's bounds clip.
318+
_textView.textContainer.widthTracksTextView = NO;
319+
_textView.textContainer.heightTracksTextView = NO;
314320
_textView.delegate = self;
315321
// Chat text supports selection and contextual actions, but not drag-and-drop.
316322
_textView.textDragInteraction.enabled = NO;
@@ -477,6 +483,7 @@ - (void)drawRect:(CGRect)rect
477483
}
478484
if (frameChanged) {
479485
_textView.frame = _view.frame;
486+
_textView.textContainer.size = CGSizeMake(CGRectGetWidth(_view.frame), CGFLOAT_MAX);
480487
}
481488

482489
// Text attachments have no native link element. Expose their existing runs

‎apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts‎

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { MarkdownNode } from "react-native-nitro-markdown/headless";
2+
import { isMarkdownFileLinkLabel } from "@t3tools/client-runtime/markdown-links";
23
import { collectComposerInlineTokens } from "@t3tools/shared/composerInlineTokens";
34
import { imageMimeType } from "@t3tools/shared/image";
45
import { isWindowsAbsolutePath } from "@t3tools/shared/path";
@@ -109,19 +110,34 @@ export function nativeMarkdownContextCopyRanges(
109110
}>,
110111
) {
111112
let offset = 0;
112-
return runs.flatMap(({ run, text, inlineImageLength }) => {
113+
const ranges: Array<{ start: number; end: number; text: string }> = [];
114+
for (const [index, { run, text, inlineImageLength }] of runs.entries()) {
113115
const start = offset;
114116
offset += text.length + inlineImageLength;
115117
const reference = parseComposerContextHref(run.href ?? "");
116118
const source = reference
117119
? formatComposerContextReference({ ...reference, label: run.text })
118120
: run.skillName
119121
? `$${run.skillName}`
120-
: run.fileIcon && run.href
121-
? (run.sourceText ?? `[${run.text}](<${run.href}>)`)
122-
: null;
123-
return source === null ? [] : [{ start, end: offset, text: source }];
124-
});
122+
: run.sourceText !== undefined
123+
? run.sourceText
124+
: run.fileIcon && run.href
125+
? `[${run.text}](<${run.href}>)`
126+
: null;
127+
if (source === null) continue;
128+
const previous = ranges.at(-1);
129+
if (
130+
run.sourceText !== undefined &&
131+
!runs[index - 1]?.run.fileIcon &&
132+
previous?.end === start &&
133+
previous.text === source
134+
) {
135+
previous.end = offset;
136+
} else {
137+
ranges.push({ start, end: offset, text: source });
138+
}
139+
}
140+
return ranges;
125141
}
126142

127143
import type { SelectableMarkdownSkill } from "./SelectableMarkdownText.types";
@@ -181,6 +197,7 @@ interface RunContext {
181197
readonly href?: string;
182198
readonly externalHost?: string;
183199
readonly fileIcon?: MarkdownFileIcon;
200+
readonly sourceText?: string;
184201
readonly role?: NativeMarkdownTextRun["role"];
185202
readonly headingLevel?: number;
186203
readonly depth?: number;
@@ -268,6 +285,7 @@ function sameRunStyle(left: NativeMarkdownTextRun, right: NativeMarkdownTextRun)
268285
left.href === right.href &&
269286
left.externalHost === right.externalHost &&
270287
left.fileIcon === right.fileIcon &&
288+
left.sourceText === right.sourceText &&
271289
left.skillName === right.skillName &&
272290
left.skillLabel === right.skillLabel &&
273291
left.role === right.role &&
@@ -298,6 +316,7 @@ function appendRun(
298316
...(context.href ? { href: context.href } : {}),
299317
...(context.externalHost ? { externalHost: context.externalHost } : {}),
300318
...(context.fileIcon ? { fileIcon: context.fileIcon } : {}),
319+
...(context.sourceText !== undefined ? { sourceText: context.sourceText } : {}),
301320
...(context.role ? { role: context.role } : {}),
302321
...(context.headingLevel ? { headingLevel: context.headingLevel } : {}),
303322
...(context.depth ? { depth: context.depth } : {}),
@@ -432,6 +451,36 @@ function nodeTextContent(node: MarkdownNode): string {
432451
return (node.children ?? []).map(nodeTextContent).join("");
433452
}
434453

454+
function fileLinkLabelMarkdown(node: MarkdownNode): string {
455+
const children = (node.children ?? []).map(fileLinkLabelMarkdown).join("");
456+
switch (node.type) {
457+
case "bold":
458+
return `**${children}**`;
459+
case "italic":
460+
return `*${children}*`;
461+
case "strikethrough":
462+
return `~~${children}~~`;
463+
case "code_inline": {
464+
const content = nodeTextContent(node);
465+
const fence = "`".repeat(
466+
Math.max(0, ...(content.match(/`+/g) ?? []).map((run) => run.length)) + 1,
467+
);
468+
const padding = /^`|`$|^ .* $/.test(content) && content.trim().length > 0 ? " " : "";
469+
return `${fence}${padding}${content}${padding}${fence}`;
470+
}
471+
case "soft_break":
472+
return "\n";
473+
case "line_break":
474+
return " \n";
475+
case "text":
476+
return textNodeContent(nodeTextContent(node)).replace(/[\\[\]*_`~]/g, "\\$&");
477+
case "image":
478+
return `![${(node.alt ?? "").replace(/[\\[\]]/g, "\\$&")}](<${node.href ?? ""}>)`;
479+
default:
480+
return node.content ?? children;
481+
}
482+
}
483+
435484
function appendNode(
436485
runs: NativeMarkdownTextRun[],
437486
node: MarkdownNode,
@@ -482,6 +531,23 @@ function appendNode(
482531
}
483532
const presentation = resolveMarkdownLinkPresentation(node.href ?? "");
484533
if (presentation.kind === "file") {
534+
const label = textNodeContent(nodeTextContent(node));
535+
if (!isMarkdownFileLinkLabel(label, presentation.href)) {
536+
const fileRuns: NativeMarkdownTextRun[] = [];
537+
const fileContext = {
538+
...context,
539+
href: presentation.href,
540+
sourceText: `[${fileLinkLabelMarkdown(node)}](<${presentation.href}>)`,
541+
};
542+
appendChildren(fileRuns, node, fileContext);
543+
appendRun(fileRuns, " ", fileContext);
544+
appendRun(fileRuns, presentation.label, {
545+
...fileContext,
546+
fileIcon: presentation.icon,
547+
});
548+
runs.push(...fileRuns);
549+
return runs;
550+
}
485551
return appendRun(runs, presentation.label, {
486552
...context,
487553
href: presentation.href,

‎apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3KeyboardCommandsModule.kt‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,21 @@ class T3KeyboardCommandsView(
2929
var enabledCommands = emptySet<String>()
3030

3131
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
32-
val copiesThreadReference =
33-
event.action == KeyEvent.ACTION_DOWN &&
34-
event.repeatCount == 0 &&
35-
event.keyCode == KeyEvent.KEYCODE_C &&
36-
event.isCtrlPressed &&
37-
event.isShiftPressed &&
38-
!event.isAltPressed &&
39-
enabledCommands.contains("copyThreadReference")
40-
if (copiesThreadReference) {
41-
onCommand(mapOf("command" to "copyThreadReference"))
42-
return true
32+
val command = commandFor(event)?.takeIf { enabledCommands.contains(it) }
33+
if (command != null) onCommand(mapOf("command" to command))
34+
return command != null || super.dispatchKeyEvent(event)
35+
}
36+
37+
private fun commandFor(event: KeyEvent): String? {
38+
if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount != 0 || !event.isCtrlPressed) {
39+
return null
40+
}
41+
return when {
42+
event.keyCode == KeyEvent.KEYCODE_C && event.isShiftPressed && !event.isAltPressed ->
43+
"copyThreadReference"
44+
event.keyCode == KeyEvent.KEYCODE_H && event.isShiftPressed && !event.isAltPressed ->
45+
"cycleHost"
46+
else -> null
4347
}
44-
return super.dispatchKeyEvent(event)
4548
}
4649
}

‎apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public final class T3KeyboardCommandsView: ExpoView {
5252
title: "Copy PR Link or Thread ID"
5353
),
5454
enabledCommand("toggleSidebar", input: "\\", modifiers: .command, action: #selector(handleToggleSidebar), title: "Toggle Sidebar"),
55+
enabledCommand("cycleHost", input: "h", modifiers: [.command, .shift], action: #selector(cycleHost), title: "Next Machine"),
5556
].compactMap { $0 }
5657
if isPad {
5758
commands += (1...9).compactMap { index in
@@ -150,6 +151,7 @@ public final class T3KeyboardCommandsView: ExpoView {
150151
@objc private func openReview() { emit("review") }
151152
@objc private func copyThreadReference() { emit("copyThreadReference") }
152153
@objc private func handleToggleSidebar() { emit("toggleSidebar") }
154+
@objc private func cycleHost() { emit("cycleHost") }
153155

154156
private func emit(_ command: String) {
155157
onCommand(["command": command])

‎apps/mobile/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"expo-paste-input": "^0.1.15",
102102
"expo-quick-actions": "^6.0.2",
103103
"expo-secure-store": "~58.0.2",
104+
"expo-sensors": "~58.0.1",
104105
"expo-sharing": "58.0.13",
105106
"expo-splash-screen": "~58.0.4",
106107
"expo-sqlite": "~58.0.8",

‎apps/mobile/src/App.tsx‎

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Linking from "expo-linking";
22
import * as SplashScreen from "expo-splash-screen";
33
import { useEffect } from "react";
4-
import { StatusBar, View } from "react-native";
4+
import { StatusBar } from "react-native";
55
import { GestureHandlerRootView } from "react-native-gesture-handler";
66
import { KeyboardProvider } from "react-native-keyboard-controller";
77
import { SafeAreaProvider } from "react-native-safe-area-context";
@@ -23,6 +23,8 @@ import { OverlayPortalHost } from "./components/OverlayPortal";
2323
import { shouldHandleAppLink } from "./lib/appLinking";
2424
import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
2525
import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator";
26+
import { VoiceInputProvider } from "./features/voice-input/VoiceInputProvider";
27+
import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl";
2628

2729
import "../global.css";
2830

@@ -76,22 +78,24 @@ function AppContent() {
7678
<GestureHandlerRootView className="flex-1">
7779
<KeyboardProvider statusBarTranslucent>
7880
<SafeAreaProvider>
79-
<StatusBar barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"} />
80-
{/* The navigation theme drives the NATIVE header appearance: native-stack
81+
<VoiceInputProvider>
82+
<StatusBar barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"} />
83+
{/* The navigation theme drives the NATIVE header appearance: native-stack
8184
forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without
8285
this, React Navigation defaults to its light theme and every native
8386
header (glass buttons, title, materials) is forced light even when
8487
the system is in dark mode. */}
85-
<View style={{ flex: 1 }}>
86-
<IncomingShareProvider>
87-
<Navigation linking={appLinking} theme={navigationTheme} />
88-
</IncomingShareProvider>
89-
<ConfirmDialogHost />
90-
<ThreadArrangementHost />
91-
</View>
92-
{/* Anchored-menu overlays render here — in-window, so the
88+
<GlobalVoiceInputControl>
89+
<IncomingShareProvider>
90+
<Navigation linking={appLinking} theme={navigationTheme} />
91+
</IncomingShareProvider>
92+
<ConfirmDialogHost />
93+
<ThreadArrangementHost />
94+
</GlobalVoiceInputControl>
95+
{/* Anchored-menu overlays render here — in-window, so the
9396
keyboard stays up while a dropdown is open. */}
94-
<OverlayPortalHost />
97+
<OverlayPortalHost />
98+
</VoiceInputProvider>
9599
</SafeAreaProvider>
96100
</KeyboardProvider>
97101
</GestureHandlerRootView>

‎apps/mobile/src/Stack.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,8 @@ const RootStackConfig = createNativeStackNavigator({
701701
presentation: "fullScreenModal",
702702
headerShown: false,
703703
gestureEnabled: false,
704+
autoHideHomeIndicator: true,
705+
navigationBarHidden: true,
704706
},
705707
}),
706708
ThreadReview: createNativeStackScreen({

0 commit comments

Comments
 (0)