Skip to content

Commit bbcf34e

Browse files
author
SqlRush
committed
Restore pasted image source content
1 parent f29f29b commit bbcf34e

4 files changed

Lines changed: 155 additions & 2 deletions

File tree

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ M7 progress now includes:
505505
- `internal/session`: prompt-history `LogEntry` loading now accepts project aliases such as `projectPath`/`cwd`/`workingDirectory`/`workspacePath` and timestamp aliases such as `createdAt`/`unixTimestamp`, including RFC3339 timestamp normalization to Unix milliseconds.
506506
- `internal/session`: prompt-history display text loading now accepts aliases such as `prompt`/`text`/`input`/`content`/`value` for runtime and stored history.
507507
- `internal/session`: prompt-history pasted-content containers now accept aliases such as `pastedContent`/`pasted_content`, `pasteContents`/`paste_contents`, `pastes`, and `attachments`/`attachment`, matching interaction script fixture shapes for runtime and stored history.
508+
- `internal/session`: prompt-history pasted image loading now accepts content-block-style `source`/`imageSource` objects plus source URL/URI/cache-path aliases, and stored image entries with inline source data now restore base64 content into prompt image blocks instead of keeping only metadata.
508509
- `internal/compact`: microcompact disk cache loading now accepts broader summary-like direct and provider aliases including `body`, `markdown`, `description`/`details`, `finalSummary`, `summaryContent`, `resultText`, `completionText`, `responseText`, and `messageText`, preserving visible summary recovery through nested cache entries, JSON:API/resource envelopes, and provider-style response wrappers.
509510

510511
Still missing for full M6/M7 parity:

