Skip to content

[feature] the phone names and files a recording once it has read it - #347

Merged
Lanznx merged 1 commit into
mainfrom
ios-filing-suggestion
Sep 7, 2026
Merged

Lanznx merged 1 commit into
mainfrom
ios-filing-suggestion

Conversation

@yui0303

@yui0303 yui0303 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What this changes

A recording finished on the phone now proposes what it should be called and which folder it belongs in, as a card on the record screen. Accepting the name renames it; accepting a folder files it there, creating the folder first if it does not exist yet. Before this, iOS could do neither — every recording was called Meeting Sep 7, 3:20 PM and rename appeared nowhere in the iOS target at all.

Why

Closes #343.

The desktop got this in v0.27.0 (#327): once a recording is transcribed, a pass reads the transcript and proposes a title plus 2-3 candidate folders. That pass is desktop client code (src/lib/analysis/filingRun.ts → src/lib/ai/filing.ts), dispatched when a recording loads into replay, so a phone recording only ever got an honest name if the user later opened it on a Mac.

This is a native port rather than a shared backend service, following the precedent already in the tree: TranscriptPolisher mirrors the desktop's polish prompt word-for-word over the cloud's OpenAI-compatible chat endpoint, and FilingSuggester mirrors src/lib/ai/filing.ts the same way — same SYSTEM prompt, same folder menu, and a line-for-line port of resolveFilingFolders, which is where the product's rules actually live (cap at 3, an existing folder beats the model claiming isNew, at most one folder ever created). Moving the pass into the worker is the cleaner architecture, but it is a cross-repo change this did not need.

What the phone does differently, and why

  • It waits for the upload to settle, not for the microphone to stop. A rename or a re-file is a write against a recording the server already holds; anything earlier produces an answer with nothing to apply it to.
  • A recording auto-shared to an org is skipped entirely. The copy the user will open is the org one and there is no org rename endpoint on the phone — the desktop skips the same case for the same reason (filingRun.ts bails on replayReadOnly).
  • The transcript is capped at 24k characters, head and tail with the middle marked as elided. Dictation is capped at 120s and never had to think about length; an hour of conversation is past what the fast lane accepts, and an over-long request is not a worse suggestion, it is no suggestion at all.
  • Acceptance is derived from live state, never stored — same as the desktop card. The title row retires itself once the recording is called that, a folder row once it is filed there.
  • Every write sets filingSuggested on the meta, including a dismiss. That is the flag the desktop reads to decide whether its own pass has already been spent, so a recording dealt with on the phone is not asked about again on the Mac.

How it was verified

  • bunx tsc --noEmit passes
  • bunx vitest run passes (373 tests) — no TypeScript changed, this is the baseline holding
  • Added tests: FilingSuggesterTests.swift covers every resolveFolders rule, the JSON extraction, the title gate, transcript rendering and capping, and the request shape
  • swift build --package-path ios/ParleyKit passes
  • New user-facing strings added to both zh-Hant and en in ios/App/Parley/Localizable.xcstrings (9 keys)
  • Ran the app — see below

The verification gap, stated plainly

swift test could not be run on the machine that wrote this: it fails with no such module 'XCTest', identically on a clean tree, because only Command Line Tools are installed and the CLT SDK ships no XCTest. The app target could not be compiled either, for the same reason — that needs Xcode and XcodeGen.

What was done instead:

  • The ParleyKit objects were linked into a scratch executable and 22 assertions were run against the real module with @testable: every resolveFolders rule, org folders never offered as a home, JSON extraction (fenced / preambled / brace-inside-string / truncated), the title gate including Simplified-drift, the transcript cap, and prompt assembly. All passed.
  • FilingSuggesterTests.swift was type-checked against the real built module using a stand-in XCTest, so it will compile when CI runs it.
  • Every SwiftUI construct in the card without precedent elsewhere in this app (ForEach(_:id: \.self) over indices, .overlay { RoundedRectangle().strokeBorder() }, .contentShape(RoundedRectangle(...)), .background(_:in:), ProgressView().controlSize(.mini)) was type-checked in isolation against the macOS SDK.

That still leaves the app target uncompiled, and ci.yml does not build iOS — only the tag-triggered ios-release.yml does. So a type error in the SwiftUI would pass this PR's checks and surface at release time. Please open it in Xcode before merging rather than trusting a green tick here. Auto-merge has deliberately not been set for that reason.

No screenshots for the same reason: the card cannot be rendered here.

Known limits

  • Only a recording finished in-session gets a suggestion. Ones that upload later via MeetingUploader.syncPending (queued while offline) have no screen to put a card on.
  • The suggestion is not persisted. Leaving the record screen drops it, with filingSuggested unset — so the recording simply gets the desktop's offer later, which is the intended fallback rather than a lost outcome.
  • createFolder mints the id client-side and sends {id, name, createdAt}, matching the desktop's createCloudFolder exactly. The server's response shape for personal folders is not pinned anywhere in this repo (only the org route documents {folder}), so the client decodes that envelope when present and otherwise returns what it asked for. Worth a glance from someone who can see the worker.
  • The desktop appends a UI-language instruction to the prompt so titles follow the app language; iOS has no such setting, so the model answers in the transcript's language. One line to add if the phone ever grows one.

Delivery

Merging does not put this on anyone's phone — it ships with the next App Store submission.

Every recording made on the phone was called "Meeting Sep 7, 3:20 PM" and
there was no way to change it: `rename` appears nowhere in the iOS target,
so the library filled with rows nobody could tell apart and nothing could
be done about it from the device that made them. Filing had the same
shape as the desktop's old ingest wizard — `SaveDestination` asks which
folder to use before the recording exists, at the one moment nobody knows
what the meeting was about.

The desktop answered this in v0.27.0 (#327): once there is a transcript,
a pass reads it and proposes a title plus 2-3 candidate folders. That pass
is desktop client code, so a phone recording only got an honest name if
the user later opened it on a Mac. Now the phone runs its own.

It is a native port, not a shared service. `TranscriptPolisher` already
mirrors the desktop's polish prompt word-for-word over the cloud's
OpenAI-compatible chat endpoint, and `FilingSuggester` mirrors
`src/lib/ai/filing.ts` the same way: same SYSTEM prompt, same folder
menu, and a line-for-line port of `resolveFilingFolders`, which is where
the product's rules actually live (cap at 3, an existing folder beats the
model claiming "new", at most one folder ever created). The alternative
was moving the pass into the worker, which is the cleaner architecture and
a cross-repo change this did not need.

Three things the phone does differently, each for a reason:

The pass waits for the UPLOAD to settle rather than for the microphone to
stop. A rename or a re-file is a write against a recording the server
already holds, so anything earlier produces an answer with nothing to
apply it to.

A recording auto-shared to an org is skipped entirely. The copy the user
will open is the org one and there is no org rename endpoint on the phone
— the desktop skips the same case for the same reason.

The transcript is capped at 24k characters, head and tail with the middle
marked as elided. Dictation is capped at 120s and never had to think about
this; an hour of conversation is past what the fast lane will accept, and
an over-long request is not a worse suggestion, it is no suggestion.

Acceptance is derived from live state, never stored: the title row retires
itself once the recording is called that, a folder row once it is filed
there. Every write — accept or dismiss — sets `filingSuggested` on the
meta, which is what stops the Mac asking again about a recording the user
already dealt with on their phone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

✅ SonarQube Quality Gate passed — pathorsAI_parley

0 open issues on this PR.

@Lanznx
Lanznx merged commit d778209 into main Sep 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] iOS names and files a finished recording, the way the desktop already does

3 participants