@saadnvd1/agent-os 0.3.503 → 0.3.505

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +47 -0
  2. package/app/api/projects/[id]/session-logs/route.ts +39 -0
  3. package/app/api/session-logs/[id]/route.ts +18 -0
  4. package/components/Pane/DesktopTabBar.tsx +4 -1
  5. package/components/Pane/MobileTabBar.tsx +12 -1
  6. package/components/Pane/PaneViewToggle.tsx +14 -2
  7. package/components/Pane/index.tsx +28 -1
  8. package/components/PrStageBadge.tsx +15 -6
  9. package/components/Projects/ProjectSettingsDialog.tsx +11 -0
  10. package/components/SessionList/LiveLine.tsx +75 -0
  11. package/components/SessionList/RowParts.tsx +1 -7
  12. package/components/SessionList/RowShell.tsx +1 -1
  13. package/components/SessionList/RowSubtitle.tsx +4 -0
  14. package/components/SessionList/SessionRow.tsx +9 -3
  15. package/components/SessionLogs/LogDocument.tsx +29 -0
  16. package/components/SessionLogs/ProjectLogsSection.tsx +140 -0
  17. package/contexts/PaneContext.tsx +1 -1
  18. package/data/session-logs/index.ts +73 -0
  19. package/lib/db/migrations.ts +24 -0
  20. package/lib/done/index.ts +2 -0
  21. package/lib/orchestrator/brief.ts +1 -0
  22. package/lib/orchestrator/logs.ts +81 -0
  23. package/lib/orchestrator/serve.ts +5 -0
  24. package/lib/orchestrator/tool-names.ts +1 -0
  25. package/lib/orchestrator/tool-schemas.ts +13 -0
  26. package/lib/orchestrator/tools.ts +1 -0
  27. package/lib/session-logs/build.ts +253 -0
  28. package/lib/session-logs/folder.ts +145 -0
  29. package/lib/session-logs/gather.ts +301 -0
  30. package/lib/session-logs/index.ts +178 -0
  31. package/lib/session-logs/store.ts +105 -0
  32. package/lib/sidebar/live-status.ts +52 -0
  33. package/lib/tasks/finish.ts +3 -0
  34. package/lib/tasks/pr-stage.ts +14 -0
  35. package/lib/tasks/session.ts +4 -0
  36. package/package.json +1 -1
  37. package/scripts/backfill-session-logs.ts +70 -0
