Skip to content

Commit 7ea89d6

Browse files
committed
added editing of messages
1 parent b71e20a commit 7ea89d6

2 files changed

Lines changed: 106 additions & 8 deletions

File tree

interview_coder/AppModel.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ final class AppModel: NSObject, ObservableObject {
7171
// Session preview overlays
7272
@Published var previewImported: ImportedFile? = nil
7373
@Published var previewText: String? = nil
74+
75+
// Inline edit for latest user message
76+
@Published var editingLatestUser: Bool = false
77+
@Published var editingLatestUserText: String = ""
7478
// Config UI state
7579
@Published var cfgModel: String = ConfigService.openAIModel()
7680
@Published var cfgReasoning: String = ConfigService.openAIReasoningEffort()
@@ -440,6 +444,48 @@ final class AppModel: NSObject, ObservableObject {
440444
}
441445
func closePreviewOverlay() { previewImported = nil; previewText = nil }
442446

447+
// MARK: - Edit latest user message
448+
func beginEditLatestUser() {
449+
guard let idx = conversation.lastIndex(where: { $0.role == .user }) else { return }
450+
// Initialize editor with user text only (strip any leading "User:" UI label)
451+
editingLatestUserText = stripUserPrefix(conversation[idx].text)
452+
editingLatestUser = true
453+
}
454+
455+
func cancelEditLatestUser() { editingLatestUser = false; editingLatestUserText = "" }
456+
457+
func commitEditLatestUser() {
458+
guard editingLatestUser, let idx = conversation.lastIndex(where: { $0.role == .user }) else { return }
459+
let cur = conversation[idx]
460+
let newText = stripUserPrefix(editingLatestUserText)
461+
conversation[idx] = ConversationEntry(id: cur.id, createdAt: cur.createdAt, role: .user, text: newText, images: cur.images, audio: cur.audio)
462+
editingLatestUser = false
463+
editingLatestUserText = ""
464+
// Append a new JSONL record reflecting the updated messages
465+
Task { @MainActor in if !privateMode { await logJSONLTurn() } }
466+
showToast = .init(message: "Edited last message")
467+
}
468+
469+
// Commit edit and immediately retry generation using the updated latest user message.
470+
// Note: This is intentionally scoped to the inline-edit context in the transcript and
471+
// is bound to Cmd+Enter only there, to avoid clashing with the global Cmd+Enter handler
472+
// used by the manual input composer.
473+
func commitEditLatestUserAndRegenerate() async {
474+
guard !isGenerating else { showToast = .init(message: "Already generating"); return }
475+
commitEditLatestUser()
476+
await retryLastGeneration()
477+
}
478+
479+
private func stripUserPrefix(_ s: String) -> String {
480+
let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines)
481+
let prefix = "User:"
482+
if trimmed.hasPrefix(prefix) {
483+
let rest = trimmed.dropFirst(prefix.count)
484+
return String(rest).trimmingCharacters(in: .whitespacesAndNewlines)
485+
}
486+
return s
487+
}
488+
443489
// MARK: - Session deletion
444490
func deleteCurrentSession() {
445491
let target = selectedSessionURL ?? jsonlURL

interview_coder/PanelView.swift

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,9 +1063,9 @@ private var headerBar: some View { DragHost { headerContent }.zIndex(2) }
10631063
if isDroppingIntoManual {
10641064
RoundedRectangle(cornerRadius: 6)
10651065
.fill(Color.accentColor.opacity(0.08))
1066+
RoundedRectangle(cornerRadius: 6)
1067+
.strokeBorder(Color.accentColor, style: StrokeStyle(lineWidth: 2, dash: [6,4]))
10661068
}
1067-
RoundedRectangle(cornerRadius: 6)
1068-
.strokeBorder(isDroppingIntoManual ? Color.accentColor : Color.gray.opacity(0.2), style: StrokeStyle(lineWidth: isDroppingIntoManual ? 2 : 1, dash: isDroppingIntoManual ? [6,4] : []))
10691069
}
10701070
)
10711071
.onDrop(of: [UTType.fileURL], isTargeted: $isDroppingIntoManual) { providers in
@@ -1590,6 +1590,8 @@ private extension PanelView {
15901590
var highlightIDs: Set<UUID>
15911591
var activeID: UUID?
15921592
var searchQuery: String?
1593+
@State private var hoveringUser: Bool = false
1594+
@FocusState private var editFocused: Bool
15931595

15941596
var body: some View {
15951597
VStack(alignment: .leading, spacing: 8) {
@@ -1600,12 +1602,59 @@ private extension PanelView {
16001602
.padding(.horizontal, 8)
16011603
Spacer()
16021604
if let u = item.user {
1603-
Text(TranscriptListView.previewText(u.text))
1604-
.font(.callout)
1605-
.foregroundStyle(.secondary)
1606-
.lineLimit(2)
1607-
.multilineTextAlignment(.trailing)
1608-
.frame(maxWidth: 420, alignment: .trailing)
1605+
let isLatestUser = (model.conversation.lastIndex(where: { $0.role == .user }) != nil) && (model.conversation.last(where: { $0.role == .user })?.id == u.id)
1606+
if model.editingLatestUser && isLatestUser {
1607+
// Inline editor for the latest user message
1608+
VStack(alignment: .trailing, spacing: 6) {
1609+
// Keep provided style; enforce readable text color via NSView-backed editor
1610+
ManualTextView(text: $model.editingLatestUserText, isFocused: $editFocused, fontSize: model.uiBodyPointSize, textColor: NSColor.white.withAlphaComponent(0.85))
1611+
.background(.ultraThinMaterial.opacity(0.2))
1612+
.frame(minHeight: 80, maxHeight: 120)
1613+
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.gray.opacity(0.2)))
1614+
.frame(maxWidth: 560)
1615+
.onAppear { editFocused = true }
1616+
HStack(spacing: 8) {
1617+
Text("⌘↩: save & regenerate • ESC: cancel").font(.caption2).foregroundStyle(.secondary)
1618+
}
1619+
}
1620+
.padding(8)
1621+
.frame(maxWidth: 560, alignment: .trailing)
1622+
.background(
1623+
Group {
1624+
// Cmd+Enter: commit and retry generation ONLY in this context
1625+
// This is intentionally scoped here to avoid clashing with the
1626+
// global manual input handler for Cmd+Enter.
1627+
Button("") { Task { await model.commitEditLatestUserAndRegenerate() } }
1628+
.keyboardShortcut(.return, modifiers: [.command])
1629+
.frame(width: 0, height: 0)
1630+
.opacity(0)
1631+
.allowsHitTesting(false)
1632+
// ESC: cancel edit without persisting
1633+
Button("") { model.cancelEditLatestUser() }
1634+
.keyboardShortcut(.escape, modifiers: [])
1635+
.frame(width: 0, height: 0)
1636+
.opacity(0)
1637+
.allowsHitTesting(false)
1638+
}
1639+
)
1640+
} else {
1641+
HStack(alignment: .firstTextBaseline, spacing: 6) {
1642+
Text(TranscriptListView.previewText(u.text))
1643+
.font(.callout)
1644+
.foregroundStyle(.secondary)
1645+
.lineLimit(2)
1646+
.multilineTextAlignment(.trailing)
1647+
.frame(maxWidth: 420, alignment: .trailing)
1648+
.contentShape(Rectangle())
1649+
.onHover { hovering in hoveringUser = hovering }
1650+
.onTapGesture { if isLatestUser { model.beginEditLatestUser() } }
1651+
if isLatestUser {
1652+
Button(action: { model.beginEditLatestUser() }) { Image(systemName: "pencil").font(.caption) }
1653+
.buttonStyle(.plain)
1654+
.opacity(hoveringUser ? 1 : 0)
1655+
}
1656+
}
1657+
}
16091658
}
16101659
}
16111660

@@ -2830,6 +2879,7 @@ private struct ManualTextView: NSViewRepresentable {
28302879
@Binding var text: String
28312880
var isFocused: FocusState<Bool>.Binding
28322881
var fontSize: Double = 13.0
2882+
var textColor: NSColor? = nil
28332883

28342884
final class Coordinator: NSObject, NSTextViewDelegate {
28352885
var parent: ManualTextView
@@ -2856,6 +2906,7 @@ private struct ManualTextView: NSViewRepresentable {
28562906
tv.isRichText = false
28572907
tv.isContinuousSpellCheckingEnabled = false
28582908
tv.font = .systemFont(ofSize: CGFloat(fontSize))
2909+
if let c = textColor { tv.textColor = c; tv.insertionPointColor = c }
28592910
tv.string = text
28602911
tv.delegate = context.coordinator
28612912
tv.textContainerInset = NSSize(width: 4, height: 4)
@@ -2874,6 +2925,7 @@ private struct ManualTextView: NSViewRepresentable {
28742925
tv.scrollRangeToVisible(tv.selectedRange())
28752926
}
28762927
tv.font = .systemFont(ofSize: CGFloat(fontSize))
2928+
if let c = textColor { tv.textColor = c; tv.insertionPointColor = c }
28772929
if isFocused.wrappedValue, let window = nsView.window, window.firstResponder !== tv {
28782930
window.makeFirstResponder(tv)
28792931
tv.scrollRangeToVisible(tv.selectedRange())

0 commit comments

Comments
 (0)