Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
74 changes: 74 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions subtrack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"consola": "^3.4.2",
"gunshi": "^0.35.1",
"ink": "^7.1.0",
"ink-gradient": "^4.0.1",
"ink-spinner": "^5.0.0",
"picocolors": "^1.1.1",
"react": "^19.2.7",
"sql.js": "^1.14.1"
Expand Down
25 changes: 25 additions & 0 deletions subtrack/src/__tests__/db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,31 @@ test("getSubscriptions falls back to id order for invalid sort field", async ()
expect(subs[1].name).toBe("A")
})

test("getSubscriptions sorts by status ascending", async () => {
const db = await import("../db.ts")
// status alpha order: active < cancelled < paused
db.writeSubscription({ name: "Mid", price: 100, currency: "USD", cycle: "monthly", status: "cancelled", tags: [] })
db.writeSubscription({ name: "First", price: 100, currency: "USD", cycle: "monthly", status: "active", tags: [] })
db.writeSubscription({ name: "Last", price: 100, currency: "USD", cycle: "monthly", status: "paused", tags: [] })

const subs = db.getSubscriptions("status", false)
expect(subs[0].status).toBe("active")
expect(subs[1].status).toBe("cancelled")
expect(subs[2].status).toBe("paused")
})

test("getSubscriptions sorts by status descending", async () => {
const db = await import("../db.ts")
db.writeSubscription({ name: "Mid", price: 100, currency: "USD", cycle: "monthly", status: "cancelled", tags: [] })
db.writeSubscription({ name: "First", price: 100, currency: "USD", cycle: "monthly", status: "active", tags: [] })
db.writeSubscription({ name: "Last", price: 100, currency: "USD", cycle: "monthly", status: "paused", tags: [] })

const subs = db.getSubscriptions("status", true)
expect(subs[0].status).toBe("paused")
expect(subs[1].status).toBe("cancelled")
expect(subs[2].status).toBe("active")
})

// ── getSubscription ───────────────────────────────────────

test("getSubscription returns a single subscription by id", async () => {
Expand Down
2 changes: 1 addition & 1 deletion subtrack/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export function mapTags(subs: SharedArgs[]): SharedArgs[] {
return subs
}

const SORT_FIELDS = ["id", "name", "price", "currency", "cycle"] as const
const SORT_FIELDS = ["id", "name", "price", "currency", "cycle", "status"] as const

export const getSubscriptions = (sort?: string, desc?: boolean): SharedArgs[] => {
const db = getDb()
Expand Down
15 changes: 12 additions & 3 deletions subtrack/src/tui.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { render } from "ink"
import { consola } from "consola"
import { App } from "./tui/app.tsx"

export async function handleTui(): Promise<void> {
const { waitUntilExit } = render(<App />, {
// Silence consola during TUI to prevent log pollution
const prevLevel = consola.level
consola.level = -999

const instance = render(<App />, {
exitOnCtrlC: true,
patchConsole: true,
})

try {
await waitUntilExit()
} catch (error) {
await instance.waitUntilExit()
} catch {
// App exited with an error — silently ignore for clean exit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restore logging even if render fails, and don’t swallow TUI errors.

render(<App />) happens before the try, so a render-time failure leaves consola.level muted. The broad catch also converts real TUI failures into successful exits.

Proposed fix
-  const instance = render(<App />, {
-    exitOnCtrlC: true,
-    patchConsole: true,
-  })
-
+  let instance: ReturnType<typeof render> | undefined
   try {
+    instance = render(<App />, {
+      exitOnCtrlC: true,
+      patchConsole: true,
+    })
     await instance.waitUntilExit()
-  } catch {
-    // App exited with an error — silently ignore for clean exit
   } finally {
     // Clear Ink's output and restore terminal
-    instance.clear()
-    consola.level = prevLevel
+    try {
+      instance?.clear()
+    } finally {
+      consola.level = prevLevel
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const instance = render(<App />, {
exitOnCtrlC: true,
patchConsole: true,
})
try {
await waitUntilExit()
} catch (error) {
await instance.waitUntilExit()
} catch {
// App exited with an error — silently ignore for clean exit
let instance: ReturnType<typeof render> | undefined
try {
instance = render(<App />, {
exitOnCtrlC: true,
patchConsole: true,
})
await instance.waitUntilExit()
} finally {
// Clear Ink's output and restore terminal
try {
instance?.clear()
} finally {
consola.level = prevLevel
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@subtrack/src/tui.tsx` around lines 10 - 18, Wrap the entire TUI lifecycle in
the try/catch so any failure from render(<App />) or instance.waitUntilExit() is
handled consistently, and make sure consola.level is restored in a finally block
even when render throws. In the catch for the App/render flow, do not silently
ignore the error; rethrow or propagate real TUI failures so they are not
converted into successful exits. Use the render and waitUntilExit logic in
tui.tsx as the anchor for the fix.

} finally {
// Clear Ink's output and restore terminal
instance.clear()
consola.level = prevLevel
}
}
Loading
Loading