@@ -0,0 +1,105 @@
1
+ import { db } from "../db";
2
+
3
+ // What the list and the orchestrator's tool show of a log without its text.
4
+ export interface LogHeader {
5
+ id: string;
6
+ name: string;
7
+ workspace: string | null;
8
+ project: string | null;
9
+ branch: string | null;
10
+ prNumber: number | null;
11
+ prUrl: string | null;
12
+ mergeCommit: string | null;
13
+ startedAt: string | null;
14
+ mergedAt: string | null;
15
+ durationMs: number | null;
16
+ costUsd: number;
17
+ planKept: boolean;
18
+ hasPlan: boolean;
19
+ createdAt: string;
20
+ filePath: string | null;
21
+ }
22
+
23
+ export interface LogRow {
24
+ session_id: string;
25
+ project_id: string | null;
26
+ workspace_id: string | null;
27
+ created_at: string;
28
+ markdown: string;
29
+ data: string;
30
+ file_path: string | null;
31
+ }
32
+
33
+ export const hasLog = (sessionId: string): boolean =>
34
+ !!db
35
+ .prepare(`SELECT 1 FROM session_logs WHERE session_id = ?`)
36
+ .get(sessionId);
37
+
38
+ export function getLog(sessionId: string): LogRow | null {
39
+ return (
40
+ (db
41
+ .prepare(`SELECT * FROM session_logs WHERE session_id = ?`)
42
+ .get(sessionId) as LogRow | undefined) ?? null
43
+ );
44
+ }
45
+
46
+ // True when this call stored it: a log already there is never replaced.
47
+ export function insertLog(row: {
48
+ sessionId: string;
49
+ projectId: string | null;
50
+ workspaceId: string | null;
51
+ markdown: string;
52
+ header: Omit<LogHeader, "createdAt" | "filePath">;
53
+ filePath: string | null;
54
+ }): boolean {
55
+ return (
56
+ db
57
+ .prepare(
58
+ `INSERT OR IGNORE INTO session_logs
59
+ (session_id, project_id, workspace_id, markdown, data, file_path)
60
+ VALUES (?, ?, ?, ?, ?, ?)`
61
+ )
62
+ .run(
63
+ row.sessionId,
64
+ row.projectId,
65
+ row.workspaceId,
66
+ row.markdown,
67
+ JSON.stringify(row.header),
68
+ row.filePath
69
+ ).changes > 0
70
+ );
71
+ }
72
+
73
+ export function setFilePath(sessionId: string, filePath: string): void {
74
+ db.prepare(`UPDATE session_logs SET file_path = ? WHERE session_id = ?`).run(
75
+ filePath,
76
+ sessionId
77
+ );
78
+ }
79
+
80
+ const toHeader = (r: LogRow): LogHeader => ({
81
+ ...(JSON.parse(r.data) as LogHeader),
82
+ createdAt: r.created_at,
83
+ filePath: r.file_path,
84
+ });
85
+
86
+ export const headerOf = (r: LogRow): LogHeader => toHeader(r);
87
+
88
+ // Newest merge first.
89
+ export function listLogs(
90
+ where: { projectId?: string; workspaceId?: string },
91
+ limit = 50
92
+ ): LogHeader[] {
93
+ const rows = db
94
+ .prepare(
95
+ `SELECT * FROM session_logs
96
+ WHERE (@p IS NULL OR project_id = @p) AND (@w IS NULL OR workspace_id = @w)
97
+ ORDER BY json_extract(data, '$.mergedAt') DESC, created_at DESC LIMIT @n`
98
+ )
99
+ .all({
100
+ p: where.projectId ?? null,
101
+ w: where.workspaceId ?? null,
102
+ n: Math.max(1, Math.min(limit, 500)),
103
+ }) as LogRow[];
104
+ return rows.map(toHeader);
105
+ }
@@ -0,0 +1,52 @@
1
+ import {
2
+ PR_STAGE_BUSY,
3
+ PR_STAGE_SHORT,
4
+ type PrStage,
5
+ type PrStageName,
6
+ } from "../tasks/pr-stage";
7
+
8
+ export type LiveTone = "blue" | "amber" | "red" | "green" | "purple" | "muted";
9
+
10
+ export interface LivePart {
11
+ text: string;
12
+ tone: LiveTone;
13
+ // Still moving on its own: the UI shows a spinner.
14
+ busy?: boolean;
15
+ }
16
+
17
+ export const PR_STAGE_TONE: Record<PrStageName, LiveTone> = {
18
+ merged: "purple",
19
+ conflicts: "amber",
20
+ "ci-failed": "red",
21
+ "ci-running": "blue",
22
+ "ci-settling": "blue",
23
+ "review-running": "blue",
24
+ "review-blocked": "red",
25
+ waiting: "amber",
26
+ armed: "green",
27
+ open: "muted",
28
+ };
29
+
30
+ // What the row's second line says about the session right now, in order:
31
+ // "Compacting", then "PR CI". Empty when there is nothing to say.
32
+ export function liveParts(input: {
33
+ compacting?: boolean;
34
+ prStage?: PrStage;
35
+ }): LivePart[] {
36
+ const parts: LivePart[] = [];
37
+ if (input.compacting)
38
+ parts.push({ text: "Compacting", tone: "blue", busy: true });
39
+ const s = input.prStage;
40
+ if (s) {
41
+ const word = PR_STAGE_SHORT[s.stage];
42
+ parts.push({
43
+ text: s.stage === "open" ? "PR" : `PR ${word}`,
44
+ tone: PR_STAGE_TONE[s.stage],
45
+ busy: PR_STAGE_BUSY.has(s.stage),
46
+ });
47
+ }
48
+ return parts;
49
+ }
50
+
51
+ export const liveText = (parts: LivePart[]) =>
52
+ parts.map((p) => p.text).join(" · ");
@@ -23,6 +23,7 @@ import { getProject } from "../projects";
23
23
  import { canSignOff } from "./state";
24
24
  import { forgetPR, getTaskSession, prFor, projectPathFor } from "./session";
25
25
  import { dropSessionDatabase } from "../project-config/database";
26
+ import { captureMergedTask } from "../session-logs";
26
27
 
27
28
  // Never deleted, whatever a setting says.
28
29
  export const PROTECTED_BRANCHES = new Set(["main", "master", "HEAD"]);
@@ -154,6 +155,8 @@ export async function signOffTask(
154
155
  signingOff.delete(id);
155
156
  }
156
157
  syncTaskCardInBackground(session, "merged", pr);
158
+ // Reads the plan file now: cleanup below removes the worktree.
159
+ void captureMergedTask(id, { head: opts.head ?? pr!.head ?? null });
157
160
  const head = opts.head ?? pr!.head;
