Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
45 changes: 1 addition & 44 deletions Packages/Sources/RxCodeChatKit/MessageListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,56 +15,14 @@ struct MessageListView: View {
/// (which also writes to `scrollTask`) can't cancel the session-ready flip.
@State private var readyTask: Task<Void, Never>?
@State private var anchor = AutoScrollAnchor()
@State private var isOlderCollapsed = true
@State private var isSessionReady = false

private let foldThreshold = 30
private static let log = Logger(subsystem: "com.claudework", category: "MessageListView")

var body: some View {
ScrollView {
VStack(spacing: 16) {
// Fold older messages when count exceeds threshold
if settledItems.count > foldThreshold {
let hiddenCount = settledItems.count - foldThreshold

// Expanded state: show older messages
if !isOlderCollapsed {
messageRows(settledItems.prefix(hiddenCount))
}

// Fold toggle button
Button {
withAnimation(.easeInOut(duration: 0.25)) {
isOlderCollapsed.toggle()
}
} label: {
HStack(spacing: 6) {
Group {
if isOlderCollapsed {
Text(String(format: String(localized: "Show %lld earlier messages", bundle: .module), hiddenCount))
} else {
Text("Collapse earlier messages", bundle: .module)
}
}
.font(.system(size: ClaudeTheme.size(12), weight: .medium))
Image(systemName: isOlderCollapsed ? "chevron.down" : "chevron.up")
.font(.system(size: ClaudeTheme.size(10), weight: .medium))
}
.foregroundStyle(ClaudeTheme.textTertiary)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.cornerRadiusSmall)
.fill(ClaudeTheme.surfacePrimary.opacity(0.6))
)
}
.buttonStyle(.plain)

messageRows(settledItems.suffix(foldThreshold))
} else {
messageRows(settledItems[...])
}
messageRows(settledItems[...])
}
.padding(.horizontal, 20)
.padding(.top, 16)
Expand Down Expand Up @@ -139,7 +97,6 @@ struct MessageListView: View {
isSessionReady = false
scrollTask?.cancel()
readyTask?.cancel()
isOlderCollapsed = true
scrollPosition = ScrollPosition()
rebuildSettledItems()
Self.log.info("[MessageList.task] post-rebuild settled=\(settledItems.count) sid=\(sid, privacy: .public) isLoadingFromDisk=\(chatBridge.isLoadingFromDisk)")
Expand Down
2 changes: 1 addition & 1 deletion Packages/Sources/RxCodeChatKit/WebPreviewButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct WebPreviewButton: View {
Text("View Result", bundle: .module)
Text(url.absoluteString)
.font(.caption)
.foregroundStyle(ClaudeTheme.textSecondary)
.foregroundStyle(.white.opacity(0.85))
}
.font(.subheadline)
}
Expand Down
71 changes: 71 additions & 0 deletions Packages/Sources/RxCodeCore/Models/BranchBriefingRecord.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation
import SwiftData

public struct BranchBriefingItem: Identifiable, Sendable, Equatable {
public var id: String { "\(projectId.uuidString)::\(branch)" }

public let projectId: UUID
public let branch: String
public let briefing: String
public let updatedAt: Date

public init(projectId: UUID, branch: String, briefing: String, updatedAt: Date) {
self.projectId = projectId
self.branch = branch
self.briefing = briefing
self.updatedAt = updatedAt
}
}

@Model
public final class BranchBriefingRecord {
@Attribute(.unique) public var id: String
public var projectId: UUID
public var branch: String
public var briefing: String
public var updatedAt: Date
/// Last time the branch was observed (e.g. as the current branch of its
/// project, or when the briefing was regenerated). Used to garbage-collect
/// briefings for branches that no longer exist locally or remotely.
public var lastSeenAt: Date = Date.distantPast

public init(
projectId: UUID,
branch: String,
briefing: String,
updatedAt: Date = .now,
lastSeenAt: Date = .now
) {
self.id = Self.makeId(projectId: projectId, branch: branch)
self.projectId = projectId
self.branch = branch
self.briefing = briefing
self.updatedAt = updatedAt
self.lastSeenAt = lastSeenAt
}

public static func makeId(projectId: UUID, branch: String) -> String {
"\(projectId.uuidString)::\(branch)"
}

public func apply(briefing: String, updatedAt: Date = .now) {
self.briefing = briefing
self.updatedAt = updatedAt
self.lastSeenAt = updatedAt
}

public func touch(at date: Date = .now) {
if date > lastSeenAt {
lastSeenAt = date
}
}

public func toItem() -> BranchBriefingItem {
BranchBriefingItem(
projectId: projectId,
branch: branch,
briefing: briefing,
updatedAt: updatedAt
)
}
}
20 changes: 20 additions & 0 deletions Packages/Sources/RxCodeCore/Models/MakeRunConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

public struct MakeRunConfig: Codable, Sendable, Hashable {
/// File name (relative to project root) of the Makefile to drive `make`.
/// Empty defaults to `make`'s normal lookup (Makefile / makefile / GNUmakefile).
public var makefile: String
public var target: String
/// Extra arguments appended after the target (e.g. `VAR=value`).
public var arguments: String

public init(
makefile: String = "",
target: String = "",
arguments: String = ""
) {
self.makefile = makefile
self.target = target
self.arguments = arguments
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import SwiftData
@Model
public final class QueuedMessageRecord {
@Attribute(.unique) public var id: UUID
/// Either a real session id, or "new" for the not-yet-started chat bucket.
/// Either a real session id or a project-scoped new-chat key.
/// Matches the keying scheme used by `WindowState.draftQueues`.
public var sessionKey: String
public var order: Int
Expand Down
12 changes: 12 additions & 0 deletions Packages/Sources/RxCodeCore/Models/RunProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Foundation

public enum RunProfileType: String, Codable, Sendable, CaseIterable, Hashable {
case bash
case xcode
case make
}

public struct RunProfile: Identifiable, Codable, Sendable, Hashable {
Expand All @@ -10,6 +12,12 @@ public struct RunProfile: Identifiable, Codable, Sendable, Hashable {
public var name: String
public var type: RunProfileType
public var bash: BashRunConfig
/// Populated when `type == .xcode`. Optional so existing on-disk profiles
/// (written before the Xcode type existed) decode cleanly.
public var xcode: XcodeRunConfig?
/// Populated when `type == .make`. Optional so existing on-disk profiles
/// (written before the Make type existed) decode cleanly.
public var make: MakeRunConfig?
public var beforeSteps: [RunStep]
public var afterSteps: [RunStep]
public var createdAt: Date
Expand All @@ -21,6 +29,8 @@ public struct RunProfile: Identifiable, Codable, Sendable, Hashable {
name: String,
type: RunProfileType = .bash,
bash: BashRunConfig = BashRunConfig(),
xcode: XcodeRunConfig? = nil,
make: MakeRunConfig? = nil,
beforeSteps: [RunStep] = [],
afterSteps: [RunStep] = [],
createdAt: Date = Date(),
Expand All @@ -31,6 +41,8 @@ public struct RunProfile: Identifiable, Codable, Sendable, Hashable {
self.name = name
self.type = type
self.bash = bash
self.xcode = xcode
self.make = make
self.beforeSteps = beforeSteps
self.afterSteps = afterSteps
self.createdAt = createdAt
Expand Down
58 changes: 58 additions & 0 deletions Packages/Sources/RxCodeCore/Models/ThreadEmbeddingChunk.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Foundation
import SwiftData

/// Persisted semantic-search chunk for a single thread. A thread is split into
/// one or more chunks at index time, each chunk gets its own embedding, and
/// search ranks chunks then collapses to one hit per thread.
@Model
public final class ThreadEmbeddingChunk {
@Attribute(.unique) public var id: String
public var threadId: String
public var projectId: UUID
public var chunkIndex: Int
public var text: String
/// L2-normalised `[Float]` packed as little-endian bytes. Length == dim * 4.
public var vector: Data
public var dim: Int
public var updatedAt: Date

public init(
id: String,
threadId: String,
projectId: UUID,
chunkIndex: Int,
text: String,
vector: Data,
dim: Int,
updatedAt: Date = .now
) {
self.id = id
self.threadId = threadId
self.projectId = projectId
self.chunkIndex = chunkIndex
self.text = text
self.vector = vector
self.dim = dim
self.updatedAt = updatedAt
}

public static func makeId(threadId: String, index: Int) -> String {
"\(threadId)#\(index)"
}
}

extension ThreadEmbeddingChunk {
/// Decode the persisted `Data` blob back into a `[Float]` of length `dim`.
public func floatVector() -> [Float] {
let count = vector.count / MemoryLayout<Float>.size
guard count == dim else { return [] }
return vector.withUnsafeBytes { raw -> [Float] in
let buf = raw.bindMemory(to: Float.self)
return Array(buf)
}
}

public static func encode(_ vector: [Float]) -> Data {
vector.withUnsafeBufferPointer { Data(buffer: $0) }
}
}
74 changes: 74 additions & 0 deletions Packages/Sources/RxCodeCore/Models/ThreadSummaryRecord.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Foundation
import SwiftData

public struct ThreadSummaryItem: Identifiable, Sendable, Equatable {
public var id: String { sessionId }

public let sessionId: String
public let projectId: UUID
public let branch: String
public let title: String
public let summary: String
public let updatedAt: Date

public init(
sessionId: String,
projectId: UUID,
branch: String,
title: String,
summary: String,
updatedAt: Date
) {
self.sessionId = sessionId
self.projectId = projectId
self.branch = branch
self.title = title
self.summary = summary
self.updatedAt = updatedAt
}
}

@Model
public final class ThreadSummaryRecord {
@Attribute(.unique) public var sessionId: String
public var projectId: UUID
public var branch: String
public var title: String
public var summary: String
public var updatedAt: Date

public init(
sessionId: String,
projectId: UUID,
branch: String,
title: String,
summary: String,
updatedAt: Date = .now
) {
self.sessionId = sessionId
self.projectId = projectId
self.branch = branch
self.title = title
self.summary = summary
self.updatedAt = updatedAt
}

public func apply(projectId: UUID, branch: String, title: String, summary: String, updatedAt: Date = .now) {
self.projectId = projectId
self.branch = branch
self.title = title
self.summary = summary
self.updatedAt = updatedAt
}

public func toItem() -> ThreadSummaryItem {
ThreadSummaryItem(
sessionId: sessionId,
projectId: projectId,
branch: branch,
title: title,
summary: summary,
updatedAt: updatedAt
)
}
}
36 changes: 36 additions & 0 deletions Packages/Sources/RxCodeCore/Models/XcodeRunConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

public enum XcodeAction: String, Codable, Sendable, CaseIterable, Hashable {
case build
case run
case test
case clean
}

public struct XcodeRunConfig: Codable, Sendable, Hashable {
/// File name (relative to project root) of the `.xcodeproj` or
/// `.xcworkspace` to drive `xcodebuild`.
public var container: String
public var isWorkspace: Bool
public var scheme: String
public var configuration: String
public var action: XcodeAction
/// Optional `-destination` argument. Empty string omits the flag.
public var destination: String

public init(
container: String = "",
isWorkspace: Bool = false,
scheme: String = "",
configuration: String = "Debug",
action: XcodeAction = .run,
destination: String = ""
) {
self.container = container
self.isWorkspace = isWorkspace
self.scheme = scheme
self.configuration = configuration
self.action = action
self.destination = destination
}
}
Loading