Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1aae961
fix(desktop): PR返信ダイアログの返信元コメントをMarkdownレンダリング (#214)
MocA-Love Apr 16, 2026
47b90f7
fix(desktop): スケジュール作成ダイアログの横幅を広げる (#215)
MocA-Love Apr 16, 2026
16a68ca
fix(desktop): Agent Manager 設定ダイアログでマネージャが閉じる問題を修正
MocA-Love Apr 16, 2026
0f20a81
fix(desktop): Todo セッションに ScheduleWakeup 待機ステータスを追加 (#219)
MocA-Love Apr 16, 2026
582dc24
fix(desktop): CodeRabbit レビュー対応 — text と ScheduleWakeup 同時含有ケースを拾う
MocA-Love Apr 16, 2026
8c4a57a
fix(desktop): CodeRabbit レビュー対応 — supervisor.start 失敗時に failed 遷移
MocA-Love Apr 16, 2026
1ece3dd
fix(desktop): TODO の maxConcurrentTasks を尊重しキュー消化する (#220)
MocA-Love Apr 16, 2026
6c51d3e
fix(desktop): Agent Manager の タスク/スケジュール タブの UI 統一 (#222)
MocA-Love Apr 16, 2026
85ad352
fix(desktop): AgentManager 右サイドバーにセッション全体の差分を表示
MocA-Love Apr 16, 2026
6e721e0
fix(desktop): Codex レビュー対応 — セッション全体で削除ファイルもクリック可能に
MocA-Love Apr 16, 2026
485626a
feat(desktop): TODO 詳細の添付画像を chip 化+プレビュー対応 (closes #228)
MocA-Love Apr 16, 2026
572774a
docs(readme): add TODO 添付画像 chip プレビュー entry (#229)
MocA-Love Apr 16, 2026
62317cc
fix(desktop): TODOボタンのインジケーターをステータス別に表示
MocA-Love Apr 16, 2026
10167b8
feat(desktop): TodoManager preparing 状態でも description/goal を編集可能に
MocA-Love Apr 16, 2026
f7da887
feat(desktop): TODO ライブストリームをサブエージェント入れ子 + スタイリッシュ UI に刷新
MocA-Love Apr 16, 2026
5f9cd98
fix(desktop): TodoButton バッジに waiting ステータスを追加 (#219)
MocA-Love Apr 16, 2026
24a6cb5
Merge PR #216: PR返信ダイアログの返信元をMarkdownレンダリング対応
MocA-Love Apr 16, 2026
9e7fb89
Merge PR #218: スケジュール作成ダイアログの横幅を広げる
MocA-Love Apr 16, 2026
bd09979
Merge PR #221: Agent Manager 設定ダイアログでマネージャが閉じる問題を修正
MocA-Love Apr 16, 2026
49e203e
Merge PR #223: Todo セッションに ScheduleWakeup 待機ステータスを追加
MocA-Love Apr 16, 2026
f338bec
Merge PR #224: TODO の maxConcurrentTasks を尊重しキュー消化する
MocA-Love Apr 16, 2026
7d04fe3
Merge PR #225: Agent Manager の タスク/スケジュール タブの UI 統一
MocA-Love Apr 16, 2026
e8f5ac1
Merge PR #227: AgentManager 右サイドバーにセッション全体の差分を表示
MocA-Love Apr 16, 2026
317cf20
Merge PR #229: TODO 詳細の添付画像を chip 化+プレビュー対応
MocA-Love Apr 16, 2026
935fd43
Merge PR #231: TODOボタンのインジケーターをステータス別に表示
MocA-Love Apr 16, 2026
742a986
Merge PR #233: AgentManager の preparing 状態でも description/goal を編集可能に
MocA-Love Apr 16, 2026
e836a73
Merge PR #236: TODO ライブストリームをサブエージェント入れ子 + スタイリッシュ UI に刷新
MocA-Love Apr 16, 2026
7015bb8
fix(desktop): Codex レビュー対応 — restart 取りこぼし / waiting 編集不整合 / tool_res…
MocA-Love Apr 16, 2026
dcbf774
fix(desktop): CodeRabbit レビュー対応 — 5件
MocA-Love Apr 16, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Works with any CLI agent. Built for local worktree-based development.
| **内部ブラウザの File System Access API 拒否回避** | 内部ブラウザで react-dropzone 系サイトを開くと `FileSystemFileHandle.getFile()` が NotAllowedError で落ちる問題を修正。`persist:superset` セッションに preload を追加し `DataTransferItem.getAsFileSystemHandle()` を null 返却に差し替えて legacy D&D パスへフォールバック | [#207](https://github.com/MocA-Love/superset/pull/207) | 2026-04-16 |
| **PR コメント返信** | Review タブのコメント右上に Reply ボタンを追加。ダイアログから直接返信を投稿できる。レビュースレッドへの返信と通常 PR コメントの両方に対応 | [#206](https://github.com/MocA-Love/superset/pull/206) | 2026-04-16 |
| **TODO Agent スケジュール実行** | 毎日デプロイ / 毎時 lint のような定型 TODO を UI ビルダー (毎時/毎日/毎週/毎月/cron) で登録可能。アプリ起動中に時刻が来ると TODO セッションが自動作成され発火トーストを表示。前回未完了時は skip / queue 選択可 | [#211](https://github.com/MocA-Love/superset/pull/211) | 2026-04-16 |
| **TODO 詳細の添付画像 chip 化+プレビュー** | TODO 作成時に「やってほしいこと」「ゴール」へ貼り付けた画像を、タスク詳細画面でクリップマーク + ファイル名の chip として表示。クリックでネスト Dialog の画像プレビューを開ける(AgentManager は閉じない)。`todo-agent/attachments/` 配下のみを許可するパス検証付き `readAttachment` tRPC を追加 | [#229](https://github.com/MocA-Love/superset/pull/229) | 2026-04-16 |

## Fork のビルド方法 (macOS)

Expand Down
142 changes: 118 additions & 24 deletions apps/desktop/src/main/todo-agent/git-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ async function gitOut(args: string[], cwd: string): Promise<string> {
}
}

/**
* Does `sha` resolve to a commit object in `cwd`'s git dir? Used to
* distinguish "no new commits" from "startHeadSha was orphaned by a
* reset/rebase" — otherwise both look identical in the sidebar.
*/
async function gitRevExists(sha: string, cwd: string): Promise<boolean> {
try {
await execGitWithShellPath(
["rev-parse", "--verify", "--quiet", `${sha}^{commit}`],
{ cwd },
);
return true;
} catch {
return false;
}
}

export async function getCurrentHeadSha(cwd: string): Promise<string | null> {
const out = (await gitOut(["rev-parse", "HEAD"], cwd)).trim();
return out || null;
Expand All @@ -42,12 +59,34 @@ export interface SessionGitFile {
code: string;
}

export interface SessionGitChangedFile {
path: string;
/** First letter of git's name-status code: A / M / D / R / C / T */
code: string;
}

export interface SessionGitSnapshot {
branch: string | null;
startHeadSha: string | null;
currentHeadSha: string | null;
commits: SessionGitCommit[];
workingTree: SessionGitFile[];
/**
* Files whose contents differ between `startHeadSha` and HEAD
* (two-dot `git diff`). Populated regardless of whether HEAD is a
* descendant of startHeadSha, so branch switches / rebases still
* surface the cumulative session delta instead of silently
* rendering an empty sidebar.
*/
sessionFiles: SessionGitChangedFile[];
/**
* True when `startHeadSha` is set but its commit object is no
* longer reachable (e.g. the branch was reset and the object was
* pruned, or a different repo was swapped in under the worktree).
* The UI uses this to show an explanatory message rather than a
* silently empty panel.
*/
startHeadUnreachable: boolean;
ahead: number;
behind: number;
}
Expand All @@ -68,32 +107,53 @@ export async function getSessionGitSnapshot(params: {
const branch = branchOut.trim() || null;
const currentHeadSha = currentOut.trim() || null;

// Commits produced since the session started. If start and current
// are the same (no new commits yet) this returns an empty list.
// Commits produced since the session started. Scoped to the range
// `startHeadSha..HEAD`; when HEAD is not a descendant of
// startHeadSha (branch switch / reset / rebase), this can validly
// return an empty list, and we surface cumulative file-level
// changes via `sessionFiles` below so the sidebar isn't empty.
let commits: SessionGitCommit[] = [];
let sessionFiles: SessionGitChangedFile[] = [];
let startHeadUnreachable = false;
if (startHeadSha && currentHeadSha && startHeadSha !== currentHeadSha) {
const logOut = await gitOut(
[
"log",
`${startHeadSha}..${currentHeadSha}`,
`--format=${COMMIT_FORMAT}`,
],
cwd,
);
commits = logOut
.split("\n")
.filter((l) => l.length > 0)
.map((line) => {
const [sha, shortSha, subject, authorName, authorDate] =
line.split(COMMIT_DELIM);
return {
sha: sha ?? "",
shortSha: shortSha ?? "",
subject: subject ?? "",
authorName: authorName ?? "",
authorDate: authorDate ?? "",
};
});
const reachable = await gitRevExists(startHeadSha, cwd);
if (!reachable) {
startHeadUnreachable = true;
} else {
const logOut = await gitOut(
[
"log",
`${startHeadSha}..${currentHeadSha}`,
`--format=${COMMIT_FORMAT}`,
],
cwd,
);
commits = logOut
.split("\n")
.filter((l) => l.length > 0)
.map((line) => {
const [sha, shortSha, subject, authorName, authorDate] =
line.split(COMMIT_DELIM);
return {
sha: sha ?? "",
shortSha: shortSha ?? "",
subject: subject ?? "",
authorName: authorName ?? "",
authorDate: authorDate ?? "",
};
});

// `git diff --name-status -z A B` compares the two commits
// directly (two-dot in diff has no range semantics), so it
// works even when A and B are on divergent histories. This
// is what lets the sidebar show the real session delta
// when commits are zero but files were touched.
const diffOut = await gitOut(
["diff", "--name-status", "-z", startHeadSha, currentHeadSha],
cwd,
);
sessionFiles = parseNameStatusNul(diffOut);
}
}

// Working tree state via porcelain v1 for stable parsing.
Expand Down Expand Up @@ -152,11 +212,45 @@ export async function getSessionGitSnapshot(params: {
currentHeadSha,
commits,
workingTree,
sessionFiles,
startHeadUnreachable,
ahead,
behind,
};
}

/**
* Parse `git diff --name-status -z` output.
*
* Standard entries are `<CODE>\0<path>\0`; rename/copy entries are
* `<CODE><score>\0<oldPath>\0<newPath>\0` — we keep only the new path
* and collapse the code to its first letter so the UI can render a
* single badge per file.
*/
function parseNameStatusNul(raw: string): SessionGitChangedFile[] {
const files: SessionGitChangedFile[] = [];
const parts = raw.split("\0");
let i = 0;
while (i < parts.length) {
const token = parts[i];
if (!token) {
i += 1;
continue;
}
const letter = token[0] ?? "";
if (letter === "R" || letter === "C") {
const newPath = parts[i + 2];
if (newPath) files.push({ path: newPath, code: letter });
i += 3;
continue;
}
const p = parts[i + 1];
if (p) files.push({ path: p, code: letter || token });
i += 2;
}
return files;
}

export type SessionDiffScope = "session" | "staged" | "unstaged" | "commit";

export async function getSessionFileDiff(params: {
Expand Down
45 changes: 44 additions & 1 deletion apps/desktop/src/main/todo-agent/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,12 @@ function isSessionActive(session: SelectTodoSession | undefined): boolean {
session.status === "preparing" ||
session.status === "running" ||
session.status === "verifying" ||
session.status === "paused"
session.status === "paused" ||
// `waiting` means the worker called `ScheduleWakeup` to pause
// itself and will be resumed by the scheduler tick. Count it as
// active so the overlap guard and the concurrency display do not
// treat a self-parked session as finished.
session.status === "waiting"
);
}

Expand Down Expand Up @@ -172,6 +177,11 @@ class TodoScheduler {
this.inFlight = true;
try {
const store = getTodoScheduleStore();
// Wake self-paced (`ScheduleWakeup`) sessions whose deadline
// has passed before we process new schedule fires. Doing this
// first means a schedule firing into an already-waiting
// session sees the updated status and respects overlap mode.
this.resumeDueWaitingSessions();
// Snapshot "due" using tick start time, but compute each
// schedule's firedAt from the actual moment fire() runs.
// Otherwise a slow fire leaves the next schedule in the loop
Expand All @@ -193,6 +203,39 @@ class TodoScheduler {
}
}

/**
* Scan for `waiting` sessions whose `waitingUntil` has elapsed and
* hand them back to the supervisor. The status flip is gated on the
* row still being `waiting` at claim time so a race with the user
* clicking Abort (which writes `aborted`) between `listWaitingDue`
* and the update cannot resurrect an abort into a fresh run.
*
* `supervisor.start` is currently a synchronous queue+drain wrapper
* that does not throw, so the trailing `.catch` here is purely a
* defensive log path: if a future change to `start` introduces
* validation throws, the rejection still surfaces in the console
* instead of becoming an unhandled rejection. Run-time failures
* inside `runSession` are owned by the supervisor's own drain
* pipeline and are not the scheduler's responsibility.
*/
private resumeDueWaitingSessions(): void {
const sessionStore = getTodoSessionStore();
const due = sessionStore.listWaitingDue(Date.now());
if (due.length === 0) return;
const supervisor = getTodoSupervisor();
for (const session of due) {
if (this.isStopped) return;
const claimed = sessionStore.claimWaitingForResume(session.id);
if (!claimed) continue;
void supervisor.start(session.id).catch((err) => {
console.warn(
`[todo-scheduler] supervisor.start unexpectedly rejected for ${session.id}:`,
err,
);
});
Comment thread
MocA-Love marked this conversation as resolved.
}
}

private async fire(
schedule: SelectTodoSchedule,
firedAt: number,
Expand Down
49 changes: 48 additions & 1 deletion apps/desktop/src/main/todo-agent/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
workspaces,
worktrees,
} from "@superset/local-db";
import { and, desc, eq, inArray, isNull, not } from "drizzle-orm";
import { and, desc, eq, inArray, isNull, lte, not } from "drizzle-orm";
import { localDb } from "main/lib/local-db";
import type {
TodoSessionListEntry,
Expand Down Expand Up @@ -279,6 +279,8 @@ class TodoSessionStore {
verdictReason: null,
verdictFailingTest: null,
artifactPath: template.artifactPath,
waitingUntil: null,
waitingReason: null,
startedAt: null,
completedAt: null,
});
Expand All @@ -301,6 +303,51 @@ class TodoSessionStore {
.all();
}

/**
* Sessions parked in `waiting` whose `waitingUntil` deadline has
* passed. Drives the scheduler tick that resumes `ScheduleWakeup`-
* paused sessions once their delay elapses.
*/
listWaitingDue(nowMs: number): SelectTodoSession[] {
return localDb
.select()
.from(todoSessions)
.where(
and(
eq(todoSessions.status, "waiting"),
lte(todoSessions.waitingUntil, nowMs),
),
)
.all();
}

/**
* Atomically flip a row from `waiting` → `queued` and clear the
* parking fields. Returns the updated row (so callers can tell they
* won the claim) or undefined when the session has since moved to a
* different status — typically because the user clicked Abort while
* the scheduler tick was already in flight. Used as the race guard
* before the scheduler hands a session back to the supervisor.
*/
claimWaitingForResume(sessionId: string): SelectTodoSession | undefined {
const updated = localDb
.update(todoSessions)
.set({
status: "queued",
phase: "queued",
waitingUntil: null,
waitingReason: null,
updatedAt: Date.now(),
})
.where(
and(eq(todoSessions.id, sessionId), eq(todoSessions.status, "waiting")),
)
.returning()
.get();
if (updated) this.emit(updated);
return updated;
}

/**
* Cross-workspace list used by the Agent-Manager-style view. Joins in
* workspace + project names so the manager can group and label rows
Expand Down
Loading
Loading