internal/session/history.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ func LogEntryToHistoryEntry(entry LogEntry, resolver PasteResolver) HistoryEntry
280280
pastedContents[id] = PastedContent{
281281
ID: contentID,
282282
Type: stored.Type,
283+
Content: stored.Content,
283284
MediaType: stored.MediaType,
284285
Filename: stored.Filename,
285286
Dimensions: stored.Dimensions,

internal/session/history_aliases.go

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,10 @@ func historyApplyPastedContentFields(content *PastedContent, fields map[string]j
113113
if value := historyDimensionsJSONField(fields, "dimensions", "imageDimensions", "image_dimensions"); value != nil && (overwrite || content.Dimensions == nil) {
114114
content.Dimensions = value
115115
}
116-
if value := historyStringJSONField(fields, "sourcePath", "source_path", "filePath", "file_path", "path"); value != "" && (overwrite || content.SourcePath == "") {
116+
if value := historyStringJSONField(fields, historyPastedContentSourcePathFieldNames()...); value != "" && (overwrite || content.SourcePath == "") {
117117
content.SourcePath = value
118118
}
119+
historyApplyPastedContentSourceFields(content, fields)
119120
}
120121

121122
func historyApplyStoredPastedContentFields(content *StoredPastedContent, fields map[string]json.RawMessage, overwrite bool) {
@@ -140,9 +141,10 @@ func historyApplyStoredPastedContentFields(content *StoredPastedContent, fields
140141
if value := historyDimensionsJSONField(fields, "dimensions", "imageDimensions", "image_dimensions"); value != nil && (overwrite || content.Dimensions == nil) {
141142
content.Dimensions = value
142143
}
143-
if value := historyStringJSONField(fields, "sourcePath", "source_path", "filePath", "file_path", "path"); value != "" && (overwrite || content.SourcePath == "") {
144+
if value := historyStringJSONField(fields, historyPastedContentSourcePathFieldNames()...); value != "" && (overwrite || content.SourcePath == "") {
144145
content.SourcePath = value
145146
}
147+
historyApplyStoredPastedContentSourceFields(content, fields)
146148
}
147149

148150
func canonicalPastedContentType(value string) string {
@@ -508,6 +510,45 @@ func historyPastedContentContentFieldNames() []string {
508510
}
509511
}
510512

513+
func historyPastedContentSourcePathFieldNames() []string {
514+
return []string{
515+
"sourcePath",
516+
"source_path",
517+
"sourceURL",
518+
"sourceUrl",
519+
"source_url",
520+
"sourceURI",
521+
"sourceUri",
522+
"source_uri",
523+
"filePath",
524+
"file_path",
525+
"imagePath",
526+
"image_path",
527+
"cachePath",
528+
"cache_path",
529+
"storedPath",
530+
"stored_path",
531+
"path",
532+
"url",
533+
"uri",
534+
"href",
535+
"source",
536+
"src",
537+
}
538+
}
539+
540+
func historyPastedContentSourceObjectFieldNames() []string {
541+
return []string{
542+
"source",
543+
"imageSource",
544+
"image_source",
545+
"contentSource",
546+
"content_source",
547+
"mediaSource",
548+
"media_source",
549+
}
550+
}
551+
511552
func historyPastedContentHashFieldNames() []string {
512553
return []string{
513554
"contentHash",
@@ -525,6 +566,59 @@ func historyPastedContentHashFieldNames() []string {
525566
}
526567
}
527568

569+
func historyApplyPastedContentSourceFields(content *PastedContent, fields map[string]json.RawMessage) {
570+
source := historyImageSourceJSONField(fields)
571+
if source == nil {
572+
return
573+
}
574+
if value := historyStringJSONField(source, historyPastedContentContentFieldNames()...); value != "" && content.Content == "" {
575+
content.Content = value
576+
}
577+
if value := historyStringJSONField(source, "mediaType", "media_type", "mimeType", "mime_type", "contentType", "content_type"); value != "" && content.MediaType == "" {
578+
content.MediaType = value
579+
}
580+
if value := historyStringJSONField(source, historyPastedContentSourcePathFieldNames()...); value != "" && content.SourcePath == "" {
581+
content.SourcePath = value
582+
}
583+
}
584+
585+
func historyApplyStoredPastedContentSourceFields(content *StoredPastedContent, fields map[string]json.RawMessage) {
586+
source := historyImageSourceJSONField(fields)
587+
if source == nil {
588+
return
589+
}
590+
if value := historyStringJSONField(source, historyPastedContentContentFieldNames()...); value != "" && content.Content == "" {
591+
content.Content = value
592+
}
593+
if value := historyStringJSONField(source, historyPastedContentHashFieldNames()...); value != "" && content.ContentHash == "" {
594+
content.ContentHash = value
595+
}
596+
if value := historyStringJSONField(source, "mediaType", "media_type", "mimeType", "mime_type", "contentType", "content_type"); value != "" && content.MediaType == "" {
597+
content.MediaType = value
598+
}
599+
if value := historyStringJSONField(source, historyPastedContentSourcePathFieldNames()...); value != "" && content.SourcePath == "" {
600+
content.SourcePath = value
601+
}
602+
}
603+
604+
func historyImageSourceJSONField(fields map[string]json.RawMessage) map[string]json.RawMessage {
605+
for _, name := range historyPastedContentSourceObjectFieldNames() {
606+
raw, ok := fields[name]
607+
if !ok {
608+
continue
609+
}
610+
raw = bytes.TrimSpace(raw)
611+
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) || raw[0] != '{' {
612+
continue
613+
}
614+
var source map[string]json.RawMessage
615+
if err := json.Unmarshal(raw, &source); err == nil {
616+
return source
617+
}
618+
}
619+
return nil
620+
}
621+
528622
func historyWrappedPayloadJSON(fields map[string]json.RawMessage, wrappers []string, scalarDirect []string, containerDirect []string) (json.RawMessage, bool) {
529623
if historyHasScalarPayload(fields, scalarDirect) || historyHasContainerPayload(fields, containerDirect) {
530624
return nil, false

internal/session/history_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,35 @@ func TestHistoryEntryAcceptsPastedContentBodyAndBase64DataAliases(t *testing.T)
263263
}
264264
}
265265

266+
func TestHistoryEntryAcceptsImageSourceObjectAliases(t *testing.T) {
267+
var entry HistoryEntry
268+
data := `{
269+
"display": "restore [Image #21] [Image #22]",
270+
"pastedContents": [
271+
{
272+
"imageID": "21",
273+
"type": "input_image",
274+
"fileName": "source.png",
275+
"source": {"type":"base64","media_type":"image/png","data":"AAAA"}
276+
},
277+
{
278+
"imageID": "22",
279+
"kind": "pasted-image",
280+
"imageSource": {"type":"url","url":"file:///tmp/photo.jpg","mimeType":"image/jpeg"}
281+
}
282+
]
283+
}`
284+
if err := json.Unmarshal([]byte(data), &entry); err != nil {
285+
t.Fatal(err)
286+
}
287+
if got := entry.PastedContents[21]; got.ID != 21 || got.Type != PastedContentImage || got.Content != "AAAA" || got.MediaType != "image/png" || got.Filename != "source.png" {
288+
t.Fatalf("source object image = %#v", got)
289+
}
290+
if got := entry.PastedContents[22]; got.ID != 22 || got.Type != PastedContentImage || got.Content != "" || got.MediaType != "image/jpeg" || got.SourcePath != "file:///tmp/photo.jpg" {
291+
t.Fatalf("source URL image = %#v", got)
292+
}
293+
}
294+
266295
func TestHistoryEntryAcceptsPastedContentsArrayAndSingleObject(t *testing.T) {
267296
var entry HistoryEntry
268297
if err := json.Unmarshal([]byte(`{"display":"restore","pastedContents":[{"pastedContentId":"4","kind":"text","value":"array memo"},{"imageID":"5","type":"image","base64":"AAAA","mimeType":"image/png"}]}`), &entry); err != nil {
@@ -430,6 +459,34 @@ func TestLoadHistoryAcceptsStoredPastedContentsArray(t *testing.T) {
430459
}
431460
}
432461

462+
func TestLoadHistoryPreservesStoredImageSourceContent(t *testing.T) {
463+
path := filepath.Join(t.TempDir(), "history.jsonl")
464+
line := `{"display":"look [Image #23]","pasted_contents":{"23":{"imageID":"23","kind":"pasted-image","fileName":"diagram.webp","source":{"type":"base64","mediaType":"image/webp","data":"BBBB","sourceUrl":"file:///tmp/diagram.webp"}}},"timestamp":100,"project":"/repo","sessionID":"session"}`
465+
if err := os.WriteFile(path, []byte(line+"\n"), 0o600); err != nil {
466+
t.Fatal(err)
467+
}
468+
469+
history, err := LoadHistory(path, "/repo", "session", MaxHistoryItems, nil)
470+
if err != nil {
471+
t.Fatal(err)
472+
}
473+
if len(history) != 1 {
474+
t.Fatalf("history = %#v", history)
475+
}
476+
got := history[0].PastedContents[23]
477+
if got.ID != 23 || got.Type != PastedContentImage || got.Content != "BBBB" || got.MediaType != "image/webp" || got.Filename != "diagram.webp" || got.SourcePath != "file:///tmp/diagram.webp" {
478+
t.Fatalf("stored image source = %#v", got)
479+
}
480+
messages := PromptMessages(history[0].Display, history[0].PastedContents)
481+
if len(messages) == 0 || len(messages[0].Content) != 2 || messages[0].Content[1].Type != contracts.ContentImage {
482+
t.Fatalf("prompt messages = %#v", messages)
483+
}
484+
source, ok := messages[0].Content[1].Source.(contracts.ImageSource)
485+
if !ok || source.MediaType != "image/webp" || source.Data != "BBBB" {
486+
t.Fatalf("image source = %#v", messages[0].Content[1].Source)
487+
}
488+
}
489+
433490
func TestLoadHistoryAcceptsGraphQLWrappedLogEntries(t *testing.T) {
434491
path := filepath.Join(t.TempDir(), "history.jsonl")
435492
line := `{"edge":{"node":{"properties":{"text":"restore [Pasted text #16]","attachments":[{"edge":{"node":{"properties":{"contentID":"16","pasted_type":"input_text","content_hash":"graph_hash","contentType":"text/plain"}}}}]}}},"timestamp":100,"projectPath":"/repo","sessionID":"session"}`

0 commit comments

Comments
 (0)