158
161
  resolveMergedTaskAsks(
159
162
  id,
@@ -90,3 +90,17 @@ export const PR_STAGE_LABEL: Record<PrStageName, string> = {
90
90
 
91
91
  export const prStageText = (s: Pick<PrStage, "stage" | "number">) =>
92
92
  `PR #${s.number} · ${PR_STAGE_LABEL[s.stage]}`;
93
+
94
+ // The short wording for the badge itself; the full wording is the tooltip.
95
+ export const PR_STAGE_SHORT: Record<PrStageName, string> = {
96
+ merged: "Merged",
97
+ conflicts: "Conflicts",
98
+ "ci-failed": "CI failed",
99
+ "ci-running": "CI",
100
+ "ci-settling": "CI",
101
+ "review-running": "Review",
102
+ "review-blocked": "Blocked",
103
+ waiting: "Waiting on you",
104
+ armed: "Armed",
105
+ open: "PR",
106
+ };
@@ -9,6 +9,7 @@ import { getProject } from "../projects";
9
9
  import { findPRStrict } from "./gh";
10
10
  import { currentBranch, git } from "../git-exec";
11
11
  import { lookupPR } from "./pr-poll";
12
+ import { captureMergedTask } from "../session-logs";
12
13
  import { notifyStatusChanged } from "../status/hub";
13
14
  import type { TaskPR } from "./state";
14
15
 
@@ -185,6 +186,9 @@ export async function prFor(
185
186
  db.prepare(
186
187
  `UPDATE sessions SET pr_url = ?, pr_number = ?, pr_status = ? WHERE id = ?`
187
188
  ).run(pr.url, pr.number, pr.state.toLowerCase(), session.id);
189
+ // Merged on GitHub rather than through AgentOS: its log is due too.
190
+ if (pr.state === "MERGED")
191
+ void captureMergedTask(session.id, { head: pr.head });
188
192
  }
189
193
  // Only an answer just read from GitHub: a cached one may predate a
190
194
  // retarget AgentOS made itself (a restack).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.503",
3
+ "version": "0.3.505",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Writes session logs for tasks that merged before logs existed, from what
3
+ * the database still holds. Their plan files went with their worktrees, so
4
+ * each says "plan not kept". Idempotent: a task with a log is skipped.
5
+ *
6
+ * npx tsx scripts/backfill-session-logs.ts --db <path to agent-os.db> [--no-gh] [--dry]
7
+ *
8
+ * --db is required so a run never lands on a database by accident. Files go
9
+ * where new logs go (~/.agent-os/session-logs, or a project's folder); run
10
+ * with HOME set elsewhere to keep a trial away from the real folder.
11
+ * --no-gh skips asking GitHub for each merge commit.
12
+ */
13
+
14
+ const args = process.argv.slice(2);
15
+ const flag = (name: string) => args.includes(name);
16
+ const dbPath = args[args.indexOf("--db") + 1];
17
+
18
+ if (!flag("--db") || !dbPath || dbPath.startsWith("--")) {
19
+ console.error(
20
+ "usage: backfill-session-logs.ts --db <path> [--no-gh] [--dry]"
21
+ );
22
+ process.exit(2);
23
+ }
24
+ process.env.DB_PATH = dbPath;
25
+
26
+ async function main() {
27
+ const { db } = await import("../lib/db");
28
+ const { writeLog } = await import("../lib/session-logs");
29
+ const { hasLog } = await import("../lib/session-logs/store");
30
+ const rows = db
31
+ .prepare(
32
+ `SELECT id, name FROM sessions
33
+ WHERE task_prompt IS NOT NULL AND peer_mirror = 0
34
+ AND (task_status = 'merged' OR (pr_status = 'merged' AND task_status != 'dropped'))
35
+ ORDER BY COALESCE(merged_at, updated_at)`
36
+ )
37
+ .all() as { id: string; name: string }[];
38
+ const todo = rows.filter((r) => !hasLog(r.id));
39
+ console.log(
40
+ `${rows.length} merged tasks, ${rows.length - todo.length} already logged, ${todo.length} to write${flag("--dry") ? " (dry run)" : ""}`
41
+ );
42
+ let written = 0;
43
+ for (const r of todo) {
44
+ if (flag("--dry")) {
45
+ console.log(` would log ${r.name} (${r.id.slice(0, 8)})`);
46
+ continue;
47
+ }
48
+ try {
49
+ const ok = await writeLog(r.id, {
50
+ planKept: false,
51
+ noGh: flag("--no-gh"),
52
+ });
53
+ if (ok) written++;
54
+ console.log(
55
+ ` ${ok ? "logged" : "skipped"} ${r.name} (${r.id.slice(0, 8)})`
56
+ );
57
+ } catch (error) {
58
+ console.error(` FAILED ${r.name} (${r.id.slice(0, 8)}):`, error);
59
+ }
60
+ }
61
+ console.log(`wrote ${written} logs`);
62
+ }
63
+
64
+ main().then(
65
+ () => process.exit(0),
66
+ (error) => {
67
+ console.error(error);
68
+ process.exit(1);
69
+ }
70
+ );