Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
33b7d7b
feat(web): add shift-held pull request quick actions (#15549)
maria-rcks Oct 4, 2026
5a96895
fix: expanded tool calls show their output, empty ones don't expand (…
maria-rcks Oct 4, 2026
5318d05
fix(web): show device diagnostics before hub readiness (#15435)
maria-rcks Oct 4, 2026
c8d7d50
fix(web): open thread picker for unsent drafts (#15436)
maria-rcks Oct 4, 2026
e01e589
fix(desktop): print version before initializing the app (#15440)
maria-rcks Oct 4, 2026
b3b6ae2
fix(server): recover claude skill scalar frontmatter (#15452)
maria-rcks Oct 4, 2026
253dea3
fix(server): keep settled threads asleep after restarts (#15604)
maria-rcks Oct 4, 2026
f9db42d
fix(server): avoid inferring forgejo conflicts from mergeability (#15…
maria-rcks Oct 4, 2026
32b77f4
fix(web): dismiss hovered timeline tooltips on scroll (#15455)
maria-rcks Oct 4, 2026
8360985
fix(server): discover Claude commands in each workspace (#15462)
maria-rcks Oct 4, 2026
2dfef87
fix(web): restore project action preview opening (#15490)
maria-rcks Oct 4, 2026
71fbe8e
fix(desktop): keep titlebar controls inset when zoomed (#15496)
maria-rcks Oct 4, 2026
1c62693
fix(source-control): use the Azure DevOps mark (#15512)
maria-rcks Oct 4, 2026
c346fb6
fix(web): open provider update details from both icons (#15501)
maria-rcks Oct 4, 2026
d3bec62
fix(web): reveal sidebar actions for secondary hovering pointers (#15…
maria-rcks Oct 4, 2026
1d013c7
fix(markdown): preserve descriptive file-link labels (#15509)
maria-rcks Oct 4, 2026
df4ae52
feat(clients): tool calls show the call above a muted result, without…
maria-rcks Oct 4, 2026
0ce43a4
fix(chat): repair unclosed local file links in assistant responses (#…
maria-rcks Oct 4, 2026
3b0093d
fix(server): match manual update commands to installed cli (#15539)
maria-rcks Oct 4, 2026
4ee6bfd
fix(server): preserve staging during commit message generation (#15532)
maria-rcks Oct 4, 2026
484459f
fix(mobile): Keep the last line of iOS markdown replies visible (#15737)
mwolson Oct 4, 2026
b438198
feat(release): include nightly changelogs in Discord announcements (#…
extoci Oct 4, 2026
2188bdd
revert(web): remove automatic compaction before resume (#15771)
maria-rcks Oct 4, 2026
efecd3c
fix(server): Claude threads no longer get stuck after background comm…
eimexdev Oct 4, 2026
fe417df
fix(cli): reject accidental server launches (#15795)
maria-rcks Oct 5, 2026
979ca66
feat(clients): reach one environment over several routes (#15467)
juliusmarminge Oct 5, 2026
745c225
feat(clients): learn an environment's LAN and tailnet addresses (#15468)
juliusmarminge Oct 5, 2026
ec20db4
fix(server): share MCP tool presentation across providers (#15475)
juliusmarminge Oct 5, 2026
801ca76
revert(chat): remove automatic file-link repair (#15824)
maria-rcks Oct 5, 2026
bc27482
perf(web): validate monospace fonts when selected (#15642)
maria-rcks Oct 5, 2026
f4153a7
fix(server): expand home-relative media paths (#15618)
maria-rcks Oct 5, 2026
f729e0e
fix(server): recover Linux runtime directory for device hub (#12402)
maria-rcks Oct 5, 2026
ad5178a
fix(web): center icons in thread details icon buttons (#15669)
RakshithBhat03 Oct 5, 2026
01f894e
fix(mobile): back from an agent's thread returns to its parent (#15068)
AKolenda Oct 5, 2026
f8a308f
fix(dev): worktree setup never deletes a real env file (#15845)
juliusmarminge Oct 5, 2026
1e2ecbd
fix(server): drop the duplicate Option import that breaks main CI (#1…
juliusmarminge Oct 5, 2026
de09c75
fix(dev): write bootstrap warnings directly to stderr (#15865)
maria-rcks Oct 5, 2026
a1d9d72
fix(mobile): a message that fails to send now says why in the thread …
shivamhwp Oct 5, 2026
1beb035
fix(server): queue background notifications during active tools (#15892)
Yash-Singh1 Oct 5, 2026
37de6cb
refactor(server): share one keyed lock that releases idle keys (#15577)
juliusmarminge Oct 5, 2026
06e6274
feat(server): T3 MCP tools take explicit thread and project targets (…
juliusmarminge Oct 5, 2026
c408737
fix(desktop): V2 imports stashed prompts and drafts from the V1 profi…
juliusmarminge Oct 5, 2026
2a778f7
feat(web): shell commands in the timeline are syntax highlighted (#15…
scratchyone Oct 5, 2026
250e052
fix(mobile): upgrade Uniwind and remove local patch (#14597)
Brentlok Oct 5, 2026
cf3e714
fix(server): Stop ends a Codex command after its thread was settled (…
t3dotgg Oct 5, 2026
7812230
fix(server): subagents no longer inherit parent pull-request links (#…
Lucenx9 Oct 5, 2026
93079b6
Merge upstream/main through 7812230572
andrewcai8 Oct 5, 2026
86efe2b
fix(scripts): box reconnect harness uses the route-aware catalog store
andrewcai8 Oct 5, 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
21 changes: 20 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,24 @@ jobs:
args:
- --filter=@t3tools/scripts...

- name: Read published release notes
if: needs.preflight.outputs.is_prerelease == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
RELEASE_CHANNEL: ${{ needs.preflight.outputs.release_channel }}
run: |
if [ "$RELEASE_CHANNEL" = 'nightly' ]; then
if ! gh release view "$RELEASE_TAG" --json body --jq '.body // ""' > "$RUNNER_TEMP/discord-release-notes.md"; then
: > "$RUNNER_TEMP/discord-release-notes.md"
echo '::warning::Could not read published release notes; announcing without a changelog.'
fi
else
: > "$RUNNER_TEMP/discord-release-notes.md"
fi

- name: Announce prerelease on Discord
if: needs.preflight.outputs.is_prerelease == 'true'
continue-on-error: true
Expand All @@ -1360,7 +1378,8 @@ jobs:
--release-name "${{ needs.preflight.outputs.release_name }}" \
--release-version "${{ needs.preflight.outputs.version }}" \
--tag "${{ needs.preflight.outputs.tag }}" \
--release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}"
--release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" \
--release-notes-file "$RUNNER_TEMP/discord-release-notes.md"

- name: Announce latest release on Discord
if: needs.preflight.outputs.make_latest == 'true'
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLegacyLocalStorage from "./DesktopLegacyLocalStorage.ts";
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
import * as DesktopObservability from "./DesktopObservability.ts";
Expand Down Expand Up @@ -179,6 +180,10 @@ const bootstrap = Effect.gen(function* () {
});
yield* installDesktopIpcHandlers();
yield* logBootstrapInfo("bootstrap ipc handlers registered");
// Before any window: the preload merges these items before the app reads storage.
yield* (yield* DesktopLegacyLocalStorage.DesktopLegacyLocalStorage).load(
yield* (yield* DesktopAppIdentity.DesktopAppIdentity).resolveUserDataPath,
);

yield* snapShot.initialize;

Expand Down
117 changes: 117 additions & 0 deletions apps/desktop/src/app/DesktopLegacyLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Ref from "effect/Ref";

import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
import { readChromiumLocalStorage } from "./chromiumLocalStorage.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";

/**
* Carries the renderer's localStorage (prompt stash, unsent drafts, layout,
* theme) over from the V1 desktop profile, which V2 replaced with its own
* `t3code-v2` profile. The V1 profile is only read, never opened by Chromium,
* so this works while V1 is still running.
*
* `load` runs before the window opens; the preload takes the items once,
* merges them into the new profile, and calls `complete`, which writes a
* marker so later launches skip the read. A failed read or merge, or a window
* that never completes, leaves no marker and retries next launch.
*/
export class DesktopLegacyLocalStorage extends Context.Service<
DesktopLegacyLocalStorage,
{
readonly load: (userDataPath: string) => Effect.Effect<void>;
readonly take: Effect.Effect<Option.Option<Readonly<Record<string, string>>>>;
readonly complete: Effect.Effect<void>;
}
>()("@t3tools/desktop/app/DesktopLegacyLocalStorage") {}

const MARKER_FILE_NAME = "v1-local-storage-imported";
// V1 used "T3 Code (Alpha)" when that folder existed and "t3code" otherwise.
const V1_PROFILE_NAMES = ["T3 Code (Alpha)", "t3code"];

const make = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const items = yield* Ref.make(Option.none<Readonly<Record<string, string>>>());
const markerPath = yield* Ref.make(Option.none<string>());

/** Newest file mtime in a directory; appending to a log leaves the directory's own mtime alone. */
const newestFileMtime = (directory: string) =>
Effect.gen(function* () {
let newest = 0;
for (const name of yield* fs.readDirectory(directory)) {
const info = yield* fs.stat(path.join(directory, name)).pipe(Effect.option);
if (Option.isNone(info)) continue;
const mtime = Option.match(info.value.mtime, {
onNone: () => 0,
onSome: (date) => date.getTime(),
});
newest = Math.max(newest, mtime);
}
return newest;
}).pipe(Effect.option);

/** The V1 profile whose Local Storage was written most recently, if any. */
const findV1LocalStorage = Effect.gen(function* () {
let newest: { readonly directory: string; readonly mtime: number } | null = null;
for (const name of V1_PROFILE_NAMES) {
const directory = path.join(environment.appDataDirectory, name, "Local Storage", "leveldb");
const mtime = yield* newestFileMtime(directory);
if (Option.isNone(mtime)) continue;
if (newest === null || mtime.value > newest.mtime) newest = { directory, mtime: mtime.value };
}
return newest?.directory ?? null;
});

const writeMarker = Effect.gen(function* () {
const marker = yield* Ref.get(markerPath);
if (Option.isNone(marker)) return;
yield* fs
.writeFileString(marker.value, "")
.pipe(
Effect.catch((error) =>
Effect.logWarning("Could not record the V1 Local Storage import", error),
),
);
});

const load = Effect.fn("desktop.legacyLocalStorage.load")(function* (userDataPath: string) {
// Development already shares its profile between versions.
if (environment.isDevelopment) return;
const marker = path.join(userDataPath, MARKER_FILE_NAME);
if (yield* fs.exists(marker).pipe(Effect.orElseSucceed(() => true))) return;
yield* Ref.set(markerPath, Option.some(marker));
const directory = yield* findV1LocalStorage;
if (directory === null) return yield* writeMarker;
const origin = ElectronProtocol.getDesktopUrl(false).replace(/\/$/, "");
const read = yield* readChromiumLocalStorage(directory, origin).pipe(
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Path.Path, path),
Effect.tapError((error) =>
Effect.logWarning("Could not read V1 Local Storage; will retry next launch", error),
),
Effect.option,
);
if (Option.isNone(read)) return;
yield* Effect.logInfo("V1 Local Storage ready to import", {
directory,
keys: read.value.size,
});
yield* Ref.set(items, Option.some(Object.fromEntries(read.value)));
});

return DesktopLegacyLocalStorage.of({
load,
// Taking clears the items, so a reload before `complete` cannot merge twice.
take: Ref.getAndSet(items, Option.none()),
complete: writeMarker,
});
});

export const layer = Layer.effect(DesktopLegacyLocalStorage, make);
Loading
Loading