Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
dd69647
fix(macos): restore switcher popover integration
xiaoyumuxi Aug 31, 2026
15eab0b
perf(macos): eliminate drag-induced rendering jank across all split p…
xiaoyumuxi Sep 1, 2026
095aaad
fix(macos): resolve compilation errors in GitLogView and WorkbenchView
xiaoyumuxi Sep 1, 2026
2e1e14f
fix(macos): wire all perf caches into their host views
xiaoyumuxi Sep 1, 2026
8a24996
fix(macos): stabilize gitLogQuery date boundaries and remove dead dra…
xiaoyumuxi Sep 1, 2026
3ef64e1
fix(macos): mark LitheDragUpdateScheduler init as nonisolated
xiaoyumuxi Sep 1, 2026
b708336
feat(macos): align branch popup rows with IDEA's action menu
xiaoyumuxi Sep 1, 2026
dc0703f
fix(ci): make Swift test watchdog stall-aware
1lck Sep 1, 2026
43ec8ea
fix(ci): sample the full descendant tree on Swift runner stall
1lck Sep 1, 2026
4a9e569
fix(ci): make Swift test watchdog stall-aware
1lck Sep 1, 2026
1563dec
fix(ci): sample the full descendant tree on Swift runner stall
1lck Sep 1, 2026
c5a7d4a
fix: address branch popover review findings
xiaoyumuxi Sep 2, 2026
0a034dd
feat(git): rebuild PR 354 on latest preview
Wz58luck Sep 2, 2026
a2df78f
Merge branch 'preview' into codex/pr-354-clean
1lck Sep 2, 2026
1823285
fix(git): restore deleted branches from commit ids
Wz58luck Sep 2, 2026
31d2001
fix(build): remove duplicate run state case and bound downloads
Wz58luck Sep 2, 2026
927f106
Merge preview into PR 392
1lck Sep 2, 2026
bf98629
Merge latest preview into PR 392
1lck Sep 2, 2026
783a96f
Merge pull request #392 from 1lck/worktree-branch-popup-idea-alignment
1lck Sep 2, 2026
45f2a74
Merge branch 'preview' into codex/pr-354-clean
Wz58luck Sep 2, 2026
a5637bd
fix(macos): preserve Git log commit pane width
1lck Sep 2, 2026
804f1a8
test(macos): account for activity bar divider
xiaoyumuxi Sep 2, 2026
c8f64ef
fix(macos): reserve activity bar width without divider
1lck Sep 2, 2026
e4544e1
Merge pull request #390 from 1lck/codex/restore-switcher-ui
1lck Sep 2, 2026
c63f715
Merge latest preview and resolve Git log layout
Wz58luck Sep 2, 2026
c2294c1
fix(git): preserve recovery records after failed deletion
Wz58luck Sep 2, 2026
1b70ce8
fix(windows): 隔离多窗口运行输出,修复控制台串号
Rangsh Sep 2, 2026
64b7acc
docs: add HelloGitHub recommendation badge (#414)
1lck Sep 2, 2026
bf842e0
fix(windows): 运行事件监听改为窗口作用域,彻底隔离多窗输出
Rangsh Sep 2, 2026
3a15b2b
Merge branch 'preview' into fix/windows-multi-window-run-output-leak
1lck Sep 2, 2026
64a67fe
Merge pull request #413 from Rangsh/fix/windows-multi-window-run-outp…
1lck Sep 2, 2026
3ae11c8
Merge branch 'preview' into codex/pr-354-clean
Wz58luck Sep 2, 2026
3e583aa
Merge pull request #399 from Wz58luck/codex/pr-354-clean
1lck Sep 2, 2026
72222d4
feat(windows): 支持项目显示别名以区分同名文件夹 (#416)
Rangsh Sep 2, 2026
9e45444
fix(windows): keep run test within tauri boundary
1lck Sep 2, 2026
b011f46
fix(ci): skip empty Windows test report generation
1lck Sep 2, 2026
d91f8ec
Merge pull request #418 from 1lck/fix/run-test-tauri-boundary
1lck Sep 2, 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
79 changes: 74 additions & 5 deletions .agents/skills/write-stable-tests/scripts/test-timing-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,44 @@ function processGroupIsRunning(processID) {
}
}

function processIsRunning(processID) {
try {
process.kill(processID, 0);
return true;
} catch (error) {
return error?.code === "EPERM";
}
}

function addDescendantProcessIDs(rootProcessID, processIDs) {
let rows;
try {
const listing = spawnSync("ps", ["-axo", "pid=,ppid="], {
encoding: "utf8",
timeout: 5000,
});
if (listing.status !== 0) return;
rows = listing.stdout
.split("\n")
.map((line) => line.trim().split(/\s+/).map(Number))
.filter(([pid, ppid]) => Number.isInteger(pid) && Number.isInteger(ppid));
} catch {
// The direct process group remains the portable fallback when ps is unavailable.
return;
}

processIDs.add(rootProcessID);
let changed = true;
while (changed) {
changed = false;
for (const [pid, ppid] of rows) {
if (!processIDs.has(ppid) || processIDs.has(pid)) continue;
processIDs.add(pid);
changed = true;
}
}
}

function signalProcessGroup(child, signal) {
try {
process.kill(-child.pid, signal);
Expand All @@ -27,12 +65,30 @@ function signalProcessGroup(child, signal) {
}
}

async function waitForProcessGroupExit(processID, timeoutMs, pollIntervalMs) {
function signalDescendantProcesses(rootProcessID, processIDs, signal) {
// The process-group signal handles ordinary descendants. Signal every known
// non-root PID as well because swift-testing may create a new process group.
for (const processID of processIDs) {
if (processID === rootProcessID) continue;
try {
process.kill(processID, signal);
} catch {
// It either exited between the snapshot and signal or is already gone.
}
}
}

function processTreeIsRunning(processID, processIDs) {
return processGroupIsRunning(processID)
|| [...processIDs].some((candidate) => processIsRunning(candidate));
}

async function waitForProcessTreeExit(processID, processIDs, timeoutMs, pollIntervalMs) {
const deadline = performance.now() + timeoutMs;
while (processGroupIsRunning(processID) && performance.now() < deadline) {
while (processTreeIsRunning(processID, processIDs) && performance.now() < deadline) {
await delay(pollIntervalMs);
}
return !processGroupIsRunning(processID);
return !processTreeIsRunning(processID, processIDs);
}

export async function terminateProcessTree(
Expand All @@ -52,10 +108,23 @@ export async function terminateProcessTree(
return true;
}

const processIDs = new Set();
addDescendantProcessIDs(child.pid, processIDs);
signalProcessGroup(child, "SIGTERM");
if (await waitForProcessGroupExit(child.pid, gracePeriodMs, pollIntervalMs)) return true;
signalDescendantProcesses(child.pid, processIDs, "SIGTERM");
if (await waitForProcessTreeExit(child.pid, processIDs, gracePeriodMs, pollIntervalMs)) return true;

// Refresh before forcing termination so descendants created during graceful
// shutdown cannot escape the cleanup pass.
addDescendantProcessIDs(child.pid, processIDs);
signalProcessGroup(child, "SIGKILL");
return waitForProcessGroupExit(child.pid, forcedTerminationTimeoutMs, pollIntervalMs);
signalDescendantProcesses(child.pid, processIDs, "SIGKILL");
return waitForProcessTreeExit(
child.pid,
processIDs,
forcedTerminationTimeoutMs,
pollIntervalMs,
);
}

function lineCollector(callback) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -566,10 +566,13 @@ try {
if (process.platform !== "win32") {
let rootPID = null;
let descendantPID = null;
// swift-testing may detach its helper into a separate process group. The
// timeout owner must still discover and terminate that descendant.
const descendantSource = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
const rootSource = `
const { spawn } = require("node:child_process");
const descendant = spawn(process.execPath, ["-e", ${JSON.stringify(descendantSource)}], {
detached: true,
stdio: "ignore",
});
console.log(descendant.pid);
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,10 @@ jobs:
run: ./.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 -Scope WindowsRust -SuiteTimeoutSeconds 1080

- name: Generate combined Windows test report
if: always()
if: >-
always() &&
(needs.changes.outputs.rust_core == 'true' ||
needs.changes.outputs.windows_rust == 'true')
shell: pwsh
run: node .agents/skills/write-stable-tests/scripts/generate-test-report.mjs

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<a href="#develop-lithe">Develop Lithe</a>
</p>

<p>
<a href="https://hellogithub.com/repository/1lck/Lithe-IDEA" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=633af067f07d4d16af010b9dc16c0b8d&amp;claim_uid=7NYd4yvRlGtqfgr" alt="Featured | HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54"></a>
</p>

<p>
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=&amp;group_code=163027877"><img src="https://img.shields.io/badge/QQ_Group-163027877-EB1923?style=for-the-badge&logo=qq&logoColor=white" alt="Join the Lithe QQ group 163027877"></a>
<a href="https://gcnctzuuwe9u.feishu.cn/wiki/HJFbwZ0hZirAPnkWF3xcPWkCnid?from=from_copylink"><img src="https://img.shields.io/badge/WeChat_Group-Join_Now-07C160?style=for-the-badge&logo=wechat&logoColor=white" alt="Join the Lithe WeChat group"></a>
Expand Down
4 changes: 4 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<a href="#如何开发">如何开发</a>
</p>

<p>
<a href="https://hellogithub.com/repository/1lck/Lithe-IDEA" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=633af067f07d4d16af010b9dc16c0b8d&amp;claim_uid=7NYd4yvRlGtqfgr" alt="Featured | HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54"></a>
</p>

<p>
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=&amp;group_code=163027877"><img src="https://img.shields.io/badge/QQ_群-163027877-EB1923?style=for-the-badge&logo=qq&logoColor=white" alt="加入 Lithe QQ 群 163027877"></a>
<a href="https://gcnctzuuwe9u.feishu.cn/wiki/HJFbwZ0hZirAPnkWF3xcPWkCnid?from=from_copylink"><img src="https://img.shields.io/badge/微信交流群-点击加入-07C160?style=for-the-badge&logo=wechat&logoColor=white" alt="加入 Lithe 微信交流群"></a>
Expand Down
17 changes: 17 additions & 0 deletions macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,18 @@ struct RustCoreBridge: Sendable {
let conflictedPaths: [String]
}

struct TagDeletion: Decodable, Sendable {
let name: String
let deletedTarget: String
let kind: GitTagKind
let message: String?
}

struct BranchDeletion: Decodable, Sendable {
let name: String
let deletedTarget: String
}

struct Warning: Decodable, Sendable {
let code: String
let message: String
Expand All @@ -832,6 +844,8 @@ struct RustCoreBridge: Sendable {
let invocations: [Invocation]?
let operationError: OperationError?
let stashRestore: StashRestore?
let tagDeletion: TagDeletion?
let branchDeletion: BranchDeletion?
let warnings: [Warning]?
}

Expand Down Expand Up @@ -906,6 +920,7 @@ struct RustCoreBridge: Sendable {
let fullName: String
let shortName: String
let kind: String
let peelsToCommit: Bool
let isCurrent: Bool
let upstreamShortName: String?
}
Expand Down Expand Up @@ -936,6 +951,7 @@ struct RustCoreBridge: Sendable {
fullName: reference.fullName,
shortName: reference.shortName,
kind: kind,
peelsToCommit: reference.peelsToCommit,
isCurrent: reference.isCurrent,
upstreamShortName: reference.upstreamShortName
)
Expand All @@ -946,6 +962,7 @@ struct RustCoreBridge: Sendable {
fullName: reference.fullName,
shortName: reference.shortName,
kind: kind,
peelsToCommit: reference.peelsToCommit,
isCurrent: reference.isCurrent,
upstreamShortName: reference.upstreamShortName
)
Expand Down
33 changes: 33 additions & 0 deletions macos/Sources/Lithe/Core/Rust/RustGitOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ struct RustGitOperations: GitOperations, Sendable {
conflictedPaths: $0.conflictedPaths
)
},
tagDeletion: response.tagDeletion.map {
GitTagDeletion(
name: $0.name,
deletedTarget: $0.deletedTarget,
kind: $0.kind,
message: $0.message
)
},
branchDeletion: response.branchDeletion.map {
GitBranchDeletion(
name: $0.name,
deletedTarget: $0.deletedTarget
)
},
warnings: response.warnings?.map {
GitOperationWarning(code: $0.code, message: $0.message, details: $0.details)
} ?? []
Expand Down Expand Up @@ -319,6 +333,25 @@ struct RustGitOperations: GitOperations, Sendable {
write(at: rootURL, operation: "stageAll")
}

func createTag(
named name: String,
at revision: String,
message: String?,
rootURL: URL
) -> GitProcessResult? {
write(
at: rootURL,
operation: "createTag",
revision: revision,
name: name,
message: message
)
}

func deleteTag(named name: String, rootURL: URL) -> GitProcessResult? {
write(at: rootURL, operation: "deleteTag", name: name)
}

func snapshot(at rootURL: URL) -> GitSnapshot? {
core.gitStatus(at: rootURL)?.makeSnapshot(at: rootURL)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,20 @@ extension AppModel {
var requestedStashReference: String? {
gitFeatureIfActive?.requestedStashReference
}
var recentlyDeletedTag: GitTagDeletion? {
gitFeatureIfActive?.recentlyDeletedTag
}
var recentlyDeletedBranch: GitBranchDeletion? {
gitFeatureIfActive?.recentlyDeletedBranch
}
var isCommitting: Bool { gitFeatureIfActive?.isCommitting ?? false }
var gitBlameLines: [URL: [GitBlameLine]] { gitFeatureIfActive?.gitBlameLines ?? [:] }
var gitReferences: [GitReference] { gitFeatureIfActive?.gitReferences ?? [] }
var recentGitReferences: [GitReference] { gitFeatureIfActive?.recentGitReferences ?? [] }
var gitCommits: [GitCommit] { gitFeatureIfActive?.gitCommits ?? [] }
/// Cheap stand-in for `gitCommits` as a change key. Comparing the array
/// itself made every `.task(id:)` evaluation walk the whole commit list.
var gitCommitsVersion: Int { gitFeatureIfActive?.gitCommitsVersion ?? 0 }
var gitLogMatchedCommitHashes: Set<String>? {
gitFeatureIfActive?.gitLogMatchedCommitHashes
}
Expand Down
31 changes: 31 additions & 0 deletions macos/Sources/Lithe/Models/AppModel/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,37 @@ final class AppModel: ObservableObject, Identifiable {
await gitFeature.deleteBranch(reference)
}

func restoreRecentlyDeletedBranch() async {
guard let gitFeature = await activateGitModule() else { return }
await gitFeature.restoreRecentlyDeletedBranch()
}

func dismissDeletedBranchBanner() {
gitFeatureIfActive?.dismissDeletedBranchBanner()
}

/// Returns nil on success, otherwise the error message a tag dialog
/// should show where the user typed.
@discardableResult
func createTag(at commit: GitCommit, name: String, message: String) async -> String? {
guard let gitFeature = await activateGitModule() else { return "No Git repository is open" }
return await gitFeature.createTag(at: commit, name: name, message: message)
}

func deleteTag(_ reference: GitReference) async {
guard let gitFeature = await activateGitModule() else { return }
await gitFeature.deleteTag(reference)
}

func restoreRecentlyDeletedTag() async {
guard let gitFeature = await activateGitModule() else { return }
await gitFeature.restoreRecentlyDeletedTag()
}

func dismissDeletedTagBanner() {
gitFeatureIfActive?.dismissDeletedTagBanner()
}

func mergeBranch(_ reference: GitReference) async {
guard let gitFeature = await activateGitModule() else { return }
await gitFeature.mergeBranch(reference)
Expand Down
27 changes: 27 additions & 0 deletions macos/Sources/Lithe/Services/Monitoring/LitheSignpost.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os

enum LitheSignpost {
private static let signposter = OSSignposter(
subsystem: "com.openres.Lithe",
category: "Rendering"
)

static func begin(_ name: StaticString) -> OSSignpostIntervalState {
signposter.beginInterval(name)
}

static func end(_ name: StaticString, _ state: OSSignpostIntervalState) {
signposter.endInterval(name, state)
}

#if DEBUG
private static var bodyCounts: [String: Int] = [:]

static func bodyEvaluated(_ view: StaticString) {
bodyCounts["\(view)", default: 0] += 1
}
#else
@inlinable @inline(__always)
static func bodyEvaluated(_ view: StaticString) {}
#endif
}
Loading
Loading