Files
tinycast/Tests/fallback-test.swift
Ruben Nogueiraandabue-ammar 75974f86fb Add an AI Chat window with a conversations sidebar (#1047)
* feat(ai): add an AI Chat window with a conversations sidebar

A Raycast-style AI Chat window: saved conversations on the left, the open
one on the right, and a composer that picks the model, reasoning effort and
tools per chat. The palette's chat becomes Quick AI; ⌘J moves a Quick AI
conversation into the window.

- Each chat keeps its model, effort and tool scope; reopening it restores them
- Reasoning shows as collapsible thinking blocks, one per stretch of thinking
- Image paste and attachments for vision-capable models (Claude, Codex)
- Model lists come from each CLI's own catalog, so new Claude models appear
- Context gauge with a hover card: history, budget, files, tools, tokens
- Choices render as buttons above the composer; a bare "choices" list counts
- Sources list with citation numbers at the end of the citing sentence
- Find in Chat (⌘F) steps word by word and marks each match in place
- A reply's text is one selectable NSTextView, so a drag spans paragraphs,
  lists, code and tables
- Chats are titled by their harness as soon as the first question is sent
- Several Codex chats can answer at once, each on its own thread
- Actions menu, pin, rename, copy and delete from the sidebar

* Refactor AI chat components and enhance functionality

- Updated `ChatTitle` to improve string sanitization.
- Modified `InstalledAI` to enhance name extraction logic.
- Renamed database tables from `conversation_meta` to `conversation_details` and `message_meta` to `message_details` for clarity.
- Added export functionality for chat transcripts in `AIChatCoordinator`.
- Improved UI elements in `AIChatDetailView` and `AIChatSidebarView` for better user experience.
- Enhanced `AIChatWindowChrome` to streamline toolbar actions and improve navigation.
- Updated keyboard shortcuts for new chat, settings, and other actions across various components.
- Improved chat transcript rendering in `ChatMarkdownText` and `ChatTranscriptView`.
- Enhanced `QuickAICoordinator` to support regeneration of responses.
- Updated documentation to reflect changes in database structure and new features.

* feat(ai): enhance AI chat sidebar and history functionality with new continue action

* feat(ai): update AI chat window and sidebar dimensions for improved layout

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-23 23:36:31 +06:00

129 lines
5.1 KiB
Swift

// The fallback list's pure half: identity, stored order, and the section header's elision.
import Foundation
@main
@MainActor
struct FallbackTests {
static var failures = 0
static var passes = 0
static func check(_ name: String, _ condition: Bool, _ detail: String = "") {
if condition {
passes += 1
} else {
failures += 1
print("FAIL: \(name)\(detail.isEmpty ? "" : " — \(detail)")")
}
}
static func main() {
identity()
ordering()
headers()
verbs()
print("\(passes) passed, \(failures) failed")
if failures > 0 { exit(1) }
}
// MARK: - Identity
static func identity() {
// The id *is* the row's entry id, which is what lets a stored order name a live row.
for builtin in Fallback.Builtin.allCases {
let fallback = Fallback.builtin(builtin)
check(
"\(builtin.rawValue) is its command's id", fallback.id == builtin.command.rawValue,
"got \(fallback.id)")
check(
"\(builtin.rawValue) round-trips", Fallback(id: fallback.id) == fallback,
"got \(String(describing: Fallback(id: fallback.id)))")
}
let quicklinkID = UUID()
let quicklink = Fallback.quicklink(quicklinkID)
check(
"a quicklink's id is its entry id",
quicklink.id == Quicklink.entryIDPrefix + quicklinkID.uuidString.lowercased(),
"got \(quicklink.id)")
check("a quicklink round-trips", Fallback(id: quicklink.id) == quicklink)
// Every built-in's id must be distinct, or one would silently take another's stored slot.
let ids = Set(Fallback.Builtin.allCases.map { Fallback.builtin($0).id })
check("built-in ids are distinct", ids.count == Fallback.Builtin.allCases.count)
// A command with no fallback of its own must not resolve to one.
check("a plain command is not a fallback", Fallback(id: CommandID.about.rawValue) == nil)
check("nonsense is not a fallback", Fallback(id: "banana") == nil)
check("a bare uuid is not a fallback", Fallback(id: UUID().uuidString) == nil)
}
// MARK: - Ordering
static func ordering() {
let ai = Fallback.builtin(.quickAI)
let files = Fallback.builtin(.searchFiles)
let shell = Fallback.builtin(.runShellCommand)
let link = Fallback.quicklink(UUID())
check(
"no stored order keeps the offered order",
Fallback.ordered([ai, files, shell], by: []) == [ai, files, shell])
check(
"a stored order is honoured",
Fallback.ordered([ai, files, shell], by: [shell.id, ai.id, files.id])
== [shell, ai, files])
// A quicklink created after the last reorder must land at the end, not vanish.
check(
"an unseen fallback lands last",
Fallback.ordered([ai, link, files], by: [files.id, ai.id]) == [files, ai, link])
// A deleted quicklink's id is still stored; it must not resurrect or shift its neighbours.
check(
"a stored id with nothing behind it is skipped",
Fallback.ordered([ai, files], by: [link.id, files.id, ai.id]) == [files, ai])
check("nothing available is nothing offered", Fallback.ordered([], by: [ai.id]).isEmpty)
}
// MARK: - Header
static func headers() {
check(
"a short query is quoted whole",
Fallback.sectionTitle(query: "todo") == "Use “todo” with…",
"got \(Fallback.sectionTitle(query: "todo"))")
let long = String(repeating: "a", count: 200)
let title = Fallback.sectionTitle(query: long, limit: 20)
// The point of the elision: the header must still say what the section is for.
check("a long query still ends in the verb", title.hasSuffix("” with…"), "got \(title)")
check("a long query is elided", title.contains("…a"), "got \(title)")
check("a long query is bounded by the limit", title.count <= 20 + 13, "got \(title.count)")
// The boundary is exact equality, so a query at the limit is never touched.
let exact = String(repeating: "b", count: 20)
check(
"a query at the limit is kept whole",
Fallback.sectionTitle(query: exact, limit: 20) == "Use “\(exact)” with…")
// The shipped limit has to clear a real URL, which is the query this was drawn against.
let url = "https://github.com/vgnshiyer/py-apple-books.git"
check(
"the default limit keeps a repository URL whole",
Fallback.sectionTitle(query: url) == "Use “\(url)” with…",
"got \(Fallback.sectionTitle(query: url))")
}
// MARK: - Verbs
static func verbs() {
var verbs = Fallback.Builtin.allCases.map { Fallback.builtin($0).openVerb }
verbs.append(Fallback.quicklink(UUID()).openVerb)
check("every fallback names its own action", verbs.allSatisfy { !$0.isEmpty })
check("the verbs are distinct", Set(verbs).count == verbs.count, "got \(verbs)")
